stage2: fixes for error unions, optionals, errors

* `?E` where E is an error set with only one field now lowers the same
   as `bool`.
 * Fix implementation of errUnionErrOffset and errUnionPayloadOffset to
   properly compute the offset of each field. Also name them the same
   as the corresponding LLVM functions and have the same function
   signature, to avoid confusion. This fixes a bug where wasm was
   passing the error union type instead of the payload type.
 * Fix C backend handling of optionals with zero-bit payload types.
 * C backend: separate out airOptionalPayload and airOptionalPayloadPtr
   which reduces branching and cleans up control flow.
 * Make Type.isNoReturn return true for error sets with no fields.
 * Make `?error{}` have only one possible value (null).
This commit is contained in:
Andrew Kelley 2022-05-24 15:10:18 -07:00
parent c847a462ae
commit c711c788f0
9 changed files with 192 additions and 77 deletions

View file

@ -23316,7 +23316,6 @@ pub fn typeHasOnePossibleValue(
.const_slice,
.mut_slice,
.anyopaque,
.optional,
.optional_single_mut_pointer,
.optional_single_const_pointer,
.enum_literal,
@ -23351,6 +23350,16 @@ pub fn typeHasOnePossibleValue(
.bound_fn,
=> return null,
.optional => {
var buf: Type.Payload.ElemType = undefined;
const child_ty = ty.optionalChild(&buf);
if (child_ty.isNoReturn()) {
return Value.@"null";
} else {
return null;
}
},
.error_set_single => {
const name = ty.castTag(.error_set_single).?.data;
return try Value.Tag.@"error".create(sema.arena, .{ .name = name });

View file

@ -30,7 +30,7 @@ const DebugInfoOutput = codegen.DebugInfoOutput;
const bits = @import("bits.zig");
const abi = @import("abi.zig");
const errUnionPayloadOffset = codegen.errUnionPayloadOffset;
const errUnionErrOffset = codegen.errUnionErrOffset;
const errUnionErrorOffset = codegen.errUnionErrorOffset;
const RegisterManager = abi.RegisterManager;
const RegisterLock = RegisterManager.RegisterLock;
const Register = bits.Register;
@ -3615,7 +3615,7 @@ fn isErr(self: *Self, ty: Type, operand: MCValue) !MCValue {
return MCValue{ .immediate = 0 }; // always false
}
const err_off = errUnionErrOffset(ty, self.target.*);
const err_off = errUnionErrorOffset(payload_type, self.target.*);
switch (operand) {
.stack_offset => |off| {
const offset = off - @intCast(u32, err_off);

View file

@ -30,7 +30,7 @@ const DebugInfoOutput = codegen.DebugInfoOutput;
const bits = @import("bits.zig");
const abi = @import("abi.zig");
const errUnionPayloadOffset = codegen.errUnionPayloadOffset;
const errUnionErrOffset = codegen.errUnionErrOffset;
const errUnionErrorOffset = codegen.errUnionErrorOffset;
const RegisterManager = abi.RegisterManager;
const RegisterLock = RegisterManager.RegisterLock;
const Register = bits.Register;
@ -1775,7 +1775,7 @@ fn errUnionErr(self: *Self, error_union_mcv: MCValue, error_union_ty: Type) !MCV
return error_union_mcv;
}
const err_offset = @intCast(u32, errUnionErrOffset(error_union_ty, self.target.*));
const err_offset = @intCast(u32, errUnionErrorOffset(payload_ty, self.target.*));
switch (error_union_mcv) {
.register => return self.fail("TODO errUnionErr for registers", .{}),
.stack_argument_offset => |off| {
@ -1812,7 +1812,7 @@ fn errUnionPayload(self: *Self, error_union_mcv: MCValue, error_union_ty: Type)
return MCValue.none;
}
const payload_offset = @intCast(u32, errUnionPayloadOffset(error_union_ty, self.target.*));
const payload_offset = @intCast(u32, errUnionPayloadOffset(payload_ty, self.target.*));
switch (error_union_mcv) {
.register => return self.fail("TODO errUnionPayload for registers", .{}),
.stack_argument_offset => |off| {

View file

@ -23,7 +23,7 @@ const Mir = @import("Mir.zig");
const Emit = @import("Emit.zig");
const abi = @import("abi.zig");
const errUnionPayloadOffset = codegen.errUnionPayloadOffset;
const errUnionErrOffset = codegen.errUnionErrOffset;
const errUnionErrorOffset = codegen.errUnionErrorOffset;
/// Wasm Value, created when generating an instruction
const WValue = union(enum) {
@ -2919,10 +2919,10 @@ fn airSwitchBr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
fn airIsErr(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue {
const un_op = self.air.instructions.items(.data)[inst].un_op;
const operand = try self.resolveInst(un_op);
const err_ty = self.air.typeOf(un_op);
const pl_ty = err_ty.errorUnionPayload();
const err_union_ty = self.air.typeOf(un_op);
const pl_ty = err_union_ty.errorUnionPayload();
if (err_ty.errorUnionSet().errorSetCardinality() == .zero) {
if (err_union_ty.errorUnionSet().errorSetCardinality() == .zero) {
switch (opcode) {
.i32_ne => return WValue{ .imm32 = 0 },
.i32_eq => return WValue{ .imm32 = 1 },
@ -2933,7 +2933,7 @@ fn airIsErr(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!W
try self.emitWValue(operand);
if (pl_ty.hasRuntimeBitsIgnoreComptime()) {
try self.addMemArg(.i32_load16_u, .{
.offset = operand.offset() + @intCast(u32, errUnionErrOffset(pl_ty, self.target)),
.offset = operand.offset() + @intCast(u32, errUnionErrorOffset(pl_ty, self.target)),
.alignment = Type.anyerror.abiAlignment(self.target),
});
}
@ -2985,7 +2985,7 @@ fn airUnwrapErrUnionError(self: *Self, inst: Air.Inst.Index, op_is_ptr: bool) In
return operand;
}
return self.load(operand, Type.anyerror, @intCast(u32, errUnionErrOffset(payload_ty, self.target)));
return self.load(operand, Type.anyerror, @intCast(u32, errUnionErrorOffset(payload_ty, self.target)));
}
fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
@ -3011,7 +3011,7 @@ fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
// ensure we also write '0' to the error part, so any present stack value gets overwritten by it.
try self.emitWValue(err_union);
try self.addImm32(0);
const err_val_offset = @intCast(u32, errUnionErrOffset(pl_ty, self.target));
const err_val_offset = @intCast(u32, errUnionErrorOffset(pl_ty, self.target));
try self.addMemArg(.i32_store16, .{ .offset = err_union.offset() + err_val_offset, .alignment = 2 });
return err_union;
@ -3031,7 +3031,7 @@ fn airWrapErrUnionErr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
const err_union = try self.allocStack(err_ty);
// store error value
try self.store(err_union, operand, Type.anyerror, @intCast(u32, errUnionErrOffset(pl_ty, self.target)));
try self.store(err_union, operand, Type.anyerror, @intCast(u32, errUnionErrorOffset(pl_ty, self.target)));
// write 'undefined' to the payload
const payload_ptr = try self.buildPointerOffset(err_union, @intCast(u32, errUnionPayloadOffset(pl_ty, self.target)), .new);
@ -3986,7 +3986,7 @@ fn airErrUnionPayloadPtrSet(self: *Self, inst: Air.Inst.Index) InnerError!WValue
operand,
.{ .imm32 = 0 },
Type.anyerror,
@intCast(u32, errUnionErrOffset(payload_ty, self.target)),
@intCast(u32, errUnionErrorOffset(payload_ty, self.target)),
);
if (self.liveness.isUnused(inst)) return WValue{ .none = {} };

View file

@ -30,7 +30,7 @@ const Value = @import("../../value.zig").Value;
const bits = @import("bits.zig");
const abi = @import("abi.zig");
const errUnionPayloadOffset = codegen.errUnionPayloadOffset;
const errUnionErrOffset = codegen.errUnionErrOffset;
const errUnionErrorOffset = codegen.errUnionErrorOffset;
const callee_preserved_regs = abi.callee_preserved_regs;
const caller_preserved_regs = abi.caller_preserved_regs;
@ -1799,7 +1799,7 @@ fn airUnwrapErrErr(self: *Self, inst: Air.Inst.Index) !void {
break :result operand;
}
const err_off = errUnionErrOffset(err_union_ty, self.target.*);
const err_off = errUnionErrorOffset(payload_ty, self.target.*);
switch (operand) {
.stack_offset => |off| {
const offset = off - @intCast(i32, err_off);
@ -1844,7 +1844,7 @@ fn airUnwrapErrPayload(self: *Self, inst: Air.Inst.Index) !void {
break :result MCValue.none;
}
const payload_off = errUnionPayloadOffset(err_union_ty, self.target.*);
const payload_off = errUnionPayloadOffset(payload_ty, self.target.*);
switch (operand) {
.stack_offset => |off| {
const offset = off - @intCast(i32, payload_off);
@ -1978,8 +1978,8 @@ fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) !void {
const abi_size = @intCast(u32, error_union_ty.abiSize(self.target.*));
const abi_align = error_union_ty.abiAlignment(self.target.*);
const stack_offset = @intCast(i32, try self.allocMem(inst, abi_size, abi_align));
const payload_off = errUnionPayloadOffset(error_union_ty, self.target.*);
const err_off = errUnionErrOffset(error_union_ty, self.target.*);
const payload_off = errUnionPayloadOffset(payload_ty, self.target.*);
const err_off = errUnionErrorOffset(payload_ty, self.target.*);
try self.genSetStack(payload_ty, stack_offset - @intCast(i32, payload_off), operand, .{});
try self.genSetStack(Type.anyerror, stack_offset - @intCast(i32, err_off), .{ .immediate = 0 }, .{});
@ -2007,8 +2007,8 @@ fn airWrapErrUnionErr(self: *Self, inst: Air.Inst.Index) !void {
const abi_size = @intCast(u32, error_union_ty.abiSize(self.target.*));
const abi_align = error_union_ty.abiAlignment(self.target.*);
const stack_offset = @intCast(i32, try self.allocMem(inst, abi_size, abi_align));
const payload_off = errUnionPayloadOffset(error_union_ty, self.target.*);
const err_off = errUnionErrOffset(error_union_ty, self.target.*);
const payload_off = errUnionPayloadOffset(payload_ty, self.target.*);
const err_off = errUnionErrorOffset(payload_ty, self.target.*);
try self.genSetStack(Type.anyerror, stack_offset - @intCast(i32, err_off), operand, .{});
try self.genSetStack(payload_ty, stack_offset - @intCast(i32, payload_off), .undef, .{});
@ -4670,7 +4670,7 @@ fn isErr(self: *Self, inst: Air.Inst.Index, ty: Type, operand: MCValue) !MCValue
try self.spillCompareFlagsIfOccupied();
self.compare_flags_inst = inst;
const err_off = errUnionErrOffset(ty, self.target.*);
const err_off = errUnionErrorOffset(ty.errorUnionPayload(), self.target.*);
switch (operand) {
.stack_offset => |off| {
const offset = off - @intCast(i32, err_off);

View file

@ -891,18 +891,22 @@ fn lowerDeclRef(
return Result{ .appended = {} };
}
pub fn errUnionPayloadOffset(ty: Type, target: std.Target) u64 {
const payload_ty = ty.errorUnionPayload();
return if (Type.anyerror.abiAlignment(target) >= payload_ty.abiAlignment(target))
Type.anyerror.abiSize(target)
else
0;
pub fn errUnionPayloadOffset(payload_ty: Type, target: std.Target) u64 {
const payload_align = payload_ty.abiAlignment(target);
const error_align = Type.anyerror.abiAlignment(target);
if (payload_align >= error_align) {
return 0;
} else {
return mem.alignForwardGeneric(u64, Type.anyerror.abiSize(target), payload_align);
}
}
pub fn errUnionErrOffset(ty: Type, target: std.Target) u64 {
const payload_ty = ty.errorUnionPayload();
return if (Type.anyerror.abiAlignment(target) >= payload_ty.abiAlignment(target))
0
else
payload_ty.abiSize(target);
pub fn errUnionErrorOffset(payload_ty: Type, target: std.Target) u64 {
const payload_align = payload_ty.abiAlignment(target);
const error_align = Type.anyerror.abiAlignment(target);
if (payload_align >= error_align) {
return mem.alignForwardGeneric(u64, payload_ty.abiSize(target), error_align);
} else {
return 0;
}
}

View file

@ -711,21 +711,24 @@ pub const DeclGen = struct {
.Bool => return writer.print("{}", .{val.toBool()}),
.Optional => {
var opt_buf: Type.Payload.ElemType = undefined;
const payload_type = ty.optionalChild(&opt_buf);
if (ty.optionalReprIsPayload()) {
return dg.renderValue(writer, payload_type, val, location);
}
if (payload_type.abiSize(target) == 0) {
const payload_ty = ty.optionalChild(&opt_buf);
if (!payload_ty.hasRuntimeBitsIgnoreComptime()) {
const is_null = val.castTag(.opt_payload) == null;
return writer.print("{}", .{is_null});
}
if (ty.optionalReprIsPayload()) {
return dg.renderValue(writer, payload_ty, val, location);
}
try writer.writeByte('(');
try dg.renderTypecast(writer, ty);
try writer.writeAll("){");
if (val.castTag(.opt_payload)) |pl| {
const payload_val = pl.data;
try writer.writeAll(" .is_null = false, .payload = ");
try dg.renderValue(writer, payload_type, payload_val, location);
try dg.renderValue(writer, payload_ty, payload_val, location);
try writer.writeAll(" }");
} else {
try writer.writeAll(" .is_null = true }");
@ -1360,12 +1363,12 @@ pub const DeclGen = struct {
var opt_buf: Type.Payload.ElemType = undefined;
const child_type = t.optionalChild(&opt_buf);
if (t.optionalReprIsPayload()) {
return dg.renderType(w, child_type);
if (!child_type.hasRuntimeBitsIgnoreComptime()) {
return w.writeAll("bool");
}
if (child_type.abiSize(target) == 0) {
return w.writeAll("bool");
if (t.optionalReprIsPayload()) {
return dg.renderType(w, child_type);
}
const name = dg.getTypedefName(t) orelse
@ -1816,8 +1819,9 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
.not => try airNot (f, inst),
.optional_payload => try airOptionalPayload(f, inst),
.optional_payload_ptr => try airOptionalPayload(f, inst),
.optional_payload_ptr => try airOptionalPayloadPtr(f, inst),
.optional_payload_ptr_set => try airOptionalPayloadPtrSet(f, inst),
.wrap_optional => try airWrapOptional(f, inst),
.is_err => try airIsErr(f, inst, false, "!="),
.is_non_err => try airIsErr(f, inst, false, "=="),
@ -1846,7 +1850,6 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
.cond_br => try airCondBr(f, inst),
.br => try airBr(f, inst),
.switch_br => try airSwitchBr(f, inst),
.wrap_optional => try airWrapOptional(f, inst),
.struct_field_ptr => try airStructFieldPtr(f, inst),
.array_to_slice => try airArrayToSlice(f, inst),
.cmpxchg_weak => try airCmpxchg(f, inst, "weak"),
@ -3145,7 +3148,6 @@ fn airIsNull(
const un_op = f.air.instructions.items(.data)[inst].un_op;
const writer = f.object.writer();
const operand = try f.resolveInst(un_op);
const target = f.object.dg.module.getTarget();
const local = try f.allocLocal(Type.initTag(.bool), .Const);
try writer.writeAll(" = (");
@ -3153,18 +3155,18 @@ fn airIsNull(
const ty = f.air.typeOf(un_op);
var opt_buf: Type.Payload.ElemType = undefined;
const payload_type = if (ty.zigTypeTag() == .Pointer)
const payload_ty = if (ty.zigTypeTag() == .Pointer)
ty.childType().optionalChild(&opt_buf)
else
ty.optionalChild(&opt_buf);
if (ty.isPtrLikeOptional()) {
if (!payload_ty.hasRuntimeBitsIgnoreComptime()) {
try writer.print("){s} {s} true;\n", .{ deref_suffix, operator });
} else if (ty.isPtrLikeOptional()) {
// operand is a regular pointer, test `operand !=/== NULL`
try writer.print("){s} {s} NULL;\n", .{ deref_suffix, operator });
} else if (payload_type.zigTypeTag() == .ErrorSet) {
} else if (payload_ty.zigTypeTag() == .ErrorSet) {
try writer.print("){s} {s} 0;\n", .{ deref_suffix, operator });
} else if (payload_type.abiSize(target) == 0) {
try writer.print("){s} {s} true;\n", .{ deref_suffix, operator });
} else {
try writer.print("){s}.is_null {s} true;\n", .{ deref_suffix, operator });
}
@ -3172,18 +3174,46 @@ fn airIsNull(
}
fn airOptionalPayload(f: *Function, inst: Air.Inst.Index) !CValue {
if (f.liveness.isUnused(inst))
return CValue.none;
if (f.liveness.isUnused(inst)) return CValue.none;
const ty_op = f.air.instructions.items(.data)[inst].ty_op;
const writer = f.object.writer();
const operand = try f.resolveInst(ty_op.operand);
const operand_ty = f.air.typeOf(ty_op.operand);
const opt_ty = f.air.typeOf(ty_op.operand);
const opt_ty = if (operand_ty.zigTypeTag() == .Pointer)
operand_ty.elemType()
else
operand_ty;
var buf: Type.Payload.ElemType = undefined;
const payload_ty = opt_ty.optionalChild(&buf);
if (!payload_ty.hasRuntimeBitsIgnoreComptime()) {
return CValue.none;
}
if (opt_ty.optionalReprIsPayload()) {
return operand;
}
const inst_ty = f.air.typeOfIndex(inst);
const local = try f.allocLocal(inst_ty, .Const);
try writer.writeAll(" = (");
try f.writeCValue(writer, operand);
try writer.writeAll(").payload;\n");
return local;
}
fn airOptionalPayloadPtr(f: *Function, inst: Air.Inst.Index) !CValue {
if (f.liveness.isUnused(inst)) return CValue.none;
const ty_op = f.air.instructions.items(.data)[inst].ty_op;
const writer = f.object.writer();
const operand = try f.resolveInst(ty_op.operand);
const ptr_ty = f.air.typeOf(ty_op.operand);
const opt_ty = ptr_ty.childType();
var buf: Type.Payload.ElemType = undefined;
const payload_ty = opt_ty.optionalChild(&buf);
if (!payload_ty.hasRuntimeBitsIgnoreComptime()) {
return operand;
}
if (opt_ty.optionalReprIsPayload()) {
// the operand is just a regular pointer, no need to do anything special.
@ -3192,14 +3222,10 @@ fn airOptionalPayload(f: *Function, inst: Air.Inst.Index) !CValue {
}
const inst_ty = f.air.typeOfIndex(inst);
const maybe_deref = if (operand_ty.zigTypeTag() == .Pointer) "->" else ".";
const maybe_addrof = if (inst_ty.zigTypeTag() == .Pointer) "&" else "";
const local = try f.allocLocal(inst_ty, .Const);
try writer.print(" = {s}(", .{maybe_addrof});
try writer.writeAll(" = &(");
try f.writeCValue(writer, operand);
try writer.print("){s}payload;\n", .{maybe_deref});
try writer.writeAll(")->payload;\n");
return local;
}

View file

@ -2375,7 +2375,6 @@ pub const Type = extern union {
// These types have more than one possible value, so the result is the same as
// asking whether they are comptime-only types.
.anyframe_T,
.optional,
.optional_single_mut_pointer,
.optional_single_const_pointer,
.single_const_pointer,
@ -2397,6 +2396,22 @@ pub const Type = extern union {
}
},
.optional => {
var buf: Payload.ElemType = undefined;
const child_ty = ty.optionalChild(&buf);
if (child_ty.isNoReturn()) {
// Then the optional is comptime-known to be null.
return false;
}
if (ignore_comptime_only) {
return true;
} else if (sema_kit) |sk| {
return !(try sk.sema.typeRequiresComptime(sk.block, sk.src, child_ty));
} else {
return !comptimeOnly(child_ty);
}
},
.error_union => {
// This code needs to be kept in sync with the equivalent switch prong
// in abiSizeAdvanced.
@ -2665,13 +2680,22 @@ pub const Type = extern union {
};
}
pub fn isNoReturn(self: Type) bool {
const definitely_correct_result =
self.tag_if_small_enough != .bound_fn and
self.zigTypeTag() == .NoReturn;
const fast_result = self.tag_if_small_enough == Tag.noreturn;
assert(fast_result == definitely_correct_result);
return fast_result;
/// TODO add enums with no fields here
pub fn isNoReturn(ty: Type) bool {
switch (ty.tag()) {
.noreturn => return true,
.error_set => {
const err_set_obj = ty.castTag(.error_set).?.data;
const names = err_set_obj.names.keys();
return names.len == 0;
},
.error_set_merged => {
const name_map = ty.castTag(.error_set_merged).?.data;
const names = name_map.keys();
return names.len == 0;
},
else => return false,
}
}
/// Returns 0 if the pointer is naturally aligned and the element type is 0-bit.
@ -2918,7 +2942,13 @@ pub const Type = extern union {
switch (child_type.zigTypeTag()) {
.Pointer => return AbiAlignmentAdvanced{ .scalar = @divExact(target.cpu.arch.ptrBitWidth(), 8) },
.ErrorSet => return abiAlignmentAdvanced(Type.anyerror, target, strat),
.ErrorSet => switch (child_type.errorSetCardinality()) {
// `?error{}` is comptime-known to be null.
.zero => return AbiAlignmentAdvanced{ .scalar = 0 },
.one => return AbiAlignmentAdvanced{ .scalar = 1 },
.many => return abiAlignmentAdvanced(Type.anyerror, target, strat),
},
.NoReturn => return AbiAlignmentAdvanced{ .scalar = 0 },
else => {},
}
@ -3365,6 +3395,11 @@ pub const Type = extern union {
.optional => {
var buf: Payload.ElemType = undefined;
const child_type = ty.optionalChild(&buf);
if (child_type.isNoReturn()) {
return AbiSizeAdvanced{ .scalar = 0 };
}
if (!child_type.hasRuntimeBits()) return AbiSizeAdvanced{ .scalar = 1 };
switch (child_type.zigTypeTag()) {
@ -4804,7 +4839,6 @@ pub const Type = extern union {
.const_slice,
.mut_slice,
.anyopaque,
.optional,
.optional_single_mut_pointer,
.optional_single_const_pointer,
.enum_literal,
@ -4839,6 +4873,16 @@ pub const Type = extern union {
.bound_fn,
=> return null,
.optional => {
var buf: Payload.ElemType = undefined;
const child_ty = ty.optionalChild(&buf);
if (child_ty.isNoReturn()) {
return Value.@"null";
} else {
return null;
}
},
.error_set_single => return Value.initTag(.the_only_possible_value),
.error_set => {
const err_set_obj = ty.castTag(.error_set).?.data;

View file

@ -121,7 +121,7 @@ test "debug info for optional error set" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
const SomeError = error{Hello};
const SomeError = error{ Hello, Hello2 };
var a_local_variable: ?SomeError = null;
_ = a_local_variable;
}
@ -454,6 +454,38 @@ test "optional error set is the same size as error set" {
comptime try expect(S.returnsOptErrSet() == null);
}
test "optional error set with only one error is the same size as bool" {
if (builtin.zig_backend == .stage1) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
const E = error{only};
comptime try expect(@sizeOf(?E) == @sizeOf(bool));
comptime try expect(@alignOf(?E) == @alignOf(bool));
const S = struct {
fn gimmeNull() ?E {
return null;
}
fn gimmeErr() ?E {
return error.only;
}
};
try expect(S.gimmeNull() == null);
try expect(error.only == S.gimmeErr().?);
comptime try expect(S.gimmeNull() == null);
comptime try expect(error.only == S.gimmeErr().?);
}
test "optional empty error set" {
if (builtin.zig_backend == .stage1) return error.SkipZigTest;
const T = ?error{};
var t: T = undefined;
if (t != null) {
@compileError("test failed");
}
}
test "nested catch" {
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO