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:
@@ -555,3 +555,98 @@ test "lower: objcTypeEncodingFromSignature emits nested structs (CGRect)" {
|
||||
defer alloc.free(e2);
|
||||
try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", e2);
|
||||
}
|
||||
|
||||
// ── Pack projection name resolution (Feature 1, Step 2.2) ────────────
|
||||
|
||||
const errors = @import("../errors.zig");
|
||||
|
||||
fn typeKeyword(alloc: std.mem.Allocator, name: []const u8) *Node {
|
||||
const n = alloc.create(Node) catch unreachable;
|
||||
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name, .is_generic = false } } };
|
||||
return n;
|
||||
}
|
||||
|
||||
fn protoMethod(name: []const u8) ast.ProtocolMethodDecl {
|
||||
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
|
||||
}
|
||||
|
||||
test "pack projection: type-arg vs method namespace lookups" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
// Wrap :: protocol(Target: Type) { wrap :: () -> Target; value :: () -> Target; }
|
||||
const type_kw = typeKeyword(alloc, "Type");
|
||||
defer alloc.destroy(type_kw);
|
||||
const type_params = [_]ast.StructTypeParam{.{ .name = "Target", .constraint = type_kw }};
|
||||
const methods = [_]ast.ProtocolMethodDecl{ protoMethod("wrap"), protoMethod("value") };
|
||||
const pd = ast.ProtocolDecl{ .name = "Wrap", .methods = &methods, .type_params = &type_params };
|
||||
lowering.registerProtocolDecl(&pd);
|
||||
|
||||
// type-arg namespace
|
||||
try std.testing.expectEqual(@as(?u32, 0), lowering.lookupProtocolArg("Wrap", "Target"));
|
||||
try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolArg("Wrap", "wrap"));
|
||||
try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolArg("Nope", "Target"));
|
||||
|
||||
// method (runtime-accessor) namespace
|
||||
try std.testing.expectEqual(@as(?u32, 0), lowering.lookupProtocolField("Wrap", "wrap"));
|
||||
try std.testing.expectEqual(@as(?u32, 1), lowering.lookupProtocolField("Wrap", "value"));
|
||||
try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolField("Wrap", "Target"));
|
||||
}
|
||||
|
||||
test "pack projection: position-driven resolution (Decision 4)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
const type_kw = typeKeyword(alloc, "Type");
|
||||
defer alloc.destroy(type_kw);
|
||||
const type_params = [_]ast.StructTypeParam{.{ .name = "Target", .constraint = type_kw }};
|
||||
const methods = [_]ast.ProtocolMethodDecl{protoMethod("wrap")};
|
||||
const pd = ast.ProtocolDecl{ .name = "Wrap", .methods = &methods, .type_params = &type_params };
|
||||
lowering.registerProtocolDecl(&pd);
|
||||
|
||||
// type position consults type-args only
|
||||
try std.testing.expectEqual(Lowering.PackProjection{ .type_arg = 0 }, lowering.resolvePackProjection("Wrap", "Target", .type_position));
|
||||
try std.testing.expectEqual(Lowering.PackProjection.not_found, lowering.resolvePackProjection("Wrap", "wrap", .type_position));
|
||||
|
||||
// value position consults methods only — no cross-namespace fallback
|
||||
try std.testing.expectEqual(Lowering.PackProjection{ .method = 0 }, lowering.resolvePackProjection("Wrap", "wrap", .value_position));
|
||||
try std.testing.expectEqual(Lowering.PackProjection.not_found, lowering.resolvePackProjection("Wrap", "Target", .value_position));
|
||||
}
|
||||
|
||||
test "pack projection: same-name type-arg + method warns (Decision 4)" {
|
||||
// Arena: DiagnosticList.addFmt allocates messages it never frees in deinit
|
||||
// (mixed ownership with borrowed literals) — an arena keeps the leak
|
||||
// checker clean without changing diagnostic semantics.
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var diags = errors.DiagnosticList.init(alloc, "", "test.sx");
|
||||
defer diags.deinit();
|
||||
|
||||
var lowering = Lowering.init(&module);
|
||||
lowering.diagnostics = &diags;
|
||||
|
||||
// A protocol whose type-arg and method share the name `value`.
|
||||
const type_kw = typeKeyword(alloc, "Type");
|
||||
defer alloc.destroy(type_kw);
|
||||
const type_params = [_]ast.StructTypeParam{.{ .name = "value", .constraint = type_kw }};
|
||||
const methods = [_]ast.ProtocolMethodDecl{protoMethod("value")};
|
||||
const pd = ast.ProtocolDecl{ .name = "Shadowy", .methods = &methods, .type_params = &type_params };
|
||||
lowering.registerProtocolDecl(&pd);
|
||||
|
||||
var warned = false;
|
||||
for (diags.items.items) |d| {
|
||||
if (d.level == .warn and std.mem.indexOf(u8, d.message, "type-arg and method both named 'value'") != null) warned = true;
|
||||
}
|
||||
try std.testing.expect(warned);
|
||||
|
||||
// Position still resolves deterministically despite the shadow.
|
||||
try std.testing.expectEqual(Lowering.PackProjection{ .type_arg = 0 }, lowering.resolvePackProjection("Shadowy", "value", .type_position));
|
||||
try std.testing.expectEqual(Lowering.PackProjection{ .method = 0 }, lowering.resolvePackProjection("Shadowy", "value", .value_position));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user