Commit bfada7c514

Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
2022-02-28 02:30:15
stage2: implement peer type resolution between error unions
1 parent d5131e9
Changed files (3)
src
test
behavior
src/Sema.zig
@@ -17610,6 +17610,21 @@ fn resolvePeerTypes(
     const target = sema.mod.getTarget();
 
     var chosen = instructions[0];
+    var err_set_ty: ?Type = blk: {
+        const chosen_ty = sema.typeOf(chosen);
+
+        // TODO: is this the right handling of generic poison?
+        if (chosen_ty.tag() == .generic_poison or chosen_ty.zigTypeTag() != .ErrorSet)
+            break :blk null;
+
+        // If our chosen type is inferred, we have to resolve it now.
+        if (chosen_ty.castTag(.error_set_inferred)) |inferred| {
+            try sema.resolveInferredErrorSet(inferred.data);
+        }
+
+        break :blk chosen_ty;
+    };
+
     var any_are_null = false;
     var make_the_slice_const = false;
     var convert_to_slice = false;
@@ -17711,7 +17726,175 @@ fn resolvePeerTypes(
                 },
                 else => {},
             },
+            .ErrorSet => {
+                if (chosen_ty_tag == .ErrorSet) {
+                    assert(err_set_ty != null);
+
+                    // If chosen type is anyerror, then we can use the prev type
+                    if (err_set_ty.?.isAnyError()) continue;
+
+                    // At this point, we must resolve any inferred error sets
+                    if (candidate_ty.castTag(.error_set_inferred)) |inferred| {
+                        try sema.resolveInferredErrorSet(inferred.data);
+                    }
+
+                    // If candidate is anyerror then we use it because it
+                    // is trivially a supserset of previous error set
+                    if (candidate_ty.isAnyError()) {
+                        err_set_ty = candidate_ty;
+                        chosen = candidate;
+                        chosen_i = candidate_i + 1;
+                        continue;
+                    }
+
+                    // If chosen is superset of candidate, keep it.
+                    // If candidate is superset of chosen, switch it.
+                    // If neither is a superset, merge errors.
+                    var prev_is_superset = true;
+                    for (candidate_ty.errorSetNames()) |name| {
+                        if (!err_set_ty.?.errorSetHasField(name)) {
+                            prev_is_superset = false;
+                            break;
+                        }
+                    }
+                    if (prev_is_superset) continue; // use previous
+
+                    var cand_is_superset = true;
+                    for (err_set_ty.?.errorSetNames()) |name| {
+                        if (!candidate_ty.errorSetHasField(name)) {
+                            cand_is_superset = false;
+                            break;
+                        }
+                    }
+                    if (cand_is_superset) {
+                        // Swap to candidate
+                        err_set_ty = candidate_ty;
+                        chosen = candidate;
+                        chosen_i = candidate_i + 1;
+                        continue;
+                    }
+
+                    // Merge errors
+                    err_set_ty = try err_set_ty.?.errorSetMerge(sema.arena, candidate_ty);
+                    chosen = candidate;
+                    chosen_i = candidate_i + 1;
+                    continue;
+                }
+            },
             .ErrorUnion => {
+                if (chosen_ty_tag == .ErrorSet) {
+                    if (err_set_ty.?.isAnyError()) {
+                        chosen = candidate;
+                        chosen_i = candidate_i + 1;
+                        continue;
+                    }
+
+                    const eu_set_ty = candidate_ty.errorUnionSet();
+                    if (eu_set_ty.castTag(.error_set_inferred)) |inferred| {
+                        try sema.resolveInferredErrorSet(inferred.data);
+                    }
+                    if (eu_set_ty.isAnyError()) {
+                        err_set_ty = eu_set_ty;
+                        chosen = candidate;
+                        chosen_i = candidate_i + 1;
+                        continue;
+                    }
+
+                    // If candidate is a superset of the error type, then use it.
+                    var cand_is_superset = true;
+                    for (err_set_ty.?.errorSetNames()) |name| {
+                        if (!candidate_ty.errorSetHasField(name)) {
+                            cand_is_superset = false;
+                            break;
+                        }
+                    }
+                    if (cand_is_superset) {
+                        // Swap to candidate
+                        err_set_ty = candidate_ty;
+                        chosen = candidate;
+                        chosen_i = candidate_i + 1;
+                        continue;
+                    }
+
+                    // Not a superset, create merged error set
+                    err_set_ty = try err_set_ty.?.errorSetMerge(sema.arena, eu_set_ty);
+                    chosen = candidate;
+                    chosen_i = candidate_i + 1;
+                    continue;
+                }
+
+                if (chosen_ty_tag == .ErrorUnion) {
+                    const chosen_payload_ty = chosen_ty.errorUnionPayload();
+                    const candidate_payload_ty = candidate_ty.errorUnionPayload();
+
+                    const coerce_chosen = (try sema.coerceInMemoryAllowed(block, chosen_payload_ty, candidate_payload_ty, false, target, src, src)) == .ok;
+                    const coerce_candidate = (try sema.coerceInMemoryAllowed(block, candidate_payload_ty, chosen_payload_ty, false, target, src, src)) == .ok;
+
+                    if (coerce_chosen or coerce_candidate) {
+                        // If we can coerce to the candidate, we switch to that
+                        // type. This is the same logic as the bare (non-union)
+                        // coercion check we do at the top of this func.
+                        if (coerce_candidate) {
+                            chosen = candidate;
+                            chosen_i = candidate_i + 1;
+                        }
+
+                        const chosen_set_ty = chosen_ty.errorUnionSet();
+                        const candidate_set_ty = chosen_ty.errorUnionSet();
+
+                        // If our error sets match already, then we are done.
+                        if (chosen_set_ty.eql(candidate_set_ty)) continue;
+
+                        // They don't match, so we need to figure out if we
+                        // need to merge them, use the superset, etc. This
+                        // requires resolution.
+                        if (chosen_set_ty.castTag(.error_set_inferred)) |inferred| {
+                            try sema.resolveInferredErrorSet(inferred.data);
+                        }
+                        if (candidate_set_ty.castTag(.error_set_inferred)) |inferred| {
+                            try sema.resolveInferredErrorSet(inferred.data);
+                        }
+
+                        if (chosen_set_ty.isAnyError()) {
+                            err_set_ty = chosen_set_ty;
+                            continue;
+                        }
+
+                        if (candidate_set_ty.isAnyError()) {
+                            err_set_ty = candidate_set_ty;
+                            continue;
+                        }
+
+                        if (err_set_ty == null) err_set_ty = chosen_set_ty;
+
+                        // If the previous error set type is a superset, we're done.
+                        var prev_is_superset = true;
+                        for (candidate_set_ty.errorSetNames()) |name| {
+                            if (!chosen_set_ty.errorSetHasField(name)) {
+                                prev_is_superset = false;
+                                break;
+                            }
+                        }
+                        if (prev_is_superset) continue; // use previous
+
+                        var cand_is_superset = true;
+                        for (chosen_set_ty.errorSetNames()) |name| {
+                            if (!candidate_set_ty.errorSetHasField(name)) {
+                                cand_is_superset = false;
+                                break;
+                            }
+                        }
+                        if (cand_is_superset) {
+                            err_set_ty = candidate_ty;
+                            continue;
+                        }
+
+                        // Merge errors
+                        err_set_ty = try chosen_set_ty.errorSetMerge(sema.arena, candidate_ty);
+                        continue;
+                    }
+                }
+
                 const payload_ty = candidate_ty.errorUnionPayload();
                 if (chosen_ty_tag == .Pointer and
                     chosen_ty.ptrSize() == .One and
@@ -17962,6 +18145,24 @@ fn resolvePeerTypes(
         return Type.ptr(sema.arena, target, info.data);
     }
 
+    if (err_set_ty) |ty| switch (chosen_ty.zigTypeTag()) {
+        .ErrorSet => return ty,
+
+        .ErrorUnion => {
+            const payload_ty = chosen_ty.errorUnionPayload();
+            return try Module.errorUnionType(sema.arena, ty, payload_ty);
+        },
+
+        .ComptimeInt, .ComptimeFloat => return sema.fail(block, src, "unable to make error union out of number literal", .{}),
+
+        .Null => return sema.fail(block, src, "unable to make error union out of null literal", .{}),
+
+        else => {
+            // Create error union of our error set and the chosen type
+            return try Module.errorUnionType(sema.arena, ty, chosen_ty);
+        },
+    };
+
     return chosen_ty;
 }
 
src/type.zig
@@ -4216,6 +4216,23 @@ pub const Type = extern union {
         };
     }
 
+    /// Merge ty with ty2.
+    /// Asserts that ty and ty2 are both error sets and are resolved.
+    pub fn errorSetMerge(ty: Type, arena: Allocator, ty2: Type) !Type {
+        const lhs_names = ty.errorSetNames();
+        const rhs_names = ty2.errorSetNames();
+        var names = Module.ErrorSet.NameMap{};
+        try names.ensureUnusedCapacity(arena, @intCast(u32, lhs_names.len + rhs_names.len));
+        for (lhs_names) |name| {
+            names.putAssumeCapacityNoClobber(name, {});
+        }
+        for (rhs_names) |name| {
+            names.putAssumeCapacity(name, {});
+        }
+
+        return try Tag.error_set_merged.create(arena, names);
+    }
+
     pub fn enumFields(ty: Type) Module.EnumFull.NameMap {
         return switch (ty.tag()) {
             .enum_full, .enum_nonexhaustive => ty.cast(Payload.EnumFull).?.data.fields,
test/behavior/error.zig
@@ -264,8 +264,6 @@ fn testErrToIntWithOnePossibleValue(
 }
 
 test "error union peer type resolution" {
-    if (builtin.zig_backend != .stage1) return error.SkipZigTest; // TODO
-
     try testErrorUnionPeerTypeResolution(1);
 }