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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user