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

@@ -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));
}