Files
sx/issues/0106-namespaced-import-bare-visibility-over-permissive.md
agra 7158337c73 wip(resolver): collectors + unified predicate + tightened adapters [stdlib B, BLOCKED on 0106]
Collectors (resolver.zig: collectVisibleAuthors/collectNamespaceAuthors + AuthorSet
+ VisibilityMode, 4 unit tests) + unified visibility predicate + isNameVisible/
isCImportVisible adapters routed to flat modes. Tightening surfaces issue 0106
(stdlib comptime expansion relies on the over-permissive import_graph join), so
run_examples is 467/471 here. attempt-2 folds in the coupled comptime-context fix.
2026-06-07 04:52:56 +03:00

5.3 KiB

0106 — namespaced-import internal names are silently bare-visible (over-permissive isNameVisible)

Symptom. A bare reference to a top-level name authored in a module that the consumer imports only namespaced (ns :: #import "m.sx") is silently visible from the consumer. Observed: it compiles + runs. Expected: an error — the name is reachable only as ns.name. Root: Lowering.isNameVisible walks program_index.import_graph, which records BOTH flat (#import) and namespaced (ns :: #import) edges; bare-name visibility should join only over FLAT edges (flat_import_graph). This is the latent 0102-family visibility bug the Phase B caller-mode audit (unified-resolver R5) was told to surface.

This directly gates flow stdlib/B: that step requires migrating isNameVisible/isCImportVisible to the resolver's user_bare_flat/ c_import_bare modes (which walk flat_import_graph) byte-identically. Switching the edge set drops run_examples from 471 → 467 (see face #2), so the byte-identical requirement cannot hold until this bug is fixed.

Reproduction — face #1 (user-facing over-permissiveness)

// m.sx
secret :: () -> s64 { 7 }
// main.sx
m :: #import "m.sx";
main :: () -> s32 {
    x := secret();   // bare; `secret` is only namespaced-imported as `m.secret`
    0
}
  • Observed (current master): compiles, runs, exit 0 — bare secret wrongly resolves to m's author.
  • Expected: error: 'secret' is not visible; #import the module that declares it (it is reachable only as m.secret).

(With the Phase-B edge-set change applied — isNameVisible over flat_import_graph — this repro correctly errors, confirming the diagnosis.)

Reproduction — face #2 (library comptime entanglement: why a naive fix breaks std)

// main.sx
std :: #import "modules/std.sx";
main :: () -> s32 {
    std.print("hello\n");   // legit qualified call
    0
}

print in std.sx is a comptime metaprogram:

print :: ($fmt: string, ..$args) {
    #insert build_format(fmt);   // comptime call to a std-internal fn
    #insert "out(result);";       // inserts a bare call to a std-internal fn
}

The comptime call to build_format and the inserted out(result) are bare names authored in std.sx, but they are visibility-checked in the consumer's current_source_file context (comptime / #insert expansion happens at the call site). Today they pass only because import_graph[consumer] contains the namespaced std edge. Tightening bare visibility to flat_import_graph makes them error ('build_format' / 'out' is not visible). The same shape breaks log (emit). Affected examples when the edge set is switched to flat: 0015-basic-demo, 0700-modules-import, 0718-modules-cli-exit-json, 1030-errors-log-and-comptime (467/471).

Root cause (suspected area)

  • Lowering.isNameVisible / isCImportVisiblesrc/ir/lower.zig (~1768-1840 after the Phase-B refactor; visibleOverEdges / nameVisibleOverEdges). The cross-module join uses import_graph (flat and namespaced edges) where it should use flat_import_graph for a bare name.
  • Comptime / #insert expansion context: the inserted/comptime-evaluated bare calls of a namespaced module's function are policed against the consumer's imports, not the defining module's own scope. The existing visibility check already exempts UFCS-alias rewrites and mangled local names as "compiler indirections" (lower.zig call site ~7284, identifier site ~3237); inserted / comptime-generated bare calls are the same kind of indirection and are not yet exempt — or, equivalently, the expansion should restore the defining module's current_source_file.

Investigation prompt (paste into a fresh session)

Fix issue 0106: isNameVisible over-permits bare references to a namespaced-only import's internal names. Two coupled changes, in this order:

  1. Library-internal context. Ensure a namespaced/comptime-expanded function's body — including #inserted statements and comptime calls like build_format inside std.print — is visibility-checked in its defining module's context, OR exempt compiler-generated / #inserted bare calls from the visibility check (mirror the UFCS-alias / mangled-name exemptions at src/ir/lower.zig ~7284 and ~3237). Verify with the face-#2 repro and examples 0015 / 0700 / 0718 / 1030.
  2. Tighten bare visibility to flat. Change isNameVisible (and the c_import_bare fall-through of isCImportVisible) to join over flat_import_graph instead of import_graph — i.e. route them through the resolver's user_bare_flat / c_import_bare modes (this is exactly Phase B of the unified-resolver R5 plan; src/ir/resolver.zig already defines the modes and Lowering.visibleOverEdges already takes a .flat / .all selector). Verify the face-#1 repro now errors.

Acceptance: the face-#1 repro errors ("not visible"); bash tests/run_examples.sh is back to 471 ok with bare visibility on the flat edge set; add the face-#1 repro as a pinned regression (issues/0106-… with an expected/ marker, or promote to examples/07xx-modules-…). Suspected files: src/ir/lower.zig (visibility + comptime/#insert expansion context), src/ir/resolver.zig (user_bare_flat / c_import_bare).