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).
This commit is contained in:
agra
2026-06-07 05:17:23 +03:00
parent 7158337c73
commit 6f2bf84293
8 changed files with 83 additions and 3 deletions

View File

@@ -0,0 +1,12 @@
// Bare-name visibility under a NAMESPACED-only import (regression, issue 0106).
// `a.sx` is imported only as `m :: #import` — its top-level `secret` is reachable
// ONLY as `m.secret`. A BARE `secret()` must error: bare-name visibility joins
// over the FLAT import edges (`flat_import_graph`), and a namespaced alias is not
// a flat edge. (Before the fix, `isNameVisible` walked `import_graph`, which also
// records namespaced edges, so the bare call silently resolved.)
m :: #import "0736-modules-namespaced-only-bare-not-visible/a.sx";
main :: () -> s32 {
x := secret();
0
}

View File

@@ -0,0 +1 @@
secret :: () -> s64 { 7 }

View File

@@ -0,0 +1,5 @@
error: 'secret' is not visible; #import the module that declares it
--> examples/0736-modules-namespaced-only-bare-not-visible.sx:10:10
|
10 | x := secret();
| ^^^^^^

View File

@@ -1,5 +1,36 @@
# 0106 — namespaced-import internal names are silently bare-visible (over-permissive `isNameVisible`) # 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 **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 consumer imports **only namespaced** (`ns :: #import "m.sx"`) is silently
visible from the consumer. Observed: it compiles + runs. Expected: an error — visible from the consumer. Observed: it compiles + runs. Expected: an error —

View File

@@ -399,6 +399,11 @@ function in the caller's module (or in its single flat import that provides it).
A bare call to a name that two or more flat imports both provide is ambiguous and A bare call to a name that two or more flat imports both provide is ambiguous and
is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`). is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`).
A **namespaced** import only binds its alias: the module's names are reachable
solely as `m.name`. A bare reference to one of those names errors with `'name'
is not visible; #import the module that declares it` — bare-name visibility joins
over flat (`#import "…"`) imports only, never over a namespaced alias.
### Implicit Context ### Implicit Context
Every program gets an implicit `context` with a default allocator: Every program gets an implicit `context` with a default allocator:

View File

@@ -169,6 +169,17 @@ pub const Lowering = struct {
/// `self.program_index.<field>`; populated by scan/registration code. /// `self.program_index.<field>`; populated by scan/registration code.
program_index: ProgramIndex, program_index: ProgramIndex,
current_source_file: ?[]const u8 = null, // source file of function currently being lowered current_source_file: ?[]const u8 = null, // source file of function currently being lowered
/// True while lowering the product of a `#insert` expansion — the comptime
/// call whose string drives the insert, plus the statements parsed back from
/// that string. Bare names in this synthesized code are authored by the
/// library metaprogram (e.g. `out`/`emit`/`build_format` inside `std.print`),
/// not user-typed at the call site, so the bare-name `#import` visibility
/// adapters (`isNameVisible` / `isCImportVisible`) exempt them — the same
/// "compiler indirection" exemption already given to UFCS-alias rewrites and
/// mangled local names (issue 0106). It is NOT a blanket skip: it scopes the
/// exemption to `#insert`-expanded code only; ordinary bare references still
/// get policed. Saved/restored to nest correctly.
in_insert_expansion: bool = false,
// Implicit Context parameter machinery. When the program imports // Implicit Context parameter machinery. When the program imports
// `std.sx` (and therefore declares `Context :: struct {...}`), every // `std.sx` (and therefore declares `Context :: struct {...}`), every
// default-conv sx function gains a synthetic `__sx_ctx: *void` param // default-conv sx function gains a synthetic `__sx_ctx: *void` param
@@ -1846,14 +1857,19 @@ pub const Lowering = struct {
/// Check if a C-imported function is visible from the current source file. /// Check if a C-imported function is visible from the current source file.
/// Returns true for non-C functions (always visible) or if no scoping info /// Returns true for non-C functions (always visible) or if no scoping info
/// available. Byte-identical adapter over `isVisible`. /// available. Adapter over `isVisible(.c_import_bare)`, plus the
/// `#insert`-expansion exemption (issue 0106).
fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool { fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool {
if (self.in_insert_expansion) return true;
return self.isVisible(fn_name, .c_import_bare); return self.isVisible(fn_name, .c_import_bare);
} }
/// Non-transitive `#import` visibility check for top-level decls. /// Non-transitive `#import` visibility check for top-level decls. Adapter
/// Byte-identical adapter over `isVisible`. /// over `isVisible(.user_bare_flat)`, plus the `#insert`-expansion exemption:
/// names emitted by a library metaprogram's insert are compiler
/// indirections, not user-typed call-site references (issue 0106).
fn isNameVisible(self: *Lowering, name: []const u8) bool { fn isNameVisible(self: *Lowering, name: []const u8) bool {
if (self.in_insert_expansion) return true;
return self.isVisible(name, .user_bare_flat); return self.isVisible(name, .user_bare_flat);
} }
@@ -9480,6 +9496,14 @@ pub const Lowering = struct {
/// Like lowerInsertExpr but returns the value of the last parsed expression. /// Like lowerInsertExpr but returns the value of the last parsed expression.
fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref {
// The comptime call that produces the insert string and the statements
// parsed back from it are library-metaprogram code, not user-typed bare
// names at this call site — exempt them from the `#import` visibility
// adapters (issue 0106). Saved/restored so nested `#insert`s compose.
const saved_insert = self.in_insert_expansion;
self.in_insert_expansion = true;
defer self.in_insert_expansion = saved_insert;
// Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal) // Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal)
const substituted = if (self.comptime_param_nodes) |cpn| const substituted = if (self.comptime_param_nodes) |cpn|
self.substituteComptimeNodes(expr, cpn) catch expr self.substituteComptimeNodes(expr, cpn) catch expr