| author | |
| committer | |
| log | 6534f2ef4f8161f4121326f19bc3cf89324f62c5 |
| tree | 87fb9d6f2b082a534402080f53febf483d81e7fc |
| parent | 53b87fa78a3ee2c261f3bdd6a71d5733fe17ffd5 |
* Sema: fix returned operands not coercing to the function return type
in some cases.
- When returning an error or an error union from a function with an
inferred error set, it will now populate the inferred error set.
- Implement error set coercion for the common case of inferred error
set to inferred error set, without forcing a full resolution.
* LLVM backend: update instruction lowering that handles error unions
to respect `isByRef`.
- Also implement `wrap_err_union_err`.5 files changed, 146 insertions(+), 45 deletions(-)
src/Module.zig+4| ... | @@ -782,6 +782,10 @@ pub const ErrorSet = struct { | ... | @@ -782,6 +782,10 @@ pub const ErrorSet = struct { |
| 782 | /// The length is given by `names_len`. | 782 | /// The length is given by `names_len`. |
| 783 | names_ptr: [*]const []const u8, | 783 | names_ptr: [*]const []const u8, |
| 784 | 784 | ||
| 785 | pub fn names(self: ErrorSet) []const []const u8 { | ||
| 786 | return self.names_ptr[0..self.names_len]; | ||
| 787 | } | ||
| 788 | |||
| 785 | pub fn srcLoc(self: ErrorSet) SrcLoc { | 789 | pub fn srcLoc(self: ErrorSet) SrcLoc { |
| 786 | return .{ | 790 | return .{ |
| 787 | .file_scope = self.owner_decl.getFileScope(), | 791 | .file_scope = self.owner_decl.getFileScope(), |
src/Sema.zig+49-28| ... | @@ -4845,6 +4845,8 @@ fn funcCommon( | ... | @@ -4845,6 +4845,8 @@ fn funcCommon( |
| 4845 | const error_set_ty = try Type.Tag.error_set_inferred.create(sema.arena, .{ | 4845 | const error_set_ty = try Type.Tag.error_set_inferred.create(sema.arena, .{ |
| 4846 | .func = new_func, | 4846 | .func = new_func, |
| 4847 | .map = .{}, | 4847 | .map = .{}, |
| 4848 | .functions = .{}, | ||
| 4849 | .is_anyerror = false, | ||
| 4848 | }); | 4850 | }); |
| 4849 | break :blk try Type.Tag.error_union.create(sema.arena, .{ | 4851 | break :blk try Type.Tag.error_union.create(sema.arena, .{ |
| 4850 | .error_set = error_set_ty, | 4852 | .error_set = error_set_ty, |
| ... | @@ -8466,19 +8468,13 @@ fn zirRetErrValue( | ... | @@ -8466,19 +8468,13 @@ fn zirRetErrValue( |
| 8466 | const err_name = inst_data.get(sema.code); | 8468 | const err_name = inst_data.get(sema.code); |
| 8467 | const src = inst_data.src(); | 8469 | const src = inst_data.src(); |
| 8468 | 8470 | ||
| 8469 | // Add the error tag to the inferred error set of the in-scope function. | ||
| 8470 | if (sema.fn_ret_ty.zigTypeTag() == .ErrorUnion) { | ||
| 8471 | if (sema.fn_ret_ty.errorUnionSet().castTag(.error_set_inferred)) |payload| { | ||
| 8472 | _ = try payload.data.map.getOrPut(sema.gpa, err_name); | ||
| 8473 | } | ||
| 8474 | } | ||
| 8475 | // Return the error code from the function. | 8471 | // Return the error code from the function. |
| 8476 | const kv = try sema.mod.getErrorValue(err_name); | 8472 | const kv = try sema.mod.getErrorValue(err_name); |
| 8477 | const result_inst = try sema.addConstant( | 8473 | const result_inst = try sema.addConstant( |
| 8478 | try Type.Tag.error_set_single.create(sema.arena, kv.key), | 8474 | try Type.Tag.error_set_single.create(sema.arena, kv.key), |
| 8479 | try Value.Tag.@"error".create(sema.arena, .{ .name = kv.key }), | 8475 | try Value.Tag.@"error".create(sema.arena, .{ .name = kv.key }), |
| 8480 | ); | 8476 | ); |
| 8481 | return sema.analyzeRet(block, result_inst, src, true); | 8477 | return sema.analyzeRet(block, result_inst, src); |
| 8482 | } | 8478 | } |
| 8483 | 8479 | ||
| 8484 | fn zirRetCoerce( | 8480 | fn zirRetCoerce( |
| ... | @@ -8493,7 +8489,7 @@ fn zirRetCoerce( | ... | @@ -8493,7 +8489,7 @@ fn zirRetCoerce( |
| 8493 | const operand = sema.resolveInst(inst_data.operand); | 8489 | const operand = sema.resolveInst(inst_data.operand); |
| 8494 | const src = inst_data.src(); | 8490 | const src = inst_data.src(); |
| 8495 | 8491 | ||
| 8496 | return sema.analyzeRet(block, operand, src, true); | 8492 | return sema.analyzeRet(block, operand, src); |
| 8497 | } | 8493 | } |
| 8498 | 8494 | ||
| 8499 | fn zirRetNode(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index { | 8495 | fn zirRetNode(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index { |
| ... | @@ -8504,11 +8500,7 @@ fn zirRetNode(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir | ... | @@ -8504,11 +8500,7 @@ fn zirRetNode(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir |
| 8504 | const operand = sema.resolveInst(inst_data.operand); | 8500 | const operand = sema.resolveInst(inst_data.operand); |
| 8505 | const src = inst_data.src(); | 8501 | const src = inst_data.src(); |
| 8506 | 8502 | ||
| 8507 | // TODO: we pass false here for the `need_coercion` boolean, but I'm pretty sure we need | 8503 | return sema.analyzeRet(block, operand, src); |
| 8508 | // to remove this parameter entirely. Observe the problem by looking at the incorrect compile | ||
| 8509 | // error that occurs when a behavior test case being executed at comptime fails, e.g. | ||
| 8510 | // `test { comptime foo(); } fn foo() { try expect(false); }` | ||
| 8511 | return sema.analyzeRet(block, operand, src, false); | ||
| 8512 | } | 8504 | } |
| 8513 | 8505 | ||
| 8514 | fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index { | 8506 | fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index { |
| ... | @@ -8521,7 +8513,7 @@ fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir | ... | @@ -8521,7 +8513,7 @@ fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir |
| 8521 | 8513 | ||
| 8522 | if (block.is_comptime or block.inlining != null) { | 8514 | if (block.is_comptime or block.inlining != null) { |
| 8523 | const operand = try sema.analyzeLoad(block, src, ret_ptr, src); | 8515 | const operand = try sema.analyzeLoad(block, src, ret_ptr, src); |
| 8524 | return sema.analyzeRet(block, operand, src, false); | 8516 | return sema.analyzeRet(block, operand, src); |
| 8525 | } | 8517 | } |
| 8526 | try sema.requireRuntimeBlock(block, src); | 8518 | try sema.requireRuntimeBlock(block, src); |
| 8527 | _ = try block.addUnOp(.ret_load, ret_ptr); | 8519 | _ = try block.addUnOp(.ret_load, ret_ptr); |
| ... | @@ -8533,12 +8525,25 @@ fn analyzeRet( | ... | @@ -8533,12 +8525,25 @@ fn analyzeRet( |
| 8533 | block: *Block, | 8525 | block: *Block, |
| 8534 | uncasted_operand: Air.Inst.Ref, | 8526 | uncasted_operand: Air.Inst.Ref, |
| 8535 | src: LazySrcLoc, | 8527 | src: LazySrcLoc, |
| 8536 | need_coercion: bool, | ||
| 8537 | ) CompileError!Zir.Inst.Index { | 8528 | ) CompileError!Zir.Inst.Index { |
| 8538 | const operand = if (!need_coercion) | 8529 | // Special case for returning an error to an inferred error set; we need to |
| 8539 | uncasted_operand | 8530 | // add the error tag to the inferred error set of the in-scope function, so |
| 8540 | else | 8531 | // that the coercion below works correctly. |
| 8541 | try sema.coerce(block, sema.fn_ret_ty, uncasted_operand, src); | 8532 | if (sema.fn_ret_ty.zigTypeTag() == .ErrorUnion) { |
| 8533 | if (sema.fn_ret_ty.errorUnionSet().castTag(.error_set_inferred)) |payload| { | ||
| 8534 | const op_ty = sema.typeOf(uncasted_operand); | ||
| 8535 | switch (op_ty.zigTypeTag()) { | ||
| 8536 | .ErrorSet => { | ||
| 8537 | try payload.data.addErrorSet(sema.gpa, op_ty); | ||
| 8538 | }, | ||
| 8539 | .ErrorUnion => { | ||
| 8540 | try payload.data.addErrorSet(sema.gpa, op_ty.errorUnionSet()); | ||
| 8541 | }, | ||
| 8542 | else => {}, | ||
| 8543 | } | ||
| 8544 | } | ||
| 8545 | } | ||
| 8546 | const operand = try sema.coerce(block, sema.fn_ret_ty, uncasted_operand, src); | ||
| 8542 | 8547 | ||
| 8543 | if (block.inlining) |inlining| { | 8548 | if (block.inlining) |inlining| { |
| 8544 | if (block.is_comptime) { | 8549 | if (block.is_comptime) { |
| ... | @@ -11605,14 +11610,30 @@ fn coerce( | ... | @@ -11605,14 +11610,30 @@ fn coerce( |
| 11605 | // T to E!T or E to E!T | 11610 | // T to E!T or E to E!T |
| 11606 | return sema.wrapErrorUnion(block, dest_ty, inst, inst_src); | 11611 | return sema.wrapErrorUnion(block, dest_ty, inst, inst_src); |
| 11607 | }, | 11612 | }, |
| 11608 | .ErrorSet => { | 11613 | .ErrorSet => switch (inst_ty.zigTypeTag()) { |
| 11609 | // Coercion to `anyerror`. | 11614 | .ErrorSet => { |
| 11610 | // TODO If the dest type tag is not `anyerror` it still could | 11615 | // Coercion to `anyerror`. Note that this check can return false positives |
| 11611 | // resolve to anyerror. `dest_ty` needs to have inferred error set resolution | 11616 | // in case the error sets did not get resolved. |
| 11612 | // happen before this check. | 11617 | if (dest_ty.isAnyError()) { |
| 11613 | if (dest_ty.tag() == .anyerror and inst_ty.zigTypeTag() == .ErrorSet) { | 11618 | return sema.coerceCompatibleErrorSets(block, inst, inst_src); |
| 11614 | return sema.coerceErrSetToAnyError(block, inst, inst_src); | 11619 | } |
| 11615 | } | 11620 | // If both are inferred error sets of functions, and |
| 11621 | // the dest includes the source function, the coercion is OK. | ||
| 11622 | // This check is important because it works without forcing a full resolution | ||
| 11623 | // of inferred error sets. | ||
| 11624 | if (inst_ty.castTag(.error_set_inferred)) |src_payload| { | ||
| 11625 | if (dest_ty.castTag(.error_set_inferred)) |dst_payload| { | ||
| 11626 | const src_func = src_payload.data.func; | ||
| 11627 | const dst_func = dst_payload.data.func; | ||
| 11628 | |||
| 11629 | if (src_func == dst_func or dst_payload.data.functions.contains(src_func)) { | ||
| 11630 | return sema.coerceCompatibleErrorSets(block, inst, inst_src); | ||
| 11631 | } | ||
| 11632 | } | ||
| 11633 | } | ||
| 11634 | // TODO full error set resolution and compare sets by names. | ||
| 11635 | }, | ||
| 11636 | else => {}, | ||
| 11616 | }, | 11637 | }, |
| 11617 | .Union => switch (inst_ty.zigTypeTag()) { | 11638 | .Union => switch (inst_ty.zigTypeTag()) { |
| 11618 | .Enum, .EnumLiteral => return sema.coerceEnumToUnion(block, dest_ty, dest_ty_src, inst, inst_src), | 11639 | .Enum, .EnumLiteral => return sema.coerceEnumToUnion(block, dest_ty, dest_ty_src, inst, inst_src), |
| ... | @@ -12245,7 +12266,7 @@ fn coerceVectorToArray( | ... | @@ -12245,7 +12266,7 @@ fn coerceVectorToArray( |
| 12245 | return block.addTyOp(.bitcast, array_ty, vector); | 12266 | return block.addTyOp(.bitcast, array_ty, vector); |
| 12246 | } | 12267 | } |
| 12247 | 12268 | ||
| 12248 | fn coerceErrSetToAnyError( | 12269 | fn coerceCompatibleErrorSets( |
| 12249 | sema: *Sema, | 12270 | sema: *Sema, |
| 12250 | block: *Block, | 12271 | block: *Block, |
| 12251 | err_set: Air.Inst.Ref, | 12272 | err_set: Air.Inst.Ref, |
src/codegen/llvm.zig+33-15| ... | @@ -2301,8 +2301,7 @@ pub const FuncGen = struct { | ... | @@ -2301,8 +2301,7 @@ pub const FuncGen = struct { |
| 2301 | op: llvm.IntPredicate, | 2301 | op: llvm.IntPredicate, |
| 2302 | operand_is_ptr: bool, | 2302 | operand_is_ptr: bool, |
| 2303 | ) !?*const llvm.Value { | 2303 | ) !?*const llvm.Value { |
| 2304 | if (self.liveness.isUnused(inst)) | 2304 | if (self.liveness.isUnused(inst)) return null; |
| 2305 | return null; | ||
| 2306 | 2305 | ||
| 2307 | const un_op = self.air.instructions.items(.data)[inst].un_op; | 2306 | const un_op = self.air.instructions.items(.data)[inst].un_op; |
| 2308 | const operand = try self.resolveInst(un_op); | 2307 | const operand = try self.resolveInst(un_op); |
| ... | @@ -2363,22 +2362,16 @@ pub const FuncGen = struct { | ... | @@ -2363,22 +2362,16 @@ pub const FuncGen = struct { |
| 2363 | inst: Air.Inst.Index, | 2362 | inst: Air.Inst.Index, |
| 2364 | operand_is_ptr: bool, | 2363 | operand_is_ptr: bool, |
| 2365 | ) !?*const llvm.Value { | 2364 | ) !?*const llvm.Value { |
| 2366 | if (self.liveness.isUnused(inst)) | 2365 | if (self.liveness.isUnused(inst)) return null; |
| 2367 | return null; | ||
| 2368 | 2366 | ||
| 2369 | const ty_op = self.air.instructions.items(.data)[inst].ty_op; | 2367 | const ty_op = self.air.instructions.items(.data)[inst].ty_op; |
| 2370 | const operand = try self.resolveInst(ty_op.operand); | 2368 | const operand = try self.resolveInst(ty_op.operand); |
| 2371 | const err_union_ty = self.air.typeOf(ty_op.operand); | 2369 | const err_union_ty = self.air.typeOf(ty_op.operand); |
| 2372 | const payload_ty = err_union_ty.errorUnionPayload(); | 2370 | const payload_ty = err_union_ty.errorUnionPayload(); |
| 2373 | 2371 | if (!payload_ty.hasCodeGenBits()) return null; | |
| 2374 | if (!payload_ty.hasCodeGenBits()) { | 2372 | if (operand_is_ptr or isByRef(payload_ty)) { |
| 2375 | return null; | ||
| 2376 | } | ||
| 2377 | |||
| 2378 | if (operand_is_ptr) { | ||
| 2379 | return self.builder.buildStructGEP(operand, 1, ""); | 2373 | return self.builder.buildStructGEP(operand, 1, ""); |
| 2380 | } | 2374 | } |
| 2381 | |||
| 2382 | return self.builder.buildExtractValue(operand, 1, ""); | 2375 | return self.builder.buildExtractValue(operand, 1, ""); |
| 2383 | } | 2376 | } |
| 2384 | 2377 | ||
| ... | @@ -2400,7 +2393,7 @@ pub const FuncGen = struct { | ... | @@ -2400,7 +2393,7 @@ pub const FuncGen = struct { |
| 2400 | return self.builder.buildLoad(operand, ""); | 2393 | return self.builder.buildLoad(operand, ""); |
| 2401 | } | 2394 | } |
| 2402 | 2395 | ||
| 2403 | if (operand_is_ptr) { | 2396 | if (operand_is_ptr or isByRef(payload_ty)) { |
| 2404 | const err_field_ptr = self.builder.buildStructGEP(operand, 0, ""); | 2397 | const err_field_ptr = self.builder.buildStructGEP(operand, 0, ""); |
| 2405 | return self.builder.buildLoad(err_field_ptr, ""); | 2398 | return self.builder.buildLoad(err_field_ptr, ""); |
| 2406 | } | 2399 | } |
| ... | @@ -2469,10 +2462,35 @@ pub const FuncGen = struct { | ... | @@ -2469,10 +2462,35 @@ pub const FuncGen = struct { |
| 2469 | } | 2462 | } |
| 2470 | 2463 | ||
| 2471 | fn airWrapErrUnionErr(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value { | 2464 | fn airWrapErrUnionErr(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value { |
| 2472 | if (self.liveness.isUnused(inst)) | 2465 | if (self.liveness.isUnused(inst)) return null; |
| 2473 | return null; | 2466 | |
| 2467 | const ty_op = self.air.instructions.items(.data)[inst].ty_op; | ||
| 2468 | const err_un_ty = self.air.typeOfIndex(inst); | ||
| 2469 | const payload_ty = err_un_ty.errorUnionPayload(); | ||
| 2470 | const operand = try self.resolveInst(ty_op.operand); | ||
| 2471 | if (!payload_ty.hasCodeGenBits()) { | ||
| 2472 | return operand; | ||
| 2473 | } | ||
| 2474 | const err_un_llvm_ty = try self.dg.llvmType(err_un_ty); | ||
| 2475 | if (isByRef(err_un_ty)) { | ||
| 2476 | const result_ptr = self.buildAlloca(err_un_llvm_ty); | ||
| 2477 | const err_ptr = self.builder.buildStructGEP(result_ptr, 0, ""); | ||
| 2478 | _ = self.builder.buildStore(operand, err_ptr); | ||
| 2479 | const payload_ptr = self.builder.buildStructGEP(result_ptr, 1, ""); | ||
| 2480 | var ptr_ty_payload: Type.Payload.ElemType = .{ | ||
| 2481 | .base = .{ .tag = .single_mut_pointer }, | ||
| 2482 | .data = payload_ty, | ||
| 2483 | }; | ||
| 2484 | const payload_ptr_ty = Type.initPayload(&ptr_ty_payload.base); | ||
| 2485 | // TODO store undef to payload_ptr | ||
| 2486 | _ = payload_ptr; | ||
| 2487 | _ = payload_ptr_ty; | ||
| 2488 | return result_ptr; | ||
| 2489 | } | ||
| 2474 | 2490 | ||
| 2475 | return self.todo("implement llvm codegen for 'airWrapErrUnionErr'", .{}); | 2491 | const partial = self.builder.buildInsertValue(err_un_llvm_ty.getUndef(), operand, 0, ""); |
| 2492 | // TODO set payload bytes to undef | ||
| 2493 | return partial; | ||
| 2476 | } | 2494 | } |
| 2477 | 2495 | ||
| 2478 | fn airMin(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value { | 2496 | fn airMin(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value { |
src/type.zig+42-2| ... | @@ -2619,6 +2619,17 @@ pub const Type = extern union { | ... | @@ -2619,6 +2619,17 @@ pub const Type = extern union { |
| 2619 | }; | 2619 | }; |
| 2620 | } | 2620 | } |
| 2621 | 2621 | ||
| 2622 | /// Returns true if it is an error set that includes anyerror, false otherwise. | ||
| 2623 | /// Note that the result may be a false negative if the type did not get error set | ||
| 2624 | /// resolution prior to this call. | ||
| 2625 | pub fn isAnyError(ty: Type) bool { | ||
| 2626 | return switch (ty.tag()) { | ||
| 2627 | .anyerror => true, | ||
| 2628 | .error_set_inferred => ty.castTag(.error_set_inferred).?.data.is_anyerror, | ||
| 2629 | else => false, | ||
| 2630 | }; | ||
| 2631 | } | ||
| 2632 | |||
| 2622 | /// Asserts the type is an array or vector. | 2633 | /// Asserts the type is an array or vector. |
| 2623 | pub fn arrayLen(ty: Type) u64 { | 2634 | pub fn arrayLen(ty: Type) u64 { |
| 2624 | return switch (ty.tag()) { | 2635 | return switch (ty.tag()) { |
| ... | @@ -3871,10 +3882,39 @@ pub const Type = extern union { | ... | @@ -3871,10 +3882,39 @@ pub const Type = extern union { |
| 3871 | pub const base_tag = Tag.error_set_inferred; | 3882 | pub const base_tag = Tag.error_set_inferred; |
| 3872 | 3883 | ||
| 3873 | base: Payload = Payload{ .tag = base_tag }, | 3884 | base: Payload = Payload{ .tag = base_tag }, |
| 3874 | data: struct { | 3885 | data: Data, |
| 3886 | |||
| 3887 | pub const Data = struct { | ||
| 3875 | func: *Module.Fn, | 3888 | func: *Module.Fn, |
| 3889 | /// Direct additions to the inferred error set via `return error.Foo;`. | ||
| 3876 | map: std.StringHashMapUnmanaged(void), | 3890 | map: std.StringHashMapUnmanaged(void), |
| 3877 | }, | 3891 | /// Other functions with inferred error sets which this error set includes. |
| 3892 | functions: std.AutoHashMapUnmanaged(*Module.Fn, void), | ||
| 3893 | is_anyerror: bool, | ||
| 3894 | |||
| 3895 | pub fn addErrorSet(self: *Data, gpa: *Allocator, err_set_ty: Type) !void { | ||
| 3896 | switch (err_set_ty.tag()) { | ||
| 3897 | .error_set => { | ||
| 3898 | const names = err_set_ty.castTag(.error_set).?.data.names(); | ||
| 3899 | for (names) |name| { | ||
| 3900 | try self.map.put(gpa, name, {}); | ||
| 3901 | } | ||
| 3902 | }, | ||
| 3903 | .error_set_single => { | ||
| 3904 | const name = err_set_ty.castTag(.error_set_single).?.data; | ||
| 3905 | try self.map.put(gpa, name, {}); | ||
| 3906 | }, | ||
| 3907 | .error_set_inferred => { | ||
| 3908 | const func = err_set_ty.castTag(.error_set_inferred).?.data.func; | ||
| 3909 | try self.functions.put(gpa, func, {}); | ||
| 3910 | }, | ||
| 3911 | .anyerror => { | ||
| 3912 | self.is_anyerror = true; | ||
| 3913 | }, | ||
| 3914 | else => unreachable, | ||
| 3915 | } | ||
| 3916 | } | ||
| 3917 | }; | ||
| 3878 | }; | 3918 | }; |
| 3879 | 3919 | ||
| 3880 | pub const Pointer = struct { | 3920 | pub const Pointer = struct { |
test/behavior/error.zig+18| ... | @@ -31,3 +31,21 @@ test "empty error union" { | ... | @@ -31,3 +31,21 @@ test "empty error union" { |
| 31 | const x = error{} || error{}; | 31 | const x = error{} || error{}; |
| 32 | _ = x; | 32 | _ = x; |
| 33 | } | 33 | } |
| 34 | |||
| 35 | pub fn foo() anyerror!i32 { | ||
| 36 | const x = try bar(); | ||
| 37 | return x + 1; | ||
| 38 | } | ||
| 39 | |||
| 40 | pub fn bar() anyerror!i32 { | ||
| 41 | return 13; | ||
| 42 | } | ||
| 43 | |||
| 44 | pub fn baz() anyerror!i32 { | ||
| 45 | const y = foo() catch 1234; | ||
| 46 | return y + 1; | ||
| 47 | } | ||
| 48 | |||
| 49 | test "error wrapping" { | ||
| 50 | try expect((baz() catch unreachable) == 15); | ||
| 51 | } |