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}