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

@@ -169,6 +169,17 @@ pub const Lowering = struct {
/// `self.program_index.<field>`; 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