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:
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
147
src/ir/lower.zig
147
src/ir/lower.zig
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user