From 0978566db8b7ed2b730cf01a7d359e9b52ec66ec Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 12 Mar 2026 12:22:58 +0000 Subject: [PATCH] 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. --- src/Compilation.zig | 8 +- src/IncrementalDebugServer.zig | 2 +- src/InternPool.zig | 33 ++++--- src/Sema.zig | 4 +- src/Zcu.zig | 6 +- src/Zcu/PerThread.zig | 52 +++++++--- test/incremental/add_remove_struct_fields | 98 +++++++++++++++++++ test/incremental/add_remove_toplevel_fields | 101 ++++++++++++++++++++ 8 files changed, 265 insertions(+), 39 deletions(-) create mode 100644 test/incremental/add_remove_struct_fields create mode 100644 test/incremental/add_remove_toplevel_fields diff --git a/src/Compilation.zig b/src/Compilation.zig index 9fbc36f070..53a2874990 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -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())); diff --git a/src/IncrementalDebugServer.zig b/src/IncrementalDebugServer.zig index 7d0dc8e89b..ed05ac2684 100644 --- a/src/IncrementalDebugServer.zig +++ b/src/IncrementalDebugServer.zig @@ -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)}), diff --git a/src/InternPool.zig b/src/InternPool.zig index 7a561d6ca2..3cdd94a942 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -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, diff --git a/src/Sema.zig b/src/Sema.zig index a6507865b7..8f53c16739 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -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 .{ diff --git a/src/Zcu.zig b/src/Zcu.zig index 0ab053b696..fa582bd9da 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -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); diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 4abfb5157c..ced926dc10 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -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 diff --git a/test/incremental/add_remove_struct_fields b/test/incremental/add_remove_struct_fields new file mode 100644 index 0000000000..f046229101 --- /dev/null +++ b/test/incremental/add_remove_struct_fields @@ -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("\n"); +} +const std = @import("std"); +const Writer = std.Io.Writer; +#expect_stdout="0 \n" diff --git a/test/incremental/add_remove_toplevel_fields b/test/incremental/add_remove_toplevel_fields new file mode 100644 index 0000000000..2d5483778a --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields @@ -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("\n"); +} +const std = @import("std"); +const Writer = std.Io.Writer; +#expect_stdout="0 \n"