wip(E4): partial source-pin + non-transitive flip [stdlib E4 attempt-1 WIP checkpoint]

Incomplete WIP from a worker killed at the 55-min wall (large blast radius:
core source-pin + ~8 example migrations + ~10 library module migrations).
Committed so the resumed session continues on a clean tree. May not build.
This commit is contained in:
agra
2026-06-08 11:12:08 +03:00
parent 4d539eeacd
commit 33a6f5c650
28 changed files with 202 additions and 58 deletions

View File

@@ -1,4 +1,5 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
#import "modules/math/math.sx"; #import "modules/math/math.sx";
#import "modules/compiler.sx"; #import "modules/compiler.sx";
#import "modules/test.sx"; #import "modules/test.sx";

View File

@@ -13,6 +13,9 @@
// both impl modules. // both impl modules.
#import "modules/std.sx"; #import "modules/std.sx";
// `Wrap` is declared in the shared types module; bare-import visibility is
// non-transitive, so naming it here means importing it here (not via impl-a/b).
#import "./0411-protocols-impl-duplicate-types.sx";
#import "./0411-protocols-impl-duplicate-impl-a.sx"; #import "./0411-protocols-impl-duplicate-impl-a.sx";
#import "./0411-protocols-impl-duplicate-impl-b.sx"; #import "./0411-protocols-impl-duplicate-impl-b.sx";

View File

@@ -9,6 +9,9 @@
// registration tripped: `duplicate impl 'Into' for source 's64'`. Now the flat // registration tripped: `duplicate impl 'Into' for source 's64'`. Now the flat
// decl list also dedups by node identity, so this builds and prints 7. // decl list also dedups by node identity, so this builds and prints 7.
#import "modules/std.sx"; #import "modules/std.sx";
// `Wrapped` lives in the shared `common.sx`; bare-import visibility is
// non-transitive, so naming it here means importing it here (not via mid_a/b).
#import "0709-modules-issue-0056/common.sx";
#import "0709-modules-issue-0056/mid_a.sx"; #import "0709-modules-issue-0056/mid_a.sx";
#import "0709-modules-issue-0056/mid_b.sx"; #import "0709-modules-issue-0056/mid_b.sx";

View File

@@ -18,6 +18,7 @@
// and freed in one `deinit`; the writer path allocates nothing. // and freed in one `deinit`; the writer path allocates nothing.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
#import "modules/std/json.sx"; #import "modules/std/json.sx";
#import "modules/fs.sx"; #import "modules/fs.sx";

View File

@@ -20,6 +20,7 @@
// `JsonParseError` variant on the error channel, never a bogus value. // `JsonParseError` variant on the error channel, never a bogus value.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
#import "modules/std/json.sx"; #import "modules/std/json.sx";
// Canonical document: no insignificant whitespace, escapes in the writer's // Canonical document: no insignificant whitespace, escapes in the writer's

View File

@@ -23,6 +23,7 @@
// and decoded strings go through `alloc`, and the writer allocates nothing. // and decoded strings go through `alloc`, and the writer allocates nothing.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
#import "modules/std/json.sx"; #import "modules/std/json.sx";
// The writer's EXACT output for the PART A document (insertion order, // The writer's EXACT output for the PART A document (insertion order,

View File

@@ -12,6 +12,7 @@
// independent identities. // independent identities.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
// `cli` is imported BOTH flat (so its types — `FlagSpec` / `Command` / `Diag` — // `cli` is imported BOTH flat (so its types — `FlagSpec` / `Command` / `Diag` —
// are bare-visible) AND namespaced (so the same-name `cli.parse` stays a // are bare-visible) AND namespaced (so the same-name `cli.parse` stays a
// distinct qualified identity from `json.parse`). Post-E1 a bare reference to a // distinct qualified identity from `json.parse`). Post-E1 a bare reference to a

View File

@@ -0,0 +1,20 @@
// `#import` is non-transitive for TYPES, exactly like values/functions (0706):
// when A imports B and B imports C, A must NOT see C's top-level TYPE names.
// This file imports `b.sx` (which in turn imports `c.sx`) and then references
// C's type `COnly` directly — the compiler rejects it with a
// "type ... is not visible; #import the module that declares it" diagnostic.
//
// `b.sx` ↔ `c.sx` together still compile: `b_make`'s return type `COnly`
// resolves because b.sx directly imports c.sx.
//
// Regression (Phase E4): before the bare-TYPE gate went single-hop this
// 2-flat-hop type was wrongly visible (the interim transitive closure).
#import "modules/std.sx";
#import "0763-modules-import-type-non-transitive/b.sx";
main :: () -> s32 {
x : COnly = .{ v = 5 };
print("{}\n", x.v);
0
}

View File

@@ -0,0 +1,7 @@
#import "c.sx";
// b.sx directly imports c.sx, so it CAN name `COnly` — proving the type is
// only one flat hop away here, two hops away from a file that imports b.sx.
b_make :: () -> COnly {
.{ v = 99 }
}

View File

@@ -0,0 +1 @@
COnly :: struct { v: s64 = 0; }

View File

@@ -4,6 +4,7 @@
// whether the recovery happens BEFORE or AFTER the first dispatch. // whether the recovery happens BEFORE or AFTER the first dispatch.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
main :: () -> s32 { main :: () -> s32 {
gpa := GPA.init(); gpa := GPA.init();

View File

@@ -9,6 +9,7 @@
// the operand's storage, so it never allocates and never reaches this // the operand's storage, so it never allocates and never reaches this
// path. See specs.md §3 — Protocol value ownership and lifetime. // path. See specs.md §3 — Protocol value ownership and lifetime.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it.
Tracer :: struct { Tracer :: struct {
count: s64; count: s64;

View File

@@ -1,5 +1,5 @@
error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-a.sx and /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-b.sx error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-a.sx and /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-b.sx
--> /Users/agra/projects/sx/examples/0411-protocols-impl-duplicate.sx:20:17 --> /Users/agra/projects/sx/examples/0411-protocols-impl-duplicate.sx:23:17
| |
20 | w : Wrap = xx 7; 23 | w : Wrap = xx 7;
| ^ | ^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: type 'COnly' is not visible; #import the module that declares it
--> /Users/agra/projects/sx/examples/0763-modules-import-type-non-transitive.sx:17:9
|
17 | x : COnly = .{ v = 5 };
| ^^^^^

View File

@@ -21,6 +21,7 @@
// the Metal backend takes — caller branches on OS). // the Metal backend takes — caller branches on OS).
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx";
#import "modules/compiler.sx"; #import "modules/compiler.sx";
#import "modules/opengl.sx"; #import "modules/opengl.sx";
#import "modules/gpu/types.sx"; #import "modules/gpu/types.sx";

View File

@@ -7,6 +7,7 @@
// non-iOS targets don't reach the Metal-touching code paths. // non-iOS targets don't reach the Metal-touching code paths.
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx";
#import "modules/std/objc.sx"; #import "modules/std/objc.sx";
#import "modules/compiler.sx"; #import "modules/compiler.sx";
#import "modules/gpu/types.sx"; #import "modules/gpu/types.sx";

View File

@@ -58,6 +58,9 @@
// ===================================================================== // =====================================================================
#import "modules/std.sx"; #import "modules/std.sx";
// `Array`/`Object` methods take an explicit `alloc: Allocator`; bare-import
// visibility is non-transitive, so the module that names the type imports it.
#import "modules/allocators.sx";
#import "modules/fs.sx"; #import "modules/fs.sx";
// The writer's failure contract: a too-small caller buffer (Overflow) or // The writer's failure contract: a too-small caller buffer (Overflow) or

View File

@@ -22,6 +22,12 @@
// caller returns. If you need that, ship a `Block_copy`-backed sibling // caller returns. If you need that, ship a `Block_copy`-backed sibling
// API and use it instead. // API and use it instead.
// `build_block_convert` (below) is a comptime metaprogram that emits sx source
// with `concat` / `int_to_string`; those live in std.sx. A metaprogram body
// resolves its bare names in its OWN module (issue 0106), so this module must
// import std.sx itself rather than relying on the call site's visibility.
#import "modules/std.sx";
// Standard 32-byte block header plus two trailing slots for the sx closure // Standard 32-byte block header plus two trailing slots for the sx closure
// it wraps. Total = 48 bytes. // it wraps. Total = 48 bytes.
Block :: struct { Block :: struct {

View File

@@ -1,4 +1,5 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx";
#import "modules/math"; #import "modules/math";
#import "modules/ui/types.sx"; #import "modules/ui/types.sx";
#import "modules/ui/render.sx"; #import "modules/ui/render.sx";

View File

@@ -1,4 +1,5 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx";
#import "modules/opengl.sx"; #import "modules/opengl.sx";
#import "modules/gpu/types.sx"; #import "modules/gpu/types.sx";
#import "modules/gpu/api.sx"; #import "modules/gpu/api.sx";

View File

@@ -1,5 +1,6 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx"; #import "modules/allocators.sx";
#import "modules/ui/glyph_cache.sx"; // `font: GlyphCache` — name it, import it (non-transitive).
#import "modules/opengl.sx"; #import "modules/opengl.sx";
#import "modules/gpu/api.sx"; #import "modules/gpu/api.sx";
#import "modules/ui/types.sx"; #import "modules/ui/types.sx";

View File

@@ -1,4 +1,5 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "modules/allocators.sx";
// --- State(T) — a handle to persistent storage --- // --- State(T) — a handle to persistent storage ---

View File

@@ -401,12 +401,17 @@ is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`).
A **namespaced** import only binds its alias: reach the module's members as A **namespaced** import only binds its alias: reach the module's members as
`m.name`. Bare-name visibility joins over flat (`#import "…"`) imports, never over `m.name`. Bare-name visibility joins over flat (`#import "…"`) imports, never over
a namespaced alias. For **functions and constants** that join is non-transitive: a a namespaced alias. That join is **non-transitive for every bare member kind —
flat import of a flat import is NOT bare-visible (when `A` imports `B` and `B` functions, constants, AND types alike**: a flat import of a flat import is NOT
imports `C`, `A` does not see `C`'s top-level names — qualify them). A bare bare-visible (when `A` imports `B` and `B` imports `C`, `A` does not see `C`'s
reference to a namespaced-only import's member — function, module constant, or top-level names — including its types — so qualify them, or `#import "C"` directly
**type** — is not visible and is rejected (`type 'X' is not visible; #import the if you reference them). A bare reference to a namespaced-only import's member —
module that declares it`); qualify it as `m.name`. function, module constant, or **type** — is likewise not visible and is rejected
(`type 'X' is not visible; #import the module that declares it`); qualify it as
`m.name`. (A library's own *internal* type references still resolve: a generic
struct / pack fn / protocol body is instantiated in the module that defines it, so
e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call
site.)
### Implicit Context ### Implicit Context

View File

@@ -783,6 +783,11 @@ pub const ProtocolDecl = struct {
/// True when the declared NAME was a backtick raw identifier — exempt from /// True when the declared NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089). /// the reserved-type-name decl check (issue 0089).
is_raw: bool = false, is_raw: bool = false,
/// Defining module path (stamped by `resolveImports`), so a parameterized
/// protocol instantiated cross-module resolves its method signature types in
/// the module that declares it (E4 — the protocol analog of
/// `StructTemplate.source_file`). Null for a synthesized/sourceless decl.
source_file: ?[]const u8 = null,
}; };
pub const ForeignRuntime = enum { pub const ForeignRuntime = enum {

View File

@@ -661,14 +661,39 @@ fn reportDuplicateName(diagnostics: ?*errors.DiagnosticList, added: bool, name:
fn stampFnBodySource(decl: *Node, file_path: []const u8) void { fn stampFnBodySource(decl: *Node, file_path: []const u8) void {
switch (decl.data) { switch (decl.data) {
.fn_decl => |fd| fd.body.source_file = file_path, .fn_decl => |fd| fd.body.source_file = file_path,
.struct_decl => |sd| stampStructMethodSources(sd, file_path),
// A parameterized protocol is instantiated cross-module; record its
// defining path so the instantiation resolves method-signature types in
// this module (E4).
.protocol_decl => decl.data.protocol_decl.source_file = file_path,
.const_decl => |cd| switch (cd.value.data) { .const_decl => |cd| switch (cd.value.data) {
.fn_decl => |fd| fd.body.source_file = file_path, .fn_decl => |fd| fd.body.source_file = file_path,
// `List :: struct { … append :: (…) { … } }` — the methods of a
// (possibly generic) struct are monomorphized in their template's
// OWN module (issue 0106 + the E4 instantiation source-pin), so their
// bodies need the defining path stamped just like a top-level fn.
.struct_decl => |sd| stampStructMethodSources(sd, file_path),
.protocol_decl => cd.value.data.protocol_decl.source_file = file_path,
else => {}, else => {},
}, },
else => {}, else => {},
} }
} }
/// Stamp the defining module path onto every method (and struct-level fn
/// constant) body of a struct decl, so a generic-struct method monomorphized at
/// a cross-module call site still pins to the module that declares it.
fn stampStructMethodSources(sd: ast.StructDecl, file_path: []const u8) void {
for (sd.methods) |m| {
if (m.data == .fn_decl) m.data.fn_decl.body.source_file = file_path;
}
for (sd.constants) |c| {
if (c.data == .const_decl and c.data.const_decl.value.data == .fn_decl) {
c.data.const_decl.value.data.fn_decl.body.source_file = file_path;
}
}
}
/// `reportDuplicateName` keyed off a node whose `declName()` carries the name /// `reportDuplicateName` keyed off a node whose `declName()` carries the name
/// (the regular authored-decl sites; an `import_decl` has no `declName`, so a /// (the regular authored-decl sites; an `import_decl` has no `declName`, so a
/// namespace alias must use `reportDuplicateName` with the alias directly). /// namespace alias must use `reportDuplicateName` with the alias directly).

View File

@@ -1778,9 +1778,10 @@ pub const Lowering = struct {
/// the legacy stub there and defers the diagnostic to the checker. /// the legacy stub there and defers the diagnostic to the checker.
undeclared, undeclared,
/// `name` IS a registered named type, but it is reachable from the /// `name` IS a registered named type, but it is reachable from the
/// querying module ONLY through a namespaced import — not bare-visible /// querying module ONLY through a namespaced import (or over more than one
/// over the transitive flat-import closure (the type analog of Phase B's /// flat hop) — not bare-visible over the single-hop direct flat-import set
/// bare-call tightening, F1). The user must qualify it (`ns.Type`). /// (the type analog of Phase B's bare-call tightening, F1). The user must
/// qualify it (`ns.Type`) or `#import` the declaring module directly.
/// `resolveNominalLeaf` surfaces the "not visible" diagnostic and returns /// `resolveNominalLeaf` surfaces the "not visible" diagnostic and returns
/// the `.unresolved` poison sentinel — NEVER the global `findByName` match /// the `.unresolved` poison sentinel — NEVER the global `findByName` match
/// (which would leak the type) and NEVER a silent empty-struct stub (which /// (which would leak the type) and NEVER a silent empty-struct stub (which
@@ -1915,17 +1916,15 @@ pub const Lowering = struct {
// `module_consts_by_source`, never in `type_aliases_by_source`, so it is // `module_consts_by_source`, never in `type_aliases_by_source`, so it is
// correctly excluded too. // correctly excluded too.
// //
// The TYPE reachability here is the TRANSITIVE flat-import closure, NOT the // The TYPE reachability here is SINGLE-HOP — `from`'s own author plus its
// single-hop `collectVisibleAuthors`/`isNameVisible` set the bare VALUE / // DIRECT flat-import edges (`flatTypeAuthorCount`), the same non-transitive
// FUNCTION / CONST leaves use. That asymmetry (types transitive, values // set the bare VALUE / FUNCTION / CONST leaves use (E4, consistent with
// non-transitive — 0706) is the open model-consistency question (R3, // 0706). A library template's INTERNAL type refs (`List.append`'s
// sequenced as E4 per Agra): the value/function model needs the source pin // `alloc: Allocator`) still resolve because every instantiation kind
// for a library template's INTERNAL type refs (`List.append`'s // (generic struct / fn / pack fn / param protocol / type fn) is
// `alloc: Allocator`, instantiated in the caller's source context) before // source-pinned to the template's defining module, so the query
// the type gate can go single-hop too. Until that lands, the transitive // originates THERE — where the type is a direct flat import — not at the
// type closure is the only byte-identical option; the gate stays // cross-module call site.
// type-author-aware and local-safe regardless of which reachability E4
// settles on.
const name_id = table.internString(name); const name_id = table.internString(name);
const registered = table.findByName(name_id); const registered = table.findByName(name_id);
@@ -1956,10 +1955,10 @@ pub const Lowering = struct {
// `internNamedTypeDecl` adopting that stub when the type registers. // `internNamedTypeDecl` adopting that stub when the type registers.
// //
// The querying source's OWN author wins outright (own-wins, 0105 case // The querying source's OWN author wins outright (own-wins, 0105 case
// 3); otherwise the transitive flat-import closure is searched, and ≥2 // 3); otherwise the single-hop direct flat-import set is searched, and
// DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single- // ≥2 DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single-
// author (E1) keeps ≤1 author across the closure, so this stays byte- // author keeps ≤1 author across that set, so this stays byte-identical
// identical to the legacy leaf. // to the legacy leaf.
if (self.moduleTypeAuthor(from, name)) |author| switch (author) { if (self.moduleTypeAuthor(from, name)) |author| switch (author) {
.alias => |tid| return .{ .resolved = tid }, .alias => |tid| return .{ .resolved = tid },
.named => |ref| { .named => |ref| {
@@ -2122,10 +2121,12 @@ pub const Lowering = struct {
}; };
} }
/// What bare `name`'s type authors look like across the TRANSITIVE /// What bare `name`'s type authors look like across the SINGLE-HOP flat-import
/// flat-import closure of `from` (the querying source's OWN author is consulted /// set of `from` — its DIRECT bare `#import` edges only, NOT the transitive
/// by `selectNominalLeaf` first — own-wins — so this surveys only the /// closure (E4: consistent with the bare VALUE/FUNCTION/CONST leaves and
/// cross-module flat authors): /// example 0706; the interim transitive closure E1 shipped is gone). The
/// querying source's OWN author is consulted by `selectNominalLeaf` first
/// (own-wins), so this surveys only the cross-module direct-flat authors:
/// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4); /// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4);
/// - `.one` — exactly one distinct resolved TypeId; /// - `.one` — exactly one distinct resolved TypeId;
/// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId /// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId
@@ -2135,37 +2136,29 @@ pub const Lowering = struct {
/// local / leak / forward-alias arms. /// local / leak / forward-alias arms.
/// Distinctness is BY TypeId: each distinct author holds a distinct /// Distinctness is BY TypeId: each distinct author holds a distinct
/// `nominal_id` TypeId, while a diamond import of the SAME module yields the /// `nominal_id` TypeId, while a diamond import of the SAME module yields the
/// same TypeId, so byte-identical de-dup falls out. The closure walk lives in /// same TypeId, so byte-identical de-dup falls out. A library template's
/// `lower.zig`, NOT `resolver.zig` — the single-graph-walk invariant (one /// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s
/// `flat_import_graph` iterator in `resolver.zig`) is untouched. /// `alloc: Allocator`) stay resolvable because instantiation is source-pinned
/// to the template's defining module (E4 #1), so the query originates THERE —
/// where the type is a direct flat import — not at the cross-module call site.
/// The walk lives in `lower.zig`, NOT `resolver.zig` — the single-graph-walk
/// invariant (one `flat_import_graph` iterator in `resolver.zig`) is untouched.
const FlatTypeAuthorCount = union(enum) { none, one: TypeId, ambiguous, unregistered }; const FlatTypeAuthorCount = union(enum) { none, one: TypeId, ambiguous, unregistered };
fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount { fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount {
const graph = self.program_index.flat_import_graph orelse return .none; const graph = self.program_index.flat_import_graph orelse return .none;
const direct = graph.get(from) orelse return .none;
var found: ?TypeId = null; var found: ?TypeId = null;
var saw_author = false; var saw_author = false;
var visited = std.StringHashMap(void).init(self.alloc); var it = direct.iterator();
defer visited.deinit(); while (it.next()) |kv| {
var queue = std.ArrayList([]const u8).empty; const dep = kv.key_ptr.*;
defer queue.deinit(self.alloc); if (self.moduleTypeAuthor(dep, name) != null) {
visited.put(from, {}) catch return .none; saw_author = true;
queue.append(self.alloc, from) catch return .none; if (self.moduleTypeAuthorTid(dep, name)) |tid| {
var i: usize = 0; if (found) |f| {
while (i < queue.items.len) : (i += 1) { if (tid != f) return .ambiguous;
const deps = graph.get(queue.items[i]) orelse continue; } else found = tid;
var it = deps.iterator();
while (it.next()) |kv| {
const dep = kv.key_ptr.*;
if (visited.contains(dep)) continue;
visited.put(dep, {}) catch continue;
if (self.moduleTypeAuthor(dep, name) != null) {
saw_author = true;
if (self.moduleTypeAuthorTid(dep, name)) |tid| {
if (found) |f| {
if (tid != f) return .ambiguous;
} else found = tid;
}
} }
queue.append(self.alloc, dep) catch continue;
} }
} }
if (found) |t| return .{ .one = t }; if (found) |t| return .{ .one = t };
@@ -2238,6 +2231,9 @@ pub const Lowering = struct {
fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId { fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId {
const from = self.current_source_file orelse const from = self.current_source_file orelse
return self.typeResolver().resolveName(name, raw); return self.typeResolver().resolveName(name, raw);
if (std.mem.eql(u8, name, "BOOL")) {
std.debug.print("[BOOLLEAF] from={s} -> {s}\n", .{ from, @tagName(self.selectNominalLeaf(name, from, raw)) });
}
return switch (self.selectNominalLeaf(name, from, raw)) { return switch (self.selectNominalLeaf(name, from, raw)) {
.resolved => |t| t, .resolved => |t| t,
// A forward alias (`.pending`) or a forward / not-yet-interned named // A forward alias (`.pending`) or a forward / not-yet-interned named
@@ -11551,6 +11547,14 @@ pub const Lowering = struct {
self.pack_arg_types = pre_pat; self.pack_arg_types = pre_pat;
self.pack_constraint = if (pack_proto != null) pre_pcon else null; self.pack_constraint = if (pack_proto != null) pre_pcon else null;
// Resolve the declared return + fixed-prefix param types in the pack fn's
// OWN module (E4), so a 2-flat-hop library type named in the signature is
// bare-visible — mirrors the body pin further down and the
// `monomorphizeFunction` pin. The comptime call-site args below are
// lowered AFTER this restore, in the caller's context (issue 0106).
const saved_sig_src = self.current_source_file;
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
const declared_is_generic_ret = blk: { const declared_is_generic_ret = blk: {
const rt = fd.return_type orelse break :blk false; const rt = fd.return_type orelse break :blk false;
if (rt.data != .type_expr) break :blk false; if (rt.data != .type_expr) break :blk false;
@@ -11590,6 +11594,7 @@ pub const Lowering = struct {
.ty = ty, .ty = ty,
}) catch return; }) catch return;
} }
self.setCurrentSourceFile(saved_sig_src);
const name_id = self.module.types.internString(owned_name); const name_id = self.module.types.internString(owned_name);
_ = self.builder.beginFunction(name_id, params.items, ret_ty); _ = self.builder.beginFunction(name_id, params.items, ret_ty);
@@ -11740,6 +11745,19 @@ pub const Lowering = struct {
// Install type bindings // Install type bindings
self.type_bindings = bindings.*; self.type_bindings = bindings.*;
// Pin to the template's defining module for the whole monomorphization
// (return type, param types, body), so a library-internal bare TYPE ref
// — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a
// body reference to a type visible only in the template's module —
// resolves where it is visible, not at the (possibly cross-module) call
// site. This is the issue-0100-F1 plain-fn pin extended to generic
// instantiation; without it the non-transitive bare-TYPE gate (E4) would
// reject a 2-flat-hop library type the call site cannot see directly.
// A synthesized / sourceless body keeps the caller's context.
const saved_source_mono = self.current_source_file;
defer self.setCurrentSourceFile(saved_source_mono);
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
// Resolve return type with type bindings active. The body's tail // Resolve return type with type bindings active. The body's tail
// expression inherits this as its target_type so bare `.{...}` // expression inherits this as its target_type so bare `.{...}`
// literals resolve to the monomorphised return type instead of // literals resolve to the monomorphised return type instead of
@@ -12969,7 +12987,7 @@ pub const Lowering = struct {
if (fd.params.len > 0) { if (fd.params.len > 0) {
var types_list = std.ArrayList(TypeId).empty; var types_list = std.ArrayList(TypeId).empty;
for (fd.params[1..]) |p| { for (fd.params[1..]) |p| {
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable;
} }
return types_list.items; return types_list.items;
} }
@@ -12987,7 +13005,7 @@ pub const Lowering = struct {
} }
var types_list = std.ArrayList(TypeId).empty; var types_list = std.ArrayList(TypeId).empty;
for (fd.params[1..]) |p| { for (fd.params[1..]) |p| {
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable;
} }
self.type_bindings = saved_bindings; self.type_bindings = saved_bindings;
return types_list.items; return types_list.items;
@@ -13376,6 +13394,20 @@ pub const Lowering = struct {
return self.resolveType(type_ann); return self.resolveType(type_ann);
} }
/// `resolveParamType` with the visibility context pinned to `src`, the
/// DEFINING module of the param's function. An imported method's
/// default-param type (`alloc: Allocator`) is bare-visible only inside its
/// own module, so typing a cross-module call's args against it must resolve
/// in that module's context, not the call site's (E4 — the param analog of
/// `resolveTypeInSource`). `src == null` falls back unchanged.
fn resolveParamTypeInSource(self: *Lowering, src: ?[]const u8, p: *const ast.Param) TypeId {
const pinned = src orelse return self.resolveParamType(p);
const saved = self.current_source_file;
defer self.setCurrentSourceFile(saved);
self.setCurrentSourceFile(pinned);
return self.resolveParamType(p);
}
/// Construct a `TypeResolver` view over the current lowering state (borrows /// Construct a `TypeResolver` view over the current lowering state (borrows
/// only; cheap by-value, reflects current `diagnostics` / `program_index`). /// only; cheap by-value, reflects current `diagnostics` / `program_index`).
fn typeResolver(self: *Lowering) TypeResolver { fn typeResolver(self: *Lowering) TypeResolver {
@@ -14074,6 +14106,14 @@ pub const Lowering = struct {
self.comptime_value_bindings = saved_value_bindings; self.comptime_value_bindings = saved_value_bindings;
} }
// Resolve the type fn's body (inline struct/union fields, or the returned
// type expression) in its OWN module (E4), so a 2-flat-hop library type
// named there is bare-visible — not the cross-module call site. The arg
// exprs above were already resolved in the caller's context.
const saved_tf_src = self.current_source_file;
defer self.setCurrentSourceFile(saved_tf_src);
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
// Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)")
// or just the template name itself (inline use like "Sx(f32)") // or just the template name itself (inline use like "Sx(f32)")
const has_alias = !std.mem.eql(u8, alias_name, template_name); const has_alias = !std.mem.eql(u8, alias_name, template_name);
@@ -14689,9 +14729,16 @@ pub const Lowering = struct {
const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info);
table.updatePreservingKey(id, struct_info); table.updatePreservingKey(id, struct_info);
// Method infos resolved with the type-arg binding (T → s64). // Method infos resolved with the type-arg binding (T → s64), pinned to
// the protocol's OWN module (E4) so a method-signature type visible only
// there resolves correctly when instantiated cross-module. `Self` and the
// bound type-args short-circuit before the leaf; a concrete library type
// in a signature is the case this pin protects.
const saved_tb = self.type_bindings; const saved_tb = self.type_bindings;
self.type_bindings = tb; self.type_bindings = tb;
const saved_pp_src = self.current_source_file;
defer self.setCurrentSourceFile(saved_pp_src);
if (pd.source_file) |src| self.setCurrentSourceFile(src);
var method_infos = std.ArrayList(ProtocolMethodInfo).empty; var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
for (pd.methods) |method| { for (pd.methods) |method| {
var ptypes = std.ArrayList(TypeId).empty; var ptypes = std.ArrayList(TypeId).empty;