From 6f2bf842934d420e8a22049b1d44cdd6ee47dfca Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 05:17:23 +0300 Subject: [PATCH] fix(lower): #insert-expansion visibility exemption + close 0106 [stdlib B attempt-2] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ('' 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). --- ...odules-namespaced-only-bare-not-visible.sx | 12 +++++++ .../a.sx | 1 + ...ules-namespaced-only-bare-not-visible.exit | 1 + ...es-namespaced-only-bare-not-visible.stderr | 5 +++ ...es-namespaced-only-bare-not-visible.stdout | 1 + ...-import-bare-visibility-over-permissive.md | 31 +++++++++++++++++++ readme.md | 5 +++ src/ir/lower.zig | 30 ++++++++++++++++-- 8 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 examples/0736-modules-namespaced-only-bare-not-visible.sx create mode 100644 examples/0736-modules-namespaced-only-bare-not-visible/a.sx create mode 100644 examples/expected/0736-modules-namespaced-only-bare-not-visible.exit create mode 100644 examples/expected/0736-modules-namespaced-only-bare-not-visible.stderr create mode 100644 examples/expected/0736-modules-namespaced-only-bare-not-visible.stdout diff --git a/examples/0736-modules-namespaced-only-bare-not-visible.sx b/examples/0736-modules-namespaced-only-bare-not-visible.sx new file mode 100644 index 0000000..6c3a595 --- /dev/null +++ b/examples/0736-modules-namespaced-only-bare-not-visible.sx @@ -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 +} diff --git a/examples/0736-modules-namespaced-only-bare-not-visible/a.sx b/examples/0736-modules-namespaced-only-bare-not-visible/a.sx new file mode 100644 index 0000000..088e199 --- /dev/null +++ b/examples/0736-modules-namespaced-only-bare-not-visible/a.sx @@ -0,0 +1 @@ +secret :: () -> s64 { 7 } diff --git a/examples/expected/0736-modules-namespaced-only-bare-not-visible.exit b/examples/expected/0736-modules-namespaced-only-bare-not-visible.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0736-modules-namespaced-only-bare-not-visible.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0736-modules-namespaced-only-bare-not-visible.stderr b/examples/expected/0736-modules-namespaced-only-bare-not-visible.stderr new file mode 100644 index 0000000..d6eeb6b --- /dev/null +++ b/examples/expected/0736-modules-namespaced-only-bare-not-visible.stderr @@ -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(); + | ^^^^^^ diff --git a/examples/expected/0736-modules-namespaced-only-bare-not-visible.stdout b/examples/expected/0736-modules-namespaced-only-bare-not-visible.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0736-modules-namespaced-only-bare-not-visible.stdout @@ -0,0 +1 @@ + diff --git a/issues/0106-namespaced-import-bare-visibility-over-permissive.md b/issues/0106-namespaced-import-bare-visibility-over-permissive.md index 602c5df..b25e9ad 100644 --- a/issues/0106-namespaced-import-bare-visibility-over-permissive.md +++ b/issues/0106-namespaced-import-bare-visibility-over-permissive.md @@ -1,5 +1,36 @@ # 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 `'' 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 — diff --git a/readme.md b/readme.md index 7ddb706..e491899 100644 --- a/readme.md +++ b/readme.md @@ -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 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 Every program gets an implicit `context` with a default allocator: diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 28c1c6c..9e9a2cf 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -169,6 +169,17 @@ pub const Lowering = struct { /// `self.program_index.`; populated by scan/registration code. program_index: ProgramIndex, 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 // `std.sx` (and therefore declares `Context :: struct {...}`), every // 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. /// 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 { + if (self.in_insert_expansion) return true; return self.isVisible(fn_name, .c_import_bare); } - /// Non-transitive `#import` visibility check for top-level decls. - /// Byte-identical adapter over `isVisible`. + /// Non-transitive `#import` visibility check for top-level decls. Adapter + /// 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 { + if (self.in_insert_expansion) return true; 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. 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) const substituted = if (self.comptime_param_nodes) |cpn| self.substituteComptimeNodes(expr, cpn) catch expr