compiler: split default field values back out from layout resolution

I was trying out combining struct layout resolution with resolution of
default field values, but it broke a few cases which it's not clear we
want to break. The simplest such case was a struct with a field which
was a slice of itself, with a default value of `&.{}`.

So, at least for now, I'm accepting defeat and splitting this back out.
This allows a couple of behavior tests which were removed to be
re-introduced---I will do that in the commit following this one.

I have *not* made this separate phase of resolution "lazy": instead, it
is tied to layout resolution, in the sense that if a struct's layout is
referenced, then its default field values are also referenced. I chose
this approach for simplicity---not of the implementation (it's actually
slightly *more* code to do it this way!), but in terms of the language
specification. I think this behavior is easier to understand and keep in
your head. It can be easily changed in future if we decide we want to.

This partially reverts the commit titled "compiler: merge struct default
value resolution into layout resolution".
This commit is contained in:
Matthew Lugg 2026-02-27 10:40:12 +00:00
parent 7d54db2888
commit 5768b0542b
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
9 changed files with 396 additions and 72 deletions

View file

@ -3647,6 +3647,7 @@ const Header = extern struct {
nav_val_deps_len: u32,
nav_ty_deps_len: u32,
type_layout_deps_len: u32,
struct_defaults_deps_len: u32,
func_ies_deps_len: u32,
zon_file_deps_len: u32,
embed_file_deps_len: u32,
@ -3696,6 +3697,7 @@ pub fn saveState(comp: *Compilation) !void {
.nav_val_deps_len = @intCast(ip.nav_val_deps.count()),
.nav_ty_deps_len = @intCast(ip.nav_ty_deps.count()),
.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()),
.embed_file_deps_len = @intCast(ip.embed_file_deps.count()),
@ -3720,7 +3722,7 @@ pub fn saveState(comp: *Compilation) !void {
},
});
try bufs.ensureTotalCapacityPrecise(24 + 9 * pt_headers.items.len);
try bufs.ensureTotalCapacityPrecise(26 + 9 * pt_headers.items.len);
addBuf(&bufs, mem.asBytes(&header));
addBuf(&bufs, @ptrCast(pt_headers.items));
@ -3732,6 +3734,8 @@ pub fn saveState(comp: *Compilation) !void {
addBuf(&bufs, @ptrCast(ip.nav_ty_deps.values()));
addBuf(&bufs, @ptrCast(ip.type_layout_deps.keys()));
addBuf(&bufs, @ptrCast(ip.type_layout_deps.values()));
addBuf(&bufs, @ptrCast(ip.struct_defaults_deps.keys()));
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()));

View file

@ -307,7 +307,7 @@ fn handleCommand(zcu: *Zcu, w: *Io.Writer, cmd_str: []const u8, arg_str: []const
switch (dependee) {
.src_hash, .namespace, .namespace_name, .zon_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, .func_ies => |ip_index| try w.print("{t} {d}", .{ dependee, @intFromEnum(ip_index) }),
.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)}),
}
try w.writeByte('\n');
@ -374,6 +374,8 @@ fn parseAnalUnit(str: []const u8) ?AnalUnit {
return .wrap(.{ .nav_ty = @enumFromInt(parseIndex(idx_str) orelse return null) });
} else if (std.mem.eql(u8, kind, "type_layout")) {
return .wrap(.{ .type_layout = @enumFromInt(parseIndex(idx_str) orelse return null) });
} else if (std.mem.eql(u8, kind, "struct_defaults")) {
return .wrap(.{ .struct_defaults = @enumFromInt(parseIndex(idx_str) orelse return null) });
} else if (std.mem.eql(u8, kind, "func")) {
return .wrap(.{ .func = @enumFromInt(parseIndex(idx_str) orelse return null) });
} else if (std.mem.eql(u8, kind, "memoized_state")) {

View file

@ -54,6 +54,9 @@ func_ies_deps: std.AutoArrayHashMapUnmanaged(Index, DepEntry.Index),
/// Dependencies on the resolved layout of a `struct`, `union`, or `enum` type.
/// Value is index into `dep_entries` of the first dependency on this type's layout.
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),
@ -108,6 +111,7 @@ pub const empty: InternPool = .{
.nav_ty_deps = .empty,
.func_ies_deps = .empty,
.type_layout_deps = .empty,
.struct_defaults_deps = .empty,
.zon_file_deps = .empty,
.embed_file_deps = .empty,
.namespace_deps = .empty,
@ -419,6 +423,7 @@ pub const AnalUnit = packed struct(u64) {
nav_val,
nav_ty,
type_layout,
struct_defaults,
func,
memoized_state,
};
@ -432,6 +437,8 @@ pub const AnalUnit = packed struct(u64) {
nav_ty: Nav.Index,
/// This `AnalUnit` resolves the layout of the given `struct`, `union`, or `enum` type.
type_layout: InternPool.Index,
/// This `AnalUnit` resolves the default field values of the given `struct` type.
struct_defaults: InternPool.Index,
/// This `AnalUnit` analyzes the body of the given runtime function.
func: InternPool.Index,
/// This `AnalUnit` resolves all state which is memoized in fields on `Zcu`.
@ -851,6 +858,7 @@ pub const Dependee = union(enum) {
/// Index is the function, not its IES.
func_ies: Index,
type_layout: Index,
struct_defaults: Index,
zon_file: FileIndex,
embed_file: Zcu.EmbedFile.Index,
namespace: TrackedInst.Index,
@ -904,6 +912,7 @@ pub fn dependencyIterator(ip: *const InternPool, dependee: Dependee) DependencyI
.nav_ty => |x| ip.nav_ty_deps.get(x),
.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),
.embed_file => |x| ip.embed_file_deps.get(x),
.namespace => |x| ip.namespace_deps.get(x),
@ -978,6 +987,7 @@ pub fn addDependency(ip: *InternPool, gpa: Allocator, depender: AnalUnit, depend
.nav_ty => ip.nav_ty_deps,
.func_ies => ip.func_ies_deps,
.type_layout => ip.type_layout_deps,
.struct_defaults => ip.struct_defaults_deps,
.zon_file => ip.zon_file_deps,
.embed_file => ip.embed_file_deps,
.namespace => ip.namespace_deps,
@ -6454,6 +6464,7 @@ pub fn deinit(ip: *InternPool, gpa: Allocator, io: Io) void {
ip.nav_ty_deps.deinit(gpa);
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.embed_file_deps.deinit(gpa);
ip.namespace_deps.deinit(gpa);
@ -10619,6 +10630,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
const nav_ty_deps_len = ip.nav_ty_deps.count();
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 embed_file_deps_len = ip.embed_file_deps.count();
const namespace_deps_len = ip.namespace_deps.count();
@ -10629,6 +10641,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
const nav_ty_deps_size = nav_ty_deps_len * 8;
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 embed_file_deps_size = embed_file_deps_len * 8;
const namespace_deps_size = namespace_deps_len * 8;
@ -10642,6 +10655,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
\\ {d} nav_ty: {d} bytes
\\ {d} func_ies: {d} bytes
\\ {d} type_layout: {d} bytes
\\ {d} struct_defaults: {d} bytes
\\ {d} zon_file: {d} bytes
\\ {d} embed_file: {d} bytes
\\ {d} namespace: {d} bytes
@ -10649,7 +10663,7 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
\\
, .{
dep_entries_size + src_hash_deps_size + nav_val_deps_size + nav_ty_deps_size +
func_ies_deps_size + type_layout_deps_size + zon_file_deps_size +
func_ies_deps_size + type_layout_deps_size + struct_defaults_deps_size + zon_file_deps_size +
embed_file_deps_size + namespace_deps_size + namespace_name_deps_size,
dep_entries_len,
dep_entries_size,
@ -10663,6 +10677,8 @@ fn dumpDependencyStatsFallible(ip: *const InternPool, w: *Io.Writer) !void {
func_ies_deps_size,
type_layout_deps_len,
type_layout_deps_size,
struct_defaults_deps_len,
struct_defaults_deps_size,
zon_file_deps_len,
zon_file_deps_size,
embed_file_deps_len,

View file

@ -4533,6 +4533,10 @@ fn validateStructInit(
if (explicit) continue;
if (struct_ty.structFieldIsComptime(i, zcu)) continue;
if (!struct_ty.isTuple(zcu)) {
try sema.ensureStructDefaultsResolved(struct_ty, init_src);
}
const default_val = struct_ty.structFieldDefaultValue(i, zcu) orelse {
const field_name = struct_ty.structFieldName(i, zcu).unwrap() orelse {
const template = "missing tuple field with index {d}";
@ -5850,6 +5854,7 @@ fn zirDisableInstrumentation(sema: *Sema) CompileError!void {
.nav_val,
.nav_ty,
.type_layout,
.struct_defaults,
.memoized_state,
=> return, // does nothing outside a function
};
@ -5868,6 +5873,7 @@ fn zirDisableIntrinsics(sema: *Sema) CompileError!void {
.nav_val,
.nav_ty,
.type_layout,
.struct_defaults,
.memoized_state,
=> return, // does nothing outside a function
};
@ -7091,7 +7097,14 @@ fn analyzeCall(
});
if (func_ty_info.cc == .auto) {
switch (sema.owner.unwrap()) {
.@"comptime", .nav_ty, .nav_val, .type_layout, .memoized_state => {},
.@"comptime",
.nav_ty,
.nav_val,
.type_layout,
.struct_defaults,
.memoized_state,
=> {},
.func => |owner_func| ip.funcSetHasErrorTrace(io, owner_func, true),
}
}
@ -16907,6 +16920,7 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
.struct_type => ip.loadStructType(ty.toIntern()),
else => unreachable,
};
try sema.ensureStructDefaultsResolved(ty, src); // can't do this sooner, since it's not allowed on tuples
struct_field_vals = try gpa.alloc(InternPool.Index, struct_type.field_types.len);
for (struct_field_vals, 0..) |*field_val, field_index| {
@ -18788,6 +18802,8 @@ fn finishStructInit(
continue;
}
try sema.ensureStructDefaultsResolved(struct_ty, init_src);
const field_default: InternPool.Index = d: {
if (struct_type.field_defaults.len == 0) break :d .none;
break :d struct_type.field_defaults.get(ip)[i];
@ -19454,7 +19470,14 @@ fn getErrorReturnTrace(sema: *Sema, block: *Block) CompileError!Air.Inst.Ref {
.func => |func| if (ip.funcAnalysisUnordered(func).has_error_trace and block.ownerModule().error_tracing) {
return block.addTy(.err_return_trace, opt_ptr_stack_trace_ty);
},
.@"comptime", .nav_ty, .nav_val, .type_layout, .memoized_state => {},
.@"comptime",
.nav_ty,
.nav_val,
.type_layout,
.struct_defaults,
.memoized_state,
=> {},
}
return Air.internedToRef(try pt.intern(.{ .opt = .{
.ty = opt_ptr_stack_trace_ty.toIntern(),
@ -24784,7 +24807,7 @@ fn zirBuiltinExtern(
// So, for now, just use our containing `declaration`.
.zir_index = switch (sema.owner.unwrap()) {
.@"comptime" => |cu| ip.getComptimeUnit(cu).zir_index,
.type_layout => |owner_ty| Type.fromInterned(owner_ty).typeDeclInstAllowGeneratedTag(zcu).?,
.type_layout, .struct_defaults => |owner_ty| Type.fromInterned(owner_ty).typeDeclInstAllowGeneratedTag(zcu).?,
.memoized_state => unreachable,
.nav_ty, .nav_val => |nav| ip.getNav(nav).analysis.?.zir_index,
.func => |func| zir_index: {
@ -25276,7 +25299,14 @@ fn getPanicIdFunc(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !In
try sema.ensureMemoizedStateResolved(src, .panic);
const panic_fn_index = zcu.builtin_decl_values.get(panic_id.toBuiltin());
switch (sema.owner.unwrap()) {
.@"comptime", .nav_ty, .nav_val, .type_layout, .memoized_state => {},
.@"comptime",
.nav_ty,
.nav_val,
.type_layout,
.struct_defaults,
.memoized_state,
=> {},
.func => |owner_func| zcu.intern_pool.funcSetHasErrorTrace(io, owner_func, true),
}
return panic_fn_index;
@ -33541,23 +33571,6 @@ pub fn declareDependency(sema: *Sema, dependee: InternPool.Dependee) !void {
const gop = try sema.dependencies.getOrPut(sema.gpa, dependee);
if (gop.found_existing) return;
// Avoid creating dependencies on ourselves. This situation can arise when we analyze the fields
// of a type and they use `@This()`. This dependency would be unnecessary, and in fact would
// just result in over-analysis since `Zcu.findOutdatedToAnalyze` would never be able to resolve
// the loop.
// Note that this also disallows a `nav_val`
switch (sema.owner.unwrap()) {
.nav_val => |this_nav| switch (dependee) {
.nav_val => |other_nav| if (this_nav == other_nav) return,
else => {},
},
.nav_ty => |this_nav| switch (dependee) {
.nav_ty => |other_nav| if (this_nav == other_nav) return,
else => {},
},
else => {},
}
try pt.addDependency(sema.owner, dependee);
}
@ -33923,6 +33936,7 @@ const ComptimeStoreResult = @import("Sema/comptime_ptr_access.zig").ComptimeStor
pub const type_resolution = @import("Sema/type_resolution.zig");
pub const ensureLayoutResolved = type_resolution.ensureLayoutResolved;
pub const ensureStructDefaultsResolved = type_resolution.ensureStructDefaultsResolved;
pub fn getBuiltinType(sema: *Sema, src: LazySrcLoc, decl: Zcu.BuiltinDecl) SemaError!Type {
assert(decl.kind() == .type);

View file

@ -758,6 +758,7 @@ fn lowerStruct(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool
const ip = &pt.zcu.intern_pool;
try self.sema.ensureLayoutResolved(res_ty, self.import_loc, .init);
try self.sema.ensureStructDefaultsResolved(res_ty, self.import_loc);
const struct_info = self.sema.pt.zcu.typeToStruct(res_ty).?;
const fields: @FieldType(Zoir.Node, "struct_literal") = switch (node.get(self.file.zoir.?)) {

View file

@ -134,6 +134,33 @@ fn ensureLayoutResolvedInner(sema: *Sema, ty: Type, orig_ty: Type, reason: *cons
}
}
/// Asserts that `ty` is a non-tuple `struct` type, and ensures that its fields' default values
/// are resolved. Adds incremental dependencies tracking the required type resolution.
///
/// It is not necessary to call this function to query the values of comptime fields: those values
/// are available from type *layout* resolution, see `ensureLayoutResolved`.
///
/// Asserts that the *layout* of `ty` has already been resolved---see `ensureLayoutResolved`.
pub fn ensureStructDefaultsResolved(sema: *Sema, ty: Type, src: LazySrcLoc) SemaError!void {
const pt = sema.pt;
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
assert(ip.indexToKey(ty.toIntern()) == .struct_type);
if (zcu.comp.config.incremental) assert(sema.dependencies.contains(.{ .type_layout = ty.toIntern() }));
try sema.declareDependency(.{ .struct_defaults = ty.toIntern() });
try sema.addReferenceEntry(null, src, .wrap(.{ .struct_defaults = ty.toIntern() }));
const reason: Zcu.DependencyReason = .{ .src = src, .type_layout_reason = undefined };
if (zcu.analysis_in_progress.contains(.wrap(.{ .struct_defaults = ty.toIntern() }))) {
return sema.failWithDependencyLoop(.wrap(.{ .struct_defaults = ty.toIntern() }), &reason);
}
try pt.ensureStructDefaultsUpToDate(ty, &reason);
}
/// Asserts that `struct_ty` is a non-packed non-tuple struct, and that `sema.owner` is that type.
/// This function *does* register the `src_hash` dependency on the struct.
pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
@ -193,14 +220,8 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
@memset(struct_obj.field_is_comptime_bits.getAll(ip), 0);
const zir_struct = sema.code.getStructDecl(zir_index);
// If we have any default values to resolve, we'll need to map the struct decl instruction
// to the result type.
if (zir_struct.field_default_body_lens != null) {
try sema.inst_map.ensureSpaceForInstructions(gpa, &.{zir_index});
}
var field_it = zir_struct.iterateFields();
var any_comptime_fields = false;
while (field_it.next()) |zir_field| {
{
const name_slice = sema.code.nullTerminatedString(zir_field.name);
@ -212,18 +233,21 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
const bit_bag_index = zir_field.idx / 32;
const mask = @as(u32, 1) << @intCast(zir_field.idx % 32);
struct_obj.field_is_comptime_bits.getAll(ip)[bit_bag_index] |= mask;
any_comptime_fields = true;
}
const field_ty: Type = field_ty: {
{
const field_ty_src = block.src(.{ .container_field_type = zir_field.idx });
block.comptime_reason = .{ .reason = .{
.src = field_ty_src,
.r = .{ .simple = .struct_field_types },
} };
const type_ref = try sema.resolveInlineBody(&block, zir_field.type_body, zir_index);
break :field_ty try sema.analyzeAsType(&block, field_ty_src, .struct_field_types, type_ref);
};
struct_obj.field_types.get(ip)[zir_field.idx] = field_ty.toIntern();
const field_ty: Type = field_ty: {
block.comptime_reason = .{ .reason = .{
.src = field_ty_src,
.r = .{ .simple = .struct_field_types },
} };
const type_ref = try sema.resolveInlineBody(&block, zir_field.type_body, zir_index);
break :field_ty try sema.analyzeAsType(&block, field_ty_src, .struct_field_types, type_ref);
};
struct_obj.field_types.get(ip)[zir_field.idx] = field_ty.toIntern();
}
if (struct_obj.field_aligns.len == 0) {
assert(zir_field.align_body == null);
@ -240,31 +264,13 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
};
struct_obj.field_aligns.get(ip)[zir_field.idx] = field_align;
}
}
if (struct_obj.field_defaults.len == 0) {
assert(zir_field.default_body == null);
} else {
const field_default_src = block.src(.{ .container_field_value = zir_field.idx });
const field_default: InternPool.Index = d: {
block.comptime_reason = .{ .reason = .{
.src = field_default_src,
.r = .{ .simple = .struct_field_default_value },
} };
const default_body = zir_field.default_body orelse break :d .none;
// Provide the result type
sema.inst_map.putAssumeCapacity(zir_index, .fromType(field_ty));
defer assert(sema.inst_map.remove(zir_index));
const uncoerced_default_val = try sema.resolveInlineBody(&block, default_body, zir_index);
const coerced_default_val = try sema.coerce(&block, field_ty, uncoerced_default_val, field_default_src);
const default_val = try sema.resolveConstValue(&block, field_default_src, coerced_default_val, null);
if (default_val.canMutateComptimeVarState(zcu)) {
const field_name = struct_obj.field_names.get(ip)[zir_field.idx];
return sema.failWithContainsReferenceToComptimeVar(&block, field_default_src, field_name, "field default value", default_val);
}
break :d default_val.toIntern();
};
struct_obj.field_defaults.get(ip)[zir_field.idx] = field_default;
}
// We also resolve the default values of any `comptime` fields now. This is not necessary in
// the case of a reified struct because the the default values were already poulated and
// validated by `Sema.zirReifyStruct`.
if (any_comptime_fields) {
try resolveStructDefaultsInner(sema, &block, &struct_obj, .comptime_fields);
}
}
@ -523,6 +529,118 @@ fn resolvePackedStructLayout(
);
}
/// Asserts that `struct_ty` is a non-tuple struct, and that `sema.owner` is that type.
///
/// Also asserts that the layout of `struct_ty` has *already* been resolved (though it is okay for
/// that resolution to have failed). This requirement exists to ensure better error messages in the
/// event of a dependency loop.
///
/// This function *does* register the `src_hash` dependency on the struct.
pub fn resolveStructDefaults(sema: *Sema, struct_ty: Type) CompileError!void {
const pt = sema.pt;
const zcu = pt.zcu;
const comp = zcu.comp;
const gpa = comp.gpa;
const ip = &zcu.intern_pool;
assert(sema.owner.unwrap().struct_defaults == struct_ty.toIntern());
// We always depend on the layout of `struct_ty`. However, we don't actually need to resolve it
// now, because the caller has done so for us. Just mark the dependency so that the incremental
// compilation handling understands the dependency graph.
try sema.declareDependency(.{ .type_layout = struct_ty.toIntern() });
struct_ty.assertHasLayout(zcu);
const layout_unit: InternPool.AnalUnit = .wrap(.{ .type_layout = struct_ty.toIntern() });
if (zcu.failed_analysis.contains(layout_unit) or zcu.transitive_failed_analysis.contains(layout_unit)) {
return error.AnalysisFail;
}
const struct_obj = ip.loadStructType(struct_ty.toIntern());
assert(struct_obj.want_layout);
if (struct_obj.is_reified) {
// `Sema.zirReifyStruct` has already populated the default field values *and* (by loading
// the default values from pointers) validated their types, so we have nothing to do.
return;
}
try sema.declareDependency(.{ .src_hash = struct_obj.zir_index });
if (struct_obj.field_defaults.len == 0) {
// The struct has no default field values, so the slice has been omitted.
return;
}
var block: Block = .{
.parent = null,
.sema = sema,
.namespace = struct_obj.namespace,
.instructions = .empty,
.inlining = null,
.comptime_reason = undefined, // always set before using `block`
.src_base_inst = struct_obj.zir_index,
.type_name_ctx = struct_obj.name,
};
defer block.instructions.deinit(gpa);
return resolveStructDefaultsInner(sema, &block, &struct_obj, .normal_fields);
}
/// Asserts that the struct is not reified, and that `struct_obj.field_defaults.len` is non-zero.
fn resolveStructDefaultsInner(
sema: *Sema,
block: *Block,
struct_obj: *const InternPool.LoadedStructType,
mode: enum { comptime_fields, normal_fields },
) CompileError!void {
const pt = sema.pt;
const zcu = pt.zcu;
const comp = zcu.comp;
const gpa = comp.gpa;
const ip = &zcu.intern_pool;
assert(struct_obj.field_defaults.len > 0);
// We'll need to map the struct decl instruction to provide result types
const zir_index = struct_obj.zir_index.resolve(ip) orelse return error.AnalysisFail;
try sema.inst_map.ensureSpaceForInstructions(gpa, &.{zir_index});
const field_types = struct_obj.field_types.get(ip);
const zir_struct = sema.code.getStructDecl(zir_index);
var field_it = zir_struct.iterateFields();
while (field_it.next()) |zir_field| {
switch (mode) {
.comptime_fields => if (!zir_field.is_comptime) continue,
.normal_fields => if (zir_field.is_comptime) continue,
}
const default_val_src = block.src(.{ .container_field_value = zir_field.idx });
block.comptime_reason = .{ .reason = .{
.src = default_val_src,
.r = .{ .simple = .struct_field_default_value },
} };
const default_body = zir_field.default_body orelse {
struct_obj.field_defaults.get(ip)[zir_field.idx] = .none;
continue;
};
const field_ty: Type = .fromInterned(field_types[zir_field.idx]);
const uncoerced = ref: {
// Provide the result type
sema.inst_map.putAssumeCapacity(zir_index, .fromIntern(field_ty.toIntern()));
defer assert(sema.inst_map.remove(zir_index));
break :ref try sema.resolveInlineBody(block, default_body, zir_index);
};
const coerced = try sema.coerce(block, field_ty, uncoerced, default_val_src);
const default_val = try sema.resolveConstValue(block, default_val_src, coerced, null);
if (default_val.canMutateComptimeVarState(zcu)) {
const field_name = struct_obj.field_names.get(ip)[zir_field.idx];
return sema.failWithContainsReferenceToComptimeVar(block, default_val_src, field_name, "field default value", default_val);
}
struct_obj.field_defaults.get(ip)[zir_field.idx] = default_val.toIntern();
}
}
/// This logic must be kept in sync with `Type.getUnionLayout`.
pub fn resolveUnionLayout(sema: *Sema, union_ty: Type) CompileError!void {
const pt = sema.pt;

View file

@ -3175,6 +3175,7 @@ fn markPoDependeeUpToDateInner(zcu: *Zcu, dependee: InternPool.Dependee) !void {
.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 }),
.struct_defaults => |ty| try zcu.markPoDependeeUpToDateInner(.{ .struct_defaults = ty }),
.func => |func| try zcu.markPoDependeeUpToDateInner(.{ .func_ies = func }),
.memoized_state => |stage| try zcu.markPoDependeeUpToDateInner(.{ .memoized_state = stage }),
}
@ -3193,6 +3194,7 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
.nav_val => |nav| .{ .nav_val = nav },
.nav_ty => |nav| .{ .nav_ty = nav },
.type_layout => |ty| .{ .type_layout = ty },
.struct_defaults => |ty| .{ .struct_defaults = ty },
.func => |func_index| .{ .func_ies = func_index },
.memoized_state => |stage| .{ .memoized_state = stage },
};
@ -4249,11 +4251,18 @@ fn resolveReferencesInner(zcu: *Zcu) Allocator.Error!std.AutoArrayHashMapUnmanag
unit_idx += 1;
// `nav_val` and `nav_ty` reference each other *implicitly* to save memory.
// Likewise for `type_layout` and `struct_defaults` of a struct type.
queue_paired: {
const other: AnalUnit = .wrap(switch (unit.unwrap()) {
.nav_val => |n| .{ .nav_ty = n },
.nav_ty => |n| .{ .nav_val = n },
.@"comptime", .type_layout, .func, .memoized_state => break :queue_paired,
.struct_defaults => |ty| .{ .type_layout = ty },
.type_layout => |ty| switch (ip.indexToKey(ty)) {
.struct_type => .{ .struct_defaults = ty },
.union_type, .enum_type, .opaque_type => break :queue_paired,
else => unreachable,
},
.@"comptime", .func, .memoized_state => break :queue_paired,
});
const gop = try units.getOrPut(gpa, other);
if (gop.found_existing) break :queue_paired;
@ -4406,7 +4415,7 @@ fn formatAnalUnit(data: FormatAnalUnit, writer: *Io.Writer) Io.Writer.Error!void
}
},
.nav_val, .nav_ty => |nav, tag| return writer.print("{t}('{f}' [{}])", .{ tag, ip.getNav(nav).fqn.fmt(ip), @intFromEnum(nav) }),
.type_layout => |ty, tag| return writer.print("{t}('{f}' [{}])", .{ tag, Type.fromInterned(ty).containerTypeName(ip).fmt(ip), @intFromEnum(ty) }),
.type_layout, .struct_defaults => |ty, tag| return writer.print("{t}('{f}' [{}])", .{ tag, Type.fromInterned(ty).containerTypeName(ip).fmt(ip), @intFromEnum(ty) }),
.func => |func| {
const nav = zcu.funcInfo(func).owner_nav;
return writer.print("func('{f}' [{}])", .{ ip.getNav(nav).fqn.fmt(ip), @intFromEnum(func) });
@ -4431,7 +4440,7 @@ fn formatDependee(data: FormatDependee, writer: *Io.Writer) Io.Writer.Error!void
const fqn = ip.getNav(nav).fqn;
return writer.print("{t}('{f}')", .{ tag, fqn.fmt(ip) });
},
.type_layout => |ip_index, tag| {
.type_layout, .struct_defaults => |ip_index, tag| {
const name = Type.fromInterned(ip_index).containerTypeName(ip);
return writer.print("{t}('{f}')", .{ tag, name.fmt(ip) });
},
@ -4920,6 +4929,10 @@ fn addDependencyLoopErrorLine(
fmt_source,
dep_node.reason.type_layout_reason.msg(),
}),
.struct_defaults => |ty| try eb.printString(
"default field values of '{f}' depend on themselves for initialization here",
.{Type.fromInterned(ty).containerTypeName(ip).fmt(ip)},
),
} else switch (dep_node.unit.unwrap()) {
.@"comptime" => unreachable, // cannot be involved in a dependency loop
.nav_val => |nav| try eb.printString("{f} uses value of declaration '{f}' here", .{
@ -4940,6 +4953,10 @@ fn addDependencyLoopErrorLine(
Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
dep_node.reason.type_layout_reason.msg(),
}),
.struct_defaults => |ty| try eb.printString(
"{f} uses default field values of '{f}' here",
.{ fmt_source, Type.fromInterned(ty).containerTypeName(ip).fmt(ip) },
),
};
const src_loc = dep_node.reason.src.upgrade(zcu);
@ -4982,6 +4999,9 @@ fn formatDependencyLoopSourceUnit(data: FormatAnalUnit, w: *Io.Writer) Io.Writer
.type_layout => |ty| try w.print("type '{f}'", .{
Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
}),
.struct_defaults => |ty| try w.print("default field value of '{f}'", .{
Type.fromInterned(ty).containerTypeName(ip).fmt(ip),
}),
.func => |func| try w.print("function '{f}'", .{
ip.getNav(zcu.funcInfo(func).owner_nav).fqn.fmt(ip),
}),
@ -5030,7 +5050,7 @@ pub fn populateReferenceTrace(
const root_name: ?[]const u8 = switch (ref.referencer.unwrap()) {
.@"comptime" => "comptime",
.nav_val, .nav_ty => |nav| ip.getNav(nav).name.toSlice(ip),
.type_layout => |ty| Type.fromInterned(ty).containerTypeName(ip).toSlice(ip),
.type_layout, .struct_defaults => |ty| Type.fromInterned(ty).containerTypeName(ip).toSlice(ip),
.func => |f| ip.getNav(zcu.funcInfo(f).owner_nav).name.toSlice(ip),
.memoized_state => null,
};

View file

@ -324,6 +324,17 @@ pub fn update(
.nav_ty => |nav| pt.ensureNavTypeUpToDate(nav, null),
.nav_val => |nav| pt.ensureNavValUpToDate(nav, null),
.type_layout => |ty| pt.ensureTypeLayoutUpToDate(.fromInterned(ty), null),
.struct_defaults => |ty| res: {
// Unlike the other functions, this one requires that the type layout is resolved first.
pt.ensureTypeLayoutUpToDate(.fromInterned(ty), null) catch |err| switch (err) {
error.OutOfMemory,
error.Canceled,
=> |e| return e,
error.AnalysisFail => {}, // already reported
};
break :res pt.ensureStructDefaultsUpToDate(.fromInterned(ty), null);
},
.memoized_state => |stage| pt.ensureMemoizedStateUpToDate(stage, null),
.func => |func| pt.ensureFuncBodyUpToDate(func, null),
};
@ -1326,6 +1337,7 @@ pub fn ensureTypeLayoutUpToDate(
defer tracy_trace.end();
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const comp = zcu.comp;
const gpa = comp.gpa;
@ -1335,8 +1347,23 @@ pub fn ensureTypeLayoutUpToDate(
assert(!zcu.analysis_in_progress.contains(anal_unit));
const was_outdated = zcu.clearOutdatedState(anal_unit) or
zcu.intern_pool.setWantTypeLayout(comp.io, ty.toIntern());
const was_outdated: bool = outdated: {
if (zcu.clearOutdatedState(anal_unit)) break :outdated true;
if (ip.setWantTypeLayout(comp.io, ty.toIntern())) {
// We'll analyze the layout for the first time, but if this is a struct type then its
// default field values also need to be analyzed.
if (ip.indexToKey(ty.toIntern()) == .struct_type) {
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.other.ensureUnusedCapacity(gpa, 1);
zcu.outdated.putAssumeCapacityNoClobber(.wrap(.{ .struct_defaults = ty.toIntern() }), 0);
zcu.outdated_ready.other.putAssumeCapacityNoClobber(.wrap(.{ .struct_defaults = ty.toIntern() }), {});
}
break :outdated true;
}
break :outdated false;
};
if (was_outdated) {
// `was_outdated` is true in the initial update, so this isn't a `dev.check`.
@ -1359,7 +1386,7 @@ pub fn ensureTypeLayoutUpToDate(
info.deps.clearRetainingCapacity();
}
const unit_tracking = zcu.trackUnitSema(ty.containerTypeName(&zcu.intern_pool).toSlice(&zcu.intern_pool), null);
const unit_tracking = zcu.trackUnitSema(ty.containerTypeName(ip).toSlice(ip), null);
defer unit_tracking.end(zcu);
try zcu.analysis_in_progress.put(gpa, anal_unit, reason);
@ -1428,6 +1455,120 @@ pub fn ensureTypeLayoutUpToDate(
if (new_failed) return error.AnalysisFail;
}
/// Ensures that the default field values of the given `struct` type are fully up-to-date,
/// performing re-analysis if necessary. Asserts that `ty` is a struct (not a tuple!) type. Unlike
/// the other "ensure X up to date" functions, this particular function also asserts that the
/// *layout* of `ty` is *already* up-to-date (though it is okay for that resolution to have failed).
/// Returns `error.AnalysisFail` if an analysis error is encountered while resolving the default
/// field values; the caller is free to ignore this, since the error is already registered.
pub fn ensureStructDefaultsUpToDate(
pt: Zcu.PerThread,
ty: Type,
/// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`.
reason: ?*const Zcu.DependencyReason,
) Zcu.SemaError!void {
const tracy_trace = trace(@src());
defer tracy_trace.end();
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const comp = zcu.comp;
const gpa = comp.gpa;
assert(ip.indexToKey(ty.toIntern()) == .struct_type);
const anal_unit: AnalUnit = .wrap(.{ .struct_defaults = ty.toIntern() });
log.debug("ensureStructDefaultsUpToDate {f}", .{zcu.fmtAnalUnit(anal_unit)});
assert(!zcu.analysis_in_progress.contains(anal_unit));
const was_outdated: bool = outdated: {
if (zcu.clearOutdatedState(anal_unit)) break :outdated true;
// The type layout should already be marked as "wanted" by this point, because a struct's
// layout must always be analyzed before its default values are.
assert(!ip.setWantTypeLayout(comp.io, ty.toIntern()));
break :outdated false;
};
if (was_outdated) {
// `was_outdated` is true in the initial update, so this isn't a `dev.check`.
if (dev.env.supports(.incremental)) {
zcu.resetUnit(anal_unit);
}
// For types, we already know that we have to invalidate all dependees.
// TODO: we actually *could* detect whether everything was the same. should we bother?
try zcu.markDependeeOutdated(.marked_po, .{ .struct_defaults = ty.toIntern() });
} else {
// We can trust the current information about this unit.
if (zcu.failed_analysis.contains(anal_unit)) return error.AnalysisFail;
if (zcu.transitive_failed_analysis.contains(anal_unit)) return error.AnalysisFail;
return;
}
if (zcu.comp.debugIncremental()) {
const info = try zcu.incremental_debug_state.getUnitInfo(gpa, anal_unit);
info.last_update_gen = zcu.generation;
info.deps.clearRetainingCapacity();
}
const unit_tracking = zcu.trackUnitSema(ty.containerTypeName(ip).toSlice(ip), null);
defer unit_tracking.end(zcu);
try zcu.analysis_in_progress.put(gpa, anal_unit, reason);
defer assert(zcu.analysis_in_progress.swapRemove(anal_unit));
var analysis_arena: std.heap.ArenaAllocator = .init(gpa);
defer analysis_arena.deinit();
var comptime_err_ret_trace: std.array_list.Managed(Zcu.LazySrcLoc) = .init(gpa);
defer comptime_err_ret_trace.deinit();
const file = zcu.namespacePtr(ty.getNamespaceIndex(zcu)).fileScope(zcu);
var sema: Sema = .{
.pt = pt,
.gpa = gpa,
.arena = analysis_arena.allocator(),
.code = file.zir.?,
.owner = anal_unit,
.func_index = .none,
.func_is_naked = false,
.fn_ret_ty = .void,
.fn_ret_ty_ies = null,
.comptime_err_ret_trace = &comptime_err_ret_trace,
};
defer sema.deinit();
const new_failed: bool = if (Sema.type_resolution.resolveStructDefaults(&sema, ty)) failed: {
break :failed false;
} else |err| switch (err) {
error.AnalysisFail => failed: {
if (!zcu.failed_analysis.contains(anal_unit)) {
// If this unit caused the error, it would have an entry in `failed_analysis`.
// Since it does not, this must be a transitive failure.
try zcu.transitive_failed_analysis.put(gpa, anal_unit, {});
log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)});
}
break :failed true;
},
error.OutOfMemory,
error.Canceled,
=> |e| return e,
error.ComptimeReturn => unreachable,
error.ComptimeBreak => unreachable,
};
sema.flushExports() catch |err| switch (err) {
error.OutOfMemory => |e| return e,
};
// We don't need to `markDependeeOutdated`/`markPoDependeeUpToDate` here, because we already
// marked the struct defaults as outdated at the top of this function.
if (new_failed) return error.AnalysisFail;
}
/// Ensures that the resolved value of the given `Nav` is fully up-to-date, performing re-analysis
/// if necessary. Returns `error.AnalysisFail` if an analysis error is encountered; the caller is
/// free to ignore this, since the error is already registered.

View file

@ -3827,7 +3827,15 @@ fn updateConstInner(dwarf: *Dwarf, pt: Zcu.PerThread, debug_const_index: link.Co
try diw.writeUleb128(ty.abiAlignment(zcu).toByteUnits().?);
for (0..loaded_struct.field_types.len) |field_index| {
const is_comptime = loaded_struct.field_is_comptime_bits.get(ip, field_index);
const field_init = loaded_struct.field_defaults.getOrNone(ip, field_index);
// TODO: we currently don't emit information about default values for
// non-`comptime` fields, because these default values are resolved at a
// separate time in the compiler frontend. To emit this information, the
// frontend needs to tell us when the default values are available: like
// how `Zcu.PerThread.ensureTypeLayoutUpToDate` enqueues a link task to
// indicate completion of the type's layout, a task should be enqueued
// by `Zcu.PerThread.ensureStructDefaultsUpToDate`, and upon receiving
// it we should patch the correct default field values in.
const field_init: InternPool.Index = if (is_comptime) loaded_struct.field_defaults.getOrNone(ip, field_index) else .none;
assert(!(is_comptime and field_init == .none));
const field_type: Type = .fromInterned(loaded_struct.field_types.get(ip)[field_index]);
const has_runtime_bits, const has_comptime_state = switch (field_init) {