authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-10-20 11:00:24+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-10-29 05:34:19+00:00
log8907dc8a4fbec951e4a4d3266aded9d751f07c4e
tree0f17d870bde7af4438d1f93ee11fcd904713e204
parent4e9dd099c5d624b101222bfc4e6f415b5375f8d7

Zcu: use shortest reference trace

The logic for computing reference traces was unintentionally finding the *longest* possible trace (approximately). I think I already tried to fix this before, but misunderstood how my own code works. Here, we fix it properly: by slightly reworking the logic to use one ArrayHashMap for both the result and the traversal queue, we trivially get a proper breadth-first traversal so that we can find the shortest possible reference trace for every referenced unit.

1 files changed, 56 insertions(+), 49 deletions(-)

src/Zcu.zig+56-49
...@@ -272,7 +272,7 @@ analysis_roots_len: usize = 0,...@@ -272,7 +272,7 @@ analysis_roots_len: usize = 0,
272/// This is the cached result of `Zcu.resolveReferences`. It is computed on-demand, and272/// This is the cached result of `Zcu.resolveReferences`. It is computed on-demand, and
273/// reset to `null` when any semantic analysis occurs (since this invalidates the data).273/// reset to `null` when any semantic analysis occurs (since this invalidates the data).
274/// Allocated into `gpa`.274/// Allocated into `gpa`.
275resolved_references: ?std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) = null,275resolved_references: ?std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) = null,
276276
277/// If `true`, then semantic analysis must not occur on this update due to AstGen errors.277/// If `true`, then semantic analysis must not occur on this update due to AstGen errors.
278/// Essentially the entire pipeline after AstGen, including Sema, codegen, and link, is skipped.278/// Essentially the entire pipeline after AstGen, including Sema, codegen, and link, is skipped.
...@@ -3985,45 +3985,42 @@ pub const ResolvedReference = struct {...@@ -3985,45 +3985,42 @@ pub const ResolvedReference = struct {
3985/// If an `AnalUnit` is not in the returned map, it is unreferenced.3985/// If an `AnalUnit` is not in the returned map, it is unreferenced.
3986/// The returned hashmap is owned by the `Zcu`, so should not be freed by the caller.3986/// The returned hashmap is owned by the `Zcu`, so should not be freed by the caller.
3987/// This hashmap is cached, so repeated calls to this function are cheap.3987/// This hashmap is cached, so repeated calls to this function are cheap.
3988pub fn resolveReferences(zcu: *Zcu) !*const std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) {3988pub fn resolveReferences(zcu: *Zcu) !*const std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) {
3989 if (zcu.resolved_references == null) {3989 if (zcu.resolved_references == null) {
3990 zcu.resolved_references = try zcu.resolveReferencesInner();3990 zcu.resolved_references = try zcu.resolveReferencesInner();
3991 }3991 }
3992 return &zcu.resolved_references.?;3992 return &zcu.resolved_references.?;
3993}3993}
3994fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) {3994fn resolveReferencesInner(zcu: *Zcu) !std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) {
3995 const gpa = zcu.gpa;3995 const gpa = zcu.gpa;
3996 const comp = zcu.comp;3996 const comp = zcu.comp;
3997 const ip = &zcu.intern_pool;3997 const ip = &zcu.intern_pool;
39983998
3999 var result: std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) = .empty;3999 var units: std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) = .empty;
4000 errdefer result.deinit(gpa);4000 var types: std.AutoArrayHashMapUnmanaged(InternPool.Index, ?ResolvedReference) = .empty;
4001
4002 var checked_types: std.AutoArrayHashMapUnmanaged(InternPool.Index, void) = .empty;
4003 var type_queue: std.AutoArrayHashMapUnmanaged(InternPool.Index, ?ResolvedReference) = .empty;
4004 var unit_queue: std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) = .empty;
4005 defer {4001 defer {
4006 checked_types.deinit(gpa);4002 units.deinit(gpa);
4007 type_queue.deinit(gpa);4003 types.deinit(gpa);
4008 unit_queue.deinit(gpa);
4009 }4004 }
40104005
4011 // This is not a sufficient size, but a lower bound.4006 // This is not a sufficient size, but an approximate lower bound.
4012 try result.ensureTotalCapacity(gpa, @intCast(zcu.reference_table.count()));4007 try units.ensureTotalCapacity(gpa, @intCast(zcu.reference_table.count()));
40134008
4014 try type_queue.ensureTotalCapacity(gpa, zcu.analysis_roots_len);4009 try types.ensureTotalCapacity(gpa, zcu.analysis_roots_len);
4015 for (zcu.analysisRoots()) |mod| {4010 for (zcu.analysisRoots()) |mod| {
4016 const file = zcu.module_roots.get(mod).?.unwrap() orelse continue;4011 const file = zcu.module_roots.get(mod).?.unwrap() orelse continue;
4017 const root_ty = zcu.fileRootType(file);4012 const root_ty = zcu.fileRootType(file);
4018 if (root_ty == .none) continue;4013 if (root_ty == .none) continue;
4019 type_queue.putAssumeCapacityNoClobber(root_ty, null);4014 types.putAssumeCapacityNoClobber(root_ty, null);
4020 }4015 }
40214016
4017 var unit_idx: usize = 0;
4018 var type_idx: usize = 0;
4022 while (true) {4019 while (true) {
4023 if (type_queue.pop()) |kv| {4020 if (type_idx < types.count()) {
4024 const ty = kv.key;4021 const ty = types.keys()[type_idx];
4025 const referencer = kv.value;4022 const referencer = types.values()[type_idx];
4026 try checked_types.putNoClobber(gpa, ty, {});4023 type_idx += 1;
40274024
4028 log.debug("handle type '{f}'", .{Type.fromInterned(ty).containerTypeName(ip).fmt(ip)});4025 log.debug("handle type '{f}'", .{Type.fromInterned(ty).containerTypeName(ip).fmt(ip)});
40294026
...@@ -4037,8 +4034,7 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4037,8 +4034,7 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4037 if (has_resolution) {4034 if (has_resolution) {
4038 // this should only be referenced by the type4035 // this should only be referenced by the type
4039 const unit: AnalUnit = .wrap(.{ .type = ty });4036 const unit: AnalUnit = .wrap(.{ .type = ty });
4040 assert(!result.contains(unit));4037 try units.putNoClobber(gpa, unit, referencer);
4041 try unit_queue.putNoClobber(gpa, unit, referencer);
4042 }4038 }
40434039
4044 // If this is a union with a generated tag, its tag type is automatically referenced.4040 // If this is a union with a generated tag, its tag type is automatically referenced.
...@@ -4047,9 +4043,8 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4047,9 +4043,8 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4047 const tag_ty = union_obj.enum_tag_ty;4043 const tag_ty = union_obj.enum_tag_ty;
4048 if (tag_ty != .none) {4044 if (tag_ty != .none) {
4049 if (ip.indexToKey(tag_ty).enum_type == .generated_tag) {4045 if (ip.indexToKey(tag_ty).enum_type == .generated_tag) {
4050 if (!checked_types.contains(tag_ty)) {4046 const gop = try types.getOrPut(gpa, tag_ty);
4051 try type_queue.put(gpa, tag_ty, referencer);4047 if (!gop.found_existing) gop.value_ptr.* = referencer;
4052 }
4053 }4048 }
4054 }4049 }
4055 }4050 }
...@@ -4060,12 +4055,13 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4060,12 +4055,13 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4060 for (zcu.namespacePtr(ns).comptime_decls.items) |cu| {4055 for (zcu.namespacePtr(ns).comptime_decls.items) |cu| {
4061 // `comptime` decls are always analyzed.4056 // `comptime` decls are always analyzed.
4062 const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });4057 const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });
4063 if (!result.contains(unit)) {4058 const gop = try units.getOrPut(gpa, unit);
4059 if (!gop.found_existing) {
4064 log.debug("type '{f}': ref comptime %{}", .{4060 log.debug("type '{f}': ref comptime %{}", .{
4065 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),4061 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
4066 @intFromEnum(ip.getComptimeUnit(cu).zir_index.resolve(ip) orelse continue),4062 @intFromEnum(ip.getComptimeUnit(cu).zir_index.resolve(ip) orelse continue),
4067 });4063 });
4068 try unit_queue.put(gpa, unit, referencer);4064 gop.value_ptr.* = referencer;
4069 }4065 }
4070 }4066 }
4071 for (zcu.namespacePtr(ns).test_decls.items) |nav_id| {4067 for (zcu.namespacePtr(ns).test_decls.items) |nav_id| {
...@@ -4092,14 +4088,20 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4092,14 +4088,20 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4092 },4088 },
4093 };4089 };
4094 if (want_analysis) {4090 if (want_analysis) {
4095 log.debug("type '{f}': ref test %{}", .{4091 {
4096 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),4092 const gop = try units.getOrPut(gpa, .wrap(.{ .nav_val = nav_id }));
4097 @intFromEnum(inst_info.inst),4093 if (!gop.found_existing) {
4098 });4094 log.debug("type '{f}': ref test %{}", .{
4099 try unit_queue.put(gpa, .wrap(.{ .nav_val = nav_id }), referencer);4095 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
4096 @intFromEnum(inst_info.inst),
4097 });
4098 gop.value_ptr.* = referencer;
4099 }
4100 }
4100 // Non-fatal AstGen errors could mean this test decl failed4101 // Non-fatal AstGen errors could mean this test decl failed
4101 if (nav.status == .fully_resolved) {4102 if (nav.status == .fully_resolved) {
4102 try unit_queue.put(gpa, .wrap(.{ .func = nav.status.fully_resolved.val }), referencer);4103 const gop = try units.getOrPut(gpa, .wrap(.{ .func = nav.status.fully_resolved.val }));
4104 if (!gop.found_existing) gop.value_ptr.* = referencer;
4103 }4105 }
4104 }4106 }
4105 }4107 }
...@@ -4110,12 +4112,13 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4110,12 +4112,13 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4110 const decl = file.zir.?.getDeclaration(inst_info.inst);4112 const decl = file.zir.?.getDeclaration(inst_info.inst);
4111 if (decl.linkage == .@"export") {4113 if (decl.linkage == .@"export") {
4112 const unit: AnalUnit = .wrap(.{ .nav_val = nav });4114 const unit: AnalUnit = .wrap(.{ .nav_val = nav });
4113 if (!result.contains(unit)) {4115 const gop = try units.getOrPut(gpa, unit);
4116 if (!gop.found_existing) {
4114 log.debug("type '{f}': ref named %{}", .{4117 log.debug("type '{f}': ref named %{}", .{
4115 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),4118 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
4116 @intFromEnum(inst_info.inst),4119 @intFromEnum(inst_info.inst),
4117 });4120 });
4118 try unit_queue.put(gpa, unit, referencer);4121 gop.value_ptr.* = referencer;
4119 }4122 }
4120 }4123 }
4121 }4124 }
...@@ -4126,20 +4129,21 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4126,20 +4129,21 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4126 const decl = file.zir.?.getDeclaration(inst_info.inst);4129 const decl = file.zir.?.getDeclaration(inst_info.inst);
4127 if (decl.linkage == .@"export") {4130 if (decl.linkage == .@"export") {
4128 const unit: AnalUnit = .wrap(.{ .nav_val = nav });4131 const unit: AnalUnit = .wrap(.{ .nav_val = nav });
4129 if (!result.contains(unit)) {4132 const gop = try units.getOrPut(gpa, unit);
4133 if (!gop.found_existing) {
4130 log.debug("type '{f}': ref named %{}", .{4134 log.debug("type '{f}': ref named %{}", .{
4131 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),4135 Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
4132 @intFromEnum(inst_info.inst),4136 @intFromEnum(inst_info.inst),
4133 });4137 });
4134 try unit_queue.put(gpa, unit, referencer);4138 gop.value_ptr.* = referencer;
4135 }4139 }
4136 }4140 }
4137 }4141 }
4138 continue;4142 continue;
4139 }4143 }
4140 if (unit_queue.pop()) |kv| {4144 if (unit_idx < units.count()) {
4141 const unit = kv.key;4145 const unit = units.keys()[unit_idx];
4142 try result.putNoClobber(gpa, unit, kv.value);4146 unit_idx += 1;
41434147
4144 // `nav_val` and `nav_ty` reference each other *implicitly* to save memory.4148 // `nav_val` and `nav_ty` reference each other *implicitly* to save memory.
4145 queue_paired: {4149 queue_paired: {
...@@ -4148,8 +4152,9 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4148,8 +4152,9 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4148 .nav_ty => |n| .{ .nav_val = n },4152 .nav_ty => |n| .{ .nav_val = n },
4149 .@"comptime", .type, .func, .memoized_state => break :queue_paired,4153 .@"comptime", .type, .func, .memoized_state => break :queue_paired,
4150 });4154 });
4151 if (result.contains(other)) break :queue_paired;4155 const gop = try units.getOrPut(gpa, other);
4152 try unit_queue.put(gpa, other, kv.value); // same reference location4156 if (gop.found_existing) break :queue_paired;
4157 gop.value_ptr.* = units.values()[unit_idx]; // same reference location
4153 }4158 }
41544159
4155 log.debug("handle unit '{f}'", .{zcu.fmtAnalUnit(unit)});4160 log.debug("handle unit '{f}'", .{zcu.fmtAnalUnit(unit)});
...@@ -4159,16 +4164,17 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4159,16 +4164,17 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4159 var ref_idx = first_ref_idx;4164 var ref_idx = first_ref_idx;
4160 while (ref_idx != std.math.maxInt(u32)) {4165 while (ref_idx != std.math.maxInt(u32)) {
4161 const ref = zcu.all_references.items[ref_idx];4166 const ref = zcu.all_references.items[ref_idx];
4162 if (!result.contains(ref.referenced)) {4167 const gop = try units.getOrPut(gpa, ref.referenced);
4168 if (!gop.found_existing) {
4163 log.debug("unit '{f}': ref unit '{f}'", .{4169 log.debug("unit '{f}': ref unit '{f}'", .{
4164 zcu.fmtAnalUnit(unit),4170 zcu.fmtAnalUnit(unit),
4165 zcu.fmtAnalUnit(ref.referenced),4171 zcu.fmtAnalUnit(ref.referenced),
4166 });4172 });
4167 try unit_queue.put(gpa, ref.referenced, .{4173 gop.value_ptr.* = .{
4168 .referencer = unit,4174 .referencer = unit,
4169 .src = ref.src,4175 .src = ref.src,
4170 .inline_frame = ref.inline_frame,4176 .inline_frame = ref.inline_frame,
4171 });4177 };
4172 }4178 }
4173 ref_idx = ref.next;4179 ref_idx = ref.next;
4174 }4180 }
...@@ -4178,16 +4184,17 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4178,16 +4184,17 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4178 var ref_idx = first_ref_idx;4184 var ref_idx = first_ref_idx;
4179 while (ref_idx != std.math.maxInt(u32)) {4185 while (ref_idx != std.math.maxInt(u32)) {
4180 const ref = zcu.all_type_references.items[ref_idx];4186 const ref = zcu.all_type_references.items[ref_idx];
4181 if (!checked_types.contains(ref.referenced)) {4187 const gop = try types.getOrPut(gpa, ref.referenced);
4188 if (!gop.found_existing) {
4182 log.debug("unit '{f}': ref type '{f}'", .{4189 log.debug("unit '{f}': ref type '{f}'", .{
4183 zcu.fmtAnalUnit(unit),4190 zcu.fmtAnalUnit(unit),
4184 Type.fromInterned(ref.referenced).containerTypeName(ip).fmt(ip),4191 Type.fromInterned(ref.referenced).containerTypeName(ip).fmt(ip),
4185 });4192 });
4186 try type_queue.put(gpa, ref.referenced, .{4193 gop.value_ptr.* = .{
4187 .referencer = unit,4194 .referencer = unit,
4188 .src = ref.src,4195 .src = ref.src,
4189 .inline_frame = .none,4196 .inline_frame = .none,
4190 });4197 };
4191 }4198 }
4192 ref_idx = ref.next;4199 ref_idx = ref.next;
4193 }4200 }
...@@ -4197,7 +4204,7 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv...@@ -4197,7 +4204,7 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
4197 break;4204 break;
4198 }4205 }
41994206
4200 return result;4207 return units.move();
4201}4208}
42024209
4203pub fn analysisRoots(zcu: *Zcu) []*Package.Module {4210pub fn analysisRoots(zcu: *Zcu) []*Package.Module {