commit 5865abf7f5ea2d4500cb8387252d9efee26ecae1 (tree)
parent 12ddd5a698577e22a0bb5ed788ea4b9729a45b5d
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Mon, 9 Feb 2026 10:20:58 +0000
Sema: defer extern function type validation to declaration or call
Because of packed structs, checking whether a type is extern-compatible
requires that its layout be resolved. For functions to do this
validation as soon as the function type is created would lead to
dependency loops in cases like '*const fn (*@This()) void callconv(.c)`.
Therefore, when creating a function *type*, we no longer perform this
check immediately, instead waiting until the function is called.
Diffstat:
3 files changed, 156 insertions(+), 109 deletions(-)
diff --git a/src/Sema.zig b/src/Sema.zig
@@ -5747,6 +5747,7 @@ fn zirExport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
}
const export_ty = ptr_ty.childType(zcu);
+ try sema.ensureLayoutResolved(export_ty, src, .@"export");
if (!export_ty.validateExtern(.other, zcu)) {
return sema.failWithOwnedErrorMsg(block, msg: {
const msg = try sema.errMsg(src, "unable to export type '{f}'", .{export_ty.fmt(pt)});
@@ -6749,6 +6750,37 @@ fn analyzeCall(
} else func_src;
const func_ty_info = zcu.typeToFunc(func_ty).?;
+
+ for (func_ty_info.param_types.get(ip), 0..) |param_ty_ip, param_index| {
+ const arg_src = args_info.argSrc(block, param_index);
+ try sema.ensureLayoutResolved(.fromInterned(param_ty_ip), arg_src, .init);
+ }
+ try sema.ensureLayoutResolved(.fromInterned(func_ty_info.return_type), func_ret_ty_src, .return_type);
+ try sema.validateResolvedFuncType(
+ block,
+ func_ty_info.cc,
+ func_ty_info.param_types.get(ip),
+ .fromInterned(func_ty_info.return_type),
+ func_src,
+ maybe_func_inst,
+ );
+
+ if (!callConvIsCallable(func_ty_info.cc)) {
+ return sema.failWithOwnedErrorMsg(block, msg: {
+ const msg = try sema.errMsg(
+ func_src,
+ "unable to call function with calling convention '{s}'",
+ .{@tagName(func_ty_info.cc)},
+ );
+ errdefer msg.destroy(gpa);
+ if (maybe_func_inst) |func_inst| try sema.errNote(.{
+ .base_node_inst = func_inst,
+ .offset = .nodeOffset(.zero),
+ }, msg, "function declared here", .{});
+ break :msg msg;
+ });
+ }
+
const any_comptime_params = func_ty_info.comptime_bits != 0 or ct: {
for (func_ty_info.param_types.get(ip)) |param_ty| {
if (Type.fromInterned(param_ty).comptimeOnly(zcu)) break :ct true;
@@ -6771,22 +6803,6 @@ fn analyzeCall(
break :generic false;
};
- if (!callConvIsCallable(func_ty_info.cc)) {
- return sema.failWithOwnedErrorMsg(block, msg: {
- const msg = try sema.errMsg(
- func_src,
- "unable to call function with calling convention '{s}'",
- .{@tagName(func_ty_info.cc)},
- );
- errdefer msg.destroy(gpa);
- if (maybe_func_inst) |func_inst| try sema.errNote(.{
- .base_node_inst = func_inst,
- .offset = .nodeOffset(.zero),
- }, msg, "function declared here", .{});
- break :msg msg;
- });
- }
-
// We need this value in a few code paths.
const callee_val = try sema.resolveDefinedValue(block, call_src, callee);
// If the callee is a comptime-known *non-extern* function, `func_val` is populated.
@@ -8755,11 +8771,12 @@ fn checkCallConvSupportsVarArgs(sema: *Sema, block: *Block, src: LazySrcLoc, cc:
}
}
-fn checkParamTypeCommon(
+fn checkParamType(
sema: *Sema,
block: *Block,
param_idx: u32,
param_ty: Type,
+ param_is_comptime: bool,
param_is_noalias: bool,
param_src: LazySrcLoc,
cc: std.builtin.CallingConvention,
@@ -8774,29 +8791,22 @@ fn checkParamTypeCommon(
opaque_str, param_ty.fmt(pt),
});
}
- if (!param_ty.isGenericPoison() and
- !target_util.fnCallConvAllowsZigTypes(cc) and
- !param_ty.validateExtern(.param_ty, zcu))
- {
- return sema.failWithOwnedErrorMsg(block, msg: {
- const msg = try sema.errMsg(param_src, "parameter of type '{f}' not allowed in function with calling convention '{s}'", .{
- param_ty.fmt(pt), @tagName(cc),
- });
- errdefer msg.destroy(sema.gpa);
-
- try sema.explainWhyTypeIsNotExtern(msg, param_src, param_ty, .param_ty);
-
- try sema.addDeclaredHereNote(msg, param_ty);
- break :msg msg;
- });
+ if (!target_util.fnCallConvAllowsZigTypes(cc)) {
+ if (param_is_comptime) {
+ return sema.fail(block, param_src, "comptime parameters not allowed in function with calling convention '{t}'", .{cc});
+ }
+ if (param_ty.isGenericPoison()) {
+ return sema.fail(block, param_src, "generic parameters not allowed in function with calling convention '{t}'", .{cc});
+ }
+ // The `validateExtern` check happens later, in `validateResolvedFuncType`.
}
switch (cc) {
.x86_64_interrupt, .x86_interrupt => {
const err_code_size = target.ptrBitWidth();
switch (param_idx) {
- 0 => if (param_ty.zigTypeTag(zcu) != .pointer) return sema.fail(block, param_src, "first parameter of function with '{s}' calling convention must be a pointer type", .{@tagName(cc)}),
- 1 => if (param_ty.bitSize(zcu) != err_code_size) return sema.fail(block, param_src, "second parameter of function with '{s}' calling convention must be a {d}-bit integer", .{ @tagName(cc), err_code_size }),
- else => return sema.fail(block, param_src, "'{s}' calling convention supports up to 2 parameters, found {d}", .{ @tagName(cc), param_idx + 1 }),
+ 0 => if (param_ty.zigTypeTag(zcu) != .pointer) return sema.fail(block, param_src, "first parameter of function with '{t}' calling convention must be a pointer type", .{cc}),
+ 1 => if (param_ty.bitSize(zcu) != err_code_size) return sema.fail(block, param_src, "second parameter of function with '{t}' calling convention must be a {d}-bit integer", .{ cc, err_code_size }),
+ else => return sema.fail(block, param_src, "'{t}' calling convention supports up to 2 parameters, found {d}", .{ cc, param_idx + 1 }),
}
},
.arc_interrupt,
@@ -8812,7 +8822,7 @@ fn checkParamTypeCommon(
.m68k_interrupt,
.msp430_interrupt,
.avr_signal,
- => return sema.fail(block, param_src, "parameters are not allowed with '{s}' calling convention", .{@tagName(cc)}),
+ => return sema.fail(block, param_src, "parameters are not allowed with '{t}' calling convention", .{cc}),
else => {},
}
if (param_is_noalias and !param_ty.isGenericPoison() and !param_ty.isPtrAtRuntime(zcu) and !param_ty.isSliceAtRuntime(zcu)) {
@@ -8820,7 +8830,7 @@ fn checkParamTypeCommon(
}
}
-fn checkReturnTypeAndCallConvCommon(
+fn checkReturnTypeAndCallConv(
sema: *Sema,
block: *Block,
bare_ret_ty: Type,
@@ -8834,7 +8844,6 @@ fn checkReturnTypeAndCallConvCommon(
) CompileError!void {
const pt = sema.pt;
const zcu = pt.zcu;
- const gpa = zcu.gpa;
if (opt_varargs_src) |varargs_src| {
try sema.checkCallConvSupportsVarArgs(block, varargs_src, @"callconv");
}
@@ -8848,21 +8857,14 @@ fn checkReturnTypeAndCallConvCommon(
opaque_str, ies_ret_ty_prefix, bare_ret_ty.fmt(pt),
});
}
- if (!bare_ret_ty.isGenericPoison() and
- !target_util.fnCallConvAllowsZigTypes(@"callconv") and
- (inferred_error_set or !bare_ret_ty.validateExtern(.ret_ty, zcu)))
- {
- return sema.failWithOwnedErrorMsg(block, msg: {
- const msg = try sema.errMsg(ret_ty_src, "return type '{s}{f}' not allowed in function with calling convention '{s}'", .{
- ies_ret_ty_prefix, bare_ret_ty.fmt(pt), @tagName(@"callconv"),
- });
- errdefer msg.destroy(gpa);
- if (!inferred_error_set) {
- try sema.explainWhyTypeIsNotExtern(msg, ret_ty_src, bare_ret_ty, .ret_ty);
- try sema.addDeclaredHereNote(msg, bare_ret_ty);
- }
- break :msg msg;
- });
+ if (!target_util.fnCallConvAllowsZigTypes(@"callconv")) {
+ if (inferred_error_set) {
+ return sema.fail(block, ret_ty_src, "return type '!{f}' not allowed in function with calling convention '{t}'", .{ bare_ret_ty.fmt(pt), @"callconv" });
+ }
+ if (bare_ret_ty.isGenericPoison()) {
+ return sema.fail(block, ret_ty_src, "generic return type not allowed in function with calling convention '{t}'", .{@"callconv"});
+ }
+ // The `validateExtern` check happens later, in `validateResolvedFuncType`.
}
validate_incoming_stack_align: {
const a: u64 = switch (@"callconv") {
@@ -8897,7 +8899,7 @@ fn checkReturnTypeAndCallConvCommon(
else => false,
};
if (!ret_ok) {
- return sema.fail(block, ret_ty_src, "function with calling convention '{s}' must return 'void' or 'noreturn'", .{@tagName(@"callconv")});
+ return sema.fail(block, ret_ty_src, "function with calling convention '{t}' must return 'void' or 'noreturn'", .{@"callconv"});
}
},
.@"inline" => if (is_noinline) {
@@ -8918,18 +8920,76 @@ fn checkReturnTypeAndCallConvCommon(
}
}
};
- return sema.fail(block, callconv_src, "calling convention '{s}' only available on architectures {f}", .{
- @tagName(@"callconv"),
- ArchListFormatter{ .archs = allowed_archs },
+ return sema.fail(block, callconv_src, "calling convention '{t}' only available on architectures {f}", .{
+ @"callconv", ArchListFormatter{ .archs = allowed_archs },
});
},
- .bad_backend => |bad_backend| return sema.fail(block, callconv_src, "calling convention '{s}' not supported by compiler backend '{s}'", .{
- @tagName(@"callconv"),
- @tagName(bad_backend),
+ .bad_backend => |bad_backend| return sema.fail(block, callconv_src, "calling convention '{t}' not supported by compiler backend '{t}'", .{
+ @"callconv", bad_backend,
}),
}
}
+/// To avoid forcing type layout resolution too quickly, some validation of function types cannot be
+/// performed when the type is first constructed, and instead must happen when either (a) a function
+/// with that type is declared, or (b) a function with that type is called. That validation is
+/// handled here.
+///
+/// Asserts that all parameter types and return types have their layout fully resolved.
+fn validateResolvedFuncType(
+ sema: *Sema,
+ block: *Block,
+ @"callconv": std.builtin.CallingConvention,
+ param_types: []const InternPool.Index,
+ ret_ty: Type,
+ src: LazySrcLoc,
+ maybe_func_decl_inst: ?InternPool.TrackedInst.Index,
+) SemaError!void {
+ const pt = sema.pt;
+ const zcu = pt.zcu;
+ const gpa = zcu.comp.gpa;
+ if (!target_util.fnCallConvAllowsZigTypes(@"callconv")) {
+ // Check that all parameter types are extern-compatible.
+ for (param_types, 0..) |param_ty_ip, param_index| {
+ const param_ty: Type = .fromInterned(param_ty_ip);
+ if (!param_ty.validateExtern(.param_ty, zcu)) {
+ const param_src: LazySrcLoc = if (maybe_func_decl_inst) |inst| .{
+ .base_node_inst = inst,
+ .offset = .{ .fn_proto_param = .{
+ .fn_proto_node_offset = .zero,
+ .param_index = @intCast(param_index),
+ } },
+ } else src;
+ return sema.failWithOwnedErrorMsg(block, msg: {
+ const msg = try sema.errMsg(param_src, "parameter of type '{f}' not allowed in function with calling convention '{t}'", .{
+ param_ty.fmt(pt), @"callconv",
+ });
+ errdefer msg.destroy(gpa);
+ try sema.explainWhyTypeIsNotExtern(msg, param_src, param_ty, .param_ty);
+ try sema.addDeclaredHereNote(msg, param_ty);
+ break :msg msg;
+ });
+ }
+ }
+ // Check that the return type is extern-compatible.
+ if (!ret_ty.validateExtern(.ret_ty, zcu)) {
+ const ret_ty_src: LazySrcLoc = if (maybe_func_decl_inst) |inst| .{
+ .base_node_inst = inst,
+ .offset = .{ .node_offset_fn_type_ret_ty = .zero },
+ } else src;
+ return sema.failWithOwnedErrorMsg(block, msg: {
+ const msg = try sema.errMsg(ret_ty_src, "return type '{f}' not allowed in function with calling convention '{t}'", .{
+ ret_ty.fmt(pt), @"callconv",
+ });
+ errdefer msg.destroy(gpa);
+ try sema.explainWhyTypeIsNotExtern(msg, ret_ty_src, ret_ty, .ret_ty);
+ try sema.addDeclaredHereNote(msg, ret_ty);
+ break :msg msg;
+ });
+ }
+ }
+}
+
fn callConvIsCallable(cc: std.builtin.CallingConvention.Tag) bool {
return switch (cc) {
.naked,
@@ -9022,6 +9082,7 @@ fn funcCommon(
const io = comp.io;
const ip = &zcu.intern_pool;
+ const src = block.nodeOffset(src_node_offset);
const ret_ty_src = block.src(.{ .node_offset_fn_type_ret_ty = src_node_offset });
const cc_src = block.src(.{ .node_offset_fn_type_cc = src_node_offset });
@@ -9036,27 +9097,21 @@ fn funcCommon(
.fn_proto_node_offset = src_node_offset,
.param_index = @intCast(i),
} });
- const param_ty_generic = param_ty.isGenericPoison();
if (param_is_comptime) {
comptime_bits |= @as(u32, 1) << @intCast(i); // TODO: handle cast error
}
- if (param_is_comptime and !target_util.fnCallConvAllowsZigTypes(cc)) {
- return sema.fail(block, param_src, "comptime parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)});
- }
- if (param_ty_generic and !target_util.fnCallConvAllowsZigTypes(cc)) {
- return sema.fail(block, param_src, "generic parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)});
- }
- try sema.checkParamTypeCommon(
+ try sema.checkParamType(
block,
@intCast(i),
param_ty,
+ param_is_comptime,
is_noalias,
param_src,
cc,
);
}
- try sema.checkReturnTypeAndCallConvCommon(
+ try sema.checkReturnTypeAndCallConv(
block,
bare_return_type,
ret_ty_src,
@@ -9072,9 +9127,29 @@ fn funcCommon(
const param_types = block.params.items(.ty);
+ if (has_body) {
+ for (param_types, 0..) |param_ty_ip, param_index| {
+ const param_ty: Type = .fromInterned(param_ty_ip);
+ const param_src = block.src(.{ .fn_proto_param = .{
+ .fn_proto_node_offset = src_node_offset,
+ .param_index = @intCast(param_index),
+ } });
+ try sema.ensureLayoutResolved(param_ty, param_src, .parameter);
+ }
+ try sema.ensureLayoutResolved(bare_return_type, ret_ty_src, .return_type);
+ try sema.validateResolvedFuncType(
+ block,
+ cc,
+ param_types,
+ bare_return_type,
+ src,
+ ip.getNav(sema.owner.unwrap().nav_val).srcInst(ip),
+ );
+ }
+
if (inferred_error_set) {
assert(has_body);
- const func_val: Value = .fromInterned(try ip.getFuncDeclIes(gpa, io, pt.tid, .{
+ return .fromIntern(try ip.getFuncDeclIes(gpa, io, pt.tid, .{
.owner_nav = sema.owner.unwrap().nav_val,
.param_types = param_types,
@@ -9091,8 +9166,6 @@ fn funcCommon(
.lbrace_column = @as(u16, @truncate(src_locs.columns)),
.rbrace_column = @as(u16, @truncate(src_locs.columns >> 16)),
}));
- try sema.ensureLayoutResolved(func_val.typeOf(zcu), ret_ty_src, .return_type);
- return .fromValue(func_val);
}
const func_ty = try ip.getFuncType(gpa, io, pt.tid, .{
@@ -9106,7 +9179,6 @@ fn funcCommon(
});
if (has_body) {
- try sema.ensureLayoutResolved(.fromInterned(func_ty), ret_ty_src, .return_type);
return .fromIntern(try ip.getFuncDecl(gpa, io, pt.tid, .{
.owner_nav = sema.owner.unwrap().nav_val,
.ty = func_ty,
@@ -18256,19 +18328,6 @@ fn zirPtrType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
}
} else if (inst_data.size != .one and elem_ty.zigTypeTag(zcu) == .@"opaque") {
return sema.fail(block, elem_ty_src, "indexable pointer to opaque type '{f}' not allowed", .{elem_ty.fmt(pt)});
- } else if (inst_data.size == .c) {
- if (!elem_ty.validateExtern(.other, zcu)) {
- const msg = msg: {
- const msg = try sema.errMsg(elem_ty_src, "C pointers cannot point to non-C-ABI-compatible type '{f}'", .{elem_ty.fmt(pt)});
- errdefer msg.destroy(sema.gpa);
-
- try sema.explainWhyTypeIsNotExtern(msg, elem_ty_src, elem_ty, .other);
-
- try sema.addDeclaredHereNote(msg, elem_ty);
- break :msg msg;
- };
- return sema.failWithOwnedErrorMsg(block, msg);
- }
}
if (host_size != 0) {
@@ -19800,16 +19859,6 @@ fn zirReifyPointer(
else => {},
}
- if (size == .c and !elem_ty.validateExtern(.other, zcu)) {
- return sema.failWithOwnedErrorMsg(block, msg: {
- const msg = try sema.errMsg(src, "C pointers cannot point to non-C-ABI-compatible type '{f}'", .{elem_ty.fmt(pt)});
- errdefer msg.destroy(gpa);
- try sema.explainWhyTypeIsNotExtern(msg, elem_ty_src, elem_ty, .other);
- try sema.addDeclaredHereNote(msg, elem_ty);
- break :msg msg;
- });
- }
-
const sentinel_ty = try pt.optionalType(elem_ty.toIntern());
const sentinel_uncoerced = sema.resolveInst(extra.sentinel);
const sentinel_coerced = try sema.coerce(block, sentinel_ty, sentinel_uncoerced, sentinel_src);
@@ -19898,10 +19947,11 @@ fn zirReifyFn(
try param_attrs_arr.elemValue(pt, param_idx),
std.builtin.Type.Fn.Param.Attributes,
);
- try sema.checkParamTypeCommon(
+ try sema.checkParamType(
block,
@intCast(param_idx),
param_ty,
+ false,
param_attrs.@"noalias",
param_types_src,
fn_attrs.@"callconv",
@@ -19919,7 +19969,7 @@ fn zirReifyFn(
try sema.checkCallConvSupportsVarArgs(block, fn_attrs_src, fn_attrs.@"callconv");
}
- try sema.checkReturnTypeAndCallConvCommon(
+ try sema.checkReturnTypeAndCallConv(
block,
ret_ty,
ret_ty_src,
@@ -19929,9 +19979,6 @@ fn zirReifyFn(
false,
false,
);
- if (ret_ty.comptimeOnly(zcu)) {
- return sema.fail(block, param_attrs_src, "cannot reify function type with comptime-only return type '{f}'", .{ret_ty.fmt(pt)});
- }
return .fromIntern(try ip.getFuncType(gpa, io, pt.tid, .{
.param_types = param_types_ip,
@@ -20615,7 +20662,7 @@ fn zirCVaArg(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) C
const va_list_ref = try sema.resolveVaListRef(block, va_list_src, extra.lhs);
const arg_ty = try sema.resolveType(block, ty_src, extra.rhs);
-
+ try sema.ensureLayoutResolved(arg_ty, ty_src, .parameter);
if (!arg_ty.validateExtern(.param_ty, sema.pt.zcu)) {
const msg = msg: {
const msg = try sema.errMsg(ty_src, "cannot get '{f}' from variadic argument", .{arg_ty.fmt(sema.pt)});
@@ -24639,13 +24686,12 @@ fn zirBuiltinExtern(
return sema.fail(block, ty_src, "expected (optional) pointer", .{});
}
if (!ty.validateExtern(.other, zcu)) {
- const msg = msg: {
+ return sema.failWithOwnedErrorMsg(block, msg: {
const msg = try sema.errMsg(ty_src, "extern symbol cannot have type '{f}'", .{ty.fmt(pt)});
errdefer msg.destroy(sema.gpa);
try sema.explainWhyTypeIsNotExtern(msg, ty_src, ty, .other);
break :msg msg;
- };
- return sema.failWithOwnedErrorMsg(block, msg);
+ });
}
const options = try sema.resolveExternOptions(block, options_src, extra.rhs);
@@ -25034,7 +25080,7 @@ pub fn explainWhyTypeIsNotExtern(
src_loc: LazySrcLoc,
ty: Type,
position: Type.ExternPosition,
-) CompileError!void {
+) SemaError!void {
const pt = sema.pt;
const zcu = pt.zcu;
switch (ty.zigTypeTag(zcu)) {
diff --git a/src/Sema/type_resolution.zig b/src/Sema/type_resolution.zig
@@ -32,6 +32,7 @@ pub const LayoutResolveReason = enum {
type_info,
align_check,
bit_ptr_child,
+ @"export",
builtin_type,
/// Written after string: "while resolving type 'T' "
@@ -56,6 +57,7 @@ pub const LayoutResolveReason = enum {
.type_info => "for type information query here",
.align_check => "for alignment check here",
.bit_ptr_child => "for bit size check here",
+ .@"export" => "for export here",
.builtin_type => "from 'std.builtin'",
// zig fmt: on
};
@@ -276,7 +278,6 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
assert(!field_ty.isGenericPoison());
const field_ty_src = block.src(.{ .container_field_type = @intCast(field_index) });
try sema.ensureLayoutResolved(field_ty, field_ty_src, .field);
-
if (field_ty.zigTypeTag(zcu) == .@"opaque") {
return sema.failWithOwnedErrorMsg(&block, msg: {
const msg = try sema.errMsg(field_ty_src, "cannot directly embed opaque type '{f}' in struct", .{field_ty.fmt(pt)});
@@ -286,7 +287,6 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void {
break :msg msg;
});
}
-
if (struct_obj.layout == .@"extern" and !field_ty.validateExtern(.struct_field, zcu)) {
return sema.failWithOwnedErrorMsg(&block, msg: {
const msg = try sema.errMsg(field_ty_src, "extern structs cannot contain fields of type '{f}'", .{field_ty.fmt(pt)});
diff --git a/src/Type.zig b/src/Type.zig
@@ -3055,9 +3055,10 @@ pub const ExternPosition = enum {
};
/// Returns true if `ty` is allowed in extern types.
-/// Does not require `ty` to be resolved in any way.
+/// Asserts that `ty` is fully resolved.
/// Keep in sync with `Sema.explainWhyTypeIsNotExtern`.
pub fn validateExtern(ty: Type, position: ExternPosition, zcu: *const Zcu) bool {
+ ty.assertHasLayout(zcu);
return switch (ty.zigTypeTag(zcu)) {
.type,
.comptime_float,