commit 0978566db8b7ed2b730cf01a7d359e9b52ec66ec (tree)
parent 01cc1a58675806b72580e094609f8dde0019d6ea
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Thu, 12 Mar 2026 12:22:58 +0000
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.
Diffstat:
8 files changed, 265 insertions(+), 39 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
diff --git a/test/incremental/add_remove_toplevel_fields 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("<no fields>\n");
+}
+const std = @import("std");
+const Writer = std.Io.Writer;
+#expect_stdout="0 <no fields>\n"