diff options
| author | mlugg <mlugg@mlugg.co.uk> | 2023-06-13 15:17:31 +0100 |
|---|---|---|
| committer | mlugg <mlugg@mlugg.co.uk> | 2023-06-13 21:48:18 +0100 |
| commit | c9531eb833e1e2c432dc2bfb0ca3b25622b7001e (patch) | |
| tree | 7cdae51f8762b670aed003b3654ff429a731146d /src/Sema.zig | |
| parent | 854f26ad8ac8dc39ef7ad60f86588c0c5dba131a (diff) | |
| download | zig-c9531eb833e1e2c432dc2bfb0ca3b25622b7001e.tar.gz zig-c9531eb833e1e2c432dc2bfb0ca3b25622b7001e.zip | |
Sema: rewrite peer type resolution
The existing logic for peer type resolution was quite convoluted and
buggy. This rewrite makes it much more resilient, readable, and
extensible. The algorithm works by first iterating over the types to
select a "strategy", then applying that strategy, possibly applying peer
resolution recursively.
Several new tests have been added to cover cases which the old logic did
not correctly handle.
Resolves: #15138
Resolves: #15644
Resolves: #15693
Resolves: #15709
Resolves: #15752
Diffstat (limited to 'src/Sema.zig')
| -rw-r--r-- | src/Sema.zig | 1935 |
1 files changed, 1427 insertions, 508 deletions
diff --git a/src/Sema.zig b/src/Sema.zig index 32cd218798..f0c64fb0b7 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -27959,9 +27959,8 @@ fn coerceInMemoryAllowedErrorSets( switch (src_ty.toIntern()) { .anyerror_type => switch (ip.indexToKey(dest_ty.toIntern())) { - .inferred_error_set_type => unreachable, // Caught by dest_ty.isAnyError(mod) above. .simple_type => unreachable, // filtered out above - .error_set_type => return .from_anyerror, + .error_set_type, .inferred_error_set_type => return .from_anyerror, else => unreachable, }, @@ -28008,8 +28007,6 @@ fn coerceInMemoryAllowedErrorSets( else => unreachable, }, } - - unreachable; } fn coerceInMemoryAllowedFns( @@ -31586,6 +31583,366 @@ fn unionToTag( return block.addTyOp(.get_union_tag, enum_ty, un); } +const PeerResolveStrategy = enum { + /// The type is not known. + /// If refined no further, this is equivalent to `exact`. + unknown, + /// The type may be an error set or error union. + /// If refined no further, it is an error set. + error_set, + /// The type must be some error union. + error_union, + /// The type may be @TypeOf(null), an optional or a C pointer. + /// If refined no further, it is @TypeOf(null). + nullable, + /// The type must be some optional or a C pointer. + /// If refined no further, it is an optional. + optional, + /// The type must be either an array or a vector. + /// If refined no further, it is an array. + array, + /// The type must be a vector. + vector, + /// The type must be a C pointer. + c_ptr, + /// The type must be a pointer (C or not). + /// If refined no further, it is a non-C pointer. + ptr, + /// The type must be a function or a pointer to a function. + /// If refined no further, it is a function. + func, + /// The type must be an enum literal, or some specific enum or union. Which one is decided + /// afterwards based on the types in question. + enum_or_union, + /// The type must be some integer or float type. + /// If refined no further, it is `comptime_int`. + comptime_int, + /// The type must be some float type. + /// If refined no further, it is `comptime_float`. + comptime_float, + /// The type must be some float or fixed-width integer type. + /// If refined no further, it is some fixed-width integer type. + fixed_int, + /// The type must be some fixed-width float type. + fixed_float, + /// The type must be a struct literal or tuple type. + coercible_struct, + /// The peers must all be of the same type. + exact, + + const Reason = struct { + peers: std.DynamicBitSet, + fn reset(r: *Reason) void { + r.peers.setRangeValue(.{ .start = 0, .end = r.peers.capacity() }, false); + } + }; + + fn name(s: PeerResolveStrategy) []const u8 { + return switch (s) { + .unknown, .exact => "exact", + .error_set => "error set", + .error_union => "error union", + .nullable => "null", + .optional => "optional", + .array => "array", + .vector => "vector", + .c_ptr => "C pointer", + .ptr => "pointer", + .func => "function", + .enum_or_union => "enum or union", + .comptime_int => "comptime_int", + .comptime_float => "comptime_float", + .fixed_int => "fixed-width int", + .fixed_float => "fixed-width float", + .coercible_struct => "anonymous struct or tuple", + }; + } + + /// Given two strategies, find a strategy that satisfies both, if one exists. If no such + /// strategy exists, any strategy may be returned; an error will be emitted when the caller + /// attempts to use the strategy to resolve the type. + /// Strategy `a` comes from the peers set in `reason`, while strategy `b` comes from the peer at + /// index `b_peer_idx`. `reason` will be updated to reflect the reason for the new strategy. + fn merge(a: PeerResolveStrategy, b: PeerResolveStrategy, reason: *Reason, b_peer_idx: usize) PeerResolveStrategy { + // Our merging should be order-independent. Thus, even though the union order is arbitrary, + // by sorting the tags and switching first on the smaller, we have half as many cases to + // worry about (since we avoid the duplicates). + const s0_is_a = @enumToInt(a) <= @enumToInt(b); + const s0 = if (s0_is_a) a else b; + const s1 = if (s0_is_a) b else a; + + const ReasonMethod = enum { + all_s0, + all_s1, + either, + both, + }; + + const res: struct { ReasonMethod, PeerResolveStrategy } = switch (s0) { + .unknown => .{ .all_s1, s1 }, + .error_set => switch (s1) { + .error_set => .{ .either, .error_set }, + else => .{ .both, .error_union }, + }, + .error_union => switch (s1) { + .error_union => .{ .either, .error_union }, + else => .{ .all_s0, .error_union }, + }, + .nullable => switch (s1) { + .nullable => .{ .either, .nullable }, + .c_ptr => .{ .all_s1, .c_ptr }, + else => .{ .both, .optional }, + }, + .optional => switch (s1) { + .optional => .{ .either, .optional }, + .c_ptr => .{ .all_s1, .c_ptr }, + else => .{ .all_s0, .optional }, + }, + .array => switch (s1) { + .array => .{ .either, .array }, + .vector => .{ .all_s1, .vector }, + else => .{ .all_s0, .array }, + }, + .vector => switch (s1) { + .vector => .{ .either, .vector }, + else => .{ .all_s0, .vector }, + }, + .c_ptr => switch (s1) { + .c_ptr => .{ .either, .c_ptr }, + else => .{ .all_s0, .c_ptr }, + }, + .ptr => switch (s1) { + .ptr => .{ .either, .ptr }, + else => .{ .all_s0, .ptr }, + }, + .func => switch (s1) { + .func => .{ .either, .func }, + else => .{ .all_s1, s1 }, // doesn't override anything later + }, + .enum_or_union => switch (s1) { + .enum_or_union => .{ .either, .enum_or_union }, + else => .{ .all_s0, .enum_or_union }, + }, + .comptime_int => switch (s1) { + .comptime_int => .{ .either, .comptime_int }, + else => .{ .all_s1, s1 }, // doesn't override anything later + }, + .comptime_float => switch (s1) { + .comptime_float => .{ .either, .comptime_float }, + else => .{ .all_s1, s1 }, // doesn't override anything later + }, + .fixed_int => switch (s1) { + .fixed_int => .{ .either, .fixed_int }, + else => .{ .all_s1, s1 }, // doesn't override anything later + }, + .fixed_float => switch (s1) { + .fixed_float => .{ .either, .fixed_float }, + else => .{ .all_s1, s1 }, // doesn't override anything later + }, + .coercible_struct => switch (s1) { + .exact => .{ .all_s1, .exact }, + else => .{ .all_s0, .coercible_struct }, + }, + .exact => .{ .all_s0, .exact }, + }; + + switch (res[0]) { + .all_s0 => { + if (!s0_is_a) { + reason.reset(); + reason.peers.set(b_peer_idx); + } + }, + .all_s1 => { + if (s0_is_a) { + reason.reset(); + reason.peers.set(b_peer_idx); + } + }, + .either => { + // Prefer b, since it's a single peer + reason.reset(); + reason.peers.set(b_peer_idx); + }, + .both => { + reason.peers.set(b_peer_idx); + }, + } + + return res[1]; + } + + fn select(ty: Type, mod: *Module) PeerResolveStrategy { + return switch (ty.zigTypeTag(mod)) { + .Type, .Void, .Bool, .Opaque, .Frame, .AnyFrame => .exact, + .NoReturn, .Undefined => .unknown, + .Null => .nullable, + .ComptimeInt => .comptime_int, + .Int => .fixed_int, + .ComptimeFloat => .comptime_float, + .Float => .fixed_float, + .Pointer => if (ty.ptrInfo(mod).size == .C) .c_ptr else .ptr, + .Array => .array, + .Vector => .vector, + .Optional => .optional, + .ErrorSet => .error_set, + .ErrorUnion => .error_union, + .EnumLiteral, .Enum, .Union => .enum_or_union, + .Struct => if (ty.isTupleOrAnonStruct(mod)) .coercible_struct else .exact, + .Fn => .func, + }; + } +}; + +const PeerResolveResult = union(enum) { + /// The peer type resolution was successful, and resulted in the given type. + success: Type, + /// The chosen strategy was incompatible with the given peer. + bad_strat: struct { + strat: PeerResolveStrategy, + peer_idx: usize, + }, + /// There was some conflict between two specific peers. + conflict: struct { + peer_idx_a: usize, + peer_idx_b: usize, + }, + /// There was an error when resolving the type of a struct or tuple field. + field_error: struct { + /// The name of the field which caused the failure. + field_name: []const u8, + /// The type of this field in each peer. + field_types: []Type, + /// The error from resolving the field type. Guaranteed not to be `success`. + sub_result: *PeerResolveResult, + }, + + fn report( + result: PeerResolveResult, + sema: *Sema, + block: *Block, + src: LazySrcLoc, + instructions: []const Air.Inst.Ref, + candidate_srcs: Module.PeerTypeCandidateSrc, + strat_reason: PeerResolveStrategy.Reason, + ) !*Module.ErrorMsg { + const mod = sema.mod; + const decl_ptr = mod.declPtr(block.src_decl); + + var opt_msg: ?*Module.ErrorMsg = null; + errdefer if (opt_msg) |msg| msg.destroy(sema.gpa); + + // If we mention fields we'll want to include field types, so put peer types in a buffer + var peer_tys = try sema.arena.alloc(Type, instructions.len); + for (peer_tys, instructions) |*ty, inst| { + ty.* = sema.typeOf(inst); + } + + var cur = result; + while (true) { + var conflict_idx: [2]usize = undefined; + + switch (cur) { + .success => unreachable, + .bad_strat => |bad_strat| bad_strat: { + if (strat_reason.peers.count() == 1) { + // We can write this error more simply as a conflict between two peers + conflict_idx = .{ + strat_reason.peers.findFirstSet().?, + bad_strat.peer_idx, + }; + break :bad_strat; + } + + const fmt = "type resolution strategy failed"; + const msg = if (opt_msg) |msg| msg: { + try sema.errNote(block, src, msg, fmt, .{}); + break :msg msg; + } else msg: { + const msg = try sema.errMsg(block, src, fmt, .{}); + opt_msg = msg; + break :msg msg; + }; + + const peer_ty = peer_tys[bad_strat.peer_idx]; + const peer_src = candidate_srcs.resolve(mod, decl_ptr, bad_strat.peer_idx) orelse src; + try sema.errNote(block, peer_src, msg, "strategy '{s}' failed for type '{}' here", .{ bad_strat.strat.name(), peer_ty.fmt(mod) }); + + try sema.errNote(block, src, msg, "strategy chosen using {} peers", .{strat_reason.peers.count()}); + var it = strat_reason.peers.iterator(.{}); + while (it.next()) |strat_peer_idx| { + const strat_peer_ty = peer_tys[strat_peer_idx]; + const strat_peer_src = candidate_srcs.resolve(mod, decl_ptr, strat_peer_idx) orelse src; + try sema.errNote(block, strat_peer_src, msg, "peer of type '{}' here", .{strat_peer_ty.fmt(mod)}); + } + + // No child error + break; + }, + .conflict => |conflict| { + // Fall through to two-peer conflict handling below + conflict_idx = .{ + conflict.peer_idx_a, + conflict.peer_idx_b, + }; + }, + .field_error => |field_error| { + const fmt = "struct field '{s}' has conflicting types"; + const args = .{field_error.field_name}; + if (opt_msg) |msg| { + try sema.errNote(block, src, msg, fmt, args); + } else { + opt_msg = try sema.errMsg(block, src, fmt, args); + } + + // Continue on to child error + cur = field_error.sub_result.*; + peer_tys = field_error.field_types; + continue; + }, + } + + // This is the path for reporting a conflict between two peers. + + if (conflict_idx[1] < conflict_idx[0]) { + // b comes first in source, so it's better if it comes first in the error + std.mem.swap(usize, &conflict_idx[0], &conflict_idx[1]); + } + + const conflict_tys: [2]Type = .{ + peer_tys[conflict_idx[0]], + peer_tys[conflict_idx[1]], + }; + const conflict_srcs: [2]?LazySrcLoc = .{ + candidate_srcs.resolve(mod, decl_ptr, conflict_idx[0]), + candidate_srcs.resolve(mod, decl_ptr, conflict_idx[1]), + }; + + const fmt = "incompatible types: '{}' and '{}'"; + const args = .{ + conflict_tys[0].fmt(mod), + conflict_tys[1].fmt(mod), + }; + const msg = if (opt_msg) |msg| msg: { + try sema.errNote(block, src, msg, fmt, args); + break :msg msg; + } else msg: { + const msg = try sema.errMsg(block, src, fmt, args); + opt_msg = msg; + break :msg msg; + }; + + if (conflict_srcs[0]) |src_loc| try sema.errNote(block, src_loc, msg, "type '{}' here", .{conflict_tys[0].fmt(mod)}); + if (conflict_srcs[1]) |src_loc| try sema.errNote(block, src_loc, msg, "type '{}' here", .{conflict_tys[1].fmt(mod)}); + + // No child error + break; + } + + return opt_msg.?; + } +}; + fn resolvePeerTypes( sema: *Sema, block: *Block, @@ -31593,594 +31950,1156 @@ fn resolvePeerTypes( instructions: []const Air.Inst.Ref, candidate_srcs: Module.PeerTypeCandidateSrc, ) !Type { - const mod = sema.mod; switch (instructions.len) { 0 => return Type.noreturn, 1 => return sema.typeOf(instructions[0]), else => {}, } - const target = mod.getTarget(); + var peer_tys = try sema.arena.alloc(?Type, instructions.len); + var peer_vals = try sema.arena.alloc(?Value, instructions.len); - var chosen = instructions[0]; - // If this is non-null then it does the following thing, depending on the chosen zigTypeTag(mod). - // * ErrorSet: this is an override - // * ErrorUnion: this is an override of the error set only - // * other: at the end we make an ErrorUnion with the other thing and this - var err_set_ty: ?Type = null; - var any_are_null = false; - var seen_const = false; - var convert_to_slice = false; - var chosen_i: usize = 0; - for (instructions[1..], 0..) |candidate, candidate_i| { - const candidate_ty = sema.typeOf(candidate); - const chosen_ty = sema.typeOf(chosen); - - const candidate_ty_tag = try candidate_ty.zigTypeTagOrPoison(mod); - const chosen_ty_tag = try chosen_ty.zigTypeTagOrPoison(mod); - - // If the candidate can coerce into our chosen type, we're done. - // If the chosen type can coerce into the candidate, use that. - if ((try sema.coerceInMemoryAllowed(block, chosen_ty, candidate_ty, false, target, src, src)) == .ok) { - continue; - } - if ((try sema.coerceInMemoryAllowed(block, candidate_ty, chosen_ty, false, target, src, src)) == .ok) { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - } + for (instructions, peer_tys, peer_vals) |inst, *ty, *val| { + ty.* = sema.typeOf(inst); + val.* = try sema.resolveMaybeUndefVal(inst); + } - switch (candidate_ty_tag) { - .NoReturn, .Undefined => continue, + var strat_reason: PeerResolveStrategy.Reason = .{ + .peers = try std.DynamicBitSet.initEmpty(sema.arena, instructions.len), + }; - .Null => { - any_are_null = true; - continue; - }, + switch (try sema.resolvePeerTypesInner(block, src, peer_tys, peer_vals, &strat_reason)) { + .success => |ty| return ty, + else => |result| { + const msg = try result.report(sema, block, src, instructions, candidate_srcs, strat_reason); + return sema.failWithOwnedErrorMsg(msg); + }, + } +} - .Int => switch (chosen_ty_tag) { - .ComptimeInt => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - .Int => { - const chosen_info = chosen_ty.intInfo(mod); - const candidate_info = candidate_ty.intInfo(mod); +fn resolvePeerTypesInner( + sema: *Sema, + block: *Block, + src: LazySrcLoc, + peer_tys: []?Type, + peer_vals: []?Value, + strat_reason: *PeerResolveStrategy.Reason, +) !PeerResolveResult { + const mod = sema.mod; - if (chosen_info.bits < candidate_info.bits) { - chosen = candidate; - chosen_i = candidate_i + 1; - } - continue; - }, - .Pointer => if (chosen_ty.ptrSize(mod) == .C) continue, - else => {}, - }, - .ComptimeInt => switch (chosen_ty_tag) { - .Int, .Float, .ComptimeFloat => continue, - .Pointer => if (chosen_ty.ptrSize(mod) == .C) continue, - else => {}, - }, - .Float => switch (chosen_ty_tag) { - .Float => { - if (chosen_ty.floatBits(target) < candidate_ty.floatBits(target)) { - chosen = candidate; - chosen_i = candidate_i + 1; - } - continue; - }, - .ComptimeFloat, .ComptimeInt => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - else => {}, - }, - .ComptimeFloat => switch (chosen_ty_tag) { - .Float => continue, - .ComptimeInt => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - else => {}, - }, - .Enum => switch (chosen_ty_tag) { - .EnumLiteral => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - .Union => continue, - else => {}, - }, - .EnumLiteral => switch (chosen_ty_tag) { - .Enum, .Union => continue, - else => {}, - }, - .Union => switch (chosen_ty_tag) { - .Enum, .EnumLiteral => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, + strat_reason.reset(); + + var s: PeerResolveStrategy = .unknown; + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + s = s.merge(PeerResolveStrategy.select(ty, mod), strat_reason, i); + } + + if (s == .unknown) { + // The whole thing was noreturn or undefined - try to do an exact match + s = .exact; + } else { + // There was something other than noreturn and undefined, so we can ignore those peers + for (peer_tys) |*ty_ptr| { + const ty = ty_ptr.* orelse continue; + switch (ty.zigTypeTag(mod)) { + .NoReturn, .Undefined => ty_ptr.* = null, else => {}, - }, - .ErrorSet => switch (chosen_ty_tag) { - .ErrorSet => { - // If chosen is superset of candidate, keep it. - // If candidate is superset of chosen, switch it. - // If neither is a superset, merge errors. - const chosen_set_ty = err_set_ty orelse chosen_ty; + } + } + } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_ty, src, src)) { - continue; - } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_ty, chosen_set_ty, src, src)) { - err_set_ty = null; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; + const target = mod.getTarget(); + + switch (s) { + .unknown => unreachable, + + .error_set => { + var final_set: ?Type = null; + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + if (ty.zigTypeTag(mod) != .ErrorSet) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + if (final_set) |cur_set| { + final_set = try sema.maybeMergeErrorSets(block, src, cur_set, ty); + } else { + final_set = ty; + } + } + return .{ .success = final_set.? }; + }, + + .error_union => { + var final_set: ?Type = null; + for (peer_tys, peer_vals) |*ty_ptr, *val_ptr| { + const ty = ty_ptr.* orelse continue; + const set_ty = switch (ty.zigTypeTag(mod)) { + .ErrorSet => blk: { + ty_ptr.* = null; // no payload to decide on + val_ptr.* = null; + break :blk ty; + }, + .ErrorUnion => blk: { + const set_ty = ty.errorUnionSet(mod); + ty_ptr.* = ty.errorUnionPayload(mod); + if (val_ptr.*) |eu_val| switch (mod.intern_pool.indexToKey(eu_val.toIntern())) { + .error_union => |eu| switch (eu.val) { + .payload => |payload_ip| val_ptr.* = payload_ip.toValue(), + .err_name => val_ptr.* = null, + }, + .undef => val_ptr.* = (try sema.mod.intern(.{ .undef = ty_ptr.*.?.toIntern() })).toValue(), + else => unreachable, + }; + break :blk set_ty; + }, + else => continue, // whole type is the payload + }; + if (final_set) |cur_set| { + final_set = try sema.maybeMergeErrorSets(block, src, cur_set, set_ty); + } else { + final_set = set_ty; + } + } + assert(final_set != null); + const final_payload = switch (try sema.resolvePeerTypesInner( + block, + src, + peer_tys, + peer_vals, + strat_reason, + )) { + .success => |ty| ty, + else => |result| return result, + }; + return .{ .success = try mod.errorUnionType(final_set.?, final_payload) }; + }, + + .nullable => { + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + if (!ty.eql(Type.null, mod)) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + } + return .{ .success = Type.null }; + }, + + .optional => { + for (peer_tys, peer_vals) |*ty_ptr, *val_ptr| { + const ty = ty_ptr.* orelse continue; + switch (ty.zigTypeTag(mod)) { + .Null => { + ty_ptr.* = null; + val_ptr.* = null; + }, + .Optional => { + ty_ptr.* = ty.optionalChild(mod); + if (val_ptr.*) |opt_val| val_ptr.* = if (!opt_val.isUndef(mod)) opt_val.optionalValue(mod) else null; + }, + else => {}, + } + } + const child_ty = switch (try sema.resolvePeerTypesInner( + block, + src, + peer_tys, + peer_vals, + strat_reason, + )) { + .success => |ty| ty, + else => |result| return result, + }; + return .{ .success = try mod.optionalType(child_ty.toIntern()) }; + }, + + .array => { + // Index of the first non-null peer + var opt_first_idx: ?usize = null; + // Index of the first array or vector peer (i.e. not a tuple) + var opt_first_arr_idx: ?usize = null; + // Set to non-null once we see any peer, even a tuple + var len: u64 = undefined; + var sentinel: ?Value = undefined; + // Only set once we see a non-tuple peer + var elem_ty: Type = undefined; + + for (peer_tys, 0..) |*ty_ptr, i| { + const ty = ty_ptr.* orelse continue; + + if (!ty.isArrayOrVector(mod)) { + // We allow tuples of the correct length. We won't validate their elem type, since the elements can be coerced. + const arr_like = sema.typeIsArrayLike(ty) orelse return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + + if (opt_first_idx) |first_idx| { + if (arr_like.len != len) return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } else { + opt_first_idx = i; + len = arr_like.len; } - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_ty); + sentinel = null; + continue; - }, - .ErrorUnion => { - const chosen_set_ty = err_set_ty orelse chosen_ty.errorUnionSet(mod); + } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_ty, src, src)) { - continue; + const first_arr_idx = opt_first_arr_idx orelse { + if (opt_first_idx == null) { + opt_first_idx = i; + len = ty.arrayLen(mod); + sentinel = ty.sentinel(mod); } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_ty, chosen_set_ty, src, src)) { - err_set_ty = candidate_ty; - continue; - } - - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_ty); + opt_first_arr_idx = i; + elem_ty = ty.childType(mod); continue; - }, - else => { - if (err_set_ty) |chosen_set_ty| { - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_ty, src, src)) { - continue; - } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_ty, chosen_set_ty, src, src)) { - err_set_ty = candidate_ty; - continue; - } + }; - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_ty); - continue; + if (ty.arrayLen(mod) != len) return .{ .conflict = .{ + .peer_idx_a = first_arr_idx, + .peer_idx_b = i, + } }; + + if (!ty.childType(mod).eql(elem_ty, mod)) { + return .{ .conflict = .{ + .peer_idx_a = first_arr_idx, + .peer_idx_b = i, + } }; + } + + if (sentinel) |cur_sent| { + if (ty.sentinel(mod)) |peer_sent| { + if (!peer_sent.eql(cur_sent, elem_ty, mod)) sentinel = null; } else { - err_set_ty = candidate_ty; - continue; + sentinel = null; } - }, - }, - .ErrorUnion => switch (chosen_ty_tag) { - .ErrorSet => { - const chosen_set_ty = err_set_ty orelse chosen_ty; - const candidate_set_ty = candidate_ty.errorUnionSet(mod); + } + } + + // There should always be at least one array or vector peer + assert(opt_first_arr_idx != null); + + return .{ .success = try mod.arrayType(.{ + .len = len, + .child = elem_ty.toIntern(), + .sentinel = if (sentinel) |sent_val| sent_val.toIntern() else .none, + }) }; + }, - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_set_ty, src, src)) { - err_set_ty = chosen_set_ty; - } else if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_set_ty, chosen_set_ty, src, src)) { - err_set_ty = null; + .vector => { + var len: ?u64 = null; + var first_idx: usize = undefined; + for (peer_tys, peer_vals, 0..) |*ty_ptr, *val_ptr, i| { + const ty = ty_ptr.* orelse continue; + + if (!ty.isArrayOrVector(mod)) { + // Allow tuples of the correct length + const arr_like = sema.typeIsArrayLike(ty) orelse return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + + if (len) |expect_len| { + if (arr_like.len != expect_len) return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; } else { - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_set_ty); + len = arr_like.len; + first_idx = i; } - chosen = candidate; - chosen_i = candidate_i + 1; + + // Tuples won't participate in the child type resolution. We'll resolve without + // them, and if the tuples have a bad type, we'll get a coercion error later. + ty_ptr.* = null; + val_ptr.* = null; + continue; - }, + } - .ErrorUnion => { - const chosen_payload_ty = chosen_ty.errorUnionPayload(mod); - const candidate_payload_ty = candidate_ty.errorUnionPayload(mod); + if (len) |expect_len| { + if (ty.arrayLen(mod) != expect_len) return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } else { + len = ty.arrayLen(mod); + first_idx = i; + } - 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; + ty_ptr.* = ty.childType(mod); + val_ptr.* = null; // multiple child vals, so we can't easily use them in PTR + } - 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 child_ty = switch (try sema.resolvePeerTypesInner( + block, + src, + peer_tys, + peer_vals, + strat_reason, + )) { + .success => |ty| ty, + else => |result| return result, + }; - const chosen_set_ty = err_set_ty orelse chosen_ty.errorUnionSet(mod); - const candidate_set_ty = candidate_ty.errorUnionSet(mod); + return .{ .success = try mod.vectorType(.{ + .len = @intCast(u32, len.?), + .child = child_ty.toIntern(), + }) }; + }, - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_set_ty, src, src)) { - err_set_ty = chosen_set_ty; - } else if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_set_ty, chosen_set_ty, src, src)) { - err_set_ty = candidate_set_ty; + .c_ptr => { + var opt_ptr_info: ?Type.Payload.Pointer.Data = null; + var first_idx: usize = undefined; + for (peer_tys, peer_vals, 0..) |opt_ty, opt_val, i| { + const ty = opt_ty orelse continue; + switch (ty.zigTypeTag(mod)) { + .ComptimeInt => continue, // comptime-known integers can always coerce to C pointers + .Int => { + if (opt_val != null) { + // Always allow the coercion for comptime-known ints + continue; } else { - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_set_ty); + // Runtime-known, so check if the type is no bigger than a usize + const ptr_bits = target.ptrBitWidth(); + const bits = ty.intInfo(mod).bits; + if (bits <= ptr_bits) continue; } - continue; - } - }, + }, + .Null => continue, + else => {}, + } - else => { - if (err_set_ty) |chosen_set_ty| { - const candidate_set_ty = candidate_ty.errorUnionSet(mod); - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, candidate_set_ty, src, src)) { - err_set_ty = chosen_set_ty; - } else if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, candidate_set_ty, chosen_set_ty, src, src)) { - err_set_ty = null; - } else { - err_set_ty = try sema.errorSetMerge(chosen_set_ty, candidate_set_ty); - } + if (!ty.isPtrAtRuntime(mod)) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + + // Goes through optionals + const peer_info = ty.ptrInfo(mod); + + var ptr_info = opt_ptr_info orelse { + opt_ptr_info = peer_info; + opt_ptr_info.?.size = .C; + first_idx = i; + continue; + }; + + // Try peer -> cur, then cur -> peer + const old_pointee_type = ptr_info.pointee_type; + ptr_info.pointee_type = (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, peer_info.pointee_type)) orelse { + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + }; + + if (ptr_info.sentinel != null and peer_info.sentinel != null) { + const peer_sent = try sema.coerceValueInMemory(block, ptr_info.sentinel.?, old_pointee_type, ptr_info.pointee_type, .unneeded); + const ptr_sent = try sema.coerceValueInMemory(block, peer_info.sentinel.?, peer_info.pointee_type, ptr_info.pointee_type, .unneeded); + if (ptr_sent.eql(peer_sent, ptr_info.pointee_type, mod)) { + ptr_info.sentinel = ptr_sent; + } else { + ptr_info.sentinel = null; } - seen_const = seen_const or chosen_ty.isConstPtr(mod); - chosen = candidate; - chosen_i = candidate_i + 1; + } else { + ptr_info.sentinel = null; + } + + // Note that the align can be always non-zero; Type.ptr will canonicalize it + ptr_info.@"align" = @min(ptr_info.alignment(mod), peer_info.alignment(mod)); + if (ptr_info.@"addrspace" != peer_info.@"addrspace") { + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } + + if (ptr_info.bit_offset != peer_info.bit_offset or + ptr_info.host_size != peer_info.host_size) + { + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } + + ptr_info.mutable = ptr_info.mutable and peer_info.mutable; + ptr_info.@"volatile" = ptr_info.@"volatile" or peer_info.@"volatile"; + + opt_ptr_info = ptr_info; + } + return .{ .success = try Type.ptr(sema.arena, mod, opt_ptr_info.?) }; + }, + + .ptr => { + // If we've resolved to a `[]T` but then see a `[*]T`, we can resolve to a `[*]T` only + // if there were no actual slices. Else, we want the slice index to report a conflict. + var opt_slice_idx: ?usize = null; + + var opt_ptr_info: ?Type.Payload.Pointer.Data = null; + var first_idx: usize = undefined; + var other_idx: usize = undefined; // We sometimes need a second peer index to report a generic error + + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + const peer_info: Type.Payload.Pointer.Data = switch (ty.zigTypeTag(mod)) { + .Pointer => ty.ptrInfo(mod), + .Fn => .{ + .pointee_type = ty, + .@"addrspace" = target_util.defaultAddressSpace(target, .global_constant), + }, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, + }; + + switch (peer_info.size) { + .One, .Many => {}, + .Slice => opt_slice_idx = i, + .C => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, + } + + var ptr_info = opt_ptr_info orelse { + opt_ptr_info = peer_info; + first_idx = i; continue; - }, - }, - .Pointer => { - const cand_info = candidate_ty.ptrInfo(mod); - switch (chosen_ty_tag) { - .Pointer => { - const chosen_info = chosen_ty.ptrInfo(mod); + }; - seen_const = seen_const or !chosen_info.mutable or !cand_info.mutable; + other_idx = i; - // *[N]T to [*]T - // *[N]T to []T - if ((cand_info.size == .Many or cand_info.size == .Slice) and - chosen_info.size == .One and - chosen_info.pointee_type.zigTypeTag(mod) == .Array) - { - // In case we see i.e.: `*[1]T`, `*[2]T`, `[*]T` - convert_to_slice = false; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - } - if (cand_info.size == .One and - cand_info.pointee_type.zigTypeTag(mod) == .Array and - (chosen_info.size == .Many or chosen_info.size == .Slice)) - { - // In case we see i.e.: `*[1]T`, `*[2]T`, `[*]T` - convert_to_slice = false; - continue; - } + // We want to return this in a lot of cases, so alias it here for convenience + const generic_err: PeerResolveResult = .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; - // *[N]T and *[M]T - // Verify both are single-pointers to arrays. - // Keep the one whose element type can be coerced into. - if (chosen_info.size == .One and - cand_info.size == .One and - chosen_info.pointee_type.zigTypeTag(mod) == .Array and - cand_info.pointee_type.zigTypeTag(mod) == .Array) - { - const chosen_elem_ty = chosen_info.pointee_type.childType(mod); - const cand_elem_ty = cand_info.pointee_type.childType(mod); + // Note that the align can be always non-zero; Type.ptr will canonicalize it + ptr_info.@"align" = @min(ptr_info.alignment(mod), peer_info.alignment(mod)); - const chosen_ok = .ok == try sema.coerceInMemoryAllowed(block, chosen_elem_ty, cand_elem_ty, chosen_info.mutable, target, src, src); - if (chosen_ok) { - convert_to_slice = true; - continue; - } + if (ptr_info.@"addrspace" != peer_info.@"addrspace") { + return generic_err; + } - const cand_ok = .ok == try sema.coerceInMemoryAllowed(block, cand_elem_ty, chosen_elem_ty, cand_info.mutable, target, src, src); - if (cand_ok) { - convert_to_slice = true; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - } + if (ptr_info.bit_offset != peer_info.bit_offset or + ptr_info.host_size != peer_info.host_size) + { + return generic_err; + } - // They're both bad. Report error. - // In the future we probably want to use the - // coerceInMemoryAllowed error reporting mechanism, - // however, for now we just fall through for the - // "incompatible types" error below. - } + ptr_info.mutable = ptr_info.mutable and peer_info.mutable; + ptr_info.@"volatile" = ptr_info.@"volatile" or peer_info.@"volatile"; + + const peer_sentinel: ?Value = switch (peer_info.size) { + .One => switch (peer_info.pointee_type.zigTypeTag(mod)) { + .Array => peer_info.pointee_type.sentinel(mod), + else => null, + }, + .Many, .Slice => peer_info.sentinel, + .C => unreachable, + }; + + const cur_sentinel: ?Value = switch (ptr_info.size) { + .One => switch (ptr_info.pointee_type.zigTypeTag(mod)) { + .Array => ptr_info.pointee_type.sentinel(mod), + else => null, + }, + .Many, .Slice => ptr_info.sentinel, + .C => unreachable, + }; - // [*c]T and any other pointer size - // Whichever element type can coerce to the other one, is - // the one we will keep. If they're both OK then we keep the - // C pointer since it matches both single and many pointers. - if (cand_info.size == .C or chosen_info.size == .C) { - const cand_ok = .ok == try sema.coerceInMemoryAllowed(block, cand_info.pointee_type, chosen_info.pointee_type, cand_info.mutable, target, src, src); - const chosen_ok = .ok == try sema.coerceInMemoryAllowed(block, chosen_info.pointee_type, cand_info.pointee_type, chosen_info.mutable, target, src, src); - - if (cand_ok) { - if (chosen_ok) { - if (chosen_info.size == .C) { - continue; - } else { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; + // We abstract array handling slightly so that tuple pointers can work like array pointers + const peer_pointee_array = sema.typeIsArrayLike(peer_info.pointee_type); + const cur_pointee_array = sema.typeIsArrayLike(ptr_info.pointee_type); + + // This switch is just responsible for deciding the size and pointee (not including + // single-pointer array sentinel). + good: { + switch (peer_info.size) { + .One => switch (ptr_info.size) { + .One => { + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, peer_info.pointee_type)) |pointee| { + ptr_info.pointee_type = pointee; + break :good; + } + + const cur_arr = cur_pointee_array orelse return generic_err; + const peer_arr = peer_pointee_array orelse return generic_err; + + if (try sema.resolvePairInMemoryCoercible(block, src, cur_arr.elem_ty, peer_arr.elem_ty)) |elem_ty| { + // *[n:x]T + *[n:y]T = *[n]T + if (cur_arr.len == peer_arr.len) { + ptr_info.pointee_type = try mod.arrayType(.{ + .len = cur_arr.len, + .child = elem_ty.toIntern(), + }); + break :good; } - } else { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; + // *[a]T + *[b]T = []T + ptr_info.size = .Slice; + ptr_info.pointee_type = elem_ty; + break :good; } - } else { - if (chosen_ok) { - continue; - } else { - // They're both bad. Report error. - // In the future we probably want to use the - // coerceInMemoryAllowed error reporting mechanism, - // however, for now we just fall through for the - // "incompatible types" error below. + + if (peer_arr.elem_ty.toIntern() == .noreturn_type) { + // *struct{} + *[a]T = []T + ptr_info.size = .Slice; + ptr_info.pointee_type = cur_arr.elem_ty; + break :good; } - } - } - }, - .Int, .ComptimeInt => { - if (cand_info.size == .C) { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - } - }, - .Optional => { - const chosen_ptr_ty = chosen_ty.optionalChild(mod); - if (chosen_ptr_ty.zigTypeTag(mod) == .Pointer) { - const chosen_info = chosen_ptr_ty.ptrInfo(mod); - seen_const = seen_const or !chosen_info.mutable or !cand_info.mutable; + if (cur_arr.elem_ty.toIntern() == .noreturn_type) { + // *[a]T + *struct{} = []T + ptr_info.size = .Slice; + ptr_info.pointee_type = peer_arr.elem_ty; + break :good; + } - // *[N]T to ?![*]T - // *[N]T to ?![]T - if (cand_info.size == .One and - cand_info.pointee_type.zigTypeTag(mod) == .Array and - (chosen_info.size == .Many or chosen_info.size == .Slice)) - { - continue; - } + return generic_err; + }, + .Many => { + // Only works for *[n]T + [*]T -> [*]T + const arr = peer_pointee_array orelse return generic_err; + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, arr.elem_ty)) |pointee| { + ptr_info.pointee_type = pointee; + break :good; + } + if (arr.elem_ty.toIntern() == .noreturn_type) { + // *struct{} + [*]T -> [*]T + break :good; + } + return generic_err; + }, + .Slice => { + // Only works for *[n]T + []T -> []T + const arr = peer_pointee_array orelse return generic_err; + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, arr.elem_ty)) |pointee| { + ptr_info.pointee_type = pointee; + break :good; + } + if (arr.elem_ty.toIntern() == .noreturn_type) { + // *struct{} + []T -> []T + break :good; + } + return generic_err; + }, + .C => unreachable, + }, + .Many => switch (ptr_info.size) { + .One => { + // Only works for [*]T + *[n]T -> [*]T + const arr = cur_pointee_array orelse return generic_err; + if (try sema.resolvePairInMemoryCoercible(block, src, arr.elem_ty, peer_info.pointee_type)) |pointee| { + ptr_info.size = .Many; + ptr_info.pointee_type = pointee; + break :good; + } + if (arr.elem_ty.toIntern() == .noreturn_type) { + // [*]T + *struct{} -> [*]T + ptr_info.size = .Many; + ptr_info.pointee_type = peer_info.pointee_type; + break :good; + } + return generic_err; + }, + .Many => { + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, peer_info.pointee_type)) |pointee| { + ptr_info.pointee_type = pointee; + break :good; + } + return generic_err; + }, + .Slice => { + // Only works if no peers are actually slices + if (opt_slice_idx) |slice_idx| { + return .{ .conflict = .{ + .peer_idx_a = slice_idx, + .peer_idx_b = i, + } }; + } + // Okay, then works for [*]T + "[]T" -> [*]T + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, peer_info.pointee_type)) |pointee| { + ptr_info.size = .Many; + ptr_info.pointee_type = pointee; + break :good; + } + return generic_err; + }, + .C => unreachable, + }, + .Slice => switch (ptr_info.size) { + .One => { + // Only works for []T + *[n]T -> []T + const arr = cur_pointee_array orelse return generic_err; + if (try sema.resolvePairInMemoryCoercible(block, src, arr.elem_ty, peer_info.pointee_type)) |pointee| { + ptr_info.size = .Slice; + ptr_info.pointee_type = pointee; + break :good; + } + if (arr.elem_ty.toIntern() == .noreturn_type) { + // []T + *struct{} -> []T + ptr_info.size = .Slice; + ptr_info.pointee_type = peer_info.pointee_type; + break :good; + } + return generic_err; + }, + .Many => { + // Impossible! (current peer is an actual slice) + return generic_err; + }, + .Slice => { + if (try sema.resolvePairInMemoryCoercible(block, src, ptr_info.pointee_type, peer_info.pointee_type)) |pointee| { + ptr_info.pointee_type = pointee; + break :good; + } + return generic_err; + }, + .C => unreachable, + }, + .C => unreachable, + } + } + + const sentinel_ty = if (ptr_info.size == .One and ptr_info.pointee_type.zigTypeTag(mod) == .Array) blk: { + break :blk ptr_info.pointee_type.childType(mod); + } else ptr_info.pointee_type; + + // TODO: once InternPool is in, we need to cast the sentinels to sentinel_ty + + sentinel: { + no_sentinel: { + if (peer_sentinel == null) break :no_sentinel; + if (cur_sentinel == null) break :no_sentinel; + const peer_sent_ty = mod.intern_pool.typeOf(peer_sentinel.?.toIntern()).toType(); + const cur_sent_ty = mod.intern_pool.typeOf(cur_sentinel.?.toIntern()).toType(); + const peer_sent_coerced = try sema.coerceValueInMemory(block, peer_sentinel.?, peer_sent_ty, sentinel_ty, .unneeded); + const cur_sent_coerced = try sema.coerceValueInMemory(block, cur_sentinel.?, cur_sent_ty, sentinel_ty, .unneeded); + if (!peer_sent_coerced.eql(cur_sent_coerced, sentinel_ty, mod)) break :no_sentinel; + // Sentinels match + if (ptr_info.size == .One) { + assert(ptr_info.pointee_type.zigTypeTag(mod) == .Array); + ptr_info.pointee_type = try mod.arrayType(.{ + .len = ptr_info.pointee_type.arrayLen(mod), + .child = ptr_info.pointee_type.childType(mod).toIntern(), + .sentinel = cur_sent_coerced.toIntern(), + }); + } else { + ptr_info.sentinel = cur_sent_coerced; } + break :sentinel; + } + // Clear existing sentinel + ptr_info.sentinel = null; + if (ptr_info.pointee_type.zigTypeTag(mod) == .Array) { + ptr_info.pointee_type = try mod.arrayType(.{ + .len = ptr_info.pointee_type.arrayLen(mod), + .child = ptr_info.pointee_type.childType(mod).toIntern(), + .sentinel = .none, + }); + } + } + + opt_ptr_info = ptr_info; + } + + // Before we succeed, check the pointee type. If we tried to apply PTR to (for instance) + // &.{} and &.{}, we'll currently have a pointer type of `*[0]noreturn` - we wanted to + // coerce the empty struct to a specific type, but no peer provided one. We need to + // detect this case and emit an error. + const pointee = opt_ptr_info.?.pointee_type; + if (pointee.toIntern() == .noreturn_type or + (pointee.zigTypeTag(mod) == .Array and pointee.childType(mod).toIntern() == .noreturn_type)) + { + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = other_idx, + } }; + } + + return .{ .success = try Type.ptr(sema.arena, mod, opt_ptr_info.?) }; + }, + + .func => { + var opt_cur_ty: ?Type = null; + var first_idx: usize = undefined; + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + const cur_ty = opt_cur_ty orelse { + opt_cur_ty = ty; + first_idx = i; + continue; + }; + if (ty.zigTypeTag(mod) != .Fn) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + // ty -> cur_ty + if (.ok == try sema.coerceInMemoryAllowedFns(block, cur_ty, ty, target, src, src)) { + continue; + } + // cur_ty -> ty + if (.ok == try sema.coerceInMemoryAllowedFns(block, ty, cur_ty, target, src, src)) { + opt_cur_ty = ty; + continue; + } + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } + return .{ .success = opt_cur_ty.? }; + }, + + .enum_or_union => { + var opt_cur_ty: ?Type = null; + // The peer index which gave the current type + var cur_ty_idx: usize = undefined; + + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + switch (ty.zigTypeTag(mod)) { + .EnumLiteral, .Enum, .Union => {}, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, + } + const cur_ty = opt_cur_ty orelse { + opt_cur_ty = ty; + cur_ty_idx = i; + continue; + }; + + // We want to return this in a lot of cases, so alias it here for convenience + const generic_err: PeerResolveResult = .{ .conflict = .{ + .peer_idx_a = cur_ty_idx, + .peer_idx_b = i, + } }; + + switch (cur_ty.zigTypeTag(mod)) { + .EnumLiteral => { + opt_cur_ty = ty; + cur_ty_idx = i; }, - .ErrorUnion => { - const chosen_ptr_ty = chosen_ty.errorUnionPayload(mod); - if (chosen_ptr_ty.zigTypeTag(mod) == .Pointer) { - const chosen_info = chosen_ptr_ty.ptrInfo(mod); - - seen_const = seen_const or !chosen_info.mutable or !cand_info.mutable; - - // *[N]T to E![*]T - // *[N]T to E![]T - if (cand_info.size == .One and - cand_info.pointee_type.zigTypeTag(mod) == .Array and - (chosen_info.size == .Many or chosen_info.size == .Slice)) - { - continue; - } - } + .Enum => switch (ty.zigTypeTag(mod)) { + .EnumLiteral => {}, + .Enum => { + if (!ty.eql(cur_ty, mod)) return generic_err; + }, + .Union => { + const tag_ty = ty.unionTagTypeHypothetical(mod); + if (!tag_ty.eql(cur_ty, mod)) return generic_err; + opt_cur_ty = ty; + cur_ty_idx = i; + }, + else => unreachable, }, - .Fn => { - if (!cand_info.mutable and cand_info.pointee_type.zigTypeTag(mod) == .Fn and .ok == try sema.coerceInMemoryAllowedFns(block, chosen_ty, cand_info.pointee_type, target, src, src)) { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - } + .Union => switch (ty.zigTypeTag(mod)) { + .EnumLiteral => {}, + .Enum => { + const cur_tag_ty = cur_ty.unionTagTypeHypothetical(mod); + if (!ty.eql(cur_tag_ty, mod)) return generic_err; + }, + .Union => { + if (!ty.eql(cur_ty, mod)) return generic_err; + }, + else => unreachable, }, - else => {}, + else => unreachable, } - }, - .Optional => { - const opt_child_ty = candidate_ty.optionalChild(mod); - if ((try sema.coerceInMemoryAllowed(block, chosen_ty, opt_child_ty, false, target, src, src)) == .ok) { - seen_const = seen_const or opt_child_ty.isConstPtr(mod); - any_are_null = true; - continue; + } + return .{ .success = opt_cur_ty.? }; + }, + + .comptime_int => { + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + switch (ty.zigTypeTag(mod)) { + .ComptimeInt => {}, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, } + } + return .{ .success = Type.comptime_int }; + }, - seen_const = seen_const or chosen_ty.isConstPtr(mod); - any_are_null = false; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - .Vector => switch (chosen_ty_tag) { - .Vector => { - const chosen_len = chosen_ty.vectorLen(mod); - const candidate_len = candidate_ty.vectorLen(mod); - if (chosen_len != candidate_len) - continue; + .comptime_float => { + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + switch (ty.zigTypeTag(mod)) { + .ComptimeInt, .ComptimeFloat => {}, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, + } + } + return .{ .success = Type.comptime_float }; + }, - const chosen_child_ty = chosen_ty.childType(mod); - const candidate_child_ty = candidate_ty.childType(mod); - if (chosen_child_ty.zigTypeTag(mod) == .Int and candidate_child_ty.zigTypeTag(mod) == .Int) { - const chosen_info = chosen_child_ty.intInfo(mod); - const candidate_info = candidate_child_ty.intInfo(mod); - if (chosen_info.bits < candidate_info.bits) { - chosen = candidate; - chosen_i = candidate_i + 1; - } - continue; - } - if (chosen_child_ty.zigTypeTag(mod) == .Float and candidate_child_ty.zigTypeTag(mod) == .Float) { - if (chosen_ty.floatBits(target) < candidate_ty.floatBits(target)) { - chosen = candidate; - chosen_i = candidate_i + 1; - } + .fixed_int => { + var idx_unsigned: ?usize = null; + var idx_signed: ?usize = null; + + // TODO: this is for compatibility with legacy behavior. See beneath the loop. + var any_comptime_known = false; + + for (peer_tys, peer_vals, 0..) |opt_ty, *ptr_opt_val, i| { + const ty = opt_ty orelse continue; + const opt_val = ptr_opt_val.*; + + const peer_tag = ty.zigTypeTag(mod); + switch (peer_tag) { + .ComptimeInt => { + // If the value is undefined, we can't refine to a fixed-width int + if (opt_val == null or opt_val.?.isUndef(mod)) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + any_comptime_known = true; + ptr_opt_val.* = try sema.resolveLazyValue(opt_val.?); continue; - } - }, - .Array => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - else => {}, - }, - .Array => switch (chosen_ty_tag) { - .Vector => continue, - else => {}, - }, - .Fn => if (chosen_ty.isSinglePointer(mod) and chosen_ty.isConstPtr(mod) and chosen_ty.childType(mod).zigTypeTag(mod) == .Fn) { - if (.ok == try sema.coerceInMemoryAllowedFns(block, chosen_ty.childType(mod), candidate_ty, target, src, src)) { - continue; + }, + .Int => {}, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, } - }, - else => {}, - } - switch (chosen_ty_tag) { - .NoReturn, .Undefined => { - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - .Null => { - any_are_null = true; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; - }, - .Optional => { - const opt_child_ty = chosen_ty.optionalChild(mod); - if ((try sema.coerceInMemoryAllowed(block, opt_child_ty, candidate_ty, false, target, src, src)) == .ok) { + if (opt_val != null) any_comptime_known = true; + + const info = ty.intInfo(mod); + + const idx_ptr = switch (info.signedness) { + .unsigned => &idx_unsigned, + .signed => &idx_signed, + }; + + const largest_idx = idx_ptr.* orelse { + idx_ptr.* = i; continue; + }; + + const cur_info = peer_tys[largest_idx].?.intInfo(mod); + if (info.bits > cur_info.bits) { + idx_ptr.* = i; } - if ((try sema.coerceInMemoryAllowed(block, candidate_ty, opt_child_ty, false, target, src, src)) == .ok) { - any_are_null = true; - chosen = candidate; - chosen_i = candidate_i + 1; - continue; + } + + if (idx_signed == null) { + return .{ .success = peer_tys[idx_unsigned.?].? }; + } + + if (idx_unsigned == null) { + return .{ .success = peer_tys[idx_signed.?].? }; + } + + const unsigned_info = peer_tys[idx_unsigned.?].?.intInfo(mod); + const signed_info = peer_tys[idx_signed.?].?.intInfo(mod); + if (signed_info.bits > unsigned_info.bits) { + return .{ .success = peer_tys[idx_signed.?].? }; + } + + // TODO: this is for compatibility with legacy behavior. Before this version of PTR was + // implemented, the algorithm very often returned false positives, with the expectation + // that you'd just hit a coercion error later. One of these was that for integers, the + // largest type would always be returned, even if it couldn't fit everything. This had + // an unintentional consequence to semantics, which is that if values were known at + // comptime, they would be coerced down to the smallest type where possible. This + // behavior is unintuitive and order-dependent, so in my opinion should be eliminated, + // but for now we'll retain compatibility. + if (any_comptime_known) { + if (unsigned_info.bits > signed_info.bits) { + return .{ .success = peer_tys[idx_unsigned.?].? }; } - }, - .ErrorUnion => { - const payload_ty = chosen_ty.errorUnionPayload(mod); - if ((try sema.coerceInMemoryAllowed(block, payload_ty, candidate_ty, false, target, src, src)) == .ok) { + const idx = @min(idx_unsigned.?, idx_signed.?); + return .{ .success = peer_tys[idx].? }; + } + + return .{ .conflict = .{ + .peer_idx_a = idx_unsigned.?, + .peer_idx_b = idx_signed.?, + } }; + }, + + .fixed_float => { + var opt_cur_ty: ?Type = null; + + for (peer_tys, peer_vals, 0..) |opt_ty, opt_val, i| { + const ty = opt_ty orelse continue; + switch (ty.zigTypeTag(mod)) { + .ComptimeFloat, .ComptimeInt => {}, + .Int => { + if (opt_val == null) return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + }, + .Float => { + if (opt_cur_ty) |cur_ty| { + if (cur_ty.eql(ty, mod)) continue; + // Recreate the type so we eliminate any c_longdouble + const bits = @max(cur_ty.floatBits(target), ty.floatBits(target)); + opt_cur_ty = switch (bits) { + 16 => Type.f16, + 32 => Type.f32, + 64 => Type.f64, + 80 => Type.f80, + 128 => Type.f128, + else => unreachable, + }; + } else { + opt_cur_ty = ty; + } + }, + else => return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }, + } + } + + // Note that fixed_float is only chosen if there is at least one fixed-width float peer, + // so opt_cur_ty must be non-null. + return .{ .success = opt_cur_ty.? }; + }, + + .coercible_struct => { + // First, check that every peer has the same approximate structure (field count and names) + + var opt_first_idx: ?usize = null; + var is_tuple: bool = undefined; + var field_count: usize = undefined; + // Only defined for non-tuples. + var field_names: []InternPool.NullTerminatedString = undefined; + + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + + if (!ty.isTupleOrAnonStruct(mod)) { + return .{ .bad_strat = .{ + .strat = s, + .peer_idx = i, + } }; + } + + const first_idx = opt_first_idx orelse { + opt_first_idx = i; + is_tuple = ty.isTuple(mod); + field_count = ty.structFieldCount(mod); + if (!is_tuple) { + const names = mod.intern_pool.indexToKey(ty.toIntern()).anon_struct_type.names; + field_names = try sema.arena.dupe(InternPool.NullTerminatedString, names); + } continue; + }; + + if (ty.isTuple(mod) != is_tuple or ty.structFieldCount(mod) != field_count) { + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; } - }, - .ErrorSet => { - chosen = candidate; - chosen_i = candidate_i + 1; - if (err_set_ty) |chosen_set_ty| { - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_set_ty, chosen_ty, src, src)) { - continue; + + if (!is_tuple) { + for (field_names, 0..) |expected, field_idx| { + const actual = ty.structFieldName(field_idx, mod); + if (actual == expected) continue; + return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; } - if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, chosen_ty, chosen_set_ty, src, src)) { - err_set_ty = chosen_ty; + } + } + + assert(opt_first_idx != null); + + // Now, we'll recursively resolve the field types + const field_types = try sema.arena.alloc(InternPool.Index, field_count); + // Values for `comptime` fields - `.none` used for non-comptime fields + const field_vals = try sema.arena.alloc(InternPool.Index, field_count); + const sub_peer_tys = try sema.arena.alloc(?Type, peer_tys.len); + const sub_peer_vals = try sema.arena.alloc(?Value, peer_vals.len); + + for (field_types, field_vals, 0..) |*field_ty, *field_val, field_idx| { + // Fill buffers with types and values of the field + for (peer_tys, peer_vals, sub_peer_tys, sub_peer_vals) |opt_ty, opt_val, *peer_field_ty, *peer_field_val| { + const ty = opt_ty orelse { + peer_field_ty.* = null; + peer_field_val.* = null; continue; - } + }; + peer_field_ty.* = ty.structFieldType(field_idx, mod); + peer_field_val.* = if (opt_val) |val| try val.fieldValue(mod, field_idx) else null; + } + + // Resolve field type recursively + field_ty.* = switch (try sema.resolvePeerTypesInner(block, src, sub_peer_tys, sub_peer_vals, strat_reason)) { + .success => |ty| ty.toIntern(), + else => |result| { + const result_buf = try sema.arena.create(PeerResolveResult); + result_buf.* = result; + const field_name = if (is_tuple) name: { + break :name try std.fmt.allocPrint(sema.arena, "{d}", .{field_idx}); + } else try sema.arena.dupe(u8, mod.intern_pool.stringToSlice(field_names[field_idx])); + + // The error info needs the field types, but we can't reuse sub_peer_tys + // since the recursive call may have clobbered it. + const peer_field_tys = try sema.arena.alloc(Type, peer_tys.len); + for (peer_tys, peer_field_tys) |opt_ty, *peer_field_ty| { + // Already-resolved types won't be referenced by the error so it's fine + // to leave them undefined. + const ty = opt_ty orelse continue; + peer_field_ty.* = ty.structFieldType(field_idx, mod); + } - err_set_ty = try sema.errorSetMerge(chosen_set_ty, chosen_ty); - continue; - } else { - err_set_ty = chosen_ty; - continue; + return .{ .field_error = .{ + .field_name = field_name, + .field_types = peer_field_tys, + .sub_result = result_buf, + } }; + }, + }; + + // Decide if this is a comptime field. If it is comptime in all peers, and the + // coerced comptime values are all the same, we say it is comptime, else not. + + var comptime_val: ?Value = null; + for (peer_tys) |opt_ty| { + const struct_ty = opt_ty orelse continue; + const uncoerced_field_val = try struct_ty.structFieldValueComptime(mod, field_idx) orelse { + comptime_val = null; + break; + }; + const uncoerced_field_ty = struct_ty.structFieldType(field_idx, mod); + const uncoerced_field = try sema.addConstant(uncoerced_field_ty, uncoerced_field_val); + const coerced_inst = sema.coerceExtra(block, field_ty.toType(), uncoerced_field, src, .{ .report_err = false }) catch |err| switch (err) { + // It's possible for PTR to give false positives. Just give up on making this a comptime field, we'll get an error later anyway + error.NotCoercible => { + comptime_val = null; + break; + }, + else => |e| return e, + }; + const coerced_val = (try sema.resolveMaybeUndefVal(coerced_inst)) orelse continue; + const existing = comptime_val orelse { + comptime_val = coerced_val; + continue; + }; + if (!coerced_val.eql(existing, field_ty.toType(), mod)) { + comptime_val = null; + break; + } } - }, - else => {}, - } - // At this point, we hit a compile error. We need to recover - // the source locations. - const chosen_src = candidate_srcs.resolve( - mod, - mod.declPtr(block.src_decl), - chosen_i, - ); - const candidate_src = candidate_srcs.resolve( - mod, - mod.declPtr(block.src_decl), - candidate_i + 1, - ); + field_val.* = if (comptime_val) |v| v.toIntern() else .none; + } - const msg = msg: { - const msg = try sema.errMsg(block, src, "incompatible types: '{}' and '{}'", .{ - chosen_ty.fmt(mod), - candidate_ty.fmt(mod), - }); - errdefer msg.destroy(sema.gpa); + const final_ty = try mod.intern(.{ .anon_struct_type = .{ + .types = field_types, + .names = if (is_tuple) &.{} else field_names, + .values = field_vals, + } }); - if (chosen_src) |src_loc| - try sema.errNote(block, src_loc, msg, "type '{}' here", .{chosen_ty.fmt(mod)}); + return .{ .success = final_ty.toType() }; + }, - if (candidate_src) |src_loc| - try sema.errNote(block, src_loc, msg, "type '{}' here", .{candidate_ty.fmt(mod)}); + .exact => { + var expect_ty: ?Type = null; + var first_idx: usize = undefined; + for (peer_tys, 0..) |opt_ty, i| { + const ty = opt_ty orelse continue; + if (expect_ty) |expect| { + if (!ty.eql(expect, mod)) return .{ .conflict = .{ + .peer_idx_a = first_idx, + .peer_idx_b = i, + } }; + } else { + expect_ty = ty; + first_idx = i; + } + } + return .{ .success = expect_ty.? }; + }, + } +} - break :msg msg; - }; - return sema.failWithOwnedErrorMsg(msg); +fn maybeMergeErrorSets(sema: *Sema, block: *Block, src: LazySrcLoc, e0: Type, e1: Type) !Type { + // e0 -> e1 + if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, e1, e0, src, src)) { + return e1; } - const chosen_ty = sema.typeOf(chosen); + // e1 -> e0 + if (.ok == try sema.coerceInMemoryAllowedErrorSets(block, e0, e1, src, src)) { + return e0; + } - if (convert_to_slice) { - // turn *[N]T => []T - const chosen_child_ty = chosen_ty.childType(mod); - var info = chosen_ty.ptrInfo(mod); - info.sentinel = chosen_child_ty.sentinel(mod); - info.size = .Slice; - info.mutable = !(seen_const or chosen_child_ty.isConstPtr(mod)); - info.pointee_type = chosen_child_ty.elemType2(mod); + return sema.errorSetMerge(e0, e1); +} - const new_ptr_ty = try Type.ptr(sema.arena, mod, info); - const opt_ptr_ty = if (any_are_null) - try Type.optional(sema.arena, new_ptr_ty, mod) - else - new_ptr_ty; - const set_ty = err_set_ty orelse return opt_ptr_ty; - return try mod.errorUnionType(set_ty, opt_ptr_ty); +fn resolvePairInMemoryCoercible(sema: *Sema, block: *Block, src: LazySrcLoc, ty_a: Type, ty_b: Type) !?Type { + // ty_b -> ty_a + if (.ok == try sema.coerceInMemoryAllowed(block, ty_a, ty_b, true, sema.mod.getTarget(), src, src)) { + return ty_a; } - if (seen_const) { - // turn []T => []const T - switch (chosen_ty.zigTypeTag(mod)) { - .ErrorUnion => { - const ptr_ty = chosen_ty.errorUnionPayload(mod); - var info = ptr_ty.ptrInfo(mod); - info.mutable = false; - const new_ptr_ty = try Type.ptr(sema.arena, mod, info); - const opt_ptr_ty = if (any_are_null) - try Type.optional(sema.arena, new_ptr_ty, mod) - else - new_ptr_ty; - const set_ty = err_set_ty orelse chosen_ty.errorUnionSet(mod); - return try mod.errorUnionType(set_ty, opt_ptr_ty); - }, - .Pointer => { - var info = chosen_ty.ptrInfo(mod); - info.mutable = false; - const new_ptr_ty = try Type.ptr(sema.arena, mod, info); - const opt_ptr_ty = if (any_are_null) - try Type.optional(sema.arena, new_ptr_ty, mod) - else - new_ptr_ty; - const set_ty = err_set_ty orelse return opt_ptr_ty; - return try mod.errorUnionType(set_ty, opt_ptr_ty); - }, - else => return chosen_ty, - } + // ty_a -> ty_b + if (.ok == try sema.coerceInMemoryAllowed(block, ty_b, ty_a, true, sema.mod.getTarget(), src, src)) { + return ty_b; } - if (any_are_null) { - const opt_ty = switch (chosen_ty.zigTypeTag(mod)) { - .Null, .Optional => chosen_ty, - else => try Type.optional(sema.arena, chosen_ty, mod), - }; - const set_ty = err_set_ty orelse return opt_ty; - return try mod.errorUnionType(set_ty, opt_ty); - } + return null; +} - if (err_set_ty) |ty| switch (chosen_ty.zigTypeTag(mod)) { - .ErrorSet => return ty, - .ErrorUnion => { - const payload_ty = chosen_ty.errorUnionPayload(mod); - return try mod.errorUnionType(ty, payload_ty); +const ArrayLike = struct { + len: u64, + /// `noreturn` indicates that this type is `struct{}` so can coerce to anything + elem_ty: Type, +}; +fn typeIsArrayLike(sema: *Sema, ty: Type) ?ArrayLike { + const mod = sema.mod; + return switch (ty.zigTypeTag(mod)) { + .Array => .{ + .len = ty.arrayLen(mod), + .elem_ty = ty.childType(mod), }, - else => return try mod.errorUnionType(ty, chosen_ty), + .Struct => { + const field_count = ty.structFieldCount(mod); + if (field_count == 0) return .{ + .len = 0, + .elem_ty = Type.noreturn, + }; + if (!ty.isTuple(mod)) return null; + const elem_ty = ty.structFieldType(0, mod); + for (1..field_count) |i| { + if (!ty.structFieldType(i, mod).eql(elem_ty, mod)) { + return null; + } + } + return .{ + .len = field_count, + .elem_ty = elem_ty, + }; + }, + else => null, }; - - return chosen_ty; } pub fn resolveFnTypes(sema: *Sema, fn_ty: Type) CompileError!void { |
