code wiki / (root) / nx_import.nx

nx_import.nx source

↩ module page · 1341 lines · 52844 B

1// import.nx -- sovereign import preprocessor (replaces main.c logic). 2// 3// Responsibility: given a root source path, return a single null- 4// terminated buffer containing every imported file's text spliced 5// inline, recursively, with each file visited at most once. Output 6// is exactly what parse_module consumes. 7// 8// Subsystem boundary: 9// - syscalls.nx : file I/O (sys_read_file) 10// - str helpers : cstr_len / cstr_eq / u8_copy (below; no external dep) 11// - import.nx : path ops + import expansion (this file) 12// - nxc.nx : CLI driver that owns argv and calls expand_imports 13// 14// Invariants (enforced, not hoped): 15// I1 Each canonicalised absolute path is expanded at most once -- 16// the `seen` path table dedupes. Repeat imports emit nothing. 17// I2 Canonicalisation is pure textual (.. pops, . drops, 18// forward-slashes unified). No realpath(3). Identical output 19// on Windows and Linux/NishiOS. 20// I3 Import syntax: exactly `import "relative.nx"` at start-of- 21// line after optional whitespace. Nothing else parses as an 22// import; a substring match mid-line passes through. 23// I4 Buffer overruns fail loudly (negative return). Never 24// silently truncate -- correctness over convenience. 25// I5 Fixed capacity: MAX_IMPORTS distinct imports per tree; 26// paths up to IMPORT_PATH_LEN; output buffer sized by caller. 27// A build that needs more raises one constant in one place. 28// 29// Complexity: O(N * F) where N is total source bytes across all 30// files and F is average imports-per-file. One read per file. 31 32// nx_safety_envelope: 33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 34// sil_target: SIL1 35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39import "nx_linemap.nx" 40 41// ---- sizing knobs -------------------------------------------------- 42 43// Bumped 64 -> 256 (2026-05-20) -- bits-up HTTPS arc transitively 44// imports ~100 .nx files; previously failed at 64. 256 gives 45// headroom for AES-GCM, HTTP/2, image decoders without re-bumping. 46const MAX_IMPORTS: i64 = 256 47const IMPORT_PATH_LEN: i64 = 1024 48 49// Error codes returned by expand functions (negative = error). 50const ERR_IMPORTS_FULL: i64 = -1 51const ERR_READ_FAILED: i64 = -2 52const ERR_OUT_OVERFLOW: i64 = -3 53const ERR_BAD_IMPORT: i64 = -4 54 55// ---- LN14 QUALIFIED IMPORTS: sizing + refusal codes ---------------- 56// 57// MAX_ALIASES is DERIVED, not chosen: an alias can only be introduced by an 58// `as` clause on an import line, and MAX_IMPORTS already bounds how many 59// distinct imports a tree may hold, so the alias table can never be the thing 60// that overflows first -- ERR_IMPORTS_FULL fires before ERR_ALIAS_FULL can. 61const MAX_ALIASES: i64 = 256 62 63// Per-alias record, stored as AL_FIELDS parallel i64 slots in one flat table. 64const AL_FIELDS: i64 = 6 65const AL_F_NAMEOFF: i64 = 0 // byte offset of the alias spelling in al_names 66const AL_F_EXPADDR: i64 = 1 // *i64: absolute address of each export name 67const AL_F_EXPLEN: i64 = 2 // *i64: length of each export name 68const AL_F_EXPN: i64 = 3 // export count 69const AL_F_RWBUF: i64 = 4 // *u8: this module's line-rewrite scratch 70const AL_F_RWCAP: i64 = 5 // bytes in AL_F_RWBUF 71 72// THE SHORTEST TOP-LEVEL DECLARATION THIS SCANNER CAN RECOGNISE is `func x(` 73// plus its newline = 8 bytes, so a module of L bytes declares at most 74// L/EXP_MIN_DECL_BYTES + 1 exports. The export table is sized from THAT, from 75// the file's own measured length -- never from a guessed ceiling that would 76// truncate a big module in silence. 77const EXP_MIN_DECL_BYTES: i64 = 8 78 79// A renamed identifier grows by len(alias)+2 (the __ separator). The shortest 80// identifier is one byte, so an N-byte line holds at most N identifiers and 81// cannot exceed N*(alias_len+RW_GROWTH_SLACK) after rewriting. The rewrite 82// buffer is sized from the module's OWN longest line times that factor, 83// measured in the same pass that collects the exports. The bound is sound, so 84// ERR_ALIAS_LINE_CAP cannot fire -- it exists so that if it ever does, it 85// ANNOUNCES instead of truncating. 86const RW_GROWTH_SLACK: i64 = 3 87 88// One page for the ExpandCtx. The previous allocation was an exact 64-byte fit 89// with a comment warning that a 9th field would write past it: that is a 90// landmine wearing a comment, and this rung had to add four fields. A page is 91// the smallest thing sys_mmap can hand back, so no future field can outgrow 92// this without also outgrowing the allocator's own granule. 93const EXPAND_CTX_BYTES: i64 = 4096 94 95// Every refusal has its OWN code, because a refusal that will not say WHICH 96// rule fired cannot tell a right answer from a right-for-the-wrong-reason one. 97const ERR_ALIAS_FULL: i64 = -5 98const ERR_ALIAS_CONFLICT: i64 = -6 99const ERR_ALIAS_UNKNOWN: i64 = -7 100const ERR_ALIAS_NOEXPORT: i64 = -8 101const ERR_ALIAS_BADNAME: i64 = -9 102const ERR_ALIAS_LINE_CAP: i64 = -10 103 104// ---- cstring helpers ---------------------------------------------- 105 106func cstr_len(s: *u8) -> i64 { 107 var n: i64 = 0 108 while s[n] != 0 { n = n + 1 } 109 return n 110} 111 112func cstr_eq(a: *u8, b: *u8) -> i64 { 113 var i: i64 = 0 114 while a[i] != 0 { 115 if a[i] != b[i] { return 0 } 116 i = i + 1 117 } 118 if b[i] != 0 { return 0 } 119 return 1 120} 121 122// Copy exactly `n` bytes from src to dst. No null-term responsibility. 123func u8_copy(dst: *u8, src: *u8, n: i64) -> i64 { 124 var i: i64 = 0 125 while i < n { dst[i] = src[i]; i = i + 1 } 126 return n 127} 128 129// Shift a *u8 pointer by `delta` bytes (address arithmetic). 130func ptr_at(p: *u8, delta: i64) -> *u8 { 131 let a: i64 = (p as i64) + delta 132 return a as *u8 133} 134 135// ---- LN14 lexical predicates + diagnostics ------------------------- 136// These sit beside the cstring helpers on purpose: they are the only new 137// machinery that has no dependency on ExpandCtx, so a reader can check them 138// without holding the rest of the rung in their head. 139 140// Identifier byte: [A-Za-z0-9_]. The renamer's whole notion of a word boundary. 141func imp_ident_ch(b: i64) -> i64 { 142 if b >= 0x30 { if b <= 0x39 { return 1 } } 143 if b >= 0x41 { if b <= 0x5A { return 1 } } 144 if b >= 0x61 { if b <= 0x7A { return 1 } } 145 if b == 0x5F { return 1 } 146 return 0 147} 148 149// Does src[p..] begin with keyword `kw` followed by exactly one space? Used to 150// find top-level declarations, which in this dialect start at column 0. 151func imp_kw_at(src: *u8, p: i64, kw: *u8) -> i64 { 152 var i: i64 = 0 153 while kw[i] != 0 { 154 if src[p + i] != kw[i] { return 0 } 155 i = i + 1 156 } 157 if src[p + i] == 0x20 { return 1 } 158 return 0 159} 160 161// Is the span src[s..s+n) exactly `main`? main is DELIBERATELY never exported: 162// expand_imports_inner strips it from every non-root module, so it is not part 163// of any module's surface and mangling it would rename a symbol that is about 164// to be deleted. 165func imp_is_main(src: *u8, s: i64, n: i64) -> i64 { 166 if n != 4 { return 0 } 167 if src[s] != 0x6D { return 0 } 168 if src[s+1] != 0x61 { return 0 } 169 if src[s+2] != 0x69 { return 0 } 170 if src[s+3] != 0x6E { return 0 } 171 return 1 172} 173 174// Refusals go to fd 2 from HERE rather than from the driver, so the rule that 175// fired travels with the failure instead of being flattened into one exit code 176// three layers up. 177func imp_err(s: *u8) -> i64 { 178 var n: i64 = 0 179 while s[n] != 0 { n = n + 1 } 180 sys_write(2, s, n) 181 return 0 182} 183 184func imp_err_span(p: *u8, n: i64) -> i64 { 185 if n > 0 { sys_write(2, p, n) } 186 return 0 187} 188 189// Does the line src[s..e) contain a `::` at all? This is the ONE cheap test 190// that keeps an unaliased compile on exactly the path it took before this rung 191// existed: single colons are everywhere in type annotations, doubled ones are 192// not, so almost every line answers no in a few compares. 193func imp_has_qual(src: *u8, s: i64, e: i64) -> i64 { 194 var i: i64 = s 195 while i + 1 < e { 196 if src[i] == 0x3A { if src[i+1] == 0x3A { return 1 } } 197 i = i + 1 198 } 199 return 0 200} 201 202// ---- ImportSet ----------------------------------------------------- 203// 204// Flat MAX_IMPORTS * IMPORT_PATH_LEN byte buffer. Entry i lives at 205// offset i * IMPORT_PATH_LEN. Counter held externally. 206 207// WHICH slot holds `path`, or -1. LN14 needs the INDEX, not the boolean: the 208// alias a module was expanded under is recorded in a parallel array, and a 209// yes/no answer cannot address it. import_already delegates so there stays 210// exactly ONE seen-set search in this file rather than two that can drift. 211func imp_path_index(paths: *u8, n: i64, path: *u8) -> i64 { 212 var i: i64 = 0 213 while i < n { 214 let entry: *u8 = ptr_at(paths, i * IMPORT_PATH_LEN) 215 if cstr_eq(entry, path) == 1 { return i } 216 i = i + 1 217 } 218 return 0 - 1 219} 220 221func import_already(paths: *u8, n: i64, path: *u8) -> i64 { 222 if imp_path_index(paths, n, path) >= 0 { return 1 } 223 return 0 224} 225 226func import_add(paths: *u8, n: i64, path: *u8) -> i64 { 227 if n >= MAX_IMPORTS { return 0 } 228 let entry: *u8 = ptr_at(paths, n * IMPORT_PATH_LEN) 229 var plen: i64 = cstr_len(path) 230 if plen > IMPORT_PATH_LEN - 1 { plen = IMPORT_PATH_LEN - 1 } 231 u8_copy(entry, path, plen) 232 entry[plen] = 0 233 return 1 234} 235 236// ---- path ops ------------------------------------------------------ 237 238// Extract directory portion of `path`. Everything before the final 239// '/' or '\'. Writes null-terminated to `out`. Returns length. 240func path_dir(path: *u8, out: *u8) -> i64 { 241 let n: i64 = cstr_len(path) 242 var last: i64 = 0 - 1 243 var i: i64 = 0 244 while i < n { 245 if path[i] == 0x2F { last = i } // '/' 246 if path[i] == 0x5C { last = i } // '\\' 247 i = i + 1 248 } 249 if last < 0 { out[0] = 0; return 0 } 250 u8_copy(out, path, last) 251 out[last] = 0 252 return last 253} 254 255// Compose `dir/name` into `out`. If name is absolute (leading '/' or 256// 'X:' drive letter), copy name verbatim. Returns length. 257func join_path(dir: *u8, name: *u8, out: *u8) -> i64 { 258 if name[0] == 0x2F { 259 let nlen: i64 = cstr_len(name) 260 u8_copy(out, name, nlen) 261 out[nlen] = 0 262 return nlen 263 } 264 if name[0] != 0 { 265 if name[1] == 0x3A { // Windows drive letter 'X:' 266 let nlen: i64 = cstr_len(name) 267 u8_copy(out, name, nlen) 268 out[nlen] = 0 269 return nlen 270 } 271 } 272 let dlen: i64 = cstr_len(dir) 273 if dlen == 0 { 274 let nlen: i64 = cstr_len(name) 275 u8_copy(out, name, nlen) 276 out[nlen] = 0 277 return nlen 278 } 279 u8_copy(out, dir, dlen) 280 out[dlen] = 0x2F 281 let nlen: i64 = cstr_len(name) 282 u8_copy(ptr_at(out, dlen + 1), name, nlen) 283 let total: i64 = dlen + 1 + nlen 284 out[total] = 0 285 return total 286} 287 288// Return 1 if byte is a path separator. 289func is_sep(b: i64) -> i64 { 290 if b == 0x2F { return 1 } 291 if b == 0x5C { return 1 } 292 return 0 293} 294 295// Canonicalise `path` so semantically-identical forms collapse to one 296// key. Pure textual: pops `..`, drops `.`, unifies '/' separators. 297// `scratch` must hold >= 4096 bytes (two segment tables). Returns 298// output length. 299func canonicalise_path(path: *u8, out: *u8, scratch: *u8) -> i64 { 300 let n: i64 = cstr_len(path) 301 302 // First pass: extract segments. Each entry is (offset, length) 303 // into the original path buffer. 304 let seg_offs: *i64 = scratch as *i64 305 let seg_lens: *i64 = ptr_at(scratch, 1024) as *i64 306 var n_seg: i64 = 0 307 var i: i64 = 0 308 while i < n { 309 while i < n { 310 if is_sep(path[i]) == 0 { break } 311 i = i + 1 312 } 313 if i >= n { break } 314 let start: i64 = i 315 while i < n { 316 if is_sep(path[i]) == 1 { break } 317 i = i + 1 318 } 319 let seg_len: i64 = i - start 320 if seg_len > 0 { 321 if n_seg < 128 { 322 seg_offs[n_seg] = start 323 seg_lens[n_seg] = seg_len 324 n_seg = n_seg + 1 325 } 326 } 327 } 328 329 // Second pass: apply `.` drop and `..` pop. 330 let out_offs: *i64 = ptr_at(scratch, 2048) as *i64 331 let out_lens: *i64 = ptr_at(scratch, 3072) as *i64 332 var outn: i64 = 0 333 var k: i64 = 0 334 while k < n_seg { 335 let o: i64 = seg_offs[k] 336 let l: i64 = seg_lens[k] 337 // Is this segment "." ? 338 if l == 1 { 339 if path[o] == 0x2E { 340 k = k + 1 341 continue 342 } 343 } 344 // Is this segment ".." ? 345 if l == 2 { 346 if path[o] == 0x2E { 347 if path[o + 1] == 0x2E { 348 if outn > 0 { 349 outn = outn - 1 350 } else { 351 out_offs[outn] = o 352 out_lens[outn] = l 353 outn = outn + 1 354 } 355 k = k + 1 356 continue 357 } 358 } 359 } 360 out_offs[outn] = o 361 out_lens[outn] = l 362 outn = outn + 1 363 k = k + 1 364 } 365 366 // Rejoin with '/' separator. Preserve leading slash if absolute. 367 var pos: i64 = 0 368 if is_sep(path[0]) == 1 { out[pos] = 0x2F; pos = pos + 1 } 369 var j: i64 = 0 370 while j < outn { 371 if j > 0 { out[pos] = 0x2F; pos = pos + 1 } 372 let o: i64 = out_offs[j] 373 let l: i64 = out_lens[j] 374 u8_copy(ptr_at(out, pos), ptr_at(path, o), l) 375 pos = pos + l 376 j = j + 1 377 } 378 out[pos] = 0 379 return pos 380} 381 382// ---- import line detection ---------------------------------------- 383 384// Does src[p..] begin with `import` followed by space or tab? 385func starts_with_import(src: *u8, p: i64) -> i64 { 386 if src[p + 0] != 0x69 { return 0 } // 'i' 387 if src[p + 1] != 0x6D { return 0 } // 'm' 388 if src[p + 2] != 0x70 { return 0 } // 'p' 389 if src[p + 3] != 0x6F { return 0 } // 'o' 390 if src[p + 4] != 0x72 { return 0 } // 'r' 391 if src[p + 5] != 0x74 { return 0 } // 't' 392 let c: i64 = src[p + 6] 393 if c == 0x20 { return 1 } 394 if c == 0x09 { return 1 } 395 return 0 396} 397 398// ---- state singleton ---------------------------------------------- 399// 400// expand_imports is naturally recursive. Rather than pass many args 401// through each recursive call, we stash shared state in a small 402// struct allocated once by the top-level caller. 403struct ExpandCtx { 404 paths: *u8, // seen-set storage (MAX_IMPORTS * IMPORT_PATH_LEN) 405 n_paths: *i64, // counter into `paths` 406 out: *u8, // destination buffer 407 out_pos: *i64, // current write offset into `out` 408 out_cap: i64, // destination capacity (bytes) 409 scratch: *u8, // >= 4096 bytes for canonicalise 410 // The two slots this struct reserved for "future instrumentation (import 411 // depth, error line numbers)" -- now spent on exactly that. 412 lm: *LineMap, // 0 = mapping disabled (the default -- zero behaviour change) 413 out_line: i64, // current line in the EXPANDED output, 1-based 414 // ---- LN14 QUALIFIED IMPORTS (2026-08-25) ---- 415 // This comment used to read "the field COUNT is unchanged at 8 (64 bytes) 416 // and that matters: expand_ctx_new mmaps exactly 64, so a 9th field would 417 // have written past the allocation." That was true, and it made the struct 418 // unextendable by anyone who did not read the comment first -- a heap 419 // overrun guarded by prose. expand_ctx_new now takes a whole PAGE 420 // (EXPAND_CTX_BYTES), which is the smallest unit sys_mmap can return, so 421 // the hazard is gone rather than re-documented. 422 al_names: *u8, // MAX_ALIASES * IMPORT_PATH_LEN; 0 until the first `as` 423 al_tab: *i64, // MAX_ALIASES * AL_FIELDS slots; 0 until the first `as` 424 al_n: i64, // declared aliases. 0 means every LN14 branch is skipped 425 path_alias: *i64, // per seen-path slot: the alias it was expanded under, or -1 426} 427 428// Allocate an ExpandCtx with heap-backed storage. Caller owns the 429// pointer; compiler is run-once so OS reclaims at exit. 430func expand_ctx_new(out: *u8, out_cap: i64) -> *ExpandCtx { 431 let ctx_raw: *u8 = sys_mmap(EXPAND_CTX_BYTES) 432 let ctx: *ExpandCtx = ctx_raw as *ExpandCtx 433 ctx.paths = sys_mmap(MAX_IMPORTS * IMPORT_PATH_LEN) 434 let n_raw: *u8 = sys_mmap(16) 435 let np: *i64 = n_raw as *i64 436 *np = 0 437 ctx.n_paths = np 438 ctx.out = out 439 let op_raw: *u8 = sys_mmap(16) 440 let op: *i64 = op_raw as *i64 441 *op = 0 442 ctx.out_pos = op 443 ctx.out_cap = out_cap 444 ctx.scratch = sys_mmap(4096) 445 ctx.lm = 0 as *LineMap 446 ctx.out_line = 1 447 // LN14. The alias PLANE is lazy -- nothing is mapped until an `as` clause is 448 // actually seen -- but path_alias is not, because it must already hold -1 for 449 // every slot the seen-set fills BEFORE the first alias appears. Its cost is 450 // MAX_IMPORTS i64 = 2048 bytes for the life of one compiler run, and it is the 451 // only thing this rung charges a closure that never qualifies a name. 452 ctx.al_names = 0 as *u8 453 ctx.al_tab = 0 as *i64 454 ctx.al_n = 0 455 let pa_raw: *u8 = sys_mmap(MAX_IMPORTS * 8) 456 let pa: *i64 = pa_raw as *i64 457 var pi: i64 = 0 458 while pi < MAX_IMPORTS { pa[pi] = 0 - 1; pi = pi + 1 } 459 ctx.path_alias = pa 460 return ctx 461} 462 463// OPT-IN. A caller that wants per-file diagnostics asks for them; every existing 464// caller keeps byte-identical behaviour because the map stays null. Same additive 465// contract as nx_diag_set_source: unset yields the OLD output, never a wrong one. 466func expand_ctx_enable_linemap(ctx: *ExpandCtx) -> *LineMap { 467 ctx.lm = lm_new() 468 return ctx.lm 469} 470 471// Append one byte to the output buffer. Returns 0 on success, 472// ERR_OUT_OVERFLOW on out-of-room. 473func out_push(ctx: *ExpandCtx, b: i64) -> i64 { 474 let op: *i64 = ctx.out_pos 475 let pos: i64 = *op 476 if pos + 1 >= ctx.out_cap { return ERR_OUT_OVERFLOW } 477 ctx.out[pos] = b 478 *op = pos + 1 479 // The output line counter is maintained HERE, at the single byte-level chokepoint 480 // every spliced byte passes through, rather than at the call sites that think they 481 // are writing a line. A counter kept beside the writer can disagree with it; a 482 // counter kept INSIDE the writer cannot. 483 if b == 0x0A { ctx.out_line = ctx.out_line + 1 } 484 return 0 485} 486 487// Append n bytes that are KNOWN to contain no 0x0A (a single source line without its 488// terminator). Skips out_append's newline scan because there is nothing to count; the 489// caller pushes the terminator through out_push, which is where ctx.out_line advances. 490// Returns 0 or ERR_OUT_OVERFLOW; on overflow NOTHING is written. 491func out_append_line(ctx: *ExpandCtx, src: *u8, n: i64) -> i64 { 492 let op: *i64 = ctx.out_pos 493 let pos: i64 = *op 494 if pos + n + 1 >= ctx.out_cap { return ERR_OUT_OVERFLOW } 495 u8_copy(ptr_at(ctx.out, pos), src, n) 496 *op = pos + n 497 return 0 498} 499 500// Append n bytes. Returns 0 or ERR_OUT_OVERFLOW. 501func out_append(ctx: *ExpandCtx, src: *u8, n: i64) -> i64 { 502 let op: *i64 = ctx.out_pos 503 let pos: i64 = *op 504 if pos + n + 1 >= ctx.out_cap { return ERR_OUT_OVERFLOW } 505 u8_copy(ptr_at(ctx.out, pos), src, n) 506 *op = pos + n 507 // Same invariant as out_push. This path is not on today's splice route, but a 508 // writer that can advance the buffer without advancing the line counter is a 509 // desync waiting for its first caller. 510 var k: i64 = 0 511 while k < n { 512 if src[k] == 0x0A { ctx.out_line = ctx.out_line + 1 } 513 k = k + 1 514 } 515 return 0 516} 517 518// ==== LN14 QUALIFIED IMPORTS ======================================== 519// 520// `import "mod.nx" as ns` gives that module a namespace of its own: every 521// top-level name IT declares is rewritten to `ns__<name>` throughout its own 522// text, and a reference `ns::<name>` anywhere in the closure resolves to that 523// same mangled symbol. Two organs may therefore export the same identifier and 524// BOTH stay reachable -- which is the whole defect class the shadow census 525// counts, where a same-named module reachable by two paths puts two definitions 526// of one symbol into one flat program. 527// 528// WHY THE PREPROCESSOR AND NOT THE PARSER. The splice is already a whole-program 529// text transform and mangling is a text transform. Doing it here means the 530// tokenizer and the parser see ordinary identifiers and are not touched at all, 531// so the feature cannot regress anything that does not use it. 532// 533// COST WHEN UNUSED IS ZERO BY CONSTRUCTION. Every branch below is reached only 534// via `ctx.al_n > 0` (some alias was declared) or `alias_idx >= 0` (this frame 535// IS an aliased module). A closure with no `as` clause takes byte-for-byte the 536// same path it took before this rung, including the bulk-copy fast path, and 537// allocates nothing beyond the 2 KB path_alias array. 538// 539// WHAT IT DELIBERATELY DOES NOT DO, said plainly so the next reader does not 540// have to assume: 541// * `ns__name` is a RESERVED spelling. A program that already declares that 542// literal identifier gets a redefinition from the parser -- loud, not silent. 543// * Renaming is suppressed inside "..." literals, inside // comments, for an 544// identifier immediately preceded by '.', and for the FIRST identifier of a 545// line inside a top-level `struct { }` body (that is the field name; the 546// rest of the line is a type and IS renamed). 547// * `main` is never exported -- see imp_is_main. 548// * A module reached by TWO import paths in one closure canonicalises to two 549// different keys and is spliced twice; that was true before this rung and 550// is unchanged. Qualification is what makes the resulting duplicate symbols 551// addressable, not what prevents the second splice. 552 553// Map the alias plane. Called only from imp_alias_add, so a closure that never 554// writes `as` never pays for this. 555func imp_alias_init(ctx: *ExpandCtx) -> i64 { 556 if ctx.al_names != (0 as *u8) { return 0 } 557 ctx.al_names = sys_mmap(MAX_ALIASES * IMPORT_PATH_LEN) 558 let t_raw: *u8 = sys_mmap(MAX_ALIASES * AL_FIELDS * 8) 559 ctx.al_tab = t_raw as *i64 560 return 0 561} 562 563func imp_alias_slot(ctx: *ExpandCtx, k: i64, f: i64) -> i64 { 564 return ctx.al_tab[k * AL_FIELDS + f] 565} 566 567func imp_alias_set(ctx: *ExpandCtx, k: i64, f: i64, v: i64) -> i64 { 568 ctx.al_tab[k * AL_FIELDS + f] = v 569 return 0 570} 571 572func imp_alias_name(ctx: *ExpandCtx, k: i64) -> *u8 { 573 return ptr_at(ctx.al_names, imp_alias_slot(ctx, k, AL_F_NAMEOFF)) 574} 575 576// Which alias is spelled src[s..s+n)? -1 = none declared. 577func imp_alias_find(ctx: *ExpandCtx, src: *u8, s: i64, n: i64) -> i64 { 578 var k: i64 = 0 579 while k < ctx.al_n { 580 let nm: *u8 = imp_alias_name(ctx, k) 581 if cstr_len(nm) == n { 582 var i: i64 = 0 583 var eq: i64 = 1 584 while i < n { 585 if nm[i] != src[s + i] { eq = 0 } 586 i = i + 1 587 } 588 if eq == 1 { return k } 589 } 590 k = k + 1 591 } 592 return 0 - 1 593} 594 595// Declare the alias spelled src[s..s+n). Returns its slot, or a negative ERR_*. 596func imp_alias_add(ctx: *ExpandCtx, src: *u8, s: i64, n: i64) -> i64 { 597 if n <= 0 { return ERR_ALIAS_BADNAME } 598 if n >= IMPORT_PATH_LEN { return ERR_ALIAS_BADNAME } 599 imp_alias_init(ctx) 600 if ctx.al_n >= MAX_ALIASES { return ERR_ALIAS_FULL } 601 if imp_alias_find(ctx, src, s, n) >= 0 { return ERR_ALIAS_CONFLICT } 602 let k: i64 = ctx.al_n 603 let off: i64 = k * IMPORT_PATH_LEN 604 let dst: *u8 = ptr_at(ctx.al_names, off) 605 u8_copy(dst, ptr_at(src, s), n) 606 dst[n] = 0 607 imp_alias_set(ctx, k, AL_F_NAMEOFF, off) 608 imp_alias_set(ctx, k, AL_F_EXPADDR, 0) 609 imp_alias_set(ctx, k, AL_F_EXPLEN, 0) 610 imp_alias_set(ctx, k, AL_F_EXPN, 0) 611 imp_alias_set(ctx, k, AL_F_RWBUF, 0) 612 imp_alias_set(ctx, k, AL_F_RWCAP, 0) 613 ctx.al_n = k + 1 614 return k 615} 616 617// Collect alias k's export surface from the module text it is about to splice, 618// and size that module's rewrite buffer. BOTH facts come out of passes over 619// bytes already read -- the longest line is what bounds the rewrite, so it is 620// measured rather than assumed. 621func imp_alias_scan_exports(ctx: *ExpandCtx, k: i64, src: *u8, n: i64) -> i64 { 622 var maxline: i64 = 0 623 var ls: i64 = 0 624 var p: i64 = 0 625 while p < n { 626 if src[p] == 0x0A { 627 if p - ls > maxline { maxline = p - ls } 628 ls = p + 1 629 } 630 p = p + 1 631 } 632 if n - ls > maxline { maxline = n - ls } 633 634 let cap_n: i64 = n / EXP_MIN_DECL_BYTES + 1 635 let ea_raw: *u8 = sys_mmap(cap_n * 8) 636 let el_raw: *u8 = sys_mmap(cap_n * 8) 637 let eaddr: *i64 = ea_raw as *i64 638 let elen: *i64 = el_raw as *i64 639 var cnt: i64 = 0 640 641 p = 0 642 while p < n { 643 var at_ls: i64 = 0 644 if p == 0 { at_ls = 1 } 645 if p > 0 { if src[p-1] == 0x0A { at_ls = 1 } } 646 if at_ls == 1 { 647 var np: i64 = 0 - 1 648 if imp_kw_at(src, p, "func\x00" as *u8) == 1 { np = p + 5 } 649 if imp_kw_at(src, p, "const\x00" as *u8) == 1 { np = p + 6 } 650 if imp_kw_at(src, p, "struct\x00" as *u8) == 1 { np = p + 7 } 651 if np >= 0 { 652 while np < n { 653 if src[np] != 0x20 { break } 654 np = np + 1 655 } 656 var q: i64 = np 657 while q < n { 658 if imp_ident_ch(src[q] as i64) == 0 { break } 659 q = q + 1 660 } 661 let ln: i64 = q - np 662 if ln > 0 { 663 if imp_is_main(src, np, ln) == 0 { 664 // The bound above is sound (8 bytes is the shortest 665 // declaration that can exist), so this cannot fire -- 666 // and if it ever does it REFUSES rather than quietly 667 // exporting a prefix of the module. 668 if cnt >= cap_n { return ERR_ALIAS_FULL } 669 eaddr[cnt] = (src as i64) + np 670 elen[cnt] = ln 671 cnt = cnt + 1 672 } 673 } 674 } 675 } 676 p = p + 1 677 } 678 679 let alen: i64 = cstr_len(imp_alias_name(ctx, k)) 680 let rwcap: i64 = maxline * (alen + RW_GROWTH_SLACK) + 2 681 let rw_raw: *u8 = sys_mmap(rwcap) 682 imp_alias_set(ctx, k, AL_F_EXPADDR, eaddr as i64) 683 imp_alias_set(ctx, k, AL_F_EXPLEN, elen as i64) 684 imp_alias_set(ctx, k, AL_F_EXPN, cnt) 685 imp_alias_set(ctx, k, AL_F_RWBUF, rw_raw as i64) 686 imp_alias_set(ctx, k, AL_F_RWCAP, rwcap) 687 return cnt 688} 689 690// Does alias k's module declare the top-level name src[s..s+n)? 691func imp_alias_exports(ctx: *ExpandCtx, k: i64, src: *u8, s: i64, n: i64) -> i64 { 692 let ea: i64 = imp_alias_slot(ctx, k, AL_F_EXPADDR) 693 if ea == 0 { return 0 } 694 let eaddr: *i64 = ea as *i64 695 let elen: *i64 = imp_alias_slot(ctx, k, AL_F_EXPLEN) as *i64 696 let cnt: i64 = imp_alias_slot(ctx, k, AL_F_EXPN) 697 var j: i64 = 0 698 while j < cnt { 699 if elen[j] == n { 700 let nm: *u8 = eaddr[j] as *u8 701 var i: i64 = 0 702 var eq: i64 = 1 703 while i < n { 704 if nm[i] != src[s + i] { eq = 0 } 705 i = i + 1 706 } 707 if eq == 1 { return 1 } 708 } 709 j = j + 1 710 } 711 return 0 712} 713 714// THE LN14 RESOLVER. src[s..e) is a qualified reference `<ns>::<name>`; write 715// the mangled `<ns>__<name>` into out and return its length, or return a 716// negative ERR_ALIAS_* naming WHICH rule refused. The two refusals below are 717// the ones a caller can actually provoke, and each prints the namespace and the 718// name it was given: a suite that can only ask "was it refused?" cannot tell a 719// right answer from a right-for-the-wrong-reason one. 720func imp_qualified_name(ctx: *ExpandCtx, src: *u8, s: i64, e: i64, out: *u8, ocap: i64) -> i64 { 721 var c: i64 = 0 - 1 722 var i: i64 = s 723 while i + 1 < e { 724 if c < 0 { if src[i] == 0x3A { if src[i+1] == 0x3A { c = i } } } 725 i = i + 1 726 } 727 if c < 0 { return ERR_ALIAS_BADNAME } 728 let nsn: i64 = c - s 729 let nms: i64 = c + 2 730 let nmn: i64 = e - nms 731 if nsn <= 0 { return ERR_ALIAS_BADNAME } 732 if nmn <= 0 { return ERR_ALIAS_BADNAME } 733 let k: i64 = imp_alias_find(ctx, src, s, nsn) 734 if k < 0 { 735 imp_err("nx-import: REFUSED rule=ALIAS-UNKNOWN-NAMESPACE ns=\x00" as *u8) 736 imp_err_span(ptr_at(src, s), nsn) 737 imp_err(" -- no import declared this namespace before its first use\n\x00" as *u8) 738 return ERR_ALIAS_UNKNOWN 739 } 740 if imp_alias_exports(ctx, k, src, nms, nmn) == 0 { 741 imp_err("nx-import: REFUSED rule=ALIAS-NO-SUCH-EXPORT ns=\x00" as *u8) 742 imp_err_span(ptr_at(src, s), nsn) 743 imp_err(" name=\x00" as *u8) 744 imp_err_span(ptr_at(src, nms), nmn) 745 imp_err(" -- that module declares no top-level func, const or struct of that name\n\x00" as *u8) 746 return ERR_ALIAS_NOEXPORT 747 } 748 let need: i64 = nsn + 2 + nmn 749 if need > ocap { return ERR_ALIAS_LINE_CAP } 750 u8_copy(out, ptr_at(src, s), nsn) 751 out[nsn] = 0x5F 752 out[nsn + 1] = 0x5F 753 u8_copy(ptr_at(out, nsn + 2), ptr_at(src, nms), nmn) 754 return need 755} 756 757// Rewrite one source line src[s..e) into dst. Applies qualification everywhere, 758// and this module's own mangling when alias_idx >= 0. st[0] carries the 759// top-level struct-body flag ACROSS lines; st[1] is this line's seen-an- 760// identifier flag. Returns bytes written or a negative ERR_*. 761// 762// WHEN alias_idx < 0 THE TRANSFORM IS LENGTH-PRESERVING (`::` and `__` are both 763// two bytes), so the caller passes dst = src + s and the rewrite happens IN 764// PLACE with no buffer at all. Every write lands on a byte already read, so 765// there is no aliasing hazard -- that is why the mangled separator is two 766// characters and not one. 767func imp_rw_line(ctx: *ExpandCtx, src: *u8, s: i64, e: i64, alias_idx: i64, 768 dst: *u8, dcap: i64, st: *i64) -> i64 { 769 var o: i64 = 0 770 var p: i64 = s 771 var instr: i64 = 0 772 var incom: i64 = 0 773 st[1] = 0 774 var opens_struct: i64 = 0 775 if imp_kw_at(src, s, "struct\x00" as *u8) == 1 { opens_struct = 1 } 776 var closes_struct: i64 = 0 777 if src[s] == 0x7D { closes_struct = 1 } 778 779 while p < e { 780 let c: i64 = src[p] as i64 781 if incom == 1 { 782 if o >= dcap { return ERR_ALIAS_LINE_CAP } 783 dst[o] = src[p] 784 o = o + 1 785 p = p + 1 786 continue 787 } 788 if instr == 1 { 789 if c == 0x5C { 790 if p + 1 < e { 791 if o + 1 >= dcap { return ERR_ALIAS_LINE_CAP } 792 dst[o] = src[p] 793 dst[o+1] = src[p+1] 794 o = o + 2 795 p = p + 2 796 continue 797 } 798 } 799 if c == 0x22 { instr = 0 } 800 if o >= dcap { return ERR_ALIAS_LINE_CAP } 801 dst[o] = src[p] 802 o = o + 1 803 p = p + 1 804 continue 805 } 806 if c == 0x22 { 807 instr = 1 808 if o >= dcap { return ERR_ALIAS_LINE_CAP } 809 dst[o] = src[p] 810 o = o + 1 811 p = p + 1 812 continue 813 } 814 if c == 0x2F { 815 if src[p+1] == 0x2F { incom = 1 } 816 if o >= dcap { return ERR_ALIAS_LINE_CAP } 817 dst[o] = src[p] 818 o = o + 1 819 p = p + 1 820 continue 821 } 822 if imp_ident_ch(c) == 0 { 823 if o >= dcap { return ERR_ALIAS_LINE_CAP } 824 dst[o] = src[p] 825 o = o + 1 826 p = p + 1 827 continue 828 } 829 var q: i64 = p 830 while q < e { 831 if imp_ident_ch(src[q] as i64) == 0 { break } 832 q = q + 1 833 } 834 let il: i64 = q - p 835 var isqual: i64 = 0 836 if q + 1 < e { if src[q] == 0x3A { if src[q+1] == 0x3A { isqual = 1 } } } 837 if isqual == 1 { 838 var r: i64 = q + 2 839 while r < e { 840 if imp_ident_ch(src[r] as i64) == 0 { break } 841 r = r + 1 842 } 843 let wl: i64 = imp_qualified_name(ctx, src, p, r, ptr_at(dst, o), dcap - o) 844 if wl < 0 { return wl } 845 o = o + wl 846 p = r 847 st[1] = 1 848 continue 849 } 850 var mangle: i64 = 0 851 if alias_idx >= 0 { 852 var blocked: i64 = 0 853 if p > s { if src[p-1] == 0x2E { blocked = 1 } } 854 if st[0] == 1 { if st[1] == 0 { blocked = 1 } } 855 if blocked == 0 { 856 if imp_alias_exports(ctx, alias_idx, src, p, il) == 1 { mangle = 1 } 857 } 858 } 859 st[1] = 1 860 if mangle == 1 { 861 let an: *u8 = imp_alias_name(ctx, alias_idx) 862 let al: i64 = cstr_len(an) 863 if o + al + 2 + il > dcap { return ERR_ALIAS_LINE_CAP } 864 u8_copy(ptr_at(dst, o), an, al) 865 o = o + al 866 dst[o] = 0x5F 867 o = o + 1 868 dst[o] = 0x5F 869 o = o + 1 870 u8_copy(ptr_at(dst, o), ptr_at(src, p), il) 871 o = o + il 872 p = q 873 continue 874 } 875 if o + il > dcap { return ERR_ALIAS_LINE_CAP } 876 u8_copy(ptr_at(dst, o), ptr_at(src, p), il) 877 o = o + il 878 p = q 879 } 880 if opens_struct == 1 { st[0] = 1 } 881 if closes_struct == 1 { st[0] = 0 } 882 return o 883} 884 885// Recursively expand `path` (relative or absolute) into ctx->out. 886// Returns 0 on success, or a negative ERR_* code on failure. 887// Match `func main(` (with optional leading whitespace already consumed) 888// at offset p in src. Returns 1 if matched, 0 otherwise. Used to 889// strip self-test main functions out of imported (non-root) files, 890// mirroring the C anchor's main.c logic so a self-host compile 891// doesn't produce N duplicate `main` symbols at link time. 892func is_func_main_line(src: *u8, p: i64) -> i64 { 893 if src[p] != 0x66 { return 0 } // 'f' 894 if src[p+1] != 0x75 { return 0 } // 'u' 895 if src[p+2] != 0x6E { return 0 } // 'n' 896 if src[p+3] != 0x63 { return 0 } // 'c' 897 if src[p+4] != 0x20 { return 0 } // ' ' 898 if src[p+5] != 0x6D { return 0 } // 'm' 899 if src[p+6] != 0x61 { return 0 } // 'a' 900 if src[p+7] != 0x69 { return 0 } // 'i' 901 if src[p+8] != 0x6E { return 0 } // 'n' 902 // followed by '(' or ' (' 903 if src[p+9] == 0x28 { return 1 } // '(' 904 if src[p+9] == 0x20 { 905 if src[p+10] == 0x28 { return 1 } 906 } 907 return 0 908} 909 910// Skip past a `func main(...) ... { ... }` body, returning the new 911// cursor position past the closing '}' + trailing newline (if any). 912// STRING/COMMENT-AWARE (2026-08-04, debt 1785894599): the raw brace count died on 913// nx_js_lex's gate-main, whose KAT6 asserts the lexeme "}" -- the in-STRING 0x7D 914// closed the skip mid-body and spilled the rest of main to module level, where the 915// same-day strict stray-statement check (nx_parse 1785883072) refused the unit. The 916// dual failure is worse: a '{' inside a string OVER-skips and silently eats the 917// functions after main. Braces now count only outside "..." literals (backslash 918// escapes honored) and // line comments -- the same classes the real lexer tokenizes. 919func skip_func_main(src: *u8, p: i64) -> i64 { 920 var q: i64 = p 921 while src[q] != 0 { 922 if src[q] == 0x7B { break } // '{' 923 q = q + 1 924 } 925 if src[q] == 0x7B { 926 var depth: i64 = 1 927 q = q + 1 928 var instr: i64 = 0 // inside a "..." string literal 929 var incom: i64 = 0 // inside a // line comment 930 while src[q] != 0 { 931 if depth == 0 { break } 932 var c: i64 = src[q] as i64 933 if incom == 1 { 934 if c == 0x0A { incom = 0 } 935 q = q + 1 936 } else { if instr == 1 { 937 if c == 0x5C { // backslash: the escaped char is literal 938 q = q + 2 939 } else { 940 if c == 0x22 { instr = 0 } 941 q = q + 1 942 } 943 } else { 944 if c == 0x22 { instr = 1; q = q + 1 } else { 945 if c == 0x2F { 946 var c2: i64 = src[q + 1] as i64 947 if c2 == 0x2F { incom = 1; q = q + 2 } else { q = q + 1 } 948 } else { 949 if c == 0x7B { depth = depth + 1 } 950 if c == 0x7D { depth = depth - 1 } 951 q = q + 1 952 } } 953 } } 954 } 955 } 956 while src[q] != 0 { 957 if src[q] == 0x0A { break } 958 q = q + 1 959 } 960 if src[q] == 0x0A { q = q + 1 } 961 return q 962} 963 964// Forward decl so expand_imports can call expand_imports_inner 965// before it's defined (NishiLang requires top-down declaration 966// order today). 967func expand_imports_inner(ctx: *ExpandCtx, path: *u8, is_root: i64, alias_idx: i64) -> i64; 968 969// SITES-LIVE 2026-05-27 per operator full-blessing directive to get 970// nishifamily.com + andelinwest.com live on west NAS via native Nishi 971// containers. Resolver walk-up + sibling-subroot search: hub/foo.nx 972// imports bare nx_syscalls.nx (lives at runtime/) AND runtime/* imports 973// bare nx_search_query_parser.nx (lives at runtime/hub/). Walks parent 974// dirs + at each level tries known substrate subroots ("", "hub", 975// "wiki", "bin", "kernel"). Mirrors C bootstrap try_resolve_import. 976// (Earlier this session, I incorrectly edited runtime/import.nx -- 977// that file is for the OLDER nxc.nx driver, not the native ELF's 978// nx_compile_x86.nx driver which imports THIS file. Lesson logged.) 979 980func file_exists(path: *u8) -> i64 { 981 let fd: i64 = sys_openat_rd(path) 982 if fd < 0 { return 0 } 983 sys_close(fd) 984 return 1 985} 986 987func try_subroot(dir: *u8, subroot: *u8, ipath: *u8, 988 scratch: *u8, out: *u8) -> i64 { 989 let dir_n: i64 = cstr_len(dir) 990 let sub_n: i64 = cstr_len(subroot) 991 let ipath_n: i64 = cstr_len(ipath) 992 var pos: i64 = 0 993 if dir_n > 0 { 994 u8_copy(ptr_at(scratch, pos), dir, dir_n) 995 pos = pos + dir_n 996 scratch[pos] = 0x2F 997 pos = pos + 1 998 } 999 if sub_n > 0 { 1000 u8_copy(ptr_at(scratch, pos), subroot, sub_n) 1001 pos = pos + sub_n 1002 scratch[pos] = 0x2F 1003 pos = pos + 1 1004 } 1005 u8_copy(ptr_at(scratch, pos), ipath, ipath_n) 1006 pos = pos + ipath_n 1007 scratch[pos] = 0 1008 if file_exists(scratch) == 1 { 1009 u8_copy(out, scratch, pos + 1) 1010 return 1 1011 } 1012 return 0 1013} 1014 1015func try_resolve_import(dir: *u8, ipath: *u8, out: *u8) -> i64 { 1016 if ipath[0] == 0x2F { 1017 u8_copy(out, ipath, cstr_len(ipath) + 1) 1018 return 1 1019 } 1020 let trydir: *u8 = sys_mmap(IMPORT_PATH_LEN) 1021 let scratch: *u8 = sys_mmap(IMPORT_PATH_LEN) 1022 u8_copy(trydir, dir, cstr_len(dir) + 1) 1023 let empty: *u8 = sys_mmap(2) 1024 empty[0] = 0 1025 let sub_hub: *u8 = sys_mmap(8) 1026 sub_hub[0] = 0x68 1027 sub_hub[1] = 0x75 1028 sub_hub[2] = 0x62 1029 sub_hub[3] = 0 1030 let sub_wik: *u8 = sys_mmap(8) 1031 sub_wik[0] = 0x77 1032 sub_wik[1] = 0x69 1033 sub_wik[2] = 0x6B 1034 sub_wik[3] = 0x69 1035 sub_wik[4] = 0 1036 let sub_bin: *u8 = sys_mmap(8) 1037 sub_bin[0] = 0x62 1038 sub_bin[1] = 0x69 1039 sub_bin[2] = 0x6E 1040 sub_bin[3] = 0 1041 let sub_krn: *u8 = sys_mmap(8) 1042 sub_krn[0] = 0x6B 1043 sub_krn[1] = 0x65 1044 sub_krn[2] = 0x72 1045 sub_krn[3] = 0x6E 1046 sub_krn[4] = 0x65 1047 sub_krn[5] = 0x6C 1048 sub_krn[6] = 0 1049 // "_hdl_build" -- debt 1785608999: runtime/ entries could not import 1050 // _hdl_build/ modules (one-way ceiling; _hdl_build->runtime worked via 1051 // walk-up). Tried LAST at every level, so no import that resolves 1052 // today can change -- only previously-FAILING ones can start to. 1053 let sub_hdl: *u8 = sys_mmap(16) 1054 sub_hdl[0] = 0x5F 1055 sub_hdl[1] = 0x68 1056 sub_hdl[2] = 0x64 1057 sub_hdl[3] = 0x6C 1058 sub_hdl[4] = 0x5F 1059 sub_hdl[5] = 0x62 1060 sub_hdl[6] = 0x75 1061 sub_hdl[7] = 0x69 1062 sub_hdl[8] = 0x6C 1063 sub_hdl[9] = 0x64 1064 sub_hdl[10] = 0 1065 var hop: i64 = 0 1066 while hop < 16 { 1067 if try_subroot(trydir, empty, ipath, scratch, out) == 1 { return 1 } 1068 if try_subroot(trydir, sub_hub, ipath, scratch, out) == 1 { return 1 } 1069 if try_subroot(trydir, sub_wik, ipath, scratch, out) == 1 { return 1 } 1070 if try_subroot(trydir, sub_bin, ipath, scratch, out) == 1 { return 1 } 1071 if try_subroot(trydir, sub_krn, ipath, scratch, out) == 1 { return 1 } 1072 if try_subroot(trydir, sub_hdl, ipath, scratch, out) == 1 { return 1 } 1073 let trydir_n: i64 = cstr_len(trydir) 1074 if trydir_n == 0 { break } 1075 var i: i64 = trydir_n - 1 1076 var found: i64 = 0 1077 while i >= 0 { 1078 if trydir[i] == 0x2F { 1079 trydir[i] = 0 1080 found = 1 1081 i = 0 - 1 1082 } 1083 if i >= 0 { i = i - 1 } 1084 } 1085 if found == 0 { trydir[0] = 0 } 1086 hop = hop + 1 1087 } 1088 join_path(dir, ipath, out) 1089 return 0 1090} 1091 1092// The public entry point is UNCHANGED, so no caller of this preprocessor had to 1093// be touched for LN14: the root file is by definition not an aliased module. 1094func expand_imports(ctx: *ExpandCtx, path: *u8) -> i64 { 1095 return expand_imports_inner(ctx, path, 1, 0 - 1) 1096} 1097 1098func expand_imports_inner(ctx: *ExpandCtx, path: *u8, is_root: i64, alias_idx: i64) -> i64 { 1099 // Canonicalise for dedupe. 1100 let abspath_raw: *u8 = sys_mmap(IMPORT_PATH_LEN) 1101 canonicalise_path(path, abspath_raw, ctx.scratch) 1102 let np: *i64 = ctx.n_paths 1103 let seen: i64 = imp_path_index(ctx.paths, *np, abspath_raw) 1104 if seen >= 0 { 1105 // LN14. A module's SYMBOL NAMES depend on whether it was aliased, so one 1106 // closure cannot reach the same module both ways: the plain importer's 1107 // calls would name symbols the aliased splice has already renamed away. 1108 // Refuse by name rather than silently serve whichever spelling arrived 1109 // first -- a wrong answer here compiles, links and runs. 1110 var clash: i64 = 0 1111 if alias_idx >= 0 { clash = 1 } 1112 if ctx.path_alias[seen] >= 0 { clash = 1 } 1113 if clash == 1 { 1114 imp_err("nx-import: REFUSED rule=ALIAS-MODULE-ALREADY-IMPORTED path=\x00" as *u8) 1115 imp_err(path) 1116 imp_err(" -- a module may be imported plainly OR under exactly one alias, never both\n\x00" as *u8) 1117 return ERR_ALIAS_CONFLICT 1118 } 1119 return 0 1120 } 1121 if import_add(ctx.paths, *np, abspath_raw) == 0 { 1122 return ERR_IMPORTS_FULL 1123 } 1124 ctx.path_alias[*np] = alias_idx 1125 *np = *np + 1 1126 1127 // Read the file. 1128 let len_raw: *u8 = sys_mmap(16) 1129 let len_out: *i64 = len_raw as *i64 1130 *len_out = 0 1131 let src: *u8 = sys_read_file(path, len_out) 1132 if src == (0 as *u8) { return ERR_READ_FAILED } 1133 1134 // LN14. An ALIASED module has its export surface collected BEFORE any of its 1135 // text is spliced, because the renamer must decide the very first identifier 1136 // on the very first line and cannot do that from a set it is still building. 1137 // One pass over bytes already in hand; nothing is re-read. 1138 if alias_idx >= 0 { 1139 let ecnt: i64 = imp_alias_scan_exports(ctx, alias_idx, src, *len_out) 1140 if ecnt < 0 { return ecnt } 1141 } 1142 // Struct-body state for imp_rw_line, carried across the lines of THIS frame. 1143 // Mapped lazily at the first line that actually needs rewriting, so a frame 1144 // in an unaliased closure allocates nothing. 1145 var sst: *i64 = 0 as *i64 1146 1147 // Compute this file's dir for resolving relative imports. 1148 let dir: *u8 = sys_mmap(IMPORT_PATH_LEN) 1149 path_dir(path, dir) 1150 1151 // LINE MAP -- span 1 of this frame. This file's region of the expanded buffer 1152 // begins at the current output line and starts at its own source line 1. Every 1153 // later span in this frame re-anchors the SAME file, so interning once is enough. 1154 // All four re-anchor points below exist for one reason: they are precisely the 1155 // places where the output line and the source line stop advancing together. 1156 let fid: i64 = lm_intern_file(ctx.lm, path) 1157 lm_add_span(ctx.lm, ctx.out_line, fid, 1) 1158 var src_line: i64 = 1 1159 1160 // Walk src line by line, BY MEASURED LENGTH, never to a NUL. LN24 root cause (2026-09-02): this walk 1161 // used `src[p] != 0`, so a raw NUL anywhere in a user file silently dropped the REST OF THAT FILE from 1162 // the unit before the lexer ever saw a byte of it -- an open literal then ran on into the next spliced 1163 // file (the auto-appended crash handler) and every error named nx_crash.nx (measured twice). The 1164 // lexer's desync attribution (diag_desync_origin) can only fire on bytes it receives, so the NUL must 1165 // pass through here and be refused THERE, by file and line, in the teaching voice. 1166 let n: i64 = *len_out 1167 var p: i64 = 0 1168 while p < n { 1169 // Leading whitespace scan, preserving cursor so we can copy 1170 // the original bytes if this line isn't an import. 1171 let line_start: i64 = p 1172 while src[p] == 0x20 { p = p + 1 } 1173 while src[p] == 0x09 { p = p + 1 } 1174 1175 // Strip `func main(...)` from non-root files so a self-host 1176 // compile doesn't see N duplicate main symbols. Mirrors 1177 // main.c lines 228-274. 1178 if is_root == 0 { 1179 if is_func_main_line(src, p) == 1 { 1180 let mstart: i64 = p 1181 p = skip_func_main(src, p) 1182 // RE-ANCHOR 2: stripping a self-test main consumes SOURCE lines and 1183 // emits NO output, so the counters diverge by exactly the size of the 1184 // body we swallowed. Count it rather than estimate it. 1185 var mq: i64 = mstart 1186 while mq < p { 1187 if src[mq] == 0x0A { src_line = src_line + 1 } 1188 mq = mq + 1 1189 } 1190 lm_add_span(ctx.lm, ctx.out_line, fid, src_line) 1191 continue 1192 } 1193 } 1194 1195 if starts_with_import(src, p) == 1 { 1196 p = p + 7 1197 while src[p] == 0x20 { p = p + 1 } 1198 while src[p] == 0x09 { p = p + 1 } 1199 if src[p] != 0x22 { return ERR_BAD_IMPORT } 1200 p = p + 1 1201 1202 // Read quoted relative path. 1203 let ipath: *u8 = sys_mmap(IMPORT_PATH_LEN) 1204 var il: i64 = 0 1205 while p < n { 1206 if src[p] == 0x22 { break } 1207 if il + 1 >= IMPORT_PATH_LEN { return ERR_BAD_IMPORT } 1208 ipath[il] = src[p] 1209 il = il + 1 1210 p = p + 1 1211 } 1212 ipath[il] = 0 1213 if src[p] == 0x22 { p = p + 1 } 1214 1215 // LN14: the optional `as <ns>` clause. Recognised ONLY as the literal 1216 // two bytes `as` followed by a space or tab, immediately after the 1217 // closing quote and its whitespace -- so a trailing `// as ...` 1218 // comment cannot be mistaken for a namespace declaration. 1219 var al_idx: i64 = 0 - 1 1220 while src[p] == 0x20 { p = p + 1 } 1221 while src[p] == 0x09 { p = p + 1 } 1222 var has_as: i64 = 0 1223 if src[p] == 0x61 { if src[p+1] == 0x73 { 1224 if src[p+2] == 0x20 { has_as = 1 } 1225 if src[p+2] == 0x09 { has_as = 1 } 1226 } } 1227 if has_as == 1 { 1228 p = p + 2 1229 while src[p] == 0x20 { p = p + 1 } 1230 while src[p] == 0x09 { p = p + 1 } 1231 let ns_s: i64 = p 1232 while imp_ident_ch(src[p] as i64) == 1 { p = p + 1 } 1233 let ns_n: i64 = p - ns_s 1234 al_idx = imp_alias_add(ctx, src, ns_s, ns_n) 1235 if al_idx < 0 { 1236 imp_err("nx-import: REFUSED rule=ALIAS-BAD-OR-DUPLICATE-NAMESPACE ns=\x00" as *u8) 1237 imp_err_span(ptr_at(src, ns_s), ns_n) 1238 imp_err(" -- a namespace name must be a non-empty identifier and may be declared once\n\x00" as *u8) 1239 return al_idx 1240 } 1241 } 1242 1243 // Skip to end-of-line. 1244 while p < n { 1245 if src[p] == 0x0A { break } 1246 p = p + 1 1247 } 1248 if src[p] == 0x0A { p = p + 1; src_line = src_line + 1 } 1249 1250 // SITES-LIVE 2026-05-27: walk-up + sibling-subroot resolver 1251 // so hub/foo.nx can import bare runtime/* siblings + vice 1252 // versa. See try_resolve_import above. 1253 let full: *u8 = sys_mmap(IMPORT_PATH_LEN) 1254 try_resolve_import(dir, ipath, full) 1255 1256 // Recurse with is_root=0 so any nested file's `func main` 1257 // gets stripped (see top of expand_imports_inner). 1258 let rc: i64 = expand_imports_inner(ctx, full, 0, al_idx) 1259 if rc < 0 { return rc } 1260 // Terminating newline between spliced files. 1261 let rc2: i64 = out_push(ctx, 0x0A) 1262 if rc2 < 0 { return rc2 } 1263 // RE-ANCHOR 3: the nested file wrote an arbitrary number of output lines 1264 // (or ZERO, when the seen-set already had it). Re-anchor the PARENT here, 1265 // at the source line just past its own `import` statement. This is the span 1266 // that makes a diagnostic in file A stay attributed to A after A imports B. 1267 lm_add_span(ctx.lm, ctx.out_line, fid, src_line) 1268 continue 1269 } 1270 1271 // Not an import -- copy the whole line through. This is the ONLY path where 1272 // output and source advance in lockstep, which is why it needs no span: the 1273 // arithmetic in lm_lookup already covers it. 1274 // 1275 // ONE BOUNDED COPY PER LINE, NOT ONE CALL PER BYTE (2026-08-18, /compare/toolchain). 1276 // This loop used to route every byte of every non-import line through out_push -- 1277 // a call, a cap check, an out_pos deref and a newline test per byte -- and it was 1278 // 30% of a typical gate's whole compile (measured 31 ms of 96 ms on a ~600 KB 1279 // expanded closure; the syscall census was ~250 calls, so the cost was pure CPU). 1280 // Now the line end is found first, the bytes [line_start, e) are copied once, and 1281 // only the terminating newline goes through out_push so ctx.out_line still advances 1282 // at the single chokepoint that owns it. Output bytes are IDENTICAL: the copied 1283 // span contains no 0x0A by construction, so nothing the counter would have seen is 1284 // skipped. The only observable difference is on OVERFLOW: a line that does not fit 1285 // is refused before any of it is written instead of after a partial write, and the 1286 // caller fails the compile on that code either way. 1287 p = line_start 1288 var e: i64 = p 1289 while e < n { 1290 if src[e] == 0x0A { break } 1291 e = e + 1 1292 } 1293 if e > p { 1294 // LN14. THE FAST PATH IS THE DEFAULT AND STAYS EXACTLY AS IT WAS. 1295 // A line is rewritten only when this frame is an aliased module, or 1296 // when some alias has been declared AND this line actually contains a 1297 // `::`. A closure with no `as` clause anywhere therefore takes the 1298 // identical bulk copy, byte for byte and branch for branch. 1299 var need_rw: i64 = 0 1300 if alias_idx >= 0 { need_rw = 1 } 1301 if need_rw == 0 { 1302 if ctx.al_n > 0 { 1303 if imp_has_qual(src, p, e) == 1 { need_rw = 1 } 1304 } 1305 } 1306 var rcl: i64 = 0 1307 if need_rw == 0 { 1308 rcl = out_append_line(ctx, ptr_at(src, p), e - p) 1309 } else { 1310 if sst == (0 as *i64) { 1311 let sst_raw: *u8 = sys_mmap(16) 1312 sst = sst_raw as *i64 1313 sst[0] = 0 1314 sst[1] = 0 1315 } 1316 // An unaliased frame only ever turns `::` into `__`, which is 1317 // length-preserving, so it rewrites IN PLACE in its own private 1318 // copy of the source and needs no buffer. An aliased frame grows 1319 // lines and uses the buffer sized from its own longest line. 1320 var dst: *u8 = ptr_at(src, p) 1321 var dcap: i64 = e - p 1322 if alias_idx >= 0 { 1323 dst = imp_alias_slot(ctx, alias_idx, AL_F_RWBUF) as *u8 1324 dcap = imp_alias_slot(ctx, alias_idx, AL_F_RWCAP) 1325 } 1326 let wl: i64 = imp_rw_line(ctx, src, p, e, alias_idx, dst, dcap, sst) 1327 if wl < 0 { return wl } 1328 rcl = out_append_line(ctx, dst, wl) 1329 } 1330 if rcl < 0 { return rcl } 1331 } 1332 p = e 1333 if src[p] == 0x0A { 1334 let rc: i64 = out_push(ctx, 0x0A) 1335 if rc < 0 { return rc } 1336 p = p + 1 1337 src_line = src_line + 1 1338 } 1339 } 1340 return 0 1341}