From 3ce857d0543780e96bfd50d75d53d870ff308fef Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 18:31:39 +0000 Subject: [PATCH 01/14] Sema: fix incorrectly succeeding type resolution Resolves: #21436 --- src/InternPool.zig | 2 +- ...truct_depends_on_itself_via_non_initial_field.zig | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/cases/compile_errors/struct_depends_on_itself_via_non_initial_field.zig diff --git a/src/InternPool.zig b/src/InternPool.zig index 60d24223ce2d..8f99c9d810f7 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -4011,7 +4011,7 @@ pub const LoadedStructType = struct { pub fn haveFieldTypes(s: LoadedStructType, ip: *const InternPool) bool { const types = s.field_types.get(ip); - return types.len == 0 or types[0] != .none; + return types.len == 0 or types[types.len - 1] != .none; } pub fn haveFieldInits(s: LoadedStructType, ip: *const InternPool) bool { diff --git a/test/cases/compile_errors/struct_depends_on_itself_via_non_initial_field.zig b/test/cases/compile_errors/struct_depends_on_itself_via_non_initial_field.zig new file mode 100644 index 000000000000..b514bc10e7bf --- /dev/null +++ b/test/cases/compile_errors/struct_depends_on_itself_via_non_initial_field.zig @@ -0,0 +1,12 @@ +const A = struct { + a: u8, + bytes: [@sizeOf(A)]u8, +}; + +comptime { + _ = A; +} + +// error +// +// :1:11: error: struct 'tmp.A' depends on itself From 456f3c026b3d6fb289f42e442b202e0be152c9d3 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 19:08:05 +0000 Subject: [PATCH 02/14] Sema: fix crash on `@tagName` of undefined enum literal Resolves: #20826 --- src/Sema.zig | 2 +- .../compile_errors/tagName_on_undef_enum_literal.zig | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 test/cases/compile_errors/tagName_on_undef_enum_literal.zig diff --git a/src/Sema.zig b/src/Sema.zig index ad144bf0133a..af077c5150d7 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -21401,7 +21401,7 @@ fn zirTagName(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air try operand_ty.resolveLayout(pt); const enum_ty = switch (operand_ty.zigTypeTag(zcu)) { .enum_literal => { - const val = try sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, operand, undefined); + const val = (try sema.resolveDefinedValue(block, operand_src, operand)).?; const tag_name = ip.indexToKey(val.toIntern()).enum_literal; return sema.addNullTerminatedStrLit(tag_name); }, diff --git a/test/cases/compile_errors/tagName_on_undef_enum_literal.zig b/test/cases/compile_errors/tagName_on_undef_enum_literal.zig new file mode 100644 index 000000000000..653f23db592f --- /dev/null +++ b/test/cases/compile_errors/tagName_on_undef_enum_literal.zig @@ -0,0 +1,8 @@ +comptime { + const undef: @Type(.enum_literal) = undefined; + _ = @tagName(undef); +} + +// error +// +// :3:18: error: use of undefined value here causes undefined behavior From 0f38558435a0f73c4c025b5641bd8e531f063e0c Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 19:36:14 +0000 Subject: [PATCH 03/14] compiler: provide result type to sentinel expression in slice operation Resolves: #21867 --- lib/std/zig/AstGen.zig | 6 +++++- lib/std/zig/Zir.zig | 8 ++++++++ src/Sema.zig | 41 +++++++++++++++++++++++++++++++++++++++++ src/print_zir.zig | 1 + test/behavior/slice.zig | 13 +++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index c105a371ef8d..d43a837b2f18 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -920,7 +920,10 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE const cursor = maybeAdvanceSourceCursorToMainToken(gz, node); const start = try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, full.ast.start); const end = if (full.ast.end != 0) try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, full.ast.end) else .none; - const sentinel = if (full.ast.sentinel != 0) try expr(gz, scope, .{ .rl = .none }, full.ast.sentinel) else .none; + const sentinel = if (full.ast.sentinel != 0) s: { + const sentinel_ty = try gz.addUnNode(.slice_sentinel_ty, lhs, node); + break :s try expr(gz, scope, .{ .rl = .{ .coerced_ty = sentinel_ty } }, full.ast.sentinel); + } else .none; try emitDbgStmt(gz, cursor); if (sentinel != .none) { const result = try gz.addPlNode(.slice_sentinel, node, Zir.Inst.SliceSentinel{ @@ -2855,6 +2858,7 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As .slice_end, .slice_sentinel, .slice_length, + .slice_sentinel_ty, .import, .switch_block, .switch_block_ref, diff --git a/lib/std/zig/Zir.zig b/lib/std/zig/Zir.zig index 092a354de6f8..32872eeabcb6 100644 --- a/lib/std/zig/Zir.zig +++ b/lib/std/zig/Zir.zig @@ -599,6 +599,10 @@ pub const Inst = struct { /// Returns a pointer to the subslice. /// Uses the `pl_node` field. AST node is the slice syntax. Payload is `SliceLength`. slice_length, + /// Given a value which is a pointer to the LHS of a slice operation, return the sentinel + /// type, used as the result type of the slice sentinel (i.e. `s` in `lhs[a..b :s]`). + /// Uses the `un_node` field. AST node is the slice syntax. Operand is `lhs`. + slice_sentinel_ty, /// Same as `store` except provides a source location. /// Uses the `pl_node` union field. Payload is `Bin`. store_node, @@ -1185,6 +1189,7 @@ pub const Inst = struct { .slice_end, .slice_sentinel, .slice_length, + .slice_sentinel_ty, .import, .typeof_log2_int_type, .resolve_inferred_alloc, @@ -1472,6 +1477,7 @@ pub const Inst = struct { .slice_end, .slice_sentinel, .slice_length, + .slice_sentinel_ty, .import, .typeof_log2_int_type, .switch_block, @@ -1702,6 +1708,7 @@ pub const Inst = struct { .slice_end = .pl_node, .slice_sentinel = .pl_node, .slice_length = .pl_node, + .slice_sentinel_ty = .un_node, .store_node = .pl_node, .store_to_inferred_ptr = .pl_node, .str = .str, @@ -4162,6 +4169,7 @@ fn findTrackableInner( .slice_end, .slice_sentinel, .slice_length, + .slice_sentinel_ty, .store_node, .store_to_inferred_ptr, .str, diff --git a/src/Sema.zig b/src/Sema.zig index af077c5150d7..7b8999a21162 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1197,6 +1197,7 @@ fn analyzeBodyInner( .slice_sentinel => try sema.zirSliceSentinel(block, inst), .slice_start => try sema.zirSliceStart(block, inst), .slice_length => try sema.zirSliceLength(block, inst), + .slice_sentinel_ty => try sema.zirSliceSentinelTy(block, inst), .str => try sema.zirStr(inst), .switch_block => try sema.zirSwitchBlock(block, inst, false), .switch_block_ref => try sema.zirSwitchBlock(block, inst, true), @@ -10753,6 +10754,46 @@ fn zirSliceLength(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError return sema.analyzeSlice(block, src, array_ptr, start, len, sentinel, sentinel_src, ptr_src, start_src, end_src, true); } +fn zirSliceSentinelTy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref { + const tracy = trace(@src()); + defer tracy.end(); + + const pt = sema.pt; + const zcu = pt.zcu; + + const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node; + + const src = block.nodeOffset(inst_data.src_node); + const ptr_src = block.src(.{ .node_offset_slice_ptr = inst_data.src_node }); + const sentinel_src = block.src(.{ .node_offset_slice_sentinel = inst_data.src_node }); + + // This is like the logic in `analyzeSlice`; since we've evaluated the LHS as an lvalue, we will + // have a double pointer if it was already a pointer. + + const lhs_ptr_ty = sema.typeOf(try sema.resolveInst(inst_data.operand)); + const lhs_ty = switch (lhs_ptr_ty.zigTypeTag(zcu)) { + .pointer => lhs_ptr_ty.childType(zcu), + else => return sema.fail(block, ptr_src, "expected pointer, found '{}'", .{lhs_ptr_ty.fmt(pt)}), + }; + + const sentinel_ty: Type = switch (lhs_ty.zigTypeTag(zcu)) { + .array => lhs_ty.childType(zcu), + .pointer => switch (lhs_ty.ptrSize(zcu)) { + .many, .c, .slice => lhs_ty.childType(zcu), + .one => s: { + const lhs_elem_ty = lhs_ty.childType(zcu); + break :s switch (lhs_elem_ty.zigTypeTag(zcu)) { + .array => lhs_elem_ty.childType(zcu), // array element type + else => return sema.fail(block, sentinel_src, "slice of single-item pointer cannot have sentinel", .{}), + }; + }, + }, + else => return sema.fail(block, src, "slice of non-array type '{}'", .{lhs_ty.fmt(pt)}), + }; + + return Air.internedToRef(sentinel_ty.toIntern()); +} + /// Holds common data used when analyzing or resolving switch prong bodies, /// including setting up captures. const SwitchProngAnalysis = struct { diff --git a/src/print_zir.zig b/src/print_zir.zig index 0757ca83de58..24a966337633 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -208,6 +208,7 @@ const Writer = struct { .anyframe_type, .bit_not, .bool_not, + .slice_sentinel_ty, .negate, .negate_wrap, .load, diff --git a/test/behavior/slice.zig b/test/behavior/slice.zig index 52680b87f35e..f7cf2d712340 100644 --- a/test/behavior/slice.zig +++ b/test/behavior/slice.zig @@ -1037,3 +1037,16 @@ test "peer slices keep abi alignment with empty struct" { comptime assert(@TypeOf(slice) == []const u32); try expect(slice.len == 0); } + +test "sentinel expression in slice operation has result type" { + const sentinel = std.math.maxInt(u16); + + const arr: [3]u16 = .{ 1, 2, sentinel }; + const slice = arr[0..2 :@intCast(sentinel)]; + + comptime assert(@TypeOf(slice) == *const [2:sentinel]u16); + comptime assert(slice[2] == sentinel); + comptime assert(slice.len == 2); + comptime assert(slice[0] == 1); + comptime assert(slice[1] == 2); +} From fbe0ae4fd4128a3652071b4b5985d68ae56e9992 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 19:53:12 +0000 Subject: [PATCH 04/14] Sema: fix PTR of slice of sentinel-terminated array Resolves: #20901 --- src/Sema.zig | 4 ++-- test/behavior/cast.zig | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 7b8999a21162..97e90da92ea1 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -34603,14 +34603,14 @@ fn resolvePeerTypesInner( } // Clear existing sentinel ptr_info.sentinel = .none; - switch (ip.indexToKey(ptr_info.child)) { + if (ptr_info.flags.size == .one) switch (ip.indexToKey(ptr_info.child)) { .array_type => |array_type| ptr_info.child = (try pt.arrayType(.{ .len = array_type.len, .child = array_type.child, .sentinel = .none, })).toIntern(), else => {}, - } + }; } opt_ptr_info = ptr_info; diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index 8c2be13923be..a90519379e39 100644 --- a/test/behavior/cast.zig +++ b/test/behavior/cast.zig @@ -2649,3 +2649,19 @@ test "bitcast vector" { const bigsum: u32x8 = @bitCast(zerox32); try std.testing.expectEqual(0, @reduce(.Add, bigsum)); } + +test "peer type resolution: slice of sentinel-terminated array" { + var f: bool = undefined; + f = false; + + const a: [][2:0]u8 = &.{}; + const b: []const [2:0]u8 = &.{.{ 10, 20 }}; + + const result = if (f) a else b; + + comptime assert(@TypeOf(result) == []const [2:0]u8); + try expect(result.len == 1); + try expect(result[0].len == 2); + try expect(result[0][0] == 10); + try expect(result[0][1] == 20); +} From 9ba9f457be13c0c32981e94cf75ba7ea6619d92f Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 19:56:43 +0000 Subject: [PATCH 05/14] Sema: add fast path to PTR when all types are the same I have no idea why I didn't add this when I first implemented this PTR logic. --- src/Sema.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Sema.zig b/src/Sema.zig index 97e90da92ea1..9dc7755a10e6 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -33930,6 +33930,17 @@ fn resolvePeerTypes( else => {}, } + // Fast path: check if everything has the same type to bypass the main PTR logic. + same_type: { + const ty = sema.typeOf(instructions[0]); + for (instructions[1..]) |inst| { + if (sema.typeOf(inst).toIntern() != ty.toIntern()) { + break :same_type; + } + } + return ty; + } + const peer_tys = try sema.arena.alloc(?Type, instructions.len); const peer_vals = try sema.arena.alloc(?Value, instructions.len); From cac814cf58ca65ffd8081dca6f2f5b26d822ef5d Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 20:13:56 +0000 Subject: [PATCH 06/14] Sema: fix comparison between error set and comptime-known error union Resolves: #20613 --- src/Sema.zig | 9 ++++++++- test/behavior/error.zig | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Sema.zig b/src/Sema.zig index 9dc7755a10e6..b04cae9a8f8c 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -9145,6 +9145,7 @@ fn zirErrUnionCode(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro return sema.analyzeErrUnionCode(block, src, operand); } +/// If `operand` is comptime-known, asserts that it is an error value rather than a payload value. fn analyzeErrUnionCode(sema: *Sema, block: *Block, src: LazySrcLoc, operand: Air.Inst.Ref) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; @@ -17599,10 +17600,16 @@ fn analyzeCmp( return sema.cmpNumeric(block, src, lhs, rhs, op, lhs_src, rhs_src); } if (is_equality_cmp and lhs_ty.zigTypeTag(zcu) == .error_union and rhs_ty.zigTypeTag(zcu) == .error_set) { + if (try sema.resolveDefinedValue(block, lhs_src, lhs)) |lhs_val| { + if (lhs_val.errorUnionIsPayload(zcu)) return .bool_false; + } const casted_lhs = try sema.analyzeErrUnionCode(block, lhs_src, lhs); return sema.cmpSelf(block, src, casted_lhs, rhs, op, lhs_src, rhs_src); } if (is_equality_cmp and lhs_ty.zigTypeTag(zcu) == .error_set and rhs_ty.zigTypeTag(zcu) == .error_union) { + if (try sema.resolveDefinedValue(block, rhs_src, rhs)) |rhs_val| { + if (rhs_val.errorUnionIsPayload(zcu)) return .bool_false; + } const casted_rhs = try sema.analyzeErrUnionCode(block, rhs_src, rhs); return sema.cmpSelf(block, src, lhs, casted_rhs, op, lhs_src, rhs_src); } @@ -23254,7 +23261,7 @@ fn zirErrorCast(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData zcu.backendSupportsFeature(.error_set_has_value)) { if (dest_tag == .error_union) { - const err_code = try sema.analyzeErrUnionCode(block, operand_src, operand); + const err_code = try block.addTyOp(.unwrap_errunion_err, operand_ty, operand); const err_int = try block.addBitCast(err_int_ty, err_code); const zero_err = try pt.intRef(try pt.errorIntType(), 0); diff --git a/test/behavior/error.zig b/test/behavior/error.zig index ee9e961884b0..c915da9a6ea3 100644 --- a/test/behavior/error.zig +++ b/test/behavior/error.zig @@ -1100,3 +1100,14 @@ test "return error union with i65" { fn add(x: i65, y: i65) anyerror!i65 { return x + y; } + +test "compare error union to error set" { + const S = struct { + fn doTheTest(val: error{Foo}!i32) !void { + if (error.Foo == val) return error.Unexpected; + if (val == error.Foo) return error.Unexpected; + } + }; + try S.doTheTest(0); + try comptime S.doTheTest(0); +} From fbbf34e563a376ea1654dce827b9194ba7211b3a Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 20:33:39 +0000 Subject: [PATCH 07/14] Sema: disable runtime safety checks in comptime blocks Sometimes we emit runtime instructions in comptime scopes. These instructions will be discarded, but they allow comptime blocks to contain intermediate runtime-known values, which is necessary for expressions like `runtime_array.len` to work. Since we will always throw away these runtime instructions, including safety checks is a time waste at best and trips an assertion at worst! Resolves: #20064 --- src/Sema.zig | 16 +++++++++++++--- test/behavior/eval.zig | 11 +++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index b04cae9a8f8c..d66587abb17d 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -504,7 +504,17 @@ pub const Block = struct { }; } - pub fn wantSafety(block: *const Block) bool { + fn wantSafeTypes(block: *const Block) bool { + return block.want_safety orelse switch (block.sema.pt.zcu.optimizeMode()) { + .Debug => true, + .ReleaseSafe => true, + .ReleaseFast => false, + .ReleaseSmall => false, + }; + } + + fn wantSafety(block: *const Block) bool { + if (block.isComptime()) return false; // runtime safety checks are pointless in comptime blocks return block.want_safety orelse switch (block.sema.pt.zcu.optimizeMode()) { .Debug => true, .ReleaseSafe => true, @@ -3294,7 +3304,7 @@ fn zirUnionDecl( .tagged else if (small.layout != .auto) .none - else switch (block.wantSafety()) { + else switch (block.wantSafeTypes()) { true => .safety, false => .none, }, @@ -22219,7 +22229,7 @@ fn reifyUnion( .tagged else if (layout != .auto) .none - else switch (block.wantSafety()) { + else switch (block.wantSafeTypes()) { true => .safety, false => .none, }, diff --git a/test/behavior/eval.zig b/test/behavior/eval.zig index 4c67d292738b..0cafa1f6768a 100644 --- a/test/behavior/eval.zig +++ b/test/behavior/eval.zig @@ -1751,3 +1751,14 @@ test "comptime labeled block implicit exit" { }; comptime assert(result == {}); } + +test "comptime block has intermediate runtime-known values" { + const arr: [2]u8 = .{ 1, 2 }; + + var idx: usize = undefined; + idx = 0; + + comptime { + _ = arr[idx]; + } +} From 5317d88414324c6555338e8574811c6710df4e44 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 21:06:39 +0000 Subject: [PATCH 08/14] Sema: fix `@errorCast` with error unions Resolves: #20169 --- src/Sema.zig | 137 ++++++++++++++++++++++++---------------- test/behavior/error.zig | 21 +++++- 2 files changed, 100 insertions(+), 58 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index d66587abb17d..32f611ce3ea9 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -23175,11 +23175,12 @@ fn zirErrorCast(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData const extra = sema.code.extraData(Zir.Inst.BinNode, extended.operand).data; const src = block.nodeOffset(extra.node); const operand_src = block.builtinCallArgSrc(extra.node, 0); - const base_dest_ty = try sema.resolveDestType(block, src, extra.lhs, .remove_opt, "@errorCast"); + const dest_ty = try sema.resolveDestType(block, src, extra.lhs, .remove_opt, "@errorCast"); const operand = try sema.resolveInst(extra.rhs); - const base_operand_ty = sema.typeOf(operand); - const dest_tag = base_dest_ty.zigTypeTag(zcu); - const operand_tag = base_operand_ty.zigTypeTag(zcu); + const operand_ty = sema.typeOf(operand); + + const dest_tag = dest_ty.zigTypeTag(zcu); + const operand_tag = operand_ty.zigTypeTag(zcu); if (dest_tag != .error_set and dest_tag != .error_union) { return sema.fail(block, src, "expected error set or error union type, found '{s}'", .{@tagName(dest_tag)}); @@ -23191,107 +23192,133 @@ fn zirErrorCast(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData return sema.fail(block, src, "cannot cast an error union type to error set", .{}); } if (dest_tag == .error_union and operand_tag == .error_union and - base_dest_ty.errorUnionPayload(zcu).toIntern() != base_operand_ty.errorUnionPayload(zcu).toIntern()) + dest_ty.errorUnionPayload(zcu).toIntern() != operand_ty.errorUnionPayload(zcu).toIntern()) { return sema.failWithOwnedErrorMsg(block, msg: { const msg = try sema.errMsg(src, "payload types of error unions must match", .{}); errdefer msg.destroy(sema.gpa); - const dest_ty = base_dest_ty.errorUnionPayload(zcu); - const operand_ty = base_operand_ty.errorUnionPayload(zcu); - try sema.errNote(src, msg, "destination payload is '{}'", .{dest_ty.fmt(pt)}); - try sema.errNote(src, msg, "operand payload is '{}'", .{operand_ty.fmt(pt)}); + const dest_payload_ty = dest_ty.errorUnionPayload(zcu); + const operand_payload_ty = operand_ty.errorUnionPayload(zcu); + try sema.errNote(src, msg, "destination payload is '{}'", .{dest_payload_ty.fmt(pt)}); + try sema.errNote(src, msg, "operand payload is '{}'", .{operand_payload_ty.fmt(pt)}); try addDeclaredHereNote(sema, msg, dest_ty); try addDeclaredHereNote(sema, msg, operand_ty); break :msg msg; }); } - const dest_ty = if (dest_tag == .error_union) base_dest_ty.errorUnionSet(zcu) else base_dest_ty; - const operand_ty = if (operand_tag == .error_union) base_operand_ty.errorUnionSet(zcu) else base_operand_ty; - - // operand must be defined since it can be an invalid error value - const maybe_operand_val = try sema.resolveDefinedValue(block, operand_src, operand); + const dest_err_ty = switch (dest_tag) { + .error_union => dest_ty.errorUnionSet(zcu), + .error_set => dest_ty, + else => unreachable, + }; + const operand_err_ty = switch (operand_tag) { + .error_union => operand_ty.errorUnionSet(zcu), + .error_set => operand_ty, + else => unreachable, + }; const disjoint = disjoint: { // Try avoiding resolving inferred error sets if we can - if (!dest_ty.isAnyError(zcu) and dest_ty.errorSetIsEmpty(zcu)) break :disjoint true; - if (!operand_ty.isAnyError(zcu) and operand_ty.errorSetIsEmpty(zcu)) break :disjoint true; - if (dest_ty.isAnyError(zcu)) break :disjoint false; - if (operand_ty.isAnyError(zcu)) break :disjoint false; - const dest_err_names = dest_ty.errorSetNames(zcu); + if (!dest_err_ty.isAnyError(zcu) and dest_err_ty.errorSetIsEmpty(zcu)) break :disjoint true; + if (!operand_err_ty.isAnyError(zcu) and operand_err_ty.errorSetIsEmpty(zcu)) break :disjoint true; + if (dest_err_ty.isAnyError(zcu)) break :disjoint false; + if (operand_err_ty.isAnyError(zcu)) break :disjoint false; + const dest_err_names = dest_err_ty.errorSetNames(zcu); for (0..dest_err_names.len) |dest_err_index| { - if (Type.errorSetHasFieldIp(ip, operand_ty.toIntern(), dest_err_names.get(ip)[dest_err_index])) + if (Type.errorSetHasFieldIp(ip, operand_err_ty.toIntern(), dest_err_names.get(ip)[dest_err_index])) break :disjoint false; } - if (!ip.isInferredErrorSetType(dest_ty.toIntern()) and - !ip.isInferredErrorSetType(operand_ty.toIntern())) + if (!ip.isInferredErrorSetType(dest_err_ty.toIntern()) and + !ip.isInferredErrorSetType(operand_err_ty.toIntern())) { break :disjoint true; } - _ = try sema.resolveInferredErrorSetTy(block, src, dest_ty.toIntern()); - _ = try sema.resolveInferredErrorSetTy(block, operand_src, operand_ty.toIntern()); + _ = try sema.resolveInferredErrorSetTy(block, src, dest_err_ty.toIntern()); + _ = try sema.resolveInferredErrorSetTy(block, operand_src, operand_err_ty.toIntern()); for (0..dest_err_names.len) |dest_err_index| { - if (Type.errorSetHasFieldIp(ip, operand_ty.toIntern(), dest_err_names.get(ip)[dest_err_index])) + if (Type.errorSetHasFieldIp(ip, operand_err_ty.toIntern(), dest_err_names.get(ip)[dest_err_index])) break :disjoint false; } break :disjoint true; }; - if (disjoint and dest_tag != .error_union) { + if (disjoint and !(operand_tag == .error_union and dest_tag == .error_union)) { return sema.fail(block, src, "error sets '{}' and '{}' have no common errors", .{ - operand_ty.fmt(pt), dest_ty.fmt(pt), + operand_err_ty.fmt(pt), dest_err_ty.fmt(pt), }); } - if (maybe_operand_val) |val| { - if (!dest_ty.isAnyError(zcu)) check: { - const operand_val = zcu.intern_pool.indexToKey(val.toIntern()); - var error_name: InternPool.NullTerminatedString = undefined; - if (operand_tag == .error_union) { - if (operand_val.error_union.val != .err_name) break :check; - error_name = operand_val.error_union.val.err_name; - } else { - error_name = operand_val.err.name; - } - if (!Type.errorSetHasFieldIp(ip, dest_ty.toIntern(), error_name)) { - return sema.fail(block, src, "'error.{}' not a member of error set '{}'", .{ - error_name.fmt(ip), dest_ty.fmt(pt), - }); - } + // operand must be defined since it can be an invalid error value + if (try sema.resolveDefinedValue(block, operand_src, operand)) |operand_val| { + const err_name: InternPool.NullTerminatedString = switch (operand_tag) { + .error_set => ip.indexToKey(operand_val.toIntern()).err.name, + .error_union => switch (ip.indexToKey(operand_val.toIntern()).error_union.val) { + .err_name => |name| name, + .payload => |payload_val| { + assert(dest_tag == .error_union); // should be guaranteed from the type checks above + return sema.coerce(block, dest_ty, Air.internedToRef(payload_val), operand_src); + }, + }, + else => unreachable, + }; + + if (!dest_err_ty.isAnyError(zcu) and !Type.errorSetHasFieldIp(ip, dest_err_ty.toIntern(), err_name)) { + return sema.fail(block, src, "'error.{}' not a member of error set '{}'", .{ + err_name.fmt(ip), dest_err_ty.fmt(pt), + }); } - return Air.internedToRef((try pt.getCoerced(val, base_dest_ty)).toIntern()); + return Air.internedToRef(try pt.intern(switch (dest_tag) { + .error_set => .{ .err = .{ + .ty = dest_ty.toIntern(), + .name = err_name, + } }, + .error_union => .{ .error_union = .{ + .ty = dest_ty.toIntern(), + .val = .{ .err_name = err_name }, + } }, + else => unreachable, + })); } - try sema.requireRuntimeBlock(block, src, operand_src); const err_int_ty = try pt.errorIntType(); - if (block.wantSafety() and !dest_ty.isAnyError(zcu) and - dest_ty.toIntern() != .adhoc_inferred_error_set_type and + if (block.wantSafety() and !dest_err_ty.isAnyError(zcu) and + dest_err_ty.toIntern() != .adhoc_inferred_error_set_type and zcu.backendSupportsFeature(.error_set_has_value)) { - if (dest_tag == .error_union) { - const err_code = try block.addTyOp(.unwrap_errunion_err, operand_ty, operand); - const err_int = try block.addBitCast(err_int_ty, err_code); - const zero_err = try pt.intRef(try pt.errorIntType(), 0); + const err_code_inst = switch (operand_tag) { + .error_set => operand, + .error_union => try block.addTyOp(.unwrap_errunion_err, operand_err_ty, operand), + else => unreachable, + }; + const err_int_inst = try block.addBitCast(err_int_ty, err_code_inst); - const is_zero = try block.addBinOp(.cmp_eq, err_int, zero_err); + if (dest_tag == .error_union) { + const zero_err = try pt.intRef(err_int_ty, 0); + const is_zero = try block.addBinOp(.cmp_eq, err_int_inst, zero_err); if (disjoint) { // Error must be zero. try sema.addSafetyCheck(block, src, is_zero, .invalid_error_code); } else { // Error must be in destination set or zero. - const has_value = try block.addTyOp(.error_set_has_value, dest_ty, err_code); + const has_value = try block.addTyOp(.error_set_has_value, dest_err_ty, err_int_inst); const ok = try block.addBinOp(.bool_or, has_value, is_zero); try sema.addSafetyCheck(block, src, ok, .invalid_error_code); } } else { - const err_int_inst = try block.addBitCast(err_int_ty, operand); - const ok = try block.addTyOp(.error_set_has_value, dest_ty, err_int_inst); + const ok = try block.addTyOp(.error_set_has_value, dest_err_ty, err_int_inst); try sema.addSafetyCheck(block, src, ok, .invalid_error_code); } } - return block.addBitCast(base_dest_ty, operand); + + if (operand_tag == .error_set and dest_tag == .error_union) { + const err_val = try block.addBitCast(dest_err_ty, operand); + return block.addTyOp(.wrap_errunion_err, dest_ty, err_val); + } else { + return block.addBitCast(dest_ty, operand); + } } fn zirPtrCastFull(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref { diff --git a/test/behavior/error.zig b/test/behavior/error.zig index c915da9a6ea3..5dd9cfd1920f 100644 --- a/test/behavior/error.zig +++ b/test/behavior/error.zig @@ -1060,9 +1060,24 @@ test "errorCast to adhoc inferred error set" { try std.testing.expect((try S.baz()) == 1234); } -test "errorCast from error sets to error unions" { - const err_union: Set1!void = @errorCast(error.A); - try expectError(error.A, err_union); +test "@errorCast from error set to error union" { + const S = struct { + fn doTheTest(set: error{ A, B }) error{A}!i32 { + return @errorCast(set); + } + }; + try expectError(error.A, S.doTheTest(error.A)); + try expectError(error.A, comptime S.doTheTest(error.A)); +} + +test "@errorCast from error union to error union" { + const S = struct { + fn doTheTest(set: error{ A, B }!i32) error{A}!i32 { + return @errorCast(set); + } + }; + try expectError(error.A, S.doTheTest(error.A)); + try expectError(error.A, comptime S.doTheTest(error.A)); } test "result location initialization of error union with OPV payload" { From 3031d813874f6d6ad9ae4b793e3af6cdf632fa66 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 21:26:04 +0000 Subject: [PATCH 09/14] Sema: fix `@typeInfo` of function with generic return type and IES Resolves: #20088 --- src/Sema.zig | 14 ++++++++++---- test/behavior/type_info.zig | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 32f611ce3ea9..5f92e12b04d5 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -18145,10 +18145,16 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai const ret_ty_opt = try pt.intern(.{ .opt = .{ .ty = try pt.intern(.{ .opt_type = .type_type }), - .val = if (func_ty_info.return_type == .generic_poison_type) - .none - else - func_ty_info.return_type, + .val = opt_val: { + const ret_ty: Type = .fromInterned(func_ty_info.return_type); + if (ret_ty.toIntern() == .generic_poison_type) break :opt_val .none; + if (ret_ty.zigTypeTag(zcu) == .error_union) { + if (ret_ty.errorUnionPayload(zcu).toIntern() == .generic_poison_type) { + break :opt_val .none; + } + } + break :opt_val ret_ty.toIntern(); + }, } }); const callconv_ty = try sema.getBuiltinType(src, .CallingConvention); diff --git a/test/behavior/type_info.zig b/test/behavior/type_info.zig index 2b7a66c763f2..86e2eccc4c95 100644 --- a/test/behavior/type_info.zig +++ b/test/behavior/type_info.zig @@ -675,3 +675,12 @@ test "@typeInfo only contains pub decls" { try std.testing.expectEqualStrings("Enum", decls[0].name); try std.testing.expectEqualStrings("Struct", decls[1].name); } + +test "@typeInfo function with generic return type and inferred error set" { + const S = struct { + fn testFn(comptime T: type) !T {} + }; + + const ret_ty = @typeInfo(@TypeOf(S.testFn)).@"fn".return_type; + comptime assert(ret_ty == null); +} From b21becb2a6a82b4ac65e98fa0094a7786cbbfdc3 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 21:58:32 +0000 Subject: [PATCH 10/14] incremental: fix crash when introducing syntax error Clearing the analysis roots was very clever and all, but not actually valid. We need to avoid *any* reference to the analysis errors if there were any fatal files, and that includes sorting the errors! Resolves: #22774 --- src/Compilation.zig | 10 +++-- src/Zcu.zig | 2 + .../analysis_error_and_syntax_error | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 test/incremental/analysis_error_and_syntax_error diff --git a/src/Compilation.zig b/src/Compilation.zig index 14c216854e88..0062465ae42c 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2195,6 +2195,8 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void { zcu.compile_log_text.shrinkAndFree(gpa, 0); + zcu.skip_analysis_errors = false; + // Make sure std.zig is inside the import_table. We unconditionally need // it for start.zig. const std_mod = zcu.std_mod; @@ -3207,7 +3209,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle { }); } - if (comp.zcu) |zcu| { + if (comp.zcu) |zcu| zcu_errors: { for (zcu.failed_files.keys(), zcu.failed_files.values()) |file, error_msg| { if (error_msg) |msg| { try addModuleErrorMsg(zcu, &bundle, msg.*); @@ -3224,6 +3226,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle { } } } + if (zcu.skip_analysis_errors) break :zcu_errors; var sorted_failed_analysis: std.AutoArrayHashMapUnmanaged(InternPool.AnalUnit, *Zcu.ErrorMsg).DataList.Slice = s: { const SortOrder = struct { zcu: *Zcu, @@ -3359,7 +3362,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle { try comp.link_diags.addMessagesToBundle(&bundle, comp.bin_file); if (comp.zcu) |zcu| { - if (bundle.root_list.items.len == 0 and zcu.compile_log_sources.count() != 0) { + if (!zcu.skip_analysis_errors and bundle.root_list.items.len == 0 and zcu.compile_log_sources.count() != 0) { const values = zcu.compile_log_sources.values(); // First one will be the error; subsequent ones will be notes. const src_loc = values[0].src(); @@ -3860,10 +3863,9 @@ fn performAllTheWorkInner( // We give up right now! No updating of ZIR refs, no nothing. The idea is that this prevents // us from invalidating lots of incremental dependencies due to files with e.g. parse errors. // However, this means our analysis data is invalid, so we want to omit all analysis errors. - // To do that, let's just clear the analysis roots! assert(zcu.failed_files.count() > 0); // we will get an error - zcu.analysis_roots.clear(); // no analysis happened + zcu.skip_analysis_errors = true; return; } diff --git a/src/Zcu.zig b/src/Zcu.zig index 9b292ad78354..c870a04adffc 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -181,6 +181,8 @@ analysis_roots: std.BoundedArray(*Package.Module, 3) = .{}, /// Allocated into `gpa`. resolved_references: ?std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) = null, +skip_analysis_errors: bool = false, + stage1_flags: packed struct { have_winmain: bool = false, have_wwinmain: bool = false, diff --git a/test/incremental/analysis_error_and_syntax_error b/test/incremental/analysis_error_and_syntax_error new file mode 100644 index 000000000000..05d899a22548 --- /dev/null +++ b/test/incremental/analysis_error_and_syntax_error @@ -0,0 +1,42 @@ +#target=x86_64-linux-selfhosted +#target=x86_64-linux-cbe +#target=x86_64-windows-cbe +#target=wasm32-wasi-selfhosted +#update=initial version +#file=main.zig +pub fn main() !void { + @compileError("uh oh"); +} +#expect_error=main.zig:2:5: error: uh oh + +#update=add parse error +#file=main.zig +pub fn main() !void { + @compileError("uh oh"); +#expect_error=main.zig:3:1: error: expected statement, found 'EOF' + +#update=fix parse error +#file=main.zig +pub fn main() !void { + @compileError("uh oh"); +} +#expect_error=main.zig:2:5: error: uh oh + +#update=add parse error again +#file=main.zig +pub fn main() !void { + @compileError("uh oh"); +#expect_error=main.zig:3:1: error: expected statement, found 'EOF' + +#update=comment @compileError call +#file=main.zig +pub fn main() !void { + //@compileError("uh oh"); +#expect_error=main.zig:3:1: error: expected statement, found 'EOF' + +#update=fix parse error again +#file=main.zig +pub fn main() !void { + //@compileError("uh oh"); +} +#expect_stdout="" From 6bd92a21b7f31de913c7383ea380f3b68d387395 Mon Sep 17 00:00:00 2001 From: mlugg Date: Wed, 5 Feb 2025 23:07:43 +0000 Subject: [PATCH 11/14] behavior: add test for old bug Resolves: #2289 --- test/behavior/union.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/behavior/union.zig b/test/behavior/union.zig index 364a83814acb..16ae6e509c74 100644 --- a/test/behavior/union.zig +++ b/test/behavior/union.zig @@ -2322,3 +2322,19 @@ test "assign global tagged union" { try expect(U.global == .b); try expect(U.global.b == 123456); } + +test "set mutable union by switching on same union" { + const U = union(enum) { + foo, + bar: usize, + }; + + var val: U = .foo; + val = switch (val) { + .foo => .{ .bar = 2 }, + .bar => .foo, + }; + + try expect(val == .bar); + try expect(val.bar == 2); +} From 164700740b9530ffc7622f9b4c664278a7f68160 Mon Sep 17 00:00:00 2001 From: mlugg Date: Thu, 6 Feb 2025 00:51:44 +0000 Subject: [PATCH 12/14] behavior: add test for old bug Resolves: #13013 --- test/behavior/generics.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/behavior/generics.zig b/test/behavior/generics.zig index 4e86419258ab..f8e0735b2efb 100644 --- a/test/behavior/generics.zig +++ b/test/behavior/generics.zig @@ -589,3 +589,19 @@ comptime { // should override the result of the previous analysis. for (0..2) |_| _ = fn (void) void; } + +test "generic parameter resolves to comptime-only type but is not marked comptime" { + const S = struct { + fn foo(comptime T: type, rt_false: bool, func: fn (T) void) T { + if (rt_false) _ = foo(T, rt_false, func); + return 123; + } + fn bar(_: u8) void {} + }; + + const rt_result = S.foo(u8, false, S.bar); + try expect(rt_result == 123); + + const ct_result = comptime S.foo(u8, false, S.bar); + comptime std.debug.assert(ct_result == 123); +} From 75ec7d863efbba9dbb3eefa5e6607a5ef2fe8648 Mon Sep 17 00:00:00 2001 From: mlugg Date: Thu, 6 Feb 2025 01:11:10 +0000 Subject: [PATCH 13/14] Sema: add missing `validateRuntimeValue` calls Resolves: #13791 --- src/Sema.zig | 4 ++++ .../compile_errors/for_comptime_array_pointer.zig | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 test/cases/compile_errors/for_comptime_array_pointer.zig diff --git a/src/Sema.zig b/src/Sema.zig index 5f92e12b04d5..0e16d92e659d 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -29043,6 +29043,7 @@ fn elemValArray( } try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, array_ty, array_src); + try sema.validateRuntimeValue(block, array_src, array); if (oob_safety and block.wantSafety()) { // Runtime check is only needed if unable to comptime check. @@ -29107,6 +29108,7 @@ fn elemPtrArray( if (!init) { try sema.validateRuntimeElemAccess(block, elem_index_src, array_ty.elemType2(zcu), array_ty, array_ptr_src); + try sema.validateRuntimeValue(block, array_ptr_src, array_ptr); } // Runtime check is only needed if unable to comptime check. @@ -29164,6 +29166,7 @@ fn elemValSlice( } try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, slice_ty, slice_src); + try sema.validateRuntimeValue(block, slice_src, slice); if (oob_safety and block.wantSafety()) { const len_inst = if (maybe_slice_val) |slice_val| @@ -29220,6 +29223,7 @@ fn elemPtrSlice( } try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ptr_ty, slice_ty, slice_src); + try sema.validateRuntimeValue(block, slice_src, slice); if (oob_safety and block.wantSafety()) { const len_inst = len: { diff --git a/test/cases/compile_errors/for_comptime_array_pointer.zig b/test/cases/compile_errors/for_comptime_array_pointer.zig new file mode 100644 index 000000000000..48f461cf4261 --- /dev/null +++ b/test/cases/compile_errors/for_comptime_array_pointer.zig @@ -0,0 +1,12 @@ +export fn foo() void { + comptime var elems: [3]u32 = undefined; + for (&elems) |*elem| { + _ = elem; + } +} + +// error +// +// :3:10: error: runtime value contains reference to comptime var +// :3:10: note: comptime var pointers are not available at runtime +// :2:34: note: 'runtime_value' points to comptime var declared here From 5d935e1137ade5450e504ce9bc6bbf32301b0dfd Mon Sep 17 00:00:00 2001 From: mlugg Date: Thu, 6 Feb 2025 01:23:27 +0000 Subject: [PATCH 14/14] behavior: add test for old bug Resolves: #18435 --- test/behavior/fn.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/behavior/fn.zig b/test/behavior/fn.zig index 6cf0f1579500..dbaf6b0f2ba6 100644 --- a/test/behavior/fn.zig +++ b/test/behavior/fn.zig @@ -711,3 +711,20 @@ test "inline call propagates comptime-known argument to generic parameter and re try expect(a1 == 12340); try expect(b1 == 12340); } + +test "inline function return type is evaluated at comptime" { + const S = struct { + inline fn assertComptimeAndRet(x: anytype) @TypeOf(x) { + if (!@inComptime()) comptime unreachable; + return x; + } + + inline fn foo(val: anytype) assertComptimeAndRet(u16) { + return val; + } + }; + + const result = S.foo(123); + comptime assert(@TypeOf(result) == u16); + try expect(result == 123); +}