authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2024-02-03 03:03:17+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2024-02-04 18:38:39+00:00
loga0004cebc255405764e889effb25a42fe07d8463
tree9579c85d052b4520c0b40602add0bc797d9ceec8
parent1e91ee1e05f08013e3a4edec5d9f0aef978f3f0b
signaturelock-open Commit is signed but in an unrecognized format.

Zcu: more dependency tracking logic

* Invalidate `decl_val` dependencies * Recursively mark and un-mark all dependencies correctly * Queue analysis of outdated dependers in `Compilation.performAllTheWork` Introduces logic to invalidate `decl_val` dependencies after `Zcu.semaDecl` completes. Also, recursively un-mark dependencies as PO where needed. With this, all dependency invalidation logic is in place. The next step is analyzing outdated dependencies and triggering appropriate re-analysis.

2 files changed, 279 insertions(+), 36 deletions(-)

src/Compilation.zig+11
......@@ -3514,6 +3514,17 @@ pub fn performAllTheWork(
35143514 try processOneJob(comp, work_item, main_progress_node);
35153515 continue;
35163516 }
3517 if (comp.module) |zcu| {
3518 // If there's no work queued, check if there's anything outdated
3519 // which we need to work on, and queue it if so.
3520 if (try zcu.findOutdatedToAnalyze()) |outdated| {
3521 switch (outdated.unwrap()) {
3522 .decl => |decl| try comp.work_queue.writeItem(.{ .analyze_decl = decl }),
3523 .func => |func| try comp.work_queue.writeItem(.{ .codegen_func = func }),
3524 }
3525 continue;
3526 }
3527 }
35173528 break;
35183529 }
35193530
src/Module.zig+268-36
......@@ -149,9 +149,14 @@ error_limit: ErrorInt,
149149/// previous analysis.
150150generation: u32 = 0,
151151
152/// Value is the number of PO dependencies of this Depender.
152/// Value is the number of PO or outdated Decls which this Depender depends on.
153153potentially_outdated: std.AutoArrayHashMapUnmanaged(InternPool.Depender, u32) = .{},
154outdated: std.AutoArrayHashMapUnmanaged(InternPool.Depender, void) = .{},
154/// Value is the number of PO or outdated Decls which this Depender depends on.
155/// Once this value drops to 0, the Depender is a candidate for re-analysis.
156outdated: std.AutoArrayHashMapUnmanaged(InternPool.Depender, u32) = .{},
157/// This contains all `Depender`s in `outdated` whose PO dependency count is 0.
158/// Such `Depender`s are ready for immediate re-analysis.
159outdated_ready: std.AutoArrayHashMapUnmanaged(InternPool.Depender, void) = .{},
155160
156161stage1_flags: packed struct {
157162 have_winmain: bool = false,
......@@ -2485,6 +2490,8 @@ pub fn deinit(zcu: *Zcu) void {
24852490 zcu.global_error_set.deinit(gpa);
24862491
24872492 zcu.potentially_outdated.deinit(gpa);
2493 zcu.outdated.deinit(gpa);
2494 zcu.outdated_ready.deinit(gpa);
24882495
24892496 zcu.test_functions.deinit(gpa);
24902497
......@@ -2851,11 +2858,27 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
28512858 file.prev_zir = null;
28522859 }
28532860
2854 if (file.root_decl.unwrap()) |root_decl| {
2861 if (file.root_decl.unwrap()) |root_decl| mark_outdated: {
28552862 // The root of this file must be re-analyzed, since the file has changed.
28562863 comp.mutex.lock();
28572864 defer comp.mutex.unlock();
2858 try mod.outdated.put(gpa, InternPool.Depender.wrap(.{ .decl = root_decl }), {});
2865
2866 const root_decl_depender = InternPool.Depender.wrap(.{ .decl = root_decl });
2867
2868 const gop = try mod.outdated.getOrPut(gpa, root_decl_depender);
2869 // If this Decl is already marked as outdated, nothing needs to be done.
2870 if (gop.found_existing) break :mark_outdated;
2871
2872 log.debug("outdated: {} (root Decl)", .{root_decl});
2873
2874 // If it's already PO, forward its existing PO dependency count.
2875 // Otherwise, it has no PO dependencies yet.
2876 if (mod.potentially_outdated.fetchSwapRemove(root_decl_depender)) |kv| {
2877 gop.value_ptr.* = kv.value;
2878 } else {
2879 gop.value_ptr.* = 0;
2880 try mod.outdated_ready.put(mod.gpa, root_decl_depender, {});
2881 }
28592882 }
28602883}
28612884
......@@ -2953,6 +2976,7 @@ fn updateZirRefs(zcu: *Module, file: *File, old_zir: Zir) !void {
29532976 // Tracking failed for this instruction. Invalidate associated `src_hash` deps.
29542977 zcu.comp.mutex.lock();
29552978 defer zcu.comp.mutex.unlock();
2979 log.debug("tracking failed for %{d}", .{old_inst});
29562980 try zcu.markDependeeOutdated(.{ .src_hash = ti_idx });
29572981 continue;
29582982 };
......@@ -2962,6 +2986,12 @@ fn updateZirRefs(zcu: *Module, file: *File, old_zir: Zir) !void {
29622986 if (std.zig.srcHashEql(old_hash, new_hash)) {
29632987 break :hash_changed;
29642988 }
2989 log.debug("hash for (%{d} -> %{d}) changed: {} -> {}", .{
2990 old_inst,
2991 ti.inst,
2992 std.fmt.fmtSliceHexLower(&old_hash),
2993 std.fmt.fmtSliceHexLower(&new_hash),
2994 });
29652995 }
29662996 // The source hash associated with this instruction changed - invalidate relevant dependencies.
29672997 zcu.comp.mutex.lock();
......@@ -3042,25 +3072,80 @@ fn updateZirRefs(zcu: *Module, file: *File, old_zir: Zir) !void {
30423072}
30433073
30443074pub fn markDependeeOutdated(zcu: *Zcu, dependee: InternPool.Dependee) !void {
3075 log.debug("outdated dependee: {}", .{dependee});
30453076 var it = zcu.intern_pool.dependencyIterator(dependee);
30463077 while (it.next()) |depender| {
3047 if (zcu.outdated.contains(depender)) continue;
3048 const was_po = zcu.potentially_outdated.swapRemove(depender);
3049 try zcu.outdated.putNoClobber(zcu.gpa, depender, {});
3078 if (zcu.outdated.contains(depender)) {
3079 // We do not need to increment the PO dep count, as if the outdated
3080 // dependee is a Decl, we had already marked this as PO.
3081 continue;
3082 }
3083 const opt_po_entry = zcu.potentially_outdated.fetchSwapRemove(depender);
3084 try zcu.outdated.putNoClobber(
3085 zcu.gpa,
3086 depender,
3087 // We do not need to increment this count for the same reason as above.
3088 if (opt_po_entry) |e| e.value else 0,
3089 );
3090 log.debug("outdated: {}", .{depender});
3091 if (opt_po_entry != null) {
3092 // This is a new entry with no PO dependencies.
3093 try zcu.outdated_ready.put(zcu.gpa, depender, {});
3094 }
30503095 // If this is a Decl and was not previously PO, we must recursively
30513096 // mark dependencies on its tyval as PO.
3052 if (was_po) switch (depender.unwrap()) {
3097 if (opt_po_entry == null) switch (depender.unwrap()) {
30533098 .decl => |decl_index| try zcu.markDeclDependenciesPotentiallyOutdated(decl_index),
30543099 .func => {},
30553100 };
30563101 }
30573102}
30583103
3104fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
3105 var it = zcu.intern_pool.dependencyIterator(dependee);
3106 while (it.next()) |depender| {
3107 if (zcu.outdated.getPtr(depender)) |po_dep_count| {
3108 // This depender is already outdated, but it now has one
3109 // less PO dependency!
3110 po_dep_count.* -= 1;
3111 if (po_dep_count.* == 0) {
3112 try zcu.outdated_ready.put(zcu.gpa, depender, {});
3113 }
3114 continue;
3115 }
3116 // This depender is definitely at least PO, because this Decl was just analyzed
3117 // due to being outdated.
3118 const ptr = zcu.potentially_outdated.getPtr(depender).?;
3119 if (ptr.* > 1) {
3120 ptr.* -= 1;
3121 continue;
3122 }
3123
3124 // This dependency is no longer PO, i.e. is known to be up-to-date.
3125 assert(zcu.potentially_outdated.swapRemove(depender));
3126 // If this is a Decl, we must recursively mark dependencies on its tyval
3127 // as no longer PO.
3128 switch (depender.unwrap()) {
3129 .decl => |decl_index| try zcu.markPoDependeeUpToDate(.{ .decl_val = decl_index }),
3130 .func => {},
3131 }
3132 }
3133}
3134
30593135/// Given a Decl which is newly outdated or PO, mark all dependers which depend
30603136/// on its tyval as PO.
30613137fn markDeclDependenciesPotentiallyOutdated(zcu: *Zcu, decl_index: Decl.Index) !void {
30623138 var it = zcu.intern_pool.dependencyIterator(.{ .decl_val = decl_index });
30633139 while (it.next()) |po| {
3140 if (zcu.outdated.getPtr(po)) |po_dep_count| {
3141 // This dependency is already outdated, but it now has one more PO
3142 // dependency.
3143 if (po_dep_count.* == 0) {
3144 _ = zcu.outdated_ready.swapRemove(po);
3145 }
3146 po_dep_count.* += 1;
3147 continue;
3148 }
30643149 if (zcu.potentially_outdated.getPtr(po)) |n| {
30653150 // There is now one more PO dependency.
30663151 n.* += 1;
......@@ -3077,6 +3162,92 @@ fn markDeclDependenciesPotentiallyOutdated(zcu: *Zcu, decl_index: Decl.Index) !v
30773162 // TODO: repeat the above for `decl_ty` dependencies when they are introduced
30783163}
30793164
3165pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?InternPool.Depender {
3166 if (zcu.outdated.count() == 0 and zcu.potentially_outdated.count() == 0) {
3167 log.debug("findOutdatedToAnalyze: no outdated depender", .{});
3168 return null;
3169 }
3170
3171 // Our goal is to find an outdated Depender which itself has no outdated or
3172 // PO dependencies. Most of the time, such a Depender will exist - we track
3173 // them in the `outdated_ready` set for efficiency. However, this is not
3174 // necessarily the case, since the Decl dependency graph may contain loops
3175 // via mutually recursive definitions:
3176 // pub const A = struct { b: *B };
3177 // pub const B = struct { b: *A };
3178 // In this case, we must defer to more complex logic below.
3179
3180 if (zcu.outdated_ready.count() > 0) {
3181 log.debug("findOutdatedToAnalyze: trivial '{s} {d}'", .{
3182 @tagName(zcu.outdated_ready.keys()[0].unwrap()),
3183 switch (zcu.outdated_ready.keys()[0].unwrap()) {
3184 inline else => |x| @intFromEnum(x),
3185 },
3186 });
3187 return zcu.outdated_ready.keys()[0];
3188 }
3189
3190 // There is no single Depender which is ready for re-analysis. Instead, we
3191 // must assume that some Decl with PO dependencies is outdated - e.g. in the
3192 // above example we arbitrarily pick one of A or B. We should select a Decl,
3193 // since a Decl is definitely responsible for the loop in the dependency
3194 // graph (since you can't depend on a runtime function analysis!).
3195
3196 // The choice of this Decl could have a big impact on how much total
3197 // analysis we perform, since if analysis concludes its tyval is unchanged,
3198 // then other PO Dependers may be resolved as up-to-date. To hopefully avoid
3199 // doing too much work, let's find a Decl which the most things depend on -
3200 // the idea is that this will resolve a lot of loops (but this is only a
3201 // heuristic).
3202
3203 log.debug("findOutdatedToAnalyze: no trivial ready, using heuristic; {d} outdated, {d} PO", .{
3204 zcu.outdated.count(),
3205 zcu.potentially_outdated.count(),
3206 });
3207
3208 var chosen_decl_idx: ?Decl.Index = null;
3209 var chosen_decl_dependers: u32 = undefined;
3210
3211 for (zcu.outdated.keys()) |depender| {
3212 const decl_index = switch (depender.unwrap()) {
3213 .decl => |d| d,
3214 .func => continue,
3215 };
3216
3217 var n: u32 = 0;
3218 var it = zcu.intern_pool.dependencyIterator(.{ .decl_val = decl_index });
3219 while (it.next()) |_| n += 1;
3220
3221 if (chosen_decl_idx == null or n > chosen_decl_dependers) {
3222 chosen_decl_idx = decl_index;
3223 chosen_decl_dependers = n;
3224 }
3225 }
3226
3227 for (zcu.potentially_outdated.keys()) |depender| {
3228 const decl_index = switch (depender.unwrap()) {
3229 .decl => |d| d,
3230 .func => continue,
3231 };
3232
3233 var n: u32 = 0;
3234 var it = zcu.intern_pool.dependencyIterator(.{ .decl_val = decl_index });
3235 while (it.next()) |_| n += 1;
3236
3237 if (chosen_decl_idx == null or n > chosen_decl_dependers) {
3238 chosen_decl_idx = decl_index;
3239 chosen_decl_dependers = n;
3240 }
3241 }
3242
3243 log.debug("findOutdatedToAnalyze: heuristic returned Decl {d} ({d} dependers)", .{
3244 chosen_decl_idx.?,
3245 chosen_decl_dependers,
3246 });
3247
3248 return InternPool.Depender.wrap(.{ .decl = chosen_decl_idx.? });
3249}
3250
30803251pub fn mapOldZirToNew(
30813252 gpa: Allocator,
30823253 old_zir: Zir,
......@@ -3204,7 +3375,7 @@ pub fn mapOldZirToNew(
32043375 }
32053376}
32063377
3207/// This ensures that the Decl will have a Type and Value populated.
3378/// This ensures that the Decl will have an up-to-date Type and Value populated.
32083379/// However the resolution status of the Type may not be fully resolved.
32093380/// For example an inferred error set is not resolved until after `analyzeFnBody`.
32103381/// is called.
......@@ -3214,7 +3385,25 @@ pub fn ensureDeclAnalyzed(mod: *Module, decl_index: Decl.Index) SemaError!void {
32143385
32153386 const decl = mod.declPtr(decl_index);
32163387
3217 const subsequent_analysis = switch (decl.analysis) {
3388 // Determine whether or not this Decl is outdated, i.e. requires re-analysis
3389 // even if `complete`. If a Decl is PO, we pessismistically assume that it
3390 // *does* require re-analysis, to ensure that the Decl is definitely
3391 // up-to-date when this function returns.
3392
3393 // If analysis occurs in a poor order, this could result in over-analysis.
3394 // We do our best to avoid this by the other dependency logic in this file
3395 // which tries to limit re-analysis to Decls whose previously listed
3396 // dependencies are all up-to-date.
3397
3398 const decl_as_depender = InternPool.Depender.wrap(.{ .decl = decl_index });
3399 const was_outdated = mod.outdated.swapRemove(decl_as_depender) or
3400 mod.potentially_outdated.swapRemove(decl_as_depender);
3401
3402 if (was_outdated) {
3403 _ = mod.outdated_ready.swapRemove(decl_as_depender);
3404 }
3405
3406 switch (decl.analysis) {
32183407 .in_progress => unreachable,
32193408
32203409 .file_failure,
......@@ -3226,28 +3415,29 @@ pub fn ensureDeclAnalyzed(mod: *Module, decl_index: Decl.Index) SemaError!void {
32263415 .codegen_failure_retryable,
32273416 => return error.AnalysisFail,
32283417
3229 .complete => return,
3230
3231 .outdated => blk: {
3418 .complete => if (was_outdated) {
32323419 if (build_options.only_c) unreachable;
32333420 // The exports this Decl performs will be re-discovered, so we remove them here
32343421 // prior to re-analysis.
32353422 try mod.deleteDeclExports(decl_index);
3423 } else return,
32363424
3237 break :blk true;
3238 },
3425 .outdated => unreachable, // TODO: remove this field
32393426
3240 .unreferenced => false,
3241 };
3427 .unreferenced => {},
3428 }
32423429
32433430 var decl_prog_node = mod.sema_prog_node.start("", 0);
32443431 decl_prog_node.activate();
32453432 defer decl_prog_node.end();
32463433
3247 const type_changed = blk: {
3434 const sema_result: SemaDeclResult = blk: {
32483435 if (decl.zir_decl_index == .none and !mod.declIsRoot(decl_index)) {
32493436 // Anonymous decl. We don't semantically analyze these.
3250 break :blk false; // tv unchanged
3437 break :blk .{
3438 .invalidate_decl_val = false,
3439 .invalidate_decl_ref = false,
3440 };
32513441 }
32523442
32533443 break :blk mod.semaDecl(decl_index) catch |err| switch (err) {
......@@ -3276,9 +3466,18 @@ pub fn ensureDeclAnalyzed(mod: *Module, decl_index: Decl.Index) SemaError!void {
32763466 };
32773467 };
32783468
3279 if (subsequent_analysis) {
3280 _ = type_changed;
3281 @panic("TODO re-implement incremental compilation");
3469 // TODO: we do not yet have separate dependencies for decl values vs types.
3470 if (was_outdated) {
3471 if (sema_result.invalidate_decl_val or sema_result.invalidate_decl_ref) {
3472 // This dependency was marked as PO, meaning dependees were waiting
3473 // on its analysis result, and it has turned out to be outdated.
3474 // Update dependees accordingly.
3475 try mod.markDependeeOutdated(.{ .decl_val = decl_index });
3476 } else {
3477 // This dependency was previously PO, but turned out to be up-to-date.
3478 // We do not need to queue successive analysis.
3479 try mod.markPoDependeeUpToDate(.{ .decl_val = decl_index });
3480 }
32823481 }
32833482}
32843483
......@@ -3591,12 +3790,19 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
35913790 },
35923791 .incremental => {},
35933792 }
3793
3794 // Since this is our first time analyzing this file, there can be no dependencies on
3795 // its root Decl. Thus, we do not need to invalidate any dependencies.
35943796}
35953797
3596/// Returns `true` if the Decl type changed.
3597/// Returns `true` if this is the first time analyzing the Decl.
3598/// Returns `false` otherwise.
3599fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
3798const SemaDeclResult = packed struct {
3799 /// Whether the value of a `decl_val` of this Decl changed.
3800 invalidate_decl_val: bool,
3801 /// Whether the type of a `decl_ref` of this Decl changed.
3802 invalidate_decl_ref: bool,
3803};
3804
3805fn semaDecl(mod: *Module, decl_index: Decl.Index) !SemaDeclResult {
36003806 const tracy = trace(@src());
36013807 defer tracy.end();
36023808
......@@ -3674,8 +3880,10 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
36743880 };
36753881 defer sema.deinit();
36763882
3677 // Every Decl has a dependency on its own source.
3678 try sema.declareDependency(.{ .src_hash = try ip.trackZir(sema.gpa, decl.getFileScope(mod), decl.zir_decl_index.unwrap().?) });
3883 // Every Decl other (than file root Decls, which do not have a ZIR index) has a dependency on its own source.
3884 if (decl.zir_decl_index.unwrap()) |zir_decl_index| {
3885 try sema.declareDependency(.{ .src_hash = try ip.trackZir(sema.gpa, decl.getFileScope(mod), zir_decl_index) });
3886 }
36793887
36803888 assert(!mod.declIsRoot(decl_index));
36813889
......@@ -3735,7 +3943,11 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
37353943 decl.analysis = .complete;
37363944 decl.generation = mod.generation;
37373945
3738 return true;
3946 // TODO: usingnamespace cannot currently participate in incremental compilation
3947 return .{
3948 .invalidate_decl_val = true,
3949 .invalidate_decl_ref = true,
3950 };
37393951 }
37403952
37413953 switch (ip.indexToKey(decl_tv.val.toIntern())) {
......@@ -3771,15 +3983,16 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
37713983 // The scope needs to have the decl in it.
37723984 try sema.analyzeExport(&block_scope, export_src, .{ .name = decl.name }, decl_index);
37733985 }
3774 return type_changed or is_inline != prev_is_inline;
3986 // TODO: align, linksection, addrspace?
3987 const changed = type_changed or is_inline != prev_is_inline;
3988 return .{
3989 .invalidate_decl_val = changed,
3990 .invalidate_decl_ref = changed,
3991 };
37753992 }
37763993 },
37773994 else => {},
37783995 }
3779 var type_changed = true;
3780 if (decl.has_tv) {
3781 type_changed = !decl.ty.eql(decl_tv.ty, mod);
3782 }
37833996
37843997 decl.owns_tv = false;
37853998 var queue_linker_work = false;
......@@ -3807,6 +4020,14 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
38074020 },
38084021 }
38094022
4023 const old_has_tv = decl.has_tv;
4024 // The following values are ignored if `!old_has_tv`
4025 const old_ty = decl.ty;
4026 const old_val = decl.val;
4027 const old_align = decl.alignment;
4028 const old_linksection = decl.@"linksection";
4029 const old_addrspace = decl.@"addrspace";
4030
38104031 decl.ty = decl_tv.ty;
38114032 decl.val = Value.fromInterned((try decl_tv.val.intern(decl_tv.ty, mod)));
38124033 decl.alignment = blk: {
......@@ -3850,6 +4071,17 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
38504071 decl.analysis = .complete;
38514072 decl.generation = mod.generation;
38524073
4074 const result: SemaDeclResult = if (old_has_tv) .{
4075 .invalidate_decl_val = !decl.ty.eql(old_ty, mod) or !decl.val.eql(old_val, decl.ty, mod),
4076 .invalidate_decl_ref = !decl.ty.eql(old_ty, mod) or
4077 decl.alignment != old_align or
4078 decl.@"linksection" != old_linksection or
4079 decl.@"addrspace" != old_addrspace,
4080 } else .{
4081 .invalidate_decl_val = true,
4082 .invalidate_decl_ref = true,
4083 };
4084
38534085 const has_runtime_bits = is_extern or
38544086 (queue_linker_work and try sema.typeHasRuntimeBits(decl.ty));
38554087
......@@ -3861,7 +4093,7 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
38614093
38624094 try mod.comp.work_queue.writeItem(.{ .codegen_decl = decl_index });
38634095
3864 if (type_changed and mod.emit_h != null) {
4096 if (result.invalidate_decl_ref and mod.emit_h != null) {
38654097 try mod.comp.work_queue.writeItem(.{ .emit_h_decl = decl_index });
38664098 }
38674099 }
......@@ -3872,7 +4104,7 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
38724104 try sema.analyzeExport(&block_scope, export_src, .{ .name = decl.name }, decl_index);
38734105 }
38744106
3875 return type_changed;
4107 return result;
38764108}
38774109
38784110pub const ImportFileResult = struct {