lang 2.2: protocol-arg lookup + position-driven pack projection

Add the name-resolution primitives a `..pack.<name>` projection needs
(Decision 4). A protocol exposes two namespaces: type-args (the
`protocol($T, ...)` params) and runtime accessors (its methods — protocols
have no fields). Resolution is position-driven with no cross-namespace
fallback:

- lookupProtocolArg(protocol, name) -> ?u32   (type_params index)
- lookupProtocolField(protocol, name) -> ?u32 (methods index)
- resolvePackProjection(protocol, name, pos)  (.type_arg | .method | .not_found)

registerProtocolDecl now warns when a type-arg and a method share a name
(allowed, but `..pack.<name>` then resolves by position, which surprises
readers). 3 unit tests cover both namespaces, the position rule, and the
shadowing warning + deterministic resolution despite a shadow.

Projecting a *bound* pack (producing a new Pack of per-element results) waits
for call-site binding in Step 2.4; these primitives are what it will call
per element.
This commit is contained in:
agra
2026-05-29 16:00:03 +03:00
parent 4defadf513
commit fac235950d
2 changed files with 170 additions and 1 deletions

View File

@@ -11136,7 +11136,21 @@ pub const Lowering = struct {
/// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... }
/// Non-inline protocols: { ctx: *void, __vtable: *void }
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
// Decision 4 soft-convention warning: a type-arg and a method (the
// "runtime accessor" namespace — protocols have no fields) sharing a
// name is allowed, but `..pack.<name>` then resolves by *position*
// rather than by precedence, which surprises readers. Alert at decl.
for (pd.type_params) |tp| {
for (pd.methods) |m| {
if (std.mem.eql(u8, tp.name, m.name)) {
if (self.diagnostics) |diags| {
diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name });
}
}
}
}
// Parameterised protocols are compile-time-only — no vtable, no boxed
// instance struct. Methods reference unbound type params (e.g.
// `convert :: () -> Target`) that only get a concrete TypeId per
@@ -11235,6 +11249,66 @@ pub const Lowering = struct {
}
}
// ── Pack projection name resolution (Feature 1, Decision 4) ──────────
//
// A `..pack.<name>` projection can target two protocol namespaces:
// - type-arg namespace: the `protocol($T, ...)` params.
// - runtime-accessor namespace: the protocol's methods (protocols have
// no fields; a zero-arg method like `value` is the accessor).
// Resolution is POSITION-driven, not precedence-driven: type position
// consults type-args, value position consults methods, with NO
// cross-namespace fallback.
pub const ProjectionPosition = enum { type_position, value_position };
pub const PackProjection = union(enum) {
type_arg: u32, // index into the protocol's `type_params`
method: u32, // index into the protocol's `methods`
not_found, // `name` absent from the position-selected namespace
};
/// Find `name` in `protocol_name`'s type-arg namespace (`protocol($T,...)`).
/// Returns the `type_params` index, or null (also for unknown protocols).
pub fn lookupProtocolArg(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 {
const pd = self.protocol_ast_map.get(protocol_name) orelse return null;
for (pd.type_params, 0..) |tp, i| {
if (std.mem.eql(u8, tp.name, name)) return @intCast(i);
}
return null;
}
/// Find `name` in `protocol_name`'s runtime-accessor namespace (its methods
/// — protocols have no fields). Returns the `methods` index, or null.
pub fn lookupProtocolField(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 {
const pd = self.protocol_ast_map.get(protocol_name) orelse return null;
for (pd.methods, 0..) |m, i| {
if (std.mem.eql(u8, m.name, name)) return @intCast(i);
}
return null;
}
/// Resolve `..pack.<name>` against `protocol_name` by position (Decision 4).
/// No cross-namespace fallback: a value-position name that exists only as a
/// type-arg (or vice versa) is `.not_found`, letting the caller emit a
/// position-specific diagnostic (G3, Step 2.7).
pub fn resolvePackProjection(
self: *Lowering,
protocol_name: []const u8,
name: []const u8,
pos: ProjectionPosition,
) PackProjection {
return switch (pos) {
.type_position => if (self.lookupProtocolArg(protocol_name, name)) |i|
.{ .type_arg = i }
else
.not_found,
.value_position => if (self.lookupProtocolField(protocol_name, name)) |i|
.{ .method = i }
else
.not_found,
};
}
/// Register a foreign-class declaration. The alias goes into
/// `foreign_class_map` for method-dispatch lookup. The underlying
/// type (e.g. `*Activity`) is resolved via the existing struct