commit b00ef1aea1a456ad8b175534add8bb324cb39bba (tree)
parent 09d0b1f87a740e968ef739e75729204ff470c98a
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Mon, 9 Feb 2026 11:23:46 +0000
Zcu: prevent data races from `Type.assertHasLayout`
Diffstat:
3 files changed, 91 insertions(+), 42 deletions(-)
diff --git a/src/Type.zig b/src/Type.zig
@@ -3175,21 +3175,15 @@ pub fn assertHasLayout(ty: Type, zcu: *const Zcu) void {
},
.struct_type => {
assert(zcu.intern_pool.loadStructType(ty.toIntern()).want_layout);
- const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
- assert(!zcu.outdated.contains(unit));
- assert(!zcu.potentially_outdated.contains(unit));
+ zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
},
.union_type => {
assert(zcu.intern_pool.loadUnionType(ty.toIntern()).want_layout);
- const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
- assert(!zcu.outdated.contains(unit));
- assert(!zcu.potentially_outdated.contains(unit));
+ zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
},
.enum_type => {
assert(zcu.intern_pool.loadEnumType(ty.toIntern()).want_layout);
- const unit: InternPool.AnalUnit = .wrap(.{ .type_layout = ty.toIntern() });
- assert(!zcu.outdated.contains(unit));
- assert(!zcu.potentially_outdated.contains(unit));
+ zcu.assertUpToDate(.wrap(.{ .type_layout = ty.toIntern() }));
},
// values, not types
diff --git a/src/Zcu.zig b/src/Zcu.zig
@@ -264,6 +264,10 @@ cimport_errors: std.AutoArrayHashMapUnmanaged(AnalUnit, std.zig.ErrorBundle) = .
/// Maximum amount of distinct error values, set by --error-limit
error_limit: ErrorInt,
+/// In safe builds, `Type.assertHasLayout` may be called cross-thread, so this lock
+/// guards accesses to `outdated` and `potentially_outdated`. In unsafe builds, the
+/// lock is not needed and is compiled out.
+outdated_lock: if (std.debug.runtime_safety) std.Io.RwLock else void = if (std.debug.runtime_safety) .init,
/// Value is the number of PO dependencies of this AnalUnit.
/// This value will decrease as we perform semantic analysis to learn what is outdated.
/// If any of these PO deps is outdated, this value will be moved to `outdated`.
@@ -3063,6 +3067,8 @@ pub fn markDependeeOutdated(
) !void {
deps_log.debug("outdated dependee: {f}", .{zcu.fmtDependee(dependee)});
var it = zcu.intern_pool.dependencyIterator(dependee);
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
while (it.next()) |depender| {
if (zcu.outdated.getPtr(depender)) |po_dep_count| {
switch (marked_po) {
@@ -3107,6 +3113,12 @@ pub fn markDependeeOutdated(
}
pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
+ return markPoDependeeUpToDateInner(zcu, dependee);
+}
+/// Assumes that `zcu.outdated_lock` is already held exclusively.
+fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
deps_log.debug("up-to-date dependee: {f}", .{zcu.fmtDependee(dependee)});
var it = zcu.intern_pool.dependencyIterator(dependee);
while (it.next()) |depender| {
@@ -3142,17 +3154,19 @@ pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
// as no longer PO.
switch (depender.unwrap()) {
.@"comptime" => {},
- .nav_val => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_val = nav }),
- .nav_ty => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_ty = nav }),
- .type_layout => |ty| try zcu.markPoDependeeUpToDate(.{ .type_layout = ty }),
- .func => |func| try zcu.markPoDependeeUpToDate(.{ .func_ies = func }),
- .memoized_state => |stage| try zcu.markPoDependeeUpToDate(.{ .memoized_state = stage }),
+ .nav_val => |nav| try zcu.markPoDependeeUpToDateInner(.{ .nav_val = nav }),
+ .nav_ty => |nav| try zcu.markPoDependeeUpToDateInner(.{ .nav_ty = nav }),
+ .type_layout => |ty| try zcu.markPoDependeeUpToDateInner(.{ .type_layout = ty }),
+ .func => |func| try zcu.markPoDependeeUpToDateInner(.{ .func_ies = func }),
+ .memoized_state => |stage| try zcu.markPoDependeeUpToDateInner(.{ .memoized_state = stage }),
}
}
}
/// Given a AnalUnit which is newly outdated or PO, mark all AnalUnits which may
/// in turn be PO, due to a dependency on the original AnalUnit's tyval or IES.
+///
+/// Assumes that `zcu.outdated_lock` is already held exclusively.
fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUnit) !void {
const ip = &zcu.intern_pool;
const dependee: InternPool.Dependee = switch (maybe_outdated.unwrap()) {
@@ -3211,6 +3225,9 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
// possible situation is a cycle where everything is actually up-to-date, so we can clear out
// `zcu.potentially_outdated` and we are done.
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
+
if (zcu.outdated.count() == 0) {
// Everything is up-to-date. There could be lingering entries in `zcu.potentially_outdated`
// from a dependency loop on a previous update.
@@ -3230,7 +3247,10 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
/// During an incremental update, before semantic analysis, call this to flush all values from
/// `retryable_failures` and mark them as outdated so they get re-analyzed.
pub fn flushRetryableFailures(zcu: *Zcu) !void {
- const gpa = zcu.gpa;
+ const comp = zcu.comp;
+ const gpa = comp.gpa;
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(comp.io);
for (zcu.retryable_failures.items) |depender| {
if (zcu.outdated.contains(depender)) continue;
if (zcu.potentially_outdated.fetchSwapRemove(depender)) |kv| {
@@ -3481,8 +3501,12 @@ pub fn ensureFuncBodyAnalysisQueued(zcu: *Zcu, func: InternPool.Index) !void {
if (ip.setWantRuntimeFnAnalysis(io, func)) {
// This is the first reference to this function, so we must ensure it will be analyzed.
const unit: AnalUnit = .wrap(.{ .func = func });
- try zcu.outdated.putNoClobber(gpa, unit, 0);
- try zcu.outdated_ready.putNoClobber(gpa, unit, {});
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
+ try zcu.outdated.ensureUnusedCapacity(gpa, 1);
+ try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
+ zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
+ zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
}
}
@@ -3493,6 +3517,8 @@ pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav: InternPool.Nav.Index) !void {
const ip = &zcu.intern_pool;
if (ip.setWantNavAnalysis(io, nav)) {
// This is the first reference to this function, so we must ensure it will be analyzed.
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(zcu.comp.io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(zcu.comp.io);
try zcu.outdated.ensureUnusedCapacity(gpa, 2);
try zcu.outdated_ready.ensureUnusedCapacity(gpa, 2);
zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .nav_val = nav }), 0);
@@ -3502,6 +3528,51 @@ pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav: InternPool.Nav.Index) !void {
}
}
+/// Called when an `InternPool.ComptimeUnit` is first created to mark it as outdated so that it will
+/// be semantically analyzed.
+pub fn queueComptimeUnitAnalysis(zcu: *Zcu, cu: InternPool.ComptimeUnit.Id) Allocator.Error!void {
+ const comp = zcu.comp;
+ const gpa = comp.gpa;
+ const io = comp.io;
+ const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
+ try zcu.outdated.ensureUnusedCapacity(gpa, 1);
+ try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
+ zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
+ zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
+}
+
+/// If `unit` was marked as outdated or porentially outdated, clears that status and returns `true`.
+/// Otherwise, returns `false`.
+pub fn clearOutdatedState(zcu: *Zcu, unit: AnalUnit) bool {
+ const io = zcu.comp.io;
+ if (std.debug.runtime_safety) zcu.outdated_lock.lockUncancelable(io);
+ defer if (std.debug.runtime_safety) zcu.outdated_lock.unlock(io);
+ if (zcu.outdated.fetchSwapRemove(unit)) |kv| {
+ if (kv.value == 0) assert(zcu.outdated_ready.swapRemove(unit));
+ return true;
+ } else if (zcu.potentially_outdated.swapRemove(unit)) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/// This function takes a `*const Zcu` and `@constCast`s it so that it can be called from functions
+/// in `Type` which otherwise do not modify the `Zcu`.
+pub fn assertUpToDate(zcu: *const Zcu, unit: AnalUnit) void {
+ if (!std.debug.runtime_safety) return;
+
+ const io = zcu.comp.io;
+
+ @constCast(zcu).outdated_lock.lockSharedUncancelable(io);
+ defer @constCast(zcu).outdated_lock.unlockShared(io);
+
+ assert(!zcu.outdated.contains(unit));
+ assert(!zcu.potentially_outdated.contains(unit));
+}
+
pub const ImportResult = struct {
/// Whether `file` has been newly created; in other words, whether this is the first import of
/// this file. This should only be `true` when importing files during AstGen. After that, all
diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig
@@ -746,12 +746,11 @@ pub fn ensureMemoizedStateUpToDate(
assert(!zcu.analysis_in_progress.contains(unit));
- const was_outdated = zcu.outdated.swapRemove(unit) or zcu.potentially_outdated.swapRemove(unit);
+ const was_outdated = zcu.clearOutdatedState(unit);
const prev_failed = zcu.failed_analysis.contains(unit) or zcu.transitive_failed_analysis.contains(unit);
if (was_outdated) {
dev.check(.incremental);
- _ = zcu.outdated_ready.swapRemove(unit);
zcu.resetUnit(unit);
} else {
if (prev_failed) return error.AnalysisFail;
@@ -866,11 +865,9 @@ pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeU
// result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
// carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
- const was_outdated = zcu.outdated.swapRemove(anal_unit) or
- zcu.potentially_outdated.swapRemove(anal_unit);
+ const was_outdated = zcu.clearOutdatedState(anal_unit);
if (was_outdated) {
- _ = zcu.outdated_ready.swapRemove(anal_unit);
// `was_outdated` can be true in the initial update for comptime units, so this isn't a `dev.check`.
if (dev.env.supports(.incremental)) {
zcu.resetUnit(anal_unit);
@@ -1023,12 +1020,10 @@ pub fn ensureTypeLayoutUpToDate(
assert(!zcu.analysis_in_progress.contains(anal_unit));
- const was_outdated = zcu.outdated.swapRemove(anal_unit) or
- zcu.potentially_outdated.swapRemove(anal_unit) or
+ const was_outdated = zcu.clearOutdatedState(anal_unit) or
zcu.intern_pool.setWantTypeLayout(zcu.comp.io, ty.toIntern());
if (was_outdated) {
- _ = zcu.outdated_ready.swapRemove(anal_unit);
// `was_outdated` is true in the initial update, so this isn't a `dev.check`.
if (dev.env.supports(.incremental)) {
zcu.resetUnit(anal_unit);
@@ -1139,15 +1134,13 @@ pub fn ensureNavValUpToDate(
// result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
// carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
- const was_outdated = zcu.outdated.swapRemove(anal_unit) or
- zcu.potentially_outdated.swapRemove(anal_unit);
+ const was_outdated = zcu.clearOutdatedState(anal_unit);
const prev_failed = zcu.failed_analysis.contains(anal_unit) or
zcu.transitive_failed_analysis.contains(anal_unit);
if (was_outdated) {
dev.check(.incremental);
- _ = zcu.outdated_ready.swapRemove(anal_unit);
zcu.resetUnit(anal_unit);
} else {
// We can trust the current information about this unit.
@@ -1497,15 +1490,13 @@ pub fn ensureNavTypeUpToDate(
// result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
// carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
- const was_outdated = zcu.outdated.swapRemove(anal_unit) or
- zcu.potentially_outdated.swapRemove(anal_unit);
+ const was_outdated = zcu.clearOutdatedState(anal_unit);
const prev_failed = zcu.failed_analysis.contains(anal_unit) or
zcu.transitive_failed_analysis.contains(anal_unit);
if (was_outdated) {
dev.check(.incremental);
- _ = zcu.outdated_ready.swapRemove(anal_unit);
zcu.resetUnit(anal_unit);
} else {
// We can trust the current information about this unit.
@@ -1733,15 +1724,13 @@ pub fn ensureFuncBodyUpToDate(
assert(func.ty == func.uncoerced_ty); // analyze the body of the original function, not a coerced one
- const was_outdated = zcu.outdated.swapRemove(anal_unit) or
- zcu.potentially_outdated.swapRemove(anal_unit) or
+ const was_outdated = zcu.clearOutdatedState(anal_unit) or
ip.setWantRuntimeFnAnalysis(zcu.comp.io, func_index);
const prev_failed = zcu.failed_analysis.contains(anal_unit) or zcu.transitive_failed_analysis.contains(anal_unit);
if (was_outdated) {
dev.check(.incremental);
- _ = zcu.outdated_ready.swapRemove(anal_unit);
zcu.resetUnit(anal_unit);
} else {
// We can trust the current information about this function.
@@ -2712,27 +2701,22 @@ const ScanDeclIter = struct {
const existing_unit = iter.existing_by_inst.get(tracked_inst);
- const unit, const want_analysis = switch (decl.kind) {
+ const unit: AnalUnit, const want_analysis = switch (decl.kind) {
.@"comptime" => unit: {
const cu = if (existing_unit) |eu|
eu.unwrap().@"comptime"
else
try ip.createComptimeUnit(gpa, io, pt.tid, tracked_inst, namespace_index);
- const unit: AnalUnit = .wrap(.{ .@"comptime" = cu });
-
try namespace.comptime_decls.append(gpa, cu);
if (existing_unit == null) {
// For a `comptime` declaration, whether to analyze is based solely on whether the unit
// is outdated. So, add this fresh one to `outdated` and `outdated_ready`.
- try zcu.outdated.ensureUnusedCapacity(gpa, 1);
- try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
- zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
- zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
+ try zcu.queueComptimeUnitAnalysis(cu);
}
- break :unit .{ unit, true };
+ break :unit .{ .wrap(.{ .@"comptime" = cu }), true };
},
else => unit: {
const name = maybe_name.unwrap().?;