incremental: handle loss of main struct instruction

My changes to how incremental compilation handles container types mean
that, at least for now, it is possible for the ZIR `.main_struct_inst`
of a source file to be lost (this happens if the number of top-level
fields in a file changes for instance). I missed a few things which
needed changing to account for this, which could lead to crashes with
certain (trivial) changes---oops!

Adds two new incremental test cases. They are currently disabled for
wasm32-wasi-selfhosted because they both trigger a crash in the WASM
backend.
This commit is contained in:
Matthew Lugg
2026-03-12 12:22:58 +00:00
committed by mlugg
parent 01cc1a5867
commit 0978566db8
8 changed files with 265 additions and 39 deletions
+4 -4
View File
@@ -3649,7 +3649,7 @@ const Header = extern struct {
type_layout_deps_len: u32,
struct_defaults_deps_len: u32,
func_ies_deps_len: u32,
zon_file_deps_len: u32,
source_file_deps_len: u32,
embed_file_deps_len: u32,
namespace_deps_len: u32,
namespace_name_deps_len: u32,
@@ -3699,7 +3699,7 @@ pub fn saveState(comp: *Compilation) !void {
.type_layout_deps_len = @intCast(ip.type_layout_deps.count()),
.struct_defaults_deps_len = @intCast(ip.struct_defaults_deps.count()),
.func_ies_deps_len = @intCast(ip.func_ies_deps.count()),
.zon_file_deps_len = @intCast(ip.zon_file_deps.count()),
.source_file_deps_len = @intCast(ip.source_file_deps.count()),
.embed_file_deps_len = @intCast(ip.embed_file_deps.count()),
.namespace_deps_len = @intCast(ip.namespace_deps.count()),
.namespace_name_deps_len = @intCast(ip.namespace_name_deps.count()),
@@ -3738,8 +3738,8 @@ pub fn saveState(comp: *Compilation) !void {
addBuf(&bufs, @ptrCast(ip.struct_defaults_deps.values()));
addBuf(&bufs, @ptrCast(ip.func_ies_deps.keys()));
addBuf(&bufs, @ptrCast(ip.func_ies_deps.values()));
addBuf(&bufs, @ptrCast(ip.zon_file_deps.keys()));
addBuf(&bufs, @ptrCast(ip.zon_file_deps.values()));
addBuf(&bufs, @ptrCast(ip.source_file_deps.keys()));
addBuf(&bufs, @ptrCast(ip.source_file_deps.values()));
addBuf(&bufs, @ptrCast(ip.embed_file_deps.keys()));
addBuf(&bufs, @ptrCast(ip.embed_file_deps.values()));
addBuf(&bufs, @ptrCast(ip.namespace_deps.keys()));
+1 -1
View File
@@ -305,7 +305,7 @@ fn handleCommand(zcu: *Zcu, w: *Io.Writer, cmd_str: []const u8, arg_str: []const
for (unit_info.deps.items, 0..) |dependee, i| {
try w.print("[{d}] ", .{i});
switch (dependee) {
.src_hash, .namespace, .namespace_name, .zon_file, .embed_file => try w.print("{f}", .{zcu.fmtDependee(dependee)}),
.src_hash, .namespace, .namespace_name, .source_file, .embed_file => try w.print("{f}", .{zcu.fmtDependee(dependee)}),
.nav_val, .nav_ty => |nav| try w.print("{t} {d}", .{ dependee, @intFromEnum(nav) }),
.type_layout, .struct_defaults, .func_ies => |ip_index| try w.print("{t} {d}", .{ dependee, @intFromEnum(ip_index) }),
.memoized_state => |stage| try w.print("memoized_state {s}", .{@tagName(stage)}),
+19 -14
View File
@@ -57,9 +57,14 @@ type_layout_deps: std.AutoArrayHashMapUnmanaged(Index, DepEntry.Index),
/// Dependencies on the resolved default field values of a `struct` type.
/// Value is index into `dep_entries` of the first dependency on this type's inits.
struct_defaults_deps: std.AutoArrayHashMapUnmanaged(Index, DepEntry.Index),
/// Dependencies on a ZON file. Triggered by `@import` of ZON.
/// Value is index into `dep_entries` of the first dependency on this ZON file.
zon_file_deps: std.AutoArrayHashMapUnmanaged(FileIndex, DepEntry.Index),
/// Dependencies on a Zig or ZON source file. Triggered by `@import`.
/// * For ZON source files, the dependency is invalidated if the file changes at all. The `@import`
/// must be re-analyzed to return the new data structure.
/// * For Zig source files, the dependency is invalidated if the file's root struct type changes
/// (which can only happen because the `.main_struct_inst` got lost). The `@import` must be
/// re-analyzed to return the new type.
/// Value is index into `dep_entries` of the first dependency on this Zig/ZON file.
source_file_deps: std.AutoArrayHashMapUnmanaged(FileIndex, DepEntry.Index),
/// Dependencies on an embedded file.
/// Introduced by `@embedFile`; invalidated when the file changes.
/// Value is index into `dep_entries` of the first dependency on this `Zcu.EmbedFile`.
@@ -112,7 +117,7 @@ pub const empty: InternPool = .{
.func_ies_deps = .empty,
.type_layout_deps = .empty,
.struct_defaults_deps = .empty,
.zon_file_deps = .empty,
.source_file_deps = .empty,
.embed_file_deps = .empty,
.namespace_deps = .empty,
.namespace_name_deps = .empty,
@@ -859,7 +864,7 @@ pub const Dependee = union(enum) {
func_ies: Index,
type_layout: Index,
struct_defaults: Index,
zon_file: FileIndex,
source_file: FileIndex,
embed_file: Zcu.EmbedFile.Index,
namespace: TrackedInst.Index,
namespace_name: NamespaceNameKey,
@@ -913,7 +918,7 @@ pub fn dependencyIterator(ip: *const InternPool, dependee: Dependee) DependencyI
.func_ies => |x| ip.func_ies_deps.get(x),
.type_layout => |x| ip.type_layout_deps.get(x),
.struct_defaults => |x| ip.struct_defaults_deps.get(x),
.zon_file => |x| ip.zon_file_deps.get(x),
.source_file => |x| ip.source_file_deps.get(x),
.embed_file => |x| ip.embed_file_deps.get(x),
.namespace => |x| ip.namespace_deps.get(x),
.namespace_name => |x| ip.namespace_name_deps.get(x),
@@ -988,7 +993,7 @@ pub fn addDependency(ip: *InternPool, gpa: Allocator, depender: AnalUnit, depend
.func_ies => ip.func_ies_deps,
.type_layout => ip.type_layout_deps,
.struct_defaults => ip.struct_defaults_deps,
.zon_file => ip.zon_file_deps,
.source_file => ip.source_file_deps,
.embed_file => ip.embed_file_deps,
.namespace => ip.namespace_deps,
.namespace_name => ip.namespace_name_deps,
@@ -6477,7 +6482,7 @@ pub fn deinit(ip: *InternPool, gpa: Allocator, io: Io) void {
ip.func_ies_deps.deinit(gpa);
ip.type_layout_deps.deinit(gpa);
ip.struct_defaults_deps.deinit(gpa);
ip.zon_file_deps.deinit(gpa);
ip.source_file_deps.deinit(gpa);
ip.embed_file_deps.deinit(gpa);
ip.namespace_deps.deinit(gpa);
ip.namespace_name_deps.deinit(gpa);
@@ -10643,7 +10648,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
const func_ies_deps_len = ip.func_ies_deps.count();
const type_layout_deps_len = ip.type_layout_deps.count();
const struct_defaults_deps_len = ip.struct_defaults_deps.count();
const zon_file_deps_len = ip.zon_file_deps.count();
const source_file_deps_len = ip.source_file_deps.count();
const embed_file_deps_len = ip.embed_file_deps.count();
const namespace_deps_len = ip.namespace_deps.count();
const namespace_name_deps_len = ip.namespace_name_deps.count();
@@ -10654,7 +10659,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
const func_ies_deps_size = func_ies_deps_len * 8;
const type_layout_deps_size = type_layout_deps_len * 8;
const struct_defaults_deps_size = struct_defaults_deps_len * 8;
const zon_file_deps_size = zon_file_deps_len * 8;
const source_file_deps_size = source_file_deps_len * 8;
const embed_file_deps_size = embed_file_deps_len * 8;
const namespace_deps_size = namespace_deps_len * 8;
const namespace_name_deps_size = namespace_name_deps_len * (@sizeOf(NamespaceNameKey) + 4);
@@ -10668,14 +10673,14 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
\\ {d} func_ies: {d} bytes
\\ {d} type_layout: {d} bytes
\\ {d} struct_defaults: {d} bytes
\\ {d} zon_file: {d} bytes
\\ {d} source_file: {d} bytes
\\ {d} embed_file: {d} bytes
\\ {d} namespace: {d} bytes
\\ {d} namespace_name: {d} bytes
\\
, .{
dep_entries_size + src_hash_deps_size + nav_val_deps_size + nav_ty_deps_size +
func_ies_deps_size + type_layout_deps_size + struct_defaults_deps_size + zon_file_deps_size +
func_ies_deps_size + type_layout_deps_size + struct_defaults_deps_size + source_file_deps_size +
embed_file_deps_size + namespace_deps_size + namespace_name_deps_size,
dep_entries_len,
dep_entries_size,
@@ -10691,8 +10696,8 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
type_layout_deps_size,
struct_defaults_deps_len,
struct_defaults_deps_size,
zon_file_deps_len,
zon_file_deps_size,
source_file_deps_len,
source_file_deps_size,
embed_file_deps_len,
embed_file_deps_size,
namespace_deps_len,
+2 -2
View File
@@ -13011,6 +13011,7 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
};
const file_index = result.file;
const file = zcu.fileByIndex(file_index);
try sema.declareDependency(.{ .source_file = file_index });
switch (file.getMode()) {
.zig => {
try pt.ensureFilePopulated(file_index);
@@ -13028,8 +13029,6 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
if (res_ty.isGenericPoison()) break :b .none;
break :b res_ty.toIntern();
};
try sema.declareDependency(.{ .zon_file = file_index });
const interned = try LowerZon.run(
sema,
file,
@@ -34084,6 +34083,7 @@ pub fn analyzeMemoizedState(sema: *Sema, stage: InternPool.MemoizedStateStage) C
// Get the main struct type of the root source file of `std`. No need for a reference entry
// because `std` is always an analysis root.
const std_file_index = zcu.module_roots.get(zcu.std_mod).?.unwrap().?;
try sema.declareDependency(.{ .source_file = std_file_index });
try pt.ensureFilePopulated(std_file_index);
const std_type: Type = .fromInterned(zcu.fileRootType(std_file_index));
break :block .{
+3 -3
View File
@@ -1014,7 +1014,7 @@ pub const File = struct {
/// changed -- this field is just a simple boolean.
///
/// When `zoir` is updated, this field is set to `true`. In `updateZirRefs`, if this is `true`,
/// we invalidate the corresponding `zon_file` dependency, and reset it to `false`.
/// we invalidate the corresponding `source_file` dependency, and reset it to `false`.
zoir_invalidated: bool,
pub const Path = struct {
@@ -4496,9 +4496,9 @@ fn formatDependee(data: FormatDependee, writer: *Io.Writer) Io.Writer.Error!void
const fqn = ip.getNav(ip.indexToKey(ip_index).func.owner_nav).fqn;
return writer.print("func_ies('{f}')", .{fqn.fmt(ip)});
},
.zon_file => |file| {
.source_file => |file| {
const file_path = zcu.fileByIndex(file).path;
return writer.print("zon_file('{f}')", .{file_path.fmt(zcu.comp)});
return writer.print("source_file('{f}')", .{file_path.fmt(zcu.comp)});
},
.embed_file => |ef_idx| {
const ef = ef_idx.get(zcu);
+37 -15
View File
@@ -838,7 +838,7 @@ fn updateZirRefs(pt: Zcu.PerThread) (Io.Cancelable || Allocator.Error)!void {
.zig => {}, // logic below
.zon => {
if (file.zoir_invalidated) {
try zcu.markDependeeOutdated(.not_marked_po, .{ .zon_file = file_index });
try zcu.markDependeeOutdated(.not_marked_po, .{ .source_file = file_index });
file.zoir_invalidated = false;
}
continue;
@@ -988,8 +988,8 @@ fn updateZirRefs(pt: Zcu.PerThread) (Io.Cancelable || Allocator.Error)!void {
// be re-analyzed (causing the struct's namespace to be re-scanned). It's fine to do this
// now because this work is fast (no actual Sema work is happening, we're just updating the
// namespace contents). We must do this after updating ZIR refs above, since `scanNamespace`
// will track some instructions.
try pt.updateFileNamespace(file_index);
// calls will track some instructions.
try pt.updateFileRootStructType(file_index);
}
}
@@ -2350,27 +2350,49 @@ fn analyzeFuncBody(
return .{ .ies_outdated = ies_outdated };
}
/// Re-scan the namespace of a file's root struct type on an incremental update.
/// The file must have successfully populated ZIR.
/// If the file's root struct type is not populated (the file is unreferenced), nothing is done.
/// This is called by `updateZirRefs` for all updated files before the main work loop.
/// This function does not perform any semantic analysis.
fn updateFileNamespace(pt: Zcu.PerThread, file_index: Zcu.File.Index) Allocator.Error!void {
/// The given file has been modified on this incremental update, so if it has a populated root
/// struct type, either re-scan its namespace, or clear it and invalidate dependencies if the
/// type is no longer valid. See comments in body for more details.
///
/// Called by `updateZirRefs` for all updated Zig source files before the main update loop.
///
/// Asserts that the file has successfully populated ZIR.
fn updateFileRootStructType(pt: Zcu.PerThread, file_index: Zcu.File.Index) Allocator.Error!void {
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const file = zcu.fileByIndex(file_index);
const file_root_type = zcu.fileRootType(file_index);
if (file_root_type == .none) return;
if (file_root_type == .none) {
// We haven't analyzed any `@import` of this file so far, so there's nothing to update. If
// an `@import` gets analyzed, then `ensureFilePopulated` will create the root struct type
// and scan the namespace.
return;
}
log.debug("updateFileNamespace mod={s} sub_file_path={s}", .{
const loaded_struct = ip.loadStructType(file_root_type);
log.debug("updateFileRootStructType mod={s} sub_file_path={s}", .{
file.mod.?.fully_qualified_name,
file.sub_file_path,
});
const namespace_index = Type.fromInterned(file_root_type).getNamespaceIndex(zcu);
const decls = file.zir.?.getStructDecl(.main_struct_inst).decls;
try pt.scanNamespace(namespace_index, decls);
zcu.namespacePtr(namespace_index).generation = zcu.generation;
if (loaded_struct.zir_index.resolve(ip) == null) {
// The file's root struct decl has been lost, so a new struct type must be interned at a new
// `InternPool.Index`. Clear the file's root type so that `ensureFilePopulated` will do that
// work, and invalidate dependencies on this file to force re-analysis of `@import` sites.
zcu.setFileRootType(file_index, .none);
try zcu.markDependeeOutdated(.not_marked_po, .{ .source_file = file_index });
} else {
// The existing struct type is valid, but the namespace contents might have changed. For
// most struct types, that would cause the surrounding declaration to be invalidated which
// causes `Sema.zirStructType` (or whatever) to call `ensureNamespaceUpToDate`. However,
// there is no "surrounding declaration" for the root struct type of a Zig source file, so
// update this namespace now.
const decls = file.zir.?.getStructDecl(.main_struct_inst).decls;
try pt.scanNamespace(loaded_struct.namespace, decls);
zcu.namespacePtr(loaded_struct.namespace).generation = zcu.generation;
}
}
/// Called by AstGen worker threads when an import is seen. If `new_file` is returned, the caller is
+98
View File
@@ -0,0 +1,98 @@
#target=x86_64-linux-selfhosted
#target=x86_64-windows-selfhosted
#target=x86_64-linux-cbe
#target=x86_64-windows-cbe
//#target=wasm32-wasi-selfhosted
#update=initial version
#file=main.zig
const S = struct { x: u8 };
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="1 100\n"
#update=add a field
#file=main.zig
const S = struct { x: u8, y: u16 = 200 };
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="2 100\n"
#update=remove all fields
#file=main.zig
const S = struct {};
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_error=main.zig:15:24: error: no field named 'x' in struct 'main.S'
#expect_error=main.zig:1:11: note: struct declared here
#update=remove reference to non-existent field
#file=main.zig
const S = struct {};
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
//const val: S = .{ .x = 100 };
//try w.print("{d}\n", .{val.x});
try w.writeAll("<no fields>\n");
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="0 <no fields>\n"
+101
View File
@@ -0,0 +1,101 @@
#target=x86_64-linux-selfhosted
#target=x86_64-windows-selfhosted
#target=x86_64-linux-cbe
#target=x86_64-windows-cbe
//#target=wasm32-wasi-selfhosted
#update=initial version
#file=main.zig
const S = @This();
x: u8,
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="1 100\n"
#update=add a field
#file=main.zig
const S = @This();
x: u8,
y: u16 = 200,
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="2 100\n"
#update=remove all fields
#file=main.zig
const S = @This();
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
const val: S = .{ .x = 100 };
try w.print("{d}\n", .{val.x});
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_error=main.zig:15:24: error: no field named 'x' in struct 'main'
#expect_error=main.zig:1:1: note: struct declared here
#update=remove reference to non-existent field
#file=main.zig
const S = @This();
pub fn main(init: std.process.Init) !void {
var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{});
printFieldCount(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
printOneField(&stdout_writer.interface) catch |err| switch (err) {
error.WriteFailed => return stdout_writer.err.?,
};
}
fn printFieldCount(w: *Writer) Writer.Error!void {
try w.print("{d} ", .{@typeInfo(S).@"struct".fields.len});
}
fn printOneField(w: *Writer) Writer.Error!void {
//const val: S = .{ .x = 100 };
//try w.print("{d}\n", .{val.x});
try w.writeAll("<no fields>\n");
}
const std = @import("std");
const Writer = std.Io.Writer;
#expect_stdout="0 <no fields>\n"