diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 6b51667..2199a92 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -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)); +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 7d4e1f9..9d40608 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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.` 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.` 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.` 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