From 2ae0ab1cff227011cb6b0f3ca0d30787f53574a6 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 9 Jun 2026 15:55:33 +0300 Subject: [PATCH] additive: compute resolver type-demanded verdicts --- src/ir/resolver.test.zig | 256 ++++++++++++++++++++++++++++++++++++++- src/ir/resolver.zig | 221 ++++++++++++++++++++++++++++----- 2 files changed, 448 insertions(+), 29 deletions(-) diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index e254603..71447ab 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -901,6 +901,117 @@ fn typeParamVerdict(rp: *const resolver.ResolvedProgram, root: *const ast.Node, return refVerdict(rp.type_refs.get(node) orelse return null); } +fn firstCallArg(func: *const ast.Node, callee_name: []const u8) ?*const ast.Node { + if (func.data != .fn_decl) return null; + return firstCallArgInNode(func.data.fn_decl.body, callee_name); +} + +fn firstCallArgInNode(node: *const ast.Node, callee_name: []const u8) ?*const ast.Node { + switch (node.data) { + .call => |*c| { + if (c.callee.data == .identifier and + std.mem.eql(u8, c.callee.data.identifier.name, callee_name) and + c.args.len > 0) + { + return c.args[0]; + } + if (firstCallArgInNode(c.callee, callee_name)) |found| return found; + for (c.args) |arg| if (firstCallArgInNode(arg, callee_name)) |found| return found; + }, + .block => |*b| for (b.stmts) |stmt| { + if (firstCallArgInNode(stmt, callee_name)) |found| return found; + }, + .var_decl => |*vd| { + if (vd.value) |v| if (firstCallArgInNode(v, callee_name)) |found| return found; + }, + .const_decl => |*cd| if (firstCallArgInNode(cd.value, callee_name)) |found| return found, + .binary_op => |*b| { + if (firstCallArgInNode(b.lhs, callee_name)) |found| return found; + if (firstCallArgInNode(b.rhs, callee_name)) |found| return found; + }, + .if_expr => |*e| { + if (firstCallArgInNode(e.condition, callee_name)) |found| return found; + if (firstCallArgInNode(e.then_branch, callee_name)) |found| return found; + if (e.else_branch) |b| if (firstCallArgInNode(b, callee_name)) |found| return found; + }, + .match_expr => |*e| { + if (firstCallArgInNode(e.subject, callee_name)) |found| return found; + for (e.arms) |arm| { + if (arm.pattern) |pat| if (firstCallArgInNode(pat, callee_name)) |found| return found; + if (firstCallArgInNode(arm.body, callee_name)) |found| return found; + } + }, + else => {}, + } + return null; +} + +fn localDeclValue(func: *const ast.Node, name: []const u8) ?*const ast.Node { + if (func.data != .fn_decl) return null; + const body = func.data.fn_decl.body; + if (body.data != .block) return null; + for (body.data.block.stmts) |stmt| switch (stmt.data) { + .var_decl => |*vd| if (std.mem.eql(u8, vd.name, name)) return vd.value, + .const_decl => |*cd| if (std.mem.eql(u8, cd.name, name)) return cd.value, + else => {}, + }; + return null; +} + +fn localDeclType(func: *const ast.Node, name: []const u8) ?*const ast.Node { + if (func.data != .fn_decl) return null; + const body = func.data.fn_decl.body; + if (body.data != .block) return null; + for (body.data.block.stmts) |stmt| switch (stmt.data) { + .var_decl => |*vd| if (std.mem.eql(u8, vd.name, name)) return vd.type_annotation, + .const_decl => |*cd| if (std.mem.eql(u8, cd.name, name)) return cd.type_annotation, + else => {}, + }; + return null; +} + +fn firstMatchPattern(root: *const ast.Node, name: []const u8) ?*const ast.Node { + return firstMatchPatternInNode(root, name); +} + +fn firstMatchPatternInNode(node: *const ast.Node, name: []const u8) ?*const ast.Node { + switch (node.data) { + .root => |*r| for (r.decls) |decl| { + if (firstMatchPatternInNode(decl, name)) |found| return found; + }, + .fn_decl => |*fd| { + if (firstMatchPatternInNode(fd.body, name)) |found| return found; + }, + .block => |*b| for (b.stmts) |stmt| { + if (firstMatchPatternInNode(stmt, name)) |found| return found; + }, + .var_decl => |*vd| { + if (vd.value) |v| if (firstMatchPatternInNode(v, name)) |found| return found; + }, + .const_decl => |*cd| if (firstMatchPatternInNode(cd.value, name)) |found| return found, + .if_expr => |*e| { + if (firstMatchPatternInNode(e.condition, name)) |found| return found; + if (firstMatchPatternInNode(e.then_branch, name)) |found| return found; + if (e.else_branch) |b| if (firstMatchPatternInNode(b, name)) |found| return found; + }, + .match_expr => |*e| { + for (e.arms) |arm| { + if (arm.pattern) |pat| { + const pat_name = switch (pat.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => "", + }; + if (std.mem.eql(u8, pat_name, name)) return pat; + } + if (firstMatchPatternInNode(arm.body, name)) |found| return found; + } + }, + else => {}, + } + return null; +} + // S2.2 attaches the selection verdict to every `.authors` ref, computed over the // DOMAIN-ELIGIBLE subset of the collected author set. This proves all five outcomes // on the BARE-TYPE domain over real Phase A facts: own_wins (own author), @@ -994,6 +1105,84 @@ test "resolver: verdicts — own-wins / single / ambiguous / domain-filtered / n try std.testing.expect(saw_thing_value); } +test "resolver: verdicts — foreign classes obey the requested domain filter" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = testIo(); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ .sub_path = "vals.sx", .data = "MixedValue :: 42;\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "types.sx", .data = "MixedType :: struct { x: s64 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "foreign.sx", .data = + \\MixedValue :: #foreign #objc_class("MixedValue") {} + \\MixedType :: #foreign #objc_class("MixedType") {} + \\OnlyForeign :: #foreign #objc_class("OnlyForeign") {} + \\ + }); + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = + \\#import "vals.sx"; + \\#import "types.sx"; + \\#import "foreign.sx"; + \\use_foreign :: (x: OnlyForeign) -> s64 { 0 } + \\use_mixed_type :: (x: MixedType) -> s64 { 0 } + \\read_value :: () -> s64 { MixedValue } + \\main :: () -> s32 { 0 } + \\ + }); + + var dirbuf: [4096]u8 = undefined; + const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)]; + const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); + + var prog = try buildResolved(alloc, io, absdir, main_path); + + var idx = ProgramIndex.init(alloc); + defer idx.deinit(); + idx.module_decls = &prog.decls; + idx.namespace_edges = &prog.ns_edges; + idx.flat_import_graph = &prog.flat_import_graph; + idx.import_graph = &prog.import_graph; + + var rp = resolver.resolve(prog.root, &idx, main_path, alloc); + defer rp.deinit(); + + // A pure foreign class still routes to the foreign-class side table. + const use_foreign = findFn(prog.root, "use_foreign") orelse return error.MissingUseForeign; + const only_foreign = use_foreign.data.fn_decl.params[0].type_expr; + try std.testing.expect(rp.type_refs.get(only_foreign) == null); + const only_foreign_ref = rp.foreign_class_refs.get(only_foreign) orelse return error.OnlyForeignNotKeyed; + try std.testing.expectEqual(resolver.Verdict.single, refVerdict(only_foreign_ref).?); + + // A value-position name with one flat const author plus one same-name foreign + // class is selected as a value const, not hijacked into foreign_class_refs. + var saw_value = false; + var vit = rp.value_refs.iterator(); + while (vit.next()) |e| { + const k = e.key_ptr.*; + if (k.data == .identifier and std.mem.eql(u8, k.data.identifier.name, "MixedValue")) { + try std.testing.expectEqual(resolver.Verdict.single, refVerdict(e.value_ptr.*).?); + _ = flatAuthorOfKind(e.value_ptr.authors.set, .const_decl) orelse return error.MixedValueConstMissing; + try std.testing.expect(rp.foreign_class_refs.get(k) == null); + saw_value = true; + } + } + try std.testing.expect(saw_value); + + // A type-position name with one flat struct author plus one same-name foreign + // class is a type-domain ambiguity. The foreign class is type-eligible, but it + // does not preempt the struct author by routing the site away from type_refs. + const use_mixed = findFn(prog.root, "use_mixed_type") orelse return error.MissingUseMixed; + const mixed_type = use_mixed.data.fn_decl.params[0].type_expr; + try std.testing.expect(rp.foreign_class_refs.get(mixed_type) == null); + const mixed_ref = rp.type_refs.get(mixed_type) orelse return error.MixedTypeNotKeyed; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(mixed_ref).?); + _ = flatAuthorOfKind(mixed_ref.authors.set, .struct_decl) orelse return error.MixedStructMissing; + _ = flatAuthorOfKind(mixed_ref.authors.set, .foreign_class_decl) orelse return error.MixedForeignMissing; +} + // The acceptance proof: querying the resolver produces the TARGET verdicts for the // resolver-target corpus (which the OLD per-kind selectors get WRONG on this base) — // 0811-class error-set, 0821-class protocol head, and 0829-class generic-struct @@ -1020,10 +1209,24 @@ test "resolver: verdicts — resolver-target corpus (0811/0821/0829 → ambiguou try tmp.dir.writeFile(io, .{ .sub_path = "ambig.sx", .data = \\#import "a.sx"; \\#import "b.sx"; + \\describe :: ($T: Type) -> s32 { + \\ r := if T == { + \\ case IoErr: 1; + \\ else: 0; + \\ } + \\ r + \\} \\fail :: (e: IoErr) -> s32 { 0 } + \\fail_io :: () -> !IoErr { raise error.Disk; } \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } \\use_box :: (b: Box(s64)) -> s64 { 0 } - \\main :: () -> s32 { 0 } + \\main :: () -> s32 { + \\ sz := size_of(IoErr); + \\ e : IoErr = error.Disk; + \\ t : Type = IoErr; + \\ k := describe(s64); + \\ return 0; + \\} \\ }); // Querying module authors its OWN IoErr / Cmp / Box → own-wins, even against the @@ -1034,10 +1237,24 @@ test "resolver: verdicts — resolver-target corpus (0811/0821/0829 → ambiguou \\IoErr :: error { Disk, Net } \\Cmp :: protocol(T: Type) { get :: () -> T; } \\Box :: struct($T: Type) { value: T } + \\describe :: ($T: Type) -> s32 { + \\ r := if T == { + \\ case IoErr: 1; + \\ else: 0; + \\ } + \\ r + \\} \\fail :: (e: IoErr) -> s32 { 0 } + \\fail_io :: () -> !IoErr { raise error.Disk; } \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } \\use_box :: (b: Box(s64)) -> s64 { 0 } - \\main :: () -> s32 { 0 } + \\main :: () -> s32 { + \\ sz := size_of(IoErr); + \\ e : IoErr = error.Disk; + \\ t : Type = IoErr; + \\ k := describe(s64); + \\ return 0; + \\} \\ }); @@ -1059,6 +1276,25 @@ test "resolver: verdicts — resolver-target corpus (0811/0821/0829 → ambiguou // 0811-class: bare error-set type reference → ambiguous. try std.testing.expectEqual(resolver.Verdict.ambiguous, typeParamVerdict(&rp, prog.root, "fail").?); + const fail_io = findFn(prog.root, "fail_io") orelse return error.FailIoMissing; + const fail_err = fail_io.data.fn_decl.return_type orelse return error.FailIoRetMissing; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(rp.type_refs.get(fail_err) orelse return error.FailIoTypeNotKeyed).?); + + const main_fn = findFn(prog.root, "main") orelse return error.MainMissing; + const size_arg = firstCallArg(main_fn, "size_of") orelse return error.SizeArgMissing; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(rp.type_refs.get(size_arg) orelse return error.SizeArgTypeNotKeyed).?); + try std.testing.expect(rp.value_refs.get(size_arg) == null); + + const local_e_ty = localDeclType(main_fn, "e") orelse return error.LocalETypeMissing; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(rp.type_refs.get(local_e_ty) orelse return error.LocalETypeNotKeyed).?); + + const type_value = localDeclValue(main_fn, "t") orelse return error.TypeValueMissing; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(rp.type_refs.get(type_value) orelse return error.TypeValueTypeNotKeyed).?); + try std.testing.expect(rp.value_refs.get(type_value) == null); + + const match_pat = firstMatchPattern(prog.root, "IoErr") orelse return error.MatchPatternMissing; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(rp.type_refs.get(match_pat) orelse return error.MatchPatternTypeNotKeyed).?); + try std.testing.expect(rp.value_refs.get(match_pat) == null); // 0821-class: parameterized protocol head → ambiguous (protocol_heads). const cmp_head = paramTypeNode(prog.root, "use_cmp").?; @@ -1088,6 +1324,22 @@ test "resolver: verdicts — resolver-target corpus (0811/0821/0829 → ambiguou defer rp.deinit(); try std.testing.expectEqual(resolver.Verdict.own_wins, typeParamVerdict(&rp, prog.root, "fail").?); + const fail_io = findFn(prog.root, "fail_io") orelse return error.FailIoMissing; + const fail_err = fail_io.data.fn_decl.return_type orelse return error.FailIoRetMissing; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(rp.type_refs.get(fail_err) orelse return error.FailIoTypeNotKeyed).?); + + const main_fn = findFn(prog.root, "main") orelse return error.MainMissing; + const size_arg = firstCallArg(main_fn, "size_of") orelse return error.SizeArgMissing; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(rp.type_refs.get(size_arg) orelse return error.SizeArgTypeNotKeyed).?); + + const local_e_ty = localDeclType(main_fn, "e") orelse return error.LocalETypeMissing; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(rp.type_refs.get(local_e_ty) orelse return error.LocalETypeNotKeyed).?); + + const type_value = localDeclValue(main_fn, "t") orelse return error.TypeValueMissing; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(rp.type_refs.get(type_value) orelse return error.TypeValueTypeNotKeyed).?); + + const match_pat = firstMatchPattern(prog.root, "IoErr") orelse return error.MatchPatternMissing; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(rp.type_refs.get(match_pat) orelse return error.MatchPatternTypeNotKeyed).?); const cmp_head = paramTypeNode(prog.root, "use_cmp").?; const cmp_ref = rp.protocol_heads.get(cmp_head) orelse return error.CmpHeadNotKeyed; diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index d94c5c8..2eb34f8 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -461,15 +461,25 @@ fn classifyHeadKind(raw: RawDeclRef, gs: *bool, tf: *bool, pr: *bool) void { } } -/// True when an author set resolves to a `foreign_class_decl` — the own author -/// decides when present, else any flat author. Such a reference is routed to -/// `foreign_class_refs` (its own domain) instead of the bare type/value table. -fn authorSetIsForeignClass(set: AuthorSet) bool { - if (set.own) |a| return std.meta.activeTag(a.raw) == .foreign_class_decl; - for (set.flat) |a| { - if (std.meta.activeTag(a.raw) == .foreign_class_decl) return true; - } - return false; +/// True when the already-computed BARE-TYPE verdict selected a foreign-class +/// author unambiguously. Mixed foreign/non-foreign type authors stay in +/// `type_refs` as a type-domain ambiguity; foreign classes never preempt value or +/// callable domains. +fn foreignClassWinsType(set: AuthorSet, verdict: Verdict) bool { + return switch (verdict) { + .own_wins => if (set.own) |a| std.meta.activeTag(a.raw) == .foreign_class_decl else false, + .single => blk: { + var selected: ?RawAuthor = null; + for (set.flat) |a| { + if (!eligibleKind(.bare_type, a.raw, null)) continue; + if (selected != null) break :blk false; + selected = a; + } + const a = selected orelse break :blk false; + break :blk std.meta.activeTag(a.raw) == .foreign_class_decl; + }, + .ambiguous, .not_visible, .domain_filtered => false, + }; } /// A struct author carrying a `const_decl` member named `field` — the RAW shape @@ -556,13 +566,11 @@ const Domain = enum { /// struct/fn `type_params.len > 0` test, `structHasConstMember`). fn eligibleKind(domain: Domain, raw: RawDeclRef, field: ?[]const u8) bool { return switch (domain) { - // Foreign classes are routed to their own domain before the type verdict, so - // a bare TYPE author is a non-foreign named type. A type ALIAS (`Name :: `, - // a `const_decl`) is recognised by lowering via the E0 source-keyed alias cache, - // which the resolver does not yet carry — alias authorship folds in when the - // alias facts move into the resolver (a later S2/S4 refinement), not here. + // Foreign classes are type authors too. Routing to `foreign_class_refs` happens + // only after this domain verdict proves the selected type author is a foreign + // class; mixed struct/foreign sets remain ordinary type ambiguities. .bare_type => switch (raw) { - .struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl => true, + .struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true, else => false, }, .value_const => raw == .const_decl, @@ -579,6 +587,43 @@ fn eligibleKind(domain: Domain, raw: RawDeclRef, field: ?[]const u8) bool { }; } +fn annotationIsTypeValue(node: ?*const ast.Node) bool { + const n = node orelse return false; + return switch (n.data) { + .type_expr => |te| !te.is_raw and std.mem.eql(u8, te.name, "Type"), + else => false, + }; +} + +fn callArgIsTypeDemanded(name: []const u8, index: usize) bool { + if (index == 0 and (std.mem.eql(u8, name, "size_of") or + std.mem.eql(u8, name, "align_of") or + std.mem.eql(u8, name, "field_count") or + std.mem.eql(u8, name, "type_name") or + std.mem.eql(u8, name, "type_is_unsigned") or + std.mem.eql(u8, name, "is_flags") or + std.mem.eql(u8, name, "field_name") or + std.mem.eql(u8, name, "field_value_int") or + std.mem.eql(u8, name, "field_index"))) + { + return true; + } + if (std.mem.eql(u8, name, "type_eq")) return index < 2; + if (std.mem.eql(u8, name, "has_impl")) return index < 2; + return false; +} + +fn typeCategoryPatternName(name: []const u8) bool { + const categories = [_][]const u8{ + "int", "float", "bool", "string", "void", "type", "Type", + "struct", "enum", "union", "slice", "array", "pointer", "vector", + }; + for (categories) |cat| { + if (std.mem.eql(u8, name, cat)) return true; + } + return name.len > 0 and name[0] >= 'A' and name[0] <= 'Z'; +} + /// The single owning traversal. Holds the author collector + the `ResolvedProgram` /// it populates; threads `Ctx` (ambient source + generic scope) down the tree. const ResolvePass = struct { @@ -683,7 +728,7 @@ const ResolvePass = struct { } else { self.visit(c.callee, here); } - self.visitAll(c.args, here); + self.visitCallArgs(c, here); }, .field_access => |*fa| { // `alias.member` whose base is a namespace import edge of the @@ -734,9 +779,16 @@ const ResolvePass = struct { if (e.else_branch) |b| self.visit(b, here); }, .match_expr => |*e| { + const type_patterns = self.matchPatternsAreTypeDemanded(e, here); self.visit(e.subject, here); for (e.arms) |arm| { - if (arm.pattern) |pat| self.visit(pat, here); + if (arm.pattern) |pat| { + if (type_patterns) { + self.visitTypeDemanded(pat, here); + } else { + self.visit(pat, here); + } + } self.visit(arm.body, here); } }, @@ -746,11 +798,21 @@ const ResolvePass = struct { }, .const_decl => |*cd| { if (cd.type_annotation) |ta| self.visit(ta, here); - self.visit(cd.value, here); + if (annotationIsTypeValue(cd.type_annotation)) { + self.visitTypeDemanded(cd.value, here); + } else { + self.visit(cd.value, here); + } }, .var_decl => |*vd| { if (vd.type_annotation) |ta| self.visit(ta, here); - if (vd.value) |v| self.visit(v, here); + if (vd.value) |v| { + if (annotationIsTypeValue(vd.type_annotation)) { + self.visitTypeDemanded(v, here); + } else { + self.visit(v, here); + } + } }, .assignment => |*a| { self.visit(a.target, here); @@ -919,6 +981,109 @@ const ResolvePass = struct { for (nodes) |n| if (n) |nn| self.visit(nn, ctx); } + fn visitCallArgs(self: *ResolvePass, c: *const ast.Call, ctx: Ctx) void { + const cname = if (c.callee.data == .identifier) c.callee.data.identifier.name else null; + for (c.args, 0..) |arg, i| { + if (cname) |name| { + if (callArgIsTypeDemanded(name, i)) { + self.visitTypeDemanded(arg, ctx); + continue; + } + } + self.visit(arg, ctx); + } + } + + fn visitTypeDemanded(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void { + const here = Ctx{ + .source = node.source_file orelse ctx.source, + .scope = ctx.scope, + .preseeded_decl = ctx.preseeded_decl, + }; + switch (node.data) { + .identifier => |id| { + if (!id.is_raw) { + if (lookupGeneric(here.scope, id.name)) |m| { + self.recordTemplate(&self.out.type_refs, node, m); + return; + } + } + self.recordAuthors(.bare_type, &self.out.type_refs, node, id.name, here.source); + }, + .type_expr => self.classifyType(node, here), + .pack_index_type_expr => |*p| self.recordPack(&self.out.type_refs, node, p.pack_name, p.index, here.scope), + .comptime_pack_ref => |*p| self.recordPack(&self.out.type_refs, node, p.pack_name, null, here.scope), + .error_type_expr => |*e| { + if (e.name) |name| self.recordAuthors(.bare_type, &self.out.type_refs, node, name, here.source); + }, + .parameterized_type_expr => |*p| { + self.classifyHead(node, p.name, p.is_raw, here); + for (p.args) |arg| self.visitTypeDemanded(arg, here); + }, + .call => |*c| { + if (c.callee.data == .identifier) { + const cname = c.callee.data.identifier.name; + if (std.mem.eql(u8, cname, "type_of")) { + self.visitAll(c.args, here); + return; + } + self.classifyHead(node, cname, c.callee.data.identifier.is_raw, here); + for (c.args) |arg| self.visitTypeDemanded(arg, here); + } else { + self.visit(c.callee, here); + self.visitAll(c.args, here); + } + }, + .pointer_type_expr => |*p| self.visitTypeDemanded(p.pointee_type, here), + .many_pointer_type_expr => |*p| self.visitTypeDemanded(p.element_type, here), + .slice_type_expr => |*s| self.visitTypeDemanded(s.element_type, here), + .optional_type_expr => |*o| self.visitTypeDemanded(o.inner_type, here), + .array_type_expr => |*a| { + self.visit(a.length, here); + self.visitTypeDemanded(a.element_type, here); + }, + .function_type_expr => |*ft| { + for (ft.param_types) |p| self.visitTypeDemanded(p, here); + if (ft.return_type) |rt| self.visitTypeDemanded(rt, here); + }, + .closure_type_expr => |*ct| { + for (ct.param_types) |p| self.visitTypeDemanded(p, here); + if (ct.return_type) |rt| self.visitTypeDemanded(rt, here); + }, + .tuple_type_expr => |*tt| for (tt.field_types) |ft| self.visitTypeDemanded(ft, here), + .tuple_literal => |*tl| for (tl.elements) |el| self.visitTypeDemanded(el.value, here), + .spread_expr => |*s| self.visitTypeDemanded(s.operand, here), + else => self.visit(node, here), + } + } + + fn matchPatternsAreTypeDemanded(self: *ResolvePass, me: *const ast.MatchExpr, ctx: Ctx) bool { + if (self.exprDenotesTypeValue(me.subject, ctx)) return true; + for (me.arms) |arm| { + const pat = arm.pattern orelse continue; + const name = switch (pat.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => continue, + }; + if (typeCategoryPatternName(name)) return true; + } + return false; + } + + fn exprDenotesTypeValue(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) bool { + _ = self; + return switch (node.data) { + .identifier => |id| if (lookupGeneric(ctx.scope, id.name)) |m| !paramIsValue(m.param.*) else false, + .type_expr, + .pack_index_type_expr, + .comptime_pack_ref, + => true, + .call => |*c| c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "type_of"), + else => false, + }; + } + fn visitTypeParamConstraints(self: *ResolvePass, params: []const ast.StructTypeParam, ctx: Ctx) void { for (params) |p| self.visit(p.constraint, ctx); } @@ -950,24 +1115,26 @@ const ResolvePass = struct { self.recordAuthors(.value_const, &self.out.value_refs, node, id.name, ctx.source); } - /// Collect a bare name's authors AND compute its `domain` verdict. A name whose - /// author is a `foreign_class_decl` is routed to `foreign_class_refs` (its own - /// S2.1c domain, with the foreign-class verdict) instead of the passed - /// type/value/callable table. Records own-wins / single / ambiguous when an + /// Collect a bare name's authors AND compute its `domain` verdict. Foreign + /// classes participate in the BARE-TYPE verdict like other type authors; only + /// an unambiguously selected foreign class is routed to `foreign_class_refs`. + /// Value/callable domains keep their requested destination and run their own + /// eligibility filter first. Records own-wins / single / ambiguous when an /// eligible author is visible, and `not_visible` when the name is authored for /// this domain only over a namespace edge; a builtin / local / undeclared /// spelling (no visible author and none authored anywhere) is dropped, exactly /// as S2.1 dropped the empty set. fn recordAuthors(self: *ResolvePass, domain: Domain, table: *NodeRefTable, node: *const ast.Node, name: []const u8, from: []const u8) void { const set = self.res.collectVisibleAuthors(name, from, .user_bare_flat); - const foreign = authorSetIsForeignClass(set); - const dom: Domain = if (foreign) .foreign_class else domain; - const dest = if (foreign) &self.out.foreign_class_refs else table; - const verdict = self.verdictOver(dom, name, set, null); + const verdict = self.verdictOver(domain, name, set, null); // Nothing visible AND not a domain author anywhere → a builtin / local / // undeclared spelling, never a reference of this domain — drop it (the S2.1 // empty-set behavior). An empty set owns no `flat` slice to free. if (verdict == .domain_filtered and set.distinctCount() == 0) return; + const dest = if (domain == .bare_type and foreignClassWinsType(set, verdict)) + &self.out.foreign_class_refs + else + table; self.replaceRef(dest, node, .{ .authors = .{ .set = set, .verdict = verdict } }); }