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:
12
examples/0736-modules-namespaced-only-bare-not-visible.sx
Normal file
12
examples/0736-modules-namespaced-only-bare-not-visible.sx
Normal 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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
secret :: () -> s64 { 7 }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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();
|
||||||
|
| ^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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 —
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user