authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-02-09 11:23:46+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-10 10:26:11+00:00
logb00ef1aea1a456ad8b175534add8bb324cb39bba
tree4f6f4eb841d745b33239e80d6e66850ff82ff603
parent09d0b1f87a740e968ef739e75729204ff470c98a
signaturelock-open Commit is signed but in an unrecognized format.

Zcu: prevent data races from `Type.assertHasLayout`


3 files changed, 91 insertions(+), 42 deletions(-)

src/Type.zig+3-9
......@@ -3175,21 +3175,15 @@ pub fn assertHasLayout(ty: Type, zcu: *const Zcu) void {
31753175 },
31763176 .struct_type => {
31773177 assert(zcu.intern_pool.loadStructType(ty.toIntern()).want_layout);
3178 const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
3179 assert(!zcu.outdated.contains(unit));
3180 assert(!zcu.potentially_outdated.contains(unit));
3178 zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
31813179 },
31823180 .union_type => {
31833181 assert(zcu.intern_pool.loadUnionType(ty.toIntern()).want_layout);
3184 const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
3185 assert(!zcu.outdated.contains(unit));
3186 assert(!zcu.potentially_outdated.contains(unit));
3182 zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
31873183 },
31883184 .enum_type => {
31893185 assert(zcu.intern_pool.loadEnumType(ty.toIntern()).want_layout);
3190 const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
3191 assert(!zcu.outdated.contains(unit));
3192 assert(!zcu.potentially_outdated.contains(unit));
3186 zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
31933187 },
31943188
31953189 // values, not types
src/Zcu.zig+79-8
......@@ -264,6 +264,10 @@ cimport_errors: std.AutoArrayHashMapUnmanaged(AnalUnit, std.zig.ErrorBundle) = .
264264/// Maximum amount of distinct error values, set by --error-limit
265265error_limit: ErrorInt,
266266
267/// In safe builds, `Type.assertHasLayout` may be called cross-thread, so this lock
268/// guards accesses to `outdated` and `potentially_outdated`. In unsafe builds, the
269/// lock is not needed and is compiled out.
270outdated_lock: if (std.debug.runtime_safety) std.Io.RwLock else void = if (std.debug.runtime_safety) .init,
267271/// Value is the number of PO dependencies of this AnalUnit.
268272/// This value will decrease as we perform semantic analysis to learn what is outdated.
269273/// If any of these PO deps is outdated, this value will be moved to `outdated`.
......@@ -3063,6 +3067,8 @@ pub fn markDependeeOutdated(
30633067) !void {
30643068 deps_log.debug("outdated dependee: {f}", .{zcu.fmtDependee(dependee)});
30653069 var it = zcu.intern_pool.dependencyIterator(dependee);
3070 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
3071 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
30663072 while (it.next()) |depender| {
30673073 if (zcu.outdated.getPtr(depender)) |po_dep_count| {
30683074 switch (marked_po) {
......@@ -3107,6 +3113,12 @@ pub fn markDependeeOutdated(
31073113}
31083114
31093115pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
3116 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
3117 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
3118 return markPoDependeeUpToDateInner(zcu, dependee);
3119}
3120/// Assumes that `zcu.outdated_lock` is already held exclusively.
3121fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
31103122 deps_log.debug("up-to-date dependee: {f}", .{zcu.fmtDependee(dependee)});
31113123 var it = zcu.intern_pool.dependencyIterator(dependee);
31123124 while (it.next()) |depender| {
......@@ -3142,17 +3154,19 @@ pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
31423154 // as no longer PO.
31433155 switch (depender.unwrap()) {
31443156 .@"comptime" => {},
3145 .nav_val => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_val = nav }),
3146 .nav_ty => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_ty = nav }),
3147 .type_layout => |ty| try zcu.markPoDependeeUpToDate(.{ .type_layout = ty }),
3148 .func => |func| try zcu.markPoDependeeUpToDate(.{ .func_ies = func }),
3149 .memoized_state => |stage| try zcu.markPoDependeeUpToDate(.{ .memoized_state = stage }),
3157 .nav_val => |nav| try zcu.markPoDependeeUpToDateInner(.{ .nav_val = nav }),
3158 .nav_ty => |nav| try zcu.markPoDependeeUpToDateInner(.{ .nav_ty = nav }),
3159 .type_layout => |ty| try zcu.markPoDependeeUpToDateInner(.{ .type_layout = ty }),
3160 .func => |func| try zcu.markPoDependeeUpToDateInner(.{ .func_ies = func }),
3161 .memoized_state => |stage| try zcu.markPoDependeeUpToDateInner(.{ .memoized_state = stage }),
31503162 }
31513163 }
31523164}
31533165
31543166/// Given a AnalUnit which is newly outdated or PO, mark all AnalUnits which may
31553167/// in turn be PO, due to a dependency on the original AnalUnit's tyval or IES.
3168///
3169/// Assumes that `zcu.outdated_lock` is already held exclusively.
31563170fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUnit) !void {
31573171 const ip = &zcu.intern_pool;
31583172 const dependee: InternPool.Dependee = switch (maybe_outdated.unwrap()) {
......@@ -3211,6 +3225,9 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
32113225 // possible situation is a cycle where everything is actually up-to-date, so we can clear out
32123226 // `zcu.potentially_outdated` and we are done.
32133227
3228 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
3229 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
3230
32143231 if (zcu.outdated.count() == 0) {
32153232 // Everything is up-to-date. There could be lingering entries in `zcu.potentially_outdated`
32163233 // from a dependency loop on a previous update.
......@@ -3230,7 +3247,10 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
32303247/// During an incremental update, before semantic analysis, call this to flush all values from
32313248/// `retryable_failures` and mark them as outdated so they get re-analyzed.
32323249pub fn flushRetryableFailures(zcu: *Zcu) !void {
3233 const gpa = zcu.gpa;
3250 const comp = zcu.comp;
3251 const gpa = comp.gpa;
3252 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(comp.io);
3253 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(comp.io);
32343254 for (zcu.retryable_failures.items) |depender| {
32353255 if (zcu.outdated.contains(depender)) continue;
32363256 if (zcu.potentially_outdated.fetchSwapRemove(depender)) |kv| {
......@@ -3481,8 +3501,12 @@ pub fn ensureFuncBodyAnalysisQueued(zcu: *Zcu, func: InternPool.Index) !void {
34813501 if (ip.setWantRuntimeFnAnalysis(io, func)) {
34823502 // This is the first reference to this function, so we must ensure it will be analyzed.
34833503 const unit: AnalUnit = .wrap(.{ .func = func });
3484 try zcu.outdated.putNoClobber(gpa, unit, 0);
3485 try zcu.outdated_ready.putNoClobber(gpa, unit, {});
3504 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
3505 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
3506 try zcu.outdated.ensureUnusedCapacity(gpa, 1);
3507 try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
3508 zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
3509 zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
34863510 }
34873511}
34883512
......@@ -3493,6 +3517,8 @@ pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav: InternPool.Nav.Index) !void {
34933517 const ip = &zcu.intern_pool;
34943518 if (ip.setWantNavAnalysis(io, nav)) {
34953519 // This is the first reference to this function, so we must ensure it will be analyzed.
3520 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
3521 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
34963522 try zcu.outdated.ensureUnusedCapacity(gpa, 2);
34973523 try zcu.outdated_ready.ensureUnusedCapacity(gpa, 2);
34983524 zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .nav_val = nav }), 0);
......@@ -3502,6 +3528,51 @@ pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav: InternPool.Nav.Index) !void {
35023528 }
35033529}
35043530
3531/// Called when an `InternPool.ComptimeUnit` is first created to mark it as outdated so that it will
3532/// be semantically analyzed.
3533pub fn queueComptimeUnitAnalysis(zcu: *Zcu, cu: InternPool.ComptimeUnit.Id) Allocator.Error!void {
3534 const comp = zcu.comp;
3535 const gpa = comp.gpa;
3536 const io = comp.io;
3537 const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });
3538 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
3539 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
3540 try zcu.outdated.ensureUnusedCapacity(gpa, 1);
3541 try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
3542 zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
3543 zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
3544}
3545
3546/// If `unit` was marked as outdated or porentially outdated, clears that status and returns `true`.
3547/// Otherwise, returns `false`.
3548pub fn clearOutdatedState(zcu: *Zcu, unit: AnalUnit) bool {
3549 const io = zcu.comp.io;
3550 if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
3551 defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
3552 if (zcu.outdated.fetchSwapRemove(unit)) |kv| {
3553 if (kv.value == 0) assert(zcu.outdated_ready.swapRemove(unit));
3554 return true;
3555 } else if (zcu.potentially_outdated.swapRemove(unit)) {
3556 return true;
3557 } else {
3558 return false;
3559 }
3560}
3561
3562/// This function takes a `*const Zcu` and `@constCast`s it so that it can be called from functions
3563/// in `Type` which otherwise do not modify the `Zcu`.
3564pub fn assertUpToDate(zcu: *const Zcu, unit: AnalUnit) void {
3565 if (!std.debug.runtime_safety) return;
3566
3567 const io = zcu.comp.io;
3568
3569 @constCast(zcu).outdated_lock.lockSharedUncancelable(io);
3570 defer @constCast(zcu).outdated_lock.unlockShared(io);
3571
3572 assert(!zcu.outdated.contains(unit));
3573 assert(!zcu.potentially_outdated.contains(unit));
3574}
3575
35053576pub const ImportResult = struct {
35063577 /// Whether `file` has been newly created; in other words, whether this is the first import of
35073578 /// this file. This should only be `true` when importing files during AstGen. After that, all
src/Zcu/PerThread.zig+9-25
......@@ -746,12 +746,11 @@ pub fn ensureMemoizedStateUpToDate(
746746
747747 assert(!zcu.analysis_in_progress.contains(unit));
748748
749 const was_outdated = zcu.outdated.swapRemove(unit) or zcu.potentially_outdated.swapRemove(unit);
749 const was_outdated = zcu.clearOutdatedState(unit);
750750 const prev_failed = zcu.failed_analysis.contains(unit) or zcu.transitive_failed_analysis.contains(unit);
751751
752752 if (was_outdated) {
753753 dev.check(.incremental);
754 _ = zcu.outdated_ready.swapRemove(unit);
755754 zcu.resetUnit(unit);
756755 } else {
757756 if (prev_failed) return error.AnalysisFail;
......@@ -866,11 +865,9 @@ pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeU
866865 // result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
867866 // carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
868867
869 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
870 zcu.potentially_outdated.swapRemove(anal_unit);
868 const was_outdated = zcu.clearOutdatedState(anal_unit);
871869
872870 if (was_outdated) {
873 _ = zcu.outdated_ready.swapRemove(anal_unit);
874871 // `was_outdated` can be true in the initial update for comptime units, so this isn't a `dev.check`.
875872 if (dev.env.supports(.incremental)) {
876873 zcu.resetUnit(anal_unit);
......@@ -1023,12 +1020,10 @@ pub fn ensureTypeLayoutUpToDate(
10231020
10241021 assert(!zcu.analysis_in_progress.contains(anal_unit));
10251022
1026 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
1027 zcu.potentially_outdated.swapRemove(anal_unit) or
1023 const was_outdated = zcu.clearOutdatedState(anal_unit) or
10281024 zcu.intern_pool.setWantTypeLayout(zcu.comp.io, ty.toIntern());
10291025
10301026 if (was_outdated) {
1031 _ = zcu.outdated_ready.swapRemove(anal_unit);
10321027 // `was_outdated` is true in the initial update, so this isn't a `dev.check`.
10331028 if (dev.env.supports(.incremental)) {
10341029 zcu.resetUnit(anal_unit);
......@@ -1139,15 +1134,13 @@ pub fn ensureNavValUpToDate(
11391134 // result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
11401135 // carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
11411136
1142 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
1143 zcu.potentially_outdated.swapRemove(anal_unit);
1137 const was_outdated = zcu.clearOutdatedState(anal_unit);
11441138
11451139 const prev_failed = zcu.failed_analysis.contains(anal_unit) or
11461140 zcu.transitive_failed_analysis.contains(anal_unit);
11471141
11481142 if (was_outdated) {
11491143 dev.check(.incremental);
1150 _ = zcu.outdated_ready.swapRemove(anal_unit);
11511144 zcu.resetUnit(anal_unit);
11521145 } else {
11531146 // We can trust the current information about this unit.
......@@ -1497,15 +1490,13 @@ pub fn ensureNavTypeUpToDate(
14971490 // result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
14981491 // carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
14991492
1500 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
1501 zcu.potentially_outdated.swapRemove(anal_unit);
1493 const was_outdated = zcu.clearOutdatedState(anal_unit);
15021494
15031495 const prev_failed = zcu.failed_analysis.contains(anal_unit) or
15041496 zcu.transitive_failed_analysis.contains(anal_unit);
15051497
15061498 if (was_outdated) {
15071499 dev.check(.incremental);
1508 _ = zcu.outdated_ready.swapRemove(anal_unit);
15091500 zcu.resetUnit(anal_unit);
15101501 } else {
15111502 // We can trust the current information about this unit.
......@@ -1733,15 +1724,13 @@ pub fn ensureFuncBodyUpToDate(
17331724
17341725 assert(func.ty == func.uncoerced_ty); // analyze the body of the original function, not a coerced one
17351726
1736 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
1737 zcu.potentially_outdated.swapRemove(anal_unit) or
1727 const was_outdated = zcu.clearOutdatedState(anal_unit) or
17381728 ip.setWantRuntimeFnAnalysis(zcu.comp.io, func_index);
17391729
17401730 const prev_failed = zcu.failed_analysis.contains(anal_unit) or zcu.transitive_failed_analysis.contains(anal_unit);
17411731
17421732 if (was_outdated) {
17431733 dev.check(.incremental);
1744 _ = zcu.outdated_ready.swapRemove(anal_unit);
17451734 zcu.resetUnit(anal_unit);
17461735 } else {
17471736 // We can trust the current information about this function.
......@@ -2712,27 +2701,22 @@ const ScanDeclIter = struct {
27122701
27132702 const existing_unit = iter.existing_by_inst.get(tracked_inst);
27142703
2715 const unit, const want_analysis = switch (decl.kind) {
2704 const unit: AnalUnit, const want_analysis = switch (decl.kind) {
27162705 .@"comptime" => unit: {
27172706 const cu = if (existing_unit) |eu|
27182707 eu.unwrap().@"comptime"
27192708 else
27202709 try ip.createComptimeUnit(gpa, io, pt.tid, tracked_inst, namespace_index);
27212710
2722 const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });
2723
27242711 try namespace.comptime_decls.append(gpa, cu);
27252712
27262713 if (existing_unit == null) {
27272714 // For a `comptime` declaration, whether to analyze is based solely on whether the unit
27282715 // is outdated. So, add this fresh one to `outdated` and `outdated_ready`.
2729 try zcu.outdated.ensureUnusedCapacity(gpa, 1);
2730 try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
2731 zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
2732 zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
2716 try zcu.queueComptimeUnitAnalysis(cu);
27332717 }
27342718
2735 break :unit .{ unit, true };
2719 break :unit .{ .wrap(.{ .@"comptime" = cu }), true };
27362720 },
27372721 else => unit: {
27382722 const name = maybe_name.unwrap().?;