lang: generic struct head aliases bind the template (fix 0120) — alias-follow from each author's source in head selection; loud unknown-type on the .call type tail

BoxAlias :: Box; / Box :: r.Box; now resolve instantiation, methods,
annotations, and chains through the aliased template, and re-export one
flat-import level as ordinary own decls (the facade shape the std.sx
restructure needs). selectGenericStructHead consults aliasedStructTemplate
(nominal.zig) before the global template map — own-wins/single-flat alias
author, each hop pinned to the alias author's source, ns.X RHS through
namespaceAliasVerdictFrom, depth-capped. resolveTypeCallWithBindings'
silent .unresolved tail (panicked in LLVM emission) now diagnoses
"unknown type". Also aligns the stale pre-existing calls.test.zig UFCS
plan test with the opt-in model (a47ea14). Regression: examples/0211
(+rich/+facade). Gates: zig build test 426/426, suite 587/587.
This commit is contained in:
agra
2026-06-11 18:09:01 +03:00
parent 51194a26d8
commit f2db8ecc53
13 changed files with 382 additions and 5 deletions

View File

@@ -429,14 +429,17 @@ test "plan: free-function UFCS prepends receiver, distinct from namespace_fn" {
var l = Lowering.init(&module);
const cr = CallResolver{ .l = &l };
// struct Counter, and a FREE function `bump :: (c: Counter) -> s32` — NOT
// registered as `Counter.bump`, so it can only be reached via UFCS.
// struct Counter, and a FREE ufcs function `bump :: ufcs (c: Counter) ->
// s32` — NOT registered as `Counter.bump`, so it can only be reached via
// UFCS. Dot-dispatch is OPT-IN: the fn carries `is_ufcs` and is
// registered in `fn_ast_map`, where the plan's opt-in gate reads it.
const counter = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Counter"), .fields = &.{} } });
const c_param = ast.Param{ .name = "c", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeExpr(alloc, "Counter") };
const params = [_]ast.Param{c_param};
const ret_stmt = mk(alloc, .{ .return_stmt = .{ .value = intLit(alloc, 7) } });
const body = mk(alloc, .{ .block = .{ .stmts = &[_]*Node{ret_stmt} } });
const fd = ast.FnDecl{ .name = "bump", .params = &params, .return_type = typeExpr(alloc, "s32"), .body = body };
const fd = ast.FnDecl{ .name = "bump", .params = &params, .return_type = typeExpr(alloc, "s32"), .body = body, .is_ufcs = true };
l.program_index.fn_ast_map.put("bump", &fd) catch unreachable;
l.lowerFunction(&fd, "bump", false);
const fid = l.resolveFuncByName("bump").?;
module.functions.items[@intFromEnum(fid)].has_implicit_ctx = true;

View File

@@ -1186,8 +1186,16 @@ pub const Lowering = struct {
/// edges of flat edges do not chain). Two distinct carried targets for
/// the same alias are ambiguous.
pub fn namespaceAliasVerdict(self: *Lowering, alias: []const u8) AliasVerdict {
const edges = self.program_index.namespace_edges orelse return .none;
const from = self.current_source_file orelse return .none;
return self.namespaceAliasVerdictFrom(alias, from);
}
/// `namespaceAliasVerdict` with an explicit querying source — for callers
/// resolving an alias on behalf of ANOTHER module (e.g. following a const
/// alias decl whose RHS is `ns.X`: `ns` binds in the alias author's file,
/// not the use site's).
pub fn namespaceAliasVerdictFrom(self: *Lowering, alias: []const u8, from: []const u8) AliasVerdict {
const edges = self.program_index.namespace_edges orelse return .none;
if (edges.getPtr(from)) |own| {
if (own.get(alias)) |t| return .{ .target = t };
}
@@ -1679,6 +1687,7 @@ pub const Lowering = struct {
pub const rawNamedTypePtr = lower_nominal.rawNamedTypePtr;
pub const buildGenericStructTemplate = lower_nominal.buildGenericStructTemplate;
pub const qualifiedStructTemplate = lower_nominal.qualifiedStructTemplate;
pub const aliasedStructTemplate = lower_nominal.aliasedStructTemplate;
pub const qualifiedMemberMissing = lower_nominal.qualifiedMemberMissing;
pub const bareVisibleStructDecl = lower_nominal.bareVisibleStructDecl;
pub const bareVisibleStructTemplate = lower_nominal.bareVisibleStructTemplate;

View File

@@ -974,6 +974,17 @@ pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]cons
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* };
return .not_generic;
}
// Const-alias head (`BoxAlias :: Box;` / `Box :: r.Box;`, issue 0120):
// follow the alias decl hop-by-hop to its authoring template, each hop
// resolved from that alias author's own source. Checked BEFORE the map:
// the alias may share its name with a same-name template that is NOT
// visible from here (a facade's `Box :: r.Box;` re-export of rich's
// `Box`), and the map branch would poison on that invisible author.
// Only fires when the single visible author (own-wins / single-flat)
// IS an alias-shaped const decl, so real template heads are untouched.
if (self.current_source_file) |from| {
if (self.aliasedStructTemplate(name, from)) |t| return .{ .template = t };
}
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| {
if (self.headTypeLeak(name, span)) return .poisoned;
if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt };
@@ -1230,7 +1241,14 @@ pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId
}
// Try as a named type
const name_id = self.module.types.internString(callee_name);
return self.module.types.findByName(name_id) orelse .unresolved;
if (self.module.types.findByName(name_id)) |t| return t;
// The callee names no known type constructor — not Vector, not a generic
// struct template (or alias), not a type-returning function, not a named
// type. A silent `.unresolved` here reaches LLVM emission as a panic;
// diagnose and poison (the parameterized sibling below already does).
if (self.diagnostics) |d|
d.addFmt(.err, cl.callee.span, "unknown type '{s}'", .{callee_name});
return .unresolved;
}
/// Resolve a parameterized type expr, substituting bindings for type/value params.

View File

@@ -394,6 +394,77 @@ pub fn qualifiedMemberMissing(self: *Lowering, alias: []const u8, member: []cons
return true;
}
/// The `*ConstDecl` a raw author wraps when it is a const ALIAS of another
/// name — `BoxAlias :: Box;` (identifier RHS) or `Box :: r.Box;` (namespace-
/// member RHS). Null for every other shape, including const-wrapped struct /
/// fn DEFINITIONS, which are authors in their own right.
fn constAliasOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.ConstDecl {
return switch (ref) {
.const_decl => |cd| switch (cd.value.data) {
.identifier, .field_access => cd,
else => null,
},
else => null,
};
}
/// The single author of `name` as seen from `from` — own wins, else exactly
/// one flat-import author. Null when absent or when ≥2 flat authors compete
/// (the use site then diagnoses the unresolved head; no silent pick).
fn singleVisibleAuthor(self: *Lowering, name: []const u8, from: []const u8) ?resolver_mod.RawAuthor {
var res = self.resolver();
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
if (set.own) |o| return o;
if (set.flat.len == 1) return set.flat[0];
return null;
}
/// Resolve `name`, as seen from `from`, to a generic-struct template by
/// following const ALIAS declarations (issue 0120). Entry for the head
/// selector's bare tail: the FIRST hop must be alias-shaped — a direct
/// struct author is the template map's business, never this path's. Each
/// hop resolves from the ALIAS AUTHOR's source, so visibility is the
/// author's, not the use site's (a consumer one flat hop from a facade
/// reaches the facade's `Box :: r.Box;` without seeing `r` itself).
pub fn aliasedStructTemplate(self: *Lowering, name: []const u8, from: []const u8) ?StructTemplate {
const author = singleVisibleAuthor(self, name, from) orelse return null;
if (constAliasOfRaw(author.raw) == null) return null;
return followToTemplate(self, author, 8);
}
/// One alias hop: a generic-struct author terminates the chain with its
/// rebuilt source-pinned template; an alias author recurses on its RHS —
/// bare identifier from the author's own source, `ns.X` through the
/// author's namespace edge into the target module's own member. The depth
/// cap breaks alias cycles (`A :: B; B :: A;`).
fn followToTemplate(self: *Lowering, author: resolver_mod.RawAuthor, depth: u8) ?StructTemplate {
if (depth == 0) return null;
if (structDeclOfRaw(author.raw)) |sd| {
if (sd.type_params.len == 0) return null;
return self.buildGenericStructTemplate(sd, author.source);
}
const cd = constAliasOfRaw(author.raw) orelse return null;
switch (cd.value.data) {
.identifier => |id| {
const next = singleVisibleAuthor(self, id.name, author.source) orelse return null;
return followToTemplate(self, next, depth - 1);
},
.field_access => |fa| {
if (fa.object.data != .identifier) return null;
const target = switch (self.namespaceAliasVerdictFrom(fa.object.data.identifier.name, author.source)) {
.target => |t| t,
.none, .ambiguous => return null,
};
var res = self.resolver();
const member_set = res.collectNamespaceAuthors(target, fa.field);
const member = member_set.own orelse return null;
return followToTemplate(self, member, depth - 1);
},
else => return null,
}
}
/// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` +
/// defining source) when that author is NOT the one the global last-wins
/// `struct_template_map` already holds — the E4 non-transitive selection for a