Files
sx/issues/0106-namespaced-import-bare-visibility-over-permissive.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

9.4 KiB

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

RESOLVED (flow stdlib/B attempt-3 — root fix, no exemption). 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. Pin the defining-module context during pack/comptime monomorphization. The flat tightening alone broke std.print / log.*: a library metaprogram's body (#insert build_format(fmt) comptime call + the #insert "out(result);" inserted statement) was lowered under the CALL SITE's current_source_file, so its bare names (build_format, out, emit) were policed against the consumer's imports. Root cause: monomorphizePackFn (bare print / format) and lowerComptimeCall (namespaced std.print / log.*, reached via the field-access hasComptimeParams branch) lower the metaprogram body without pinning the source context — unlike a normal function, which lowers via lowerFunctionBodyInto pinning func.source_file. Fix: both paths now save/set/restore current_source_file to the body's DEFINING module before lowering the body (the call-site ARGS are lowered first, in the caller's context, which is correct). The defining path is stamped onto each function body node by resolveImports (stampFnBodySource, mirroring how a declared function carries Function.source_file). So the metaprogram's bare build_format / out / emit resolve in std.sx / log.sx naturally — and a USER's #insert <expr> is still checked in the USER's context, so a bare reach into a namespaced-only import there errors. No #insert exemption (attempt-2's in_insert_expansion flag is deleted): the fix is the absence of an exemption, not a narrower one.
  3. Substituted caller $-args resolve in the CALLER's context (attempt-5). The point-2 defining-module pin covers the metaprogram body's OWN code only. A caller-provided comptime $-arg (e.g. a caller-owned helper passed to an imported metaprogram) is spliced into the body by substituteComptimeNodes; those nodes are CALLER-authored and must resolve in the caller's visibility context, not the callee's. Fix: the $-arg node is stamped with the caller's source_file at the cpn build site (lowerComptimeCall / monomorphizePackFn, stampCallerSource), and lowerExpr switches current_source_file to a node's source_file when present — so the substituted subtree resolves against the caller while the surrounding callee code keeps the defining-module pin. Regression: examples/0738-modules-comptime-arg-caller-context.sx (caller-owned helper as a comptime-only $-arg through a namespaced import; fail-before "'caller_name' is not visible" → pass-after "hello world").

Root cause: isNameVisible walked import_graph (flat AND namespaced edges) where a bare name should join only over flat_import_graph; and the pack / comptime monomorphizers lowered the metaprogram body under the wrong source context. Regressions: examples/0736-modules-namespaced-only-bare-not-visible.sx (+ 0736-…/a.sx) — face #1 pinned (exit 1 + the stderr); examples/0737-modules-insert-bare-not-visible.sx (+ 0737-…/a.sx) — a USER #insert secret() into a namespaced-only import errors (fail-before exit 0 on the attempt-2 exemption / pass-after exit 1). Face #2 restored WITHOUT an exemption: examples/0015 / 0700 / 0718 / 1030 pass again (run_examples 471 → 474, incl. the attempt-5 caller-context regression 0738). Fix in src/ir/lower.zig (monomorphizePackFn + lowerComptimeCall source-context pin; exemption removed) + src/imports.zig (stampFnBodySource) + 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 :: () -> i64 { 7 }
// main.sx
m :: #import "m.sx";
main :: () -> i32 {
    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 :: () -> i32 {
    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).