authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-02-01 08:07:14+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-02-01 15:48:45+00:00
log3924f173af51c50ac18be2166fe115555241778e
treeee3fa65cc85dd6022af6533068dc3fb4f46461ab
parentc225b780e3944014300555c3b45eaccd7bc7c8ae

compiler: do not propagate result type to `try` operand

This commit effectively reverts 9e683f0, and hence un-accepts #19777. While nice in theory, this proposal turned out to have a few problems. Firstly, supplying a result type implicitly coerces the operand to this type -- that's the main point of result types! But for `try`, this is actually a bad idea; we want a redundant `try` to be a compile error, not to silently coerce the non-error value to an error union. In practice, this didn't always happen, because the implementation was buggy anyway; but when it did, it was really quite silly. For instance, `try try ... try .{ ... }` was an accepted expression, with the inner initializer being initially coerced to `E!E!...E!T`. Secondly, the result type inference here didn't play nicely with `return`. If you write `return try`, the operand would actually receive a result type of `E!E!T`, since the `return` gave a result type of `E!T` and the `try` wrapped it in *another* error union. More generally, the problem here is that `try` doesn't know when it should or shouldn't nest error unions. This occasionally broke code which looked like it should work. So, this commit prevents `try` from propagating result types through to its operand. A key motivation for the original proposal here was decl literals; so, as a special case, `try .foo(...)` is still an allowed syntax form, caught by AstGen and specially lowered. This does open the doors to allowing other special cases for decl literals in future, such as `.foo(...) catch ...`, but those proposals are for another time. Resolves: #21991 Resolves: #22633

6 files changed, 105 insertions(+), 72 deletions(-)

lib/std/zig/AstGen.zig+32-19
...@@ -851,7 +851,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE...@@ -851,7 +851,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
851 .async_call_comma,851 .async_call_comma,
852 => {852 => {
853 var buf: [1]Ast.Node.Index = undefined;853 var buf: [1]Ast.Node.Index = undefined;
854 return callExpr(gz, scope, ri, node, tree.fullCall(&buf, node).?);854 return callExpr(gz, scope, ri, .none, node, tree.fullCall(&buf, node).?);
855 },855 },
856856
857 .unreachable_literal => {857 .unreachable_literal => {
...@@ -3009,8 +3009,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As...@@ -3009,8 +3009,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
3009 .validate_ptr_array_init,3009 .validate_ptr_array_init,
3010 .validate_ref_ty,3010 .validate_ref_ty,
3011 .validate_const,3011 .validate_const,
3012 .try_operand_ty,
3013 .try_ref_operand_ty,
3014 => break :b true,3012 => break :b true,
30153013
3016 .@"defer" => unreachable,3014 .@"defer" => unreachable,
...@@ -6158,20 +6156,24 @@ fn tryExpr(...@@ -6158,20 +6156,24 @@ fn tryExpr(
6158 const try_lc: LineColumn = .{ astgen.source_line - parent_gz.decl_line, astgen.source_column };6156 const try_lc: LineColumn = .{ astgen.source_line - parent_gz.decl_line, astgen.source_column };
61596157
6160 const operand_rl: ResultInfo.Loc, const block_tag: Zir.Inst.Tag = switch (ri.rl) {6158 const operand_rl: ResultInfo.Loc, const block_tag: Zir.Inst.Tag = switch (ri.rl) {
6161 .ref => .{ .ref, .try_ptr },6159 .ref, .ref_coerced_ty => .{ .ref, .try_ptr },
6162 .ref_coerced_ty => |payload_ptr_ty| .{6160 else => .{ .none, .@"try" },
6163 .{ .ref_coerced_ty = try parent_gz.addUnNode(.try_ref_operand_ty, payload_ptr_ty, node) },
6164 .try_ptr,
6165 },
6166 else => if (try ri.rl.resultType(parent_gz, node)) |payload_ty| .{
6167 // `coerced_ty` is OK due to the `rvalue` call below
6168 .{ .coerced_ty = try parent_gz.addUnNode(.try_operand_ty, payload_ty, node) },
6169 .@"try",
6170 } else .{ .none, .@"try" },
6171 };6161 };
6172 const operand_ri: ResultInfo = .{ .rl = operand_rl, .ctx = .error_handling_expr };6162 const operand_ri: ResultInfo = .{ .rl = operand_rl, .ctx = .error_handling_expr };
6173 // This could be a pointer or value depending on the `ri` parameter.6163 const operand = operand: {
6174 const operand = try reachableExpr(parent_gz, scope, operand_ri, operand_node, node);6164 // As a special case, we need to detect this form:
6165 // `try .foo(...)`
6166 // This is a decl literal form, even though we don't propagate a result type through `try`.
6167 var buf: [1]Ast.Node.Index = undefined;
6168 if (astgen.tree.fullCall(&buf, operand_node)) |full_call| {
6169 const res_ty: Zir.Inst.Ref = try ri.rl.resultType(parent_gz, operand_node) orelse .none;
6170 break :operand try callExpr(parent_gz, scope, operand_ri, res_ty, operand_node, full_call);
6171 }
6172
6173 // This could be a pointer or value depending on the `ri` parameter.
6174 break :operand try reachableExpr(parent_gz, scope, operand_ri, operand_node, node);
6175 };
6176
6175 const try_inst = try parent_gz.makeBlockInst(block_tag, node);6177 const try_inst = try parent_gz.makeBlockInst(block_tag, node);
6176 try parent_gz.instructions.append(astgen.gpa, try_inst);6178 try parent_gz.instructions.append(astgen.gpa, try_inst);
61776179
...@@ -10236,12 +10238,15 @@ fn callExpr(...@@ -10236,12 +10238,15 @@ fn callExpr(
10236 gz: *GenZir,10238 gz: *GenZir,
10237 scope: *Scope,10239 scope: *Scope,
10238 ri: ResultInfo,10240 ri: ResultInfo,
10241 /// If this is not `.none` and this call is a decl literal form (`.foo(...)`), then this
10242 /// type is used as the decl literal result type instead of the result type from `ri.rl`.
10243 override_decl_literal_type: Zir.Inst.Ref,
10239 node: Ast.Node.Index,10244 node: Ast.Node.Index,
10240 call: Ast.full.Call,10245 call: Ast.full.Call,
10241) InnerError!Zir.Inst.Ref {10246) InnerError!Zir.Inst.Ref {
10242 const astgen = gz.astgen;10247 const astgen = gz.astgen;
1024310248
10244 const callee = try calleeExpr(gz, scope, ri.rl, call.ast.fn_expr);10249 const callee = try calleeExpr(gz, scope, ri.rl, override_decl_literal_type, call.ast.fn_expr);
10245 const modifier: std.builtin.CallModifier = blk: {10250 const modifier: std.builtin.CallModifier = blk: {
10246 if (call.async_token != null) {10251 if (call.async_token != null) {
10247 break :blk .async_kw;10252 break :blk .async_kw;
...@@ -10367,6 +10372,9 @@ fn calleeExpr(...@@ -10367,6 +10372,9 @@ fn calleeExpr(
10367 gz: *GenZir,10372 gz: *GenZir,
10368 scope: *Scope,10373 scope: *Scope,
10369 call_rl: ResultInfo.Loc,10374 call_rl: ResultInfo.Loc,
10375 /// If this is not `.none` and this call is a decl literal form (`.foo(...)`), then this
10376 /// type is used as the decl literal result type instead of the result type from `call_rl`.
10377 override_decl_literal_type: Zir.Inst.Ref,
10370 node: Ast.Node.Index,10378 node: Ast.Node.Index,
10371) InnerError!Callee {10379) InnerError!Callee {
10372 const astgen = gz.astgen;10380 const astgen = gz.astgen;
...@@ -10393,7 +10401,14 @@ fn calleeExpr(...@@ -10393,7 +10401,14 @@ fn calleeExpr(
10393 .field_name_start = str_index,10401 .field_name_start = str_index,
10394 } };10402 } };
10395 },10403 },
10396 .enum_literal => if (try call_rl.resultType(gz, node)) |res_ty| {10404 .enum_literal => {
10405 const res_ty = res_ty: {
10406 if (override_decl_literal_type != .none) break :res_ty override_decl_literal_type;
10407 break :res_ty try call_rl.resultType(gz, node) orelse {
10408 // No result type; lower to a literal call of an enum literal.
10409 return .{ .direct = try expr(gz, scope, .{ .rl = .none }, node) };
10410 };
10411 };
10397 // Decl literal call syntax, e.g.10412 // Decl literal call syntax, e.g.
10398 // `const foo: T = .init();`10413 // `const foo: T = .init();`
10399 // Look up `init` in `T`, but don't try and coerce it.10414 // Look up `init` in `T`, but don't try and coerce it.
...@@ -10403,8 +10418,6 @@ fn calleeExpr(...@@ -10403,8 +10418,6 @@ fn calleeExpr(
10403 .field_name_start = str_index,10418 .field_name_start = str_index,
10404 });10419 });
10405 return .{ .direct = callee };10420 return .{ .direct = callee };
10406 } else {
10407 return .{ .direct = try expr(gz, scope, .{ .rl = .none }, node) };
10408 },10421 },
10409 else => return .{ .direct = try expr(gz, scope, .{ .rl = .none }, node) },10422 else => return .{ .direct = try expr(gz, scope, .{ .rl = .none }, node) },
10410 }10423 }
lib/std/zig/Zir.zig-16
...@@ -721,14 +721,6 @@ pub const Inst = struct {...@@ -721,14 +721,6 @@ pub const Inst = struct {
721 /// Result is always void.721 /// Result is always void.
722 /// Uses the `un_node` union field. Node is the initializer. Operand is the initializer value.722 /// Uses the `un_node` union field. Node is the initializer. Operand is the initializer value.
723 validate_const,723 validate_const,
724 /// Given a type `T`, construct the type `E!T`, where `E` is this function's error set, to be used
725 /// as the result type of a `try` operand. Generic poison is propagated.
726 /// Uses the `un_node` union field. Node is the `try` expression. Operand is the type `T`.
727 try_operand_ty,
728 /// Given a type `*T`, construct the type `*E!T`, where `E` is this function's error set, to be used
729 /// as the result type of a `try` operand whose address is taken with `&`. Generic poison is propagated.
730 /// Uses the `un_node` union field. Node is the `try` expression. Operand is the type `*T`.
731 try_ref_operand_ty,
732724
733 // The following tags all relate to struct initialization expressions.725 // The following tags all relate to struct initialization expressions.
734726
...@@ -1304,8 +1296,6 @@ pub const Inst = struct {...@@ -1304,8 +1296,6 @@ pub const Inst = struct {
1304 .array_init_elem_ptr,1296 .array_init_elem_ptr,
1305 .validate_ref_ty,1297 .validate_ref_ty,
1306 .validate_const,1298 .validate_const,
1307 .try_operand_ty,
1308 .try_ref_operand_ty,
1309 .restore_err_ret_index_unconditional,1299 .restore_err_ret_index_unconditional,
1310 .restore_err_ret_index_fn_entry,1300 .restore_err_ret_index_fn_entry,
1311 => false,1301 => false,
...@@ -1365,8 +1355,6 @@ pub const Inst = struct {...@@ -1365,8 +1355,6 @@ pub const Inst = struct {
1365 .validate_ptr_array_init,1355 .validate_ptr_array_init,
1366 .validate_ref_ty,1356 .validate_ref_ty,
1367 .validate_const,1357 .validate_const,
1368 .try_operand_ty,
1369 .try_ref_operand_ty,
1370 => true,1358 => true,
13711359
1372 .param,1360 .param,
...@@ -1749,8 +1737,6 @@ pub const Inst = struct {...@@ -1749,8 +1737,6 @@ pub const Inst = struct {
1749 .coerce_ptr_elem_ty = .pl_node,1737 .coerce_ptr_elem_ty = .pl_node,
1750 .validate_ref_ty = .un_tok,1738 .validate_ref_ty = .un_tok,
1751 .validate_const = .un_node,1739 .validate_const = .un_node,
1752 .try_operand_ty = .un_node,
1753 .try_ref_operand_ty = .un_node,
17541740
1755 .int_from_ptr = .un_node,1741 .int_from_ptr = .un_node,
1756 .compile_error = .un_node,1742 .compile_error = .un_node,
...@@ -4196,8 +4182,6 @@ fn findTrackableInner(...@@ -4196,8 +4182,6 @@ fn findTrackableInner(
4196 .coerce_ptr_elem_ty,4182 .coerce_ptr_elem_ty,
4197 .validate_ref_ty,4183 .validate_ref_ty,
4198 .validate_const,4184 .validate_const,
4199 .try_operand_ty,
4200 .try_ref_operand_ty,
4201 .struct_init_empty,4185 .struct_init_empty,
4202 .struct_init_empty_result,4186 .struct_init_empty_result,
4203 .struct_init_empty_ref_result,4187 .struct_init_empty_ref_result,
src/Sema.zig-16
...@@ -1257,8 +1257,6 @@ fn analyzeBodyInner(...@@ -1257,8 +1257,6 @@ fn analyzeBodyInner(
1257 .validate_array_init_ref_ty => try sema.zirValidateArrayInitRefTy(block, inst),1257 .validate_array_init_ref_ty => try sema.zirValidateArrayInitRefTy(block, inst),
1258 .opt_eu_base_ptr_init => try sema.zirOptEuBasePtrInit(block, inst),1258 .opt_eu_base_ptr_init => try sema.zirOptEuBasePtrInit(block, inst),
1259 .coerce_ptr_elem_ty => try sema.zirCoercePtrElemTy(block, inst),1259 .coerce_ptr_elem_ty => try sema.zirCoercePtrElemTy(block, inst),
1260 .try_operand_ty => try sema.zirTryOperandTy(block, inst, false),
1261 .try_ref_operand_ty => try sema.zirTryOperandTy(block, inst, true),
12621260
1263 .clz => try sema.zirBitCount(block, inst, .clz, Value.clz),1261 .clz => try sema.zirBitCount(block, inst, .clz, Value.clz),
1264 .ctz => try sema.zirBitCount(block, inst, .ctz, Value.ctz),1262 .ctz => try sema.zirBitCount(block, inst, .ctz, Value.ctz),
...@@ -2085,20 +2083,6 @@ fn genericPoisonReason(sema: *Sema, block: *Block, ref: Zir.Inst.Ref) GenericPoi...@@ -2085,20 +2083,6 @@ fn genericPoisonReason(sema: *Sema, block: *Block, ref: Zir.Inst.Ref) GenericPoi
2085 const un_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;2083 const un_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
2086 cur = un_node.operand;2084 cur = un_node.operand;
2087 },2085 },
2088 .try_operand_ty => {
2089 // Either the input type was itself poison, or it was a slice, which we cannot translate
2090 // to an overall result type.
2091 const un_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
2092 const operand_ref = try sema.resolveInst(un_node.operand);
2093 if (operand_ref == .generic_poison_type) {
2094 // The input was poison -- keep looking.
2095 cur = un_node.operand;
2096 continue;
2097 }
2098 // We got a poison because the result type was a slice. This is a tricky case -- let's just
2099 // not bother explaining it to the user for now...
2100 return .unknown;
2101 },
2102 .struct_init_field_type => {2086 .struct_init_field_type => {
2103 const pl_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;2087 const pl_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
2104 const extra = sema.code.extraData(Zir.Inst.FieldType, pl_node.payload_index).data;2088 const extra = sema.code.extraData(Zir.Inst.FieldType, pl_node.payload_index).data;
src/print_zir.zig-2
...@@ -278,8 +278,6 @@ const Writer = struct {...@@ -278,8 +278,6 @@ const Writer = struct {
278 .opt_eu_base_ptr_init,278 .opt_eu_base_ptr_init,
279 .restore_err_ret_index_unconditional,279 .restore_err_ret_index_unconditional,
280 .restore_err_ret_index_fn_entry,280 .restore_err_ret_index_fn_entry,
281 .try_operand_ty,
282 .try_ref_operand_ty,
283 => try self.writeUnNode(stream, inst),281 => try self.writeUnNode(stream, inst),
284282
285 .ref,283 .ref,
test/behavior/try.zig+21-19
...@@ -68,25 +68,6 @@ test "`try`ing an if/else expression" {...@@ -68,25 +68,6 @@ test "`try`ing an if/else expression" {
68 try std.testing.expectError(error.Test, S.getError2());68 try std.testing.expectError(error.Test, S.getError2());
69}69}
7070
71test "try forwards result location" {
72 if (builtin.zig_backend == .stage2_x86) return error.SkipZigTest; // TODO
73 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
74 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
75 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
76 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
77
78 const S = struct {
79 fn foo(err: bool) error{Foo}!u32 {
80 const result: error{ Foo, Bar }!u32 = if (err) error.Foo else 123;
81 const res_int: u32 = try @errorCast(result);
82 return res_int;
83 }
84 };
85
86 try expect((S.foo(false) catch return error.TestUnexpectedResult) == 123);
87 try std.testing.expectError(error.Foo, S.foo(true));
88}
89
90test "'return try' of empty error set in function returning non-error" {71test "'return try' of empty error set in function returning non-error" {
91 if (builtin.zig_backend == .stage2_x86) return error.SkipZigTest; // TODO72 if (builtin.zig_backend == .stage2_x86) return error.SkipZigTest; // TODO
92 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO73 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
...@@ -123,3 +104,24 @@ test "'return try' of empty error set in function returning non-error" {...@@ -123,3 +104,24 @@ test "'return try' of empty error set in function returning non-error" {
123 try S.doTheTest();104 try S.doTheTest();
124 try comptime S.doTheTest();105 try comptime S.doTheTest();
125}106}
107
108test "'return try' through conditional" {
109 const S = struct {
110 fn get(t: bool) !u32 {
111 return try if (t) inner() else error.TestFailed;
112 }
113 fn inner() !u16 {
114 return 123;
115 }
116 };
117
118 {
119 const result = try S.get(true);
120 try expect(result == 123);
121 }
122
123 {
124 const result = try comptime S.get(true);
125 comptime std.debug.assert(result == 123);
126 }
127}
test/cases/compile_errors/redundant_try.zig created+52
...@@ -0,0 +1,52 @@
1const S = struct { x: u32 = 0 };
2const T = struct { []const u8 };
3
4fn test0() !void {
5 const x: u8 = try 1;
6 _ = x;
7}
8
9fn test1() !void {
10 const x: S = try .{};
11 _ = x;
12}
13
14fn test2() !void {
15 const x: S = try .{ .x = 123 };
16 _ = x;
17}
18
19fn test3() !void {
20 const x: S = try try .{ .x = 123 };
21 _ = x;
22}
23
24fn test4() !void {
25 const x: T = try .{"hello"};
26 _ = x;
27}
28
29fn test5() !void {
30 const x: error{Foo}!u32 = 123;
31 _ = try try x;
32}
33
34comptime {
35 _ = &test0;
36 _ = &test1;
37 _ = &test2;
38 _ = &test3;
39 _ = &test4;
40 _ = &test5;
41}
42
43// error
44//
45// :5:23: error: expected error union type, found 'comptime_int'
46// :10:23: error: expected error union type, found '@TypeOf(.{})'
47// :15:23: error: expected error union type, found 'tmp.test2__struct_493'
48// :15:23: note: struct declared here
49// :20:27: error: expected error union type, found 'tmp.test3__struct_495'
50// :20:27: note: struct declared here
51// :25:23: error: expected error union type, found 'struct { comptime *const [5:0]u8 = "hello" }'
52// :31:13: error: expected error union type, found 'u32'