lang: qualified namespace members in value position + alias carry

Two coupled capabilities on the road to the std restructure
(current/PLAN-STDLIB.md, issue 0114):

1. alias.Type.method() / alias.Type as a call head, alias.CONST, and
   alias.Enum.variant now resolve — previously only alias.fn() and
   type-position alias.Type worked. objectIsValue treats an
   alias-rooted field_access as a type head; the call path strips the
   alias to the existing Type.method machinery; lowerFieldAccess
   resolves alias.CONST pinned to the target module and alias.Enum.x
   as a typed enum literal; resolveTypeWithBindings resolves qualified
   type_exprs pinned to the target.

2. The carry rule: namespaceAliasTarget resolves an alias from the
   file's own edges first, then from DIRECT flat imports (one level),
   diagnosing two distinct carried targets as ambiguous. All qualified
   shapes work through a carried alias — the std.sx namespace tail
   (mem.GPA.init() etc.) is now expressible.

Regression: examples/0831-modules-namespace-alias-carry.sx (direct +
carried, all seven shapes).
This commit is contained in:
agra
2026-06-11 05:52:10 +03:00
parent 2025bb361b
commit ee00db849c
11 changed files with 163 additions and 9 deletions

View File

@@ -892,7 +892,20 @@ pub const Lowering = struct {
// type decls, error types) still route through type_bridge, which reads
// the global compat maps (cut over in a later phase).
switch (node.data) {
.type_expr => |te| return self.resolveNominalLeaf(te.name, te.is_raw, node.span),
.type_expr => |te| {
// Qualified `alias.Type` (incl. a carried alias): resolve the
// base name pinned to the alias's target module.
if (std.mem.lastIndexOfScalar(u8, te.name, '.')) |dot| {
if (self.namespaceAliasTarget(te.name[0..dot], node.span)) |target| {
const saved = self.current_source_file;
self.setCurrentSourceFile(target.target_module_path);
const ty = self.resolveNominalLeaf(te.name[dot + 1 ..], te.is_raw, node.span);
self.setCurrentSourceFile(saved);
return ty;
}
}
return self.resolveNominalLeaf(te.name, te.is_raw, node.span);
},
.identifier => |id| return self.resolveNominalLeaf(id.name, id.is_raw, node.span),
// A non-spread tuple literal in a type position is a tuple-type
// literal (`(s32, s32)`); validate its elements are types and reject
@@ -1118,6 +1131,55 @@ pub const Lowering = struct {
};
}
/// Resolve a namespace alias visible from the current source file under
/// the carry rule: the file's OWN `ns :: #import` edge wins; otherwise an
/// alias declared by a DIRECT flat import is carried (one level — flat
/// edges of flat edges do not chain). Two distinct carried targets for
/// the same alias diagnose as ambiguous and resolve to null.
pub fn namespaceAliasTarget(self: *Lowering, alias: []const u8, span: ?ast.Span) ?imports_mod.NamespaceTarget {
const edges = self.program_index.namespace_edges orelse return null;
const from = self.current_source_file orelse return null;
if (edges.getPtr(from)) |own| {
if (own.get(alias)) |t| return t;
}
const flat = self.program_index.flat_import_graph orelse return null;
const direct = flat.get(from) orelse return null;
var found: ?imports_mod.NamespaceTarget = null;
var it = direct.keyIterator();
while (it.next()) |dep| {
const dep_edges = edges.getPtr(dep.*) orelse continue;
const t = dep_edges.get(alias) orelse continue;
if (found) |f| {
if (!std.mem.eql(u8, f.target_module_path, t.target_module_path)) {
if (self.diagnostics) |d| {
d.addFmt(.err, span, "namespace '{s}' is ambiguous: aliases from multiple flat-imported modules point at different targets; declare the alias locally", .{alias});
}
return null;
}
} else found = t;
}
return found;
}
/// The inner member name when `node` is a namespace-rooted prefix
/// (`alias.Member`) — the shape a qualified type/static head takes after
/// stripping the alias. Null when `node` isn't that shape.
pub fn namespaceRootedMember(self: *Lowering, node: *const Node) ?[]const u8 {
if (node.data != .field_access) return null;
const fa = node.data.field_access;
const root = switch (fa.object.data) {
.identifier => |id| id.name,
else => return null,
};
// A value binding shadows a same-named namespace alias.
if (self.scope) |s| {
if (s.lookup(root) != null) return null;
}
if (self.program_index.global_names.contains(root)) return null;
if (self.namespaceAliasTarget(root, node.span) == null) return null;
return fa.field;
}
pub fn isIntEx(self: *Lowering, ty: TypeId) bool {
if (isInt(ty)) return true;
if (!ty.isBuiltin()) {