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

@@ -783,6 +783,11 @@ pub const ProtocolDecl = struct {
/// True when the declared NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
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 {

View File

@@ -661,14 +661,39 @@ fn reportDuplicateName(diagnostics: ?*errors.DiagnosticList, added: bool, name:
fn stampFnBodySource(decl: *Node, file_path: []const u8) void {
switch (decl.data) {
.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) {
.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 => {},
}
}
/// 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
/// (the regular authored-decl sites; an `import_decl` has no `declName`, so a
/// 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.
undeclared,
/// `name` IS a registered named type, but it is reachable from the
/// querying module ONLY through a namespaced import — not bare-visible
/// over the transitive flat-import closure (the type analog of Phase B's
/// bare-call tightening, F1). The user must qualify it (`ns.Type`).
/// querying module ONLY through a namespaced import (or over more than one
/// flat hop) — not bare-visible over the single-hop direct flat-import set
/// (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
/// the `.unresolved` poison sentinel — NEVER the global `findByName` match
/// (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
// correctly excluded too.
//
// The TYPE reachability here is the TRANSITIVE flat-import closure, NOT the
// single-hop `collectVisibleAuthors`/`isNameVisible` set the bare VALUE /
// FUNCTION / CONST leaves use. That asymmetry (types transitive, values
// non-transitive — 0706) is the open model-consistency question (R3,
// sequenced as E4 per Agra): the value/function model needs the source pin
// for a library template's INTERNAL type refs (`List.append`'s
// `alloc: Allocator`, instantiated in the caller's source context) before
// the type gate can go single-hop too. Until that lands, the transitive
// type closure is the only byte-identical option; the gate stays
// type-author-aware and local-safe regardless of which reachability E4
// settles on.
// The TYPE reachability here is SINGLE-HOP — `from`'s own author plus its
// DIRECT flat-import edges (`flatTypeAuthorCount`), the same non-transitive
// set the bare VALUE / FUNCTION / CONST leaves use (E4, consistent with
// 0706). A library template's INTERNAL type refs (`List.append`'s
// `alloc: Allocator`) still resolve because every instantiation kind
// (generic struct / fn / pack fn / param protocol / type fn) is
// source-pinned to the template's defining module, so the query
// originates THERE — where the type is a direct flat import — not at the
// cross-module call site.
const name_id = table.internString(name);
const registered = table.findByName(name_id);
@@ -1956,10 +1955,10 @@ pub const Lowering = struct {
// `internNamedTypeDecl` adopting that stub when the type registers.
//
// The querying source's OWN author wins outright (own-wins, 0105 case
// 3); otherwise the transitive flat-import closure is searched, and ≥2
// DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single-
// author (E1) keeps ≤1 author across the closure, so this stays byte-
// identical to the legacy leaf.
// 3); otherwise the single-hop direct flat-import set is searched, and
// ≥2 DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single-
// author keeps ≤1 author across that set, so this stays byte-identical
// to the legacy leaf.
if (self.moduleTypeAuthor(from, name)) |author| switch (author) {
.alias => |tid| return .{ .resolved = tid },
.named => |ref| {
@@ -2122,10 +2121,12 @@ pub const Lowering = struct {
};
}
/// What bare `name`'s type authors look like across the TRANSITIVE
/// flat-import closure of `from` (the querying source's OWN author is consulted
/// by `selectNominalLeaf` first — own-wins — so this surveys only the
/// cross-module flat authors):
/// What bare `name`'s type authors look like across the SINGLE-HOP flat-import
/// set of `from` — its DIRECT bare `#import` edges only, NOT the transitive
/// closure (E4: consistent with the bare VALUE/FUNCTION/CONST leaves and
/// 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);
/// - `.one` — exactly one distinct resolved 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.
/// Distinctness is BY TypeId: each distinct author holds a distinct
/// `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
/// `lower.zig`, NOT `resolver.zig` — the single-graph-walk invariant (one
/// `flat_import_graph` iterator in `resolver.zig`) is untouched.
/// same TypeId, so byte-identical de-dup falls out. A library template's
/// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s
/// `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 };
fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount {
const graph = self.program_index.flat_import_graph orelse return .none;
const direct = graph.get(from) orelse return .none;
var found: ?TypeId = null;
var saw_author = false;
var visited = std.StringHashMap(void).init(self.alloc);
defer visited.deinit();
var queue = std.ArrayList([]const u8).empty;
defer queue.deinit(self.alloc);
visited.put(from, {}) catch return .none;
queue.append(self.alloc, from) catch return .none;
var i: usize = 0;
while (i < queue.items.len) : (i += 1) {
const deps = graph.get(queue.items[i]) orelse continue;
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;
}
var it = direct.iterator();
while (it.next()) |kv| {
const dep = kv.key_ptr.*;
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 };
@@ -2238,6 +2231,9 @@ pub const Lowering = struct {
fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId {
const from = self.current_source_file orelse
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)) {
.resolved => |t| t,
// 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_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 rt = fd.return_type orelse break :blk false;
if (rt.data != .type_expr) break :blk false;
@@ -11590,6 +11594,7 @@ pub const Lowering = struct {
.ty = ty,
}) catch return;
}
self.setCurrentSourceFile(saved_sig_src);
const name_id = self.module.types.internString(owned_name);
_ = self.builder.beginFunction(name_id, params.items, ret_ty);
@@ -11740,6 +11745,19 @@ pub const Lowering = struct {
// Install type 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
// expression inherits this as its target_type so bare `.{...}`
// literals resolve to the monomorphised return type instead of
@@ -12969,7 +12987,7 @@ pub const Lowering = struct {
if (fd.params.len > 0) {
var types_list = std.ArrayList(TypeId).empty;
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;
}
@@ -12987,7 +13005,7 @@ pub const Lowering = struct {
}
var types_list = std.ArrayList(TypeId).empty;
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;
return types_list.items;
@@ -13376,6 +13394,20 @@ pub const Lowering = struct {
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
/// only; cheap by-value, reflects current `diagnostics` / `program_index`).
fn typeResolver(self: *Lowering) TypeResolver {
@@ -14074,6 +14106,14 @@ pub const Lowering = struct {
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)")
// or just the template name itself (inline use like "Sx(f32)")
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);
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;
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;
for (pd.methods) |method| {
var ptypes = std.ArrayList(TypeId).empty;