Files
sx/issues/0130-library-decl-nested-namespace-dropped.md
agra b9cfe2554f refactor(ffi-linkage): Phase 9.3/9.4 — purge 'foreign' from issues/*.md; GATE PASS
Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern,
foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl,
findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→
dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the
renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed
issues/0043-…-foreign-class-…→…-runtime-class-….

PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/
issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant
SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party).
Suite green (644 corpus / 443 unit, 0 failed).
2026-06-15 11:18:35 +03:00

4.7 KiB

RESOLVED — 0130: #library declared behind two aliased imports is dropped — no -l flag, no JIT dlopen

RESOLVED (2026-06-12). Root cause as filed: extractLibraries and extractFrameworks (src/main.zig) walked the merged root's decls plus exactly ONE namespace_decl level, while aliased imports nest namespaces arbitrarily deep — a #library/#framework two or more aliases down never reached the AOT link args or the JIT dlopen loop. Both walks are now recursive over namespace_decl children (same seen-set dedup as before). Regression test: examples/1617-modules-library-nested-namespace.sx (+ its module dir) — main → b :: #import → c :: #import where c.sx declares #library "pcap"; libpcap is NOT in the compiler process's loaded images (unlike libz/libbz2/libsqlite3, which CoreServices/LLVM pull in and which mask the bug under sx run), so the pre-fix JIT fails symbol materialization and the pre-fix AOT link dies with undefined _pcap_lib_version. Gates: zig build test 426/426, tests/run_examples.sh 605/605, distribution repo make test 21/21 at its HEAD plus a successful make build of its P5.2 branch state (dist.sx → ops → db → sqlite, the original failing chain).

Symptom

A #library declaration in a module that is reached through TWO (or more) levels of aliased #import never makes it into the build's library list: sx build emits no -l<name> on the link line (link fails with Undefined symbols for every extern fn of that library), and sx run skips the dlopen of that library (the JIT then resolves the extern symbol only if some already-loaded image happens to export them).

  • Observed: main → b :: #import "b.sx" → c :: #import "c.sx" where c.sx declares zlib :: #library "z" → link line ends -lc (no -lz), ld: symbol(s) not found: _zlibVersion.
  • Expected: every #library reachable through the import graph is linked (AOT) / dlopened (JIT), regardless of import depth or aliasing.
  • Control: aliasing c.sx DIRECTLY from main (one namespace level) produces -lz and links fine. A plain (unaliased) #import of c.sx from a module that main aliases also works — the merged decls sit at one namespace level.

Reproduction

Three files in one directory; build a.sx.

// c.sx — declares the library + a extern fn
#import "modules/std.sx";

zlib :: #library "z";
zlibVersion :: () -> ?cstring extern zlib "zlibVersion";

zver :: () -> string {
    p := zlibVersion();
    if p == null { return ""; }
    return from_cstring(p!);
}
// b.sx — first namespace level
#import "modules/std.sx";
c :: #import "c.sx";

ver_via_b :: () -> string { return c.zver(); }
// a.sx — main; c.sx's library now sits two namespace levels deep
#import "modules/std.sx";
b :: #import "b.sx";

main :: () -> i32 {
    print("zlib {}\n", b.ver_via_b());
    return 0;
}
$ sx build -o a a.sx
Undefined symbols for architecture arm64:
  "_zlibVersion", referenced from: ...
error: linking failed

Replacing a.sx's import with c :: #import "c.sx" (and calling c.zver() directly) links and prints the zlib version — same code, one namespace level less.

Found in the distribution repo the first time a product chain nested the SQLite bindings two aliases deep: dist.sx → ops :: #import "release/ops.sx" → db :: #import "../repo/db.sx" → #import "../db/sqlite.sx" loses -lsqlite3 even though the bindings compile fine (the extern wrappers ARE in main.o; only the link flag is gone).

Investigation prompt

In /Users/agra/projects/sx: extractLibraries (src/main.zig, ~line 877) walks root.data.root.decls and exactly ONE level of namespace_decl children. Aliased imports lower to nested namespace_decl nodes, so a #library (or, same pattern, #framework — see extractFrameworks right below it, ~line 904) sitting two or more namespace levels deep is never collected. Both consumers are affected: the AOT link args (link(...) in src/target.zig receives this list) and the JIT dlopen loop (src/main.zig ~line 274). Fix: make the walk recursive over namespace_decl (a small explicit stack or recursive helper over ns.decls), dedup as today via the seen set; apply the same to extractFrameworks. Verify with the three-file libz repro from issues/0130-library-decl-nested-namespace-dropped.md: sx build must emit -lz and link, sx run must dlopen libz; add an examples/ regression mirroring the repro (one library decl behind two aliased imports) that fails on pre-fix master. Then re-run the distribution repo's make test (which now links SQLite through dist.sx's ops→db→sqlite chain) to confirm the original failure is gone.