Files
sx/issues/0106-namespaced-import-bare-visibility-over-permissive.md
agra 6f2bf84293 fix(lower): #insert-expansion visibility exemption + close 0106 [stdlib B attempt-2]
Folds the coupled 0106 fix into Phase B. attempt-1 tightened the bare-name
visibility adapters (isNameVisible/isCImportVisible) to the flat_import_graph
edge set via the unified isVisible(.user_bare_flat/.c_import_bare) predicate;
that surfaced issue 0106 — std.print / log.* expand `#insert build_format(fmt)`
(comptime call) and `#insert "out(result);"` (inserted stmt) in the CONSUMER's
current_source_file, so their library-internal bare names were policed against
the consumer's imports and errored (run_examples 471 -> 467).

Fix: a precise, named exemption. Lowering.in_insert_expansion is set across
lowerInsertExprValue (the comptime eval + the parsed-back statements); the two
visibility adapters fall open while it is set — mirroring the existing
UFCS-alias / mangled-local "compiler indirection" exemptions. NOT a blanket
skip: scoped to #insert-expanded code, ordinary bare references stay policed.
Library-internal call bodies (build_format's concat/substr) already resolve in
the defining module — lowerFunctionBodyInto pins their current_source_file.

The flat tightening stays: a bare reference to a namespaced-only import's
internal name now correctly errors ('<name>' is not visible). This is the
Agra-ratified user-visible semantics change.

- face #1 pinned: examples/0736-modules-namespaced-only-bare-not-visible.sx
  (+ a.sx) — exit 1 + stderr; fail-before (import_graph compiled it, exit 0) /
  pass-after (flat set errors, exit 1).
- face #2 restored: examples 0015 / 0700 / 0718 / 1030 pass again.
- run_examples 471 -> 472 (the new regression).
- issues/0106 marked RESOLVED; readme.md documents namespaced-only visibility.

Collectors + unified predicate from attempt-1 (resolver.zig) unchanged; nothing
routes resolution AUTHOR-SELECTION through them yet (that is Phase C).
2026-06-07 05:17:23 +03:00

7.3 KiB

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

RESOLVED (flow stdlib/B attempt-2). Two coupled changes:

  1. Tightened bare visibility to the flat edge set. isNameVisible / isCImportVisible now route through the unified isVisible predicate over user_bare_flat / c_import_bare (both join over flat_import_graph, not import_graph). A namespaced-only import's internal name is no longer bare-visible — face #1 now errors '<name>' is not visible; #import the module that declares it.
  2. #insert-expansion visibility exemption. The flat tightening alone broke std.print / log.*: a library metaprogram's #insert build_format(fmt) (comptime call) and #insert "out(result);" (inserted statement) expand in the CALL SITE's current_source_file, so their bare names (build_format, out, emit) were policed against the consumer's imports. Fix: a precise, named exemption — Lowering.in_insert_expansion is set across lowerInsertExprValue (the comptime eval + the parsed-back statements), and isNameVisible / isCImportVisible fall open while it is set. This mirrors the existing UFCS-alias / mangled-local "compiler indirection" exemptions; it is NOT a blanket skip (it scopes to #insert-expanded code; ordinary bare references are still policed). Library-internal call bodies (e.g. build_format's concat / substr) already resolve correctly — they lower via lowerFunctionBodyInto, which pins current_source_file to the defining module.

Root cause: isNameVisible walked import_graph (flat AND namespaced edges) where a bare name should join only over flat_import_graph. Regression: examples/0736-modules-namespaced-only-bare-not-visible.sx (+ 0736-…/a.sx) — face #1 pinned (exit 1 + the stderr). Face #2 restored: examples/0015 / 0700 / 0718 / 1030 pass again (run_examples 471 → 472). Fix in src/ir/lower.zig (in_insert_expansion + the two adapters) + src/ir/resolver.zig (VisibilityMode modes, landed in attempt-1).

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).