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

@@ -415,6 +415,65 @@ pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.S
return self.lowerErrorTagLiteral(fa.field, span);
}
// Namespace-alias stripping in value position. The target module's
// declarations register under their bare names, so `alias.Member`
// re-enters as `Member` (`r.LIMIT`, and `r.Color` as the receiver of
// `r.Color.green`); `alias.Type.field` re-enters as `Type.field`.
if (self.namespaceRootedMember(fa.object)) |inner| {
const root = fa.object.data.field_access.object.data.identifier.name;
if (self.namespaceAliasTarget(root, span)) |target| {
// Resolve the inner name as a TYPE in the target's context
// (the alias edge authorizes the reach).
const saved_src = self.current_source_file;
self.setCurrentSourceFile(target.target_module_path);
const ty = self.resolveNominalLeaf(inner, false, span);
self.setCurrentSourceFile(saved_src);
if (ty != .unresolved and !ty.isBuiltin()) {
const info = self.module.types.get(ty);
if (info == .@"enum" or info == .tagged_union) {
// `alias.Enum.variant` — a typed enum literal.
const synth = self.alloc.create(Node) catch null;
if (synth) |n| {
n.* = .{ .span = span, .data = .{ .enum_literal = .{ .name = fa.field } } };
const saved_tt = self.target_type;
self.target_type = ty;
const ref = self.lowerExpr(n);
self.target_type = saved_tt;
return ref;
}
}
}
// `alias.Type.member` (struct constants etc.) — strip the alias;
// the type's members register under the bare type name globally.
const synth = self.alloc.create(Node) catch null;
if (synth) |n| {
n.* = .{ .span = fa.object.span, .data = .{ .identifier = .{ .name = inner } } };
const stripped = ast.FieldAccess{ .object = n, .field = fa.field, .is_optional = fa.is_optional };
return self.lowerFieldAccess(&stripped, span);
}
}
}
if (fa.object.data == .identifier) {
const oname = fa.object.data.identifier.name;
const shadowed = if (self.scope) |s| s.lookup(oname) != null else false;
if (!shadowed and !self.program_index.global_names.contains(oname)) {
if (self.namespaceAliasTarget(oname, span)) |target| {
const synth = self.alloc.create(Node) catch null;
if (synth) |n| {
n.* = .{ .span = span, .data = .{ .identifier = .{ .name = fa.field } } };
// Lower in the TARGET module's context: the alias edge
// authorizes the member, so the bare-visibility gate must
// judge it as the target's own name, not the caller's.
const saved_src = self.current_source_file;
self.setCurrentSourceFile(target.target_module_path);
const ref = self.lowerExpr(n);
self.setCurrentSourceFile(saved_src);
return ref;
}
}
}
}
// Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
// body resolves to the comptime-known N. The mono doesn't
// materialise the `[]Any` slice that the inline path used, so