authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-05-17 16:59:53+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-05-19 00:27:01+01:00
log07a5efd072eb6af331e9e24e48ed62ef9bafc5ab
treec234b4ccebad7e39398e45d08452dac2e8aa088f
parentb77e6013422a6e7066a14dbe82e8512636ba13d2

Sema: rewrite `analyzeMinMax`

I only wanted to fix a bug originally, but this logic was kind of a rat's nest. But now... okay, it still *is*, but it's now a slightly more navigable nest, with cute little signs occasionally, painted by adorable rats desparately trying to follow the specification. Hopefully #3806 comes along at some point to simplify this logic a little. Resolves: #23139

2 files changed, 322 insertions(+), 198 deletions(-)

src/Sema.zig+281-198
...@@ -25380,8 +25380,6 @@ fn zirMinMax(...@@ -25380,8 +25380,6 @@ fn zirMinMax(
25380 const rhs_src = block.builtinCallArgSrc(inst_data.src_node, 1);25380 const rhs_src = block.builtinCallArgSrc(inst_data.src_node, 1);
25381 const lhs = try sema.resolveInst(extra.lhs);25381 const lhs = try sema.resolveInst(extra.lhs);
25382 const rhs = try sema.resolveInst(extra.rhs);25382 const rhs = try sema.resolveInst(extra.rhs);
25383 try sema.checkNumericType(block, lhs_src, sema.typeOf(lhs));
25384 try sema.checkNumericType(block, rhs_src, sema.typeOf(rhs));
25385 return sema.analyzeMinMax(block, src, air_tag, &.{ lhs, rhs }, &.{ lhs_src, rhs_src });25383 return sema.analyzeMinMax(block, src, air_tag, &.{ lhs, rhs }, &.{ lhs_src, rhs_src });
25386}25384}
2538725385
...@@ -25402,7 +25400,6 @@ fn zirMinMaxMulti(...@@ -25402,7 +25400,6 @@ fn zirMinMaxMulti(
25402 for (operands, air_refs, operand_srcs, 0..) |zir_ref, *air_ref, *op_src, i| {25400 for (operands, air_refs, operand_srcs, 0..) |zir_ref, *air_ref, *op_src, i| {
25403 op_src.* = block.builtinCallArgSrc(src_node, @intCast(i));25401 op_src.* = block.builtinCallArgSrc(src_node, @intCast(i));
25404 air_ref.* = try sema.resolveInst(zir_ref);25402 air_ref.* = try sema.resolveInst(zir_ref);
25405 try sema.checkNumericType(block, op_src.*, sema.typeOf(air_ref.*));
25406 }25403 }
2540725404
25408 return sema.analyzeMinMax(block, src, air_tag, air_refs, operand_srcs);25405 return sema.analyzeMinMax(block, src, air_tag, air_refs, operand_srcs);
...@@ -25418,227 +25415,313 @@ fn analyzeMinMax(...@@ -25418,227 +25415,313 @@ fn analyzeMinMax(
25418) CompileError!Air.Inst.Ref {25415) CompileError!Air.Inst.Ref {
25419 assert(operands.len == operand_srcs.len);25416 assert(operands.len == operand_srcs.len);
25420 assert(operands.len > 0);25417 assert(operands.len > 0);
25418
25421 const pt = sema.pt;25419 const pt = sema.pt;
25422 const zcu = pt.zcu;25420 const zcu = pt.zcu;
2542325421
25424 if (operands.len == 1) return operands[0];25422 // This function has the signature `fn (Value, Value, *Zcu) Value`.
2542525423 // It is only used on scalar values, although the values may have different types.
25424 // If either operand is undef, it returns undef.
25426 const opFunc = switch (air_tag) {25425 const opFunc = switch (air_tag) {
25427 .min => Value.numberMin,25426 .min => Value.numberMin,
25428 .max => Value.numberMax,25427 .max => Value.numberMax,
25429 else => @compileError("unreachable"),25428 else => comptime unreachable,
25430 };25429 };
2543125430
25432 // The set of runtime-known operands. Set up in the loop below.25431 if (operands.len == 1) {
25433 var runtime_known = try std.DynamicBitSet.initFull(sema.arena, operands.len);25432 try sema.checkNumericType(block, operand_srcs[0], sema.typeOf(operands[0]));
25434 // The current minmax value - initially this will always be comptime-known, then we'll add25433 return operands[0];
25435 // runtime values into the mix later.25434 }
25436 var cur_minmax: ?Air.Inst.Ref = null;25435
25437 var cur_minmax_src: LazySrcLoc = undefined; // defined if cur_minmax not null25436 // First, basic type validation; we'll make sure all the operands are numeric and agree on vector length.
25438 // The current known scalar bounds of the value.25437 // This value will be `null` for a scalar type, otherwise the length of the vector type.
25439 var bounds_status: enum {25438 const vector_len: ?u64 = vec_len: {
25440 unknown, // We've only seen undef comptime_ints so far, so do not know the bounds.25439 const first_operand_ty = sema.typeOf(operands[0]);
25441 defined, // We've seen only integers, so the bounds are defined.25440 try sema.checkNumericType(block, operand_srcs[0], first_operand_ty);
25442 non_integral, // There are floats in the mix, so the bounds aren't defined.25441 if (first_operand_ty.zigTypeTag(zcu) == .vector) {
25443 } = .unknown;25442 const vec_len = first_operand_ty.vectorLen(zcu);
25444 var cur_min_scalar: Value = undefined;25443 for (operands[1..], operand_srcs[1..]) |operand, operand_src| {
25445 var cur_max_scalar: Value = undefined;25444 const operand_ty = sema.typeOf(operand);
2544625445 try sema.checkNumericType(block, operand_src, operand_ty);
25447 // First, find all comptime-known arguments, and get their min/max25446 if (operand_ty.zigTypeTag(zcu) != .vector) {
2544825447 return sema.failWithOwnedErrorMsg(block, msg: {
25449 for (operands, operand_srcs, 0..) |operand, operand_src, operand_idx| {25448 const msg = try sema.errMsg(operand_src, "expected vector, found '{}'", .{operand_ty.fmt(pt)});
25450 // Resolve the value now to avoid redundant calls to `checkSimdBinOp` - we'll have to call25449 errdefer msg.destroy(zcu.gpa);
25451 // it in the runtime path anyway since the result type may have been refined25450 try sema.errNote(operand_srcs[0], msg, "vector operand here", .{});
25452 const unresolved_uncoerced_val = try sema.resolveValue(operand) orelse continue;25451 break :msg msg;
25453 const uncoerced_val = try sema.resolveLazyValue(unresolved_uncoerced_val);25452 });
2545425453 }
25455 runtime_known.unset(operand_idx);25454 if (operand_ty.vectorLen(zcu) != vec_len) {
2545625455 return sema.failWithOwnedErrorMsg(block, msg: {
25457 switch (bounds_status) {25456 const msg = try sema.errMsg(operand_src, "expected vector of length '{d}', found '{}'", .{ vec_len, operand_ty.fmt(pt) });
25458 .unknown, .defined => refine_bounds: {25457 errdefer msg.destroy(zcu.gpa);
25459 const ty = sema.typeOf(operand);25458 try sema.errNote(operand_srcs[0], msg, "vector of length '{d}' here", .{vec_len});
25460 if (!ty.scalarType(zcu).isInt(zcu) and !ty.scalarType(zcu).eql(Type.comptime_int, zcu)) {25459 break :msg msg;
25461 bounds_status = .non_integral;25460 });
25462 break :refine_bounds;25461 }
25463 }25462 }
25464 const scalar_bounds: ?[2]Value = bounds: {25463 break :vec_len vec_len;
25465 if (!ty.isVector(zcu)) break :bounds try uncoerced_val.intValueBounds(pt);25464 } else {
25466 var cur_bounds: [2]Value = try Value.intValueBounds(try uncoerced_val.elemValue(pt, 0), pt) orelse break :bounds null;25465 for (operands[1..], operand_srcs[1..]) |operand, operand_src| {
25467 const len = try sema.usizeCast(block, src, ty.vectorLen(zcu));25466 const operand_ty = sema.typeOf(operand);
25468 for (1..len) |i| {25467 if (operand_ty.zigTypeTag(zcu) == .vector) {
25469 const elem = try uncoerced_val.elemValue(pt, i);25468 return sema.failWithOwnedErrorMsg(block, msg: {
25470 const elem_bounds = try elem.intValueBounds(pt) orelse break :bounds null;25469 const msg = try sema.errMsg(operand_srcs[0], "expected vector, found '{}'", .{first_operand_ty.fmt(pt)});
25471 cur_bounds = .{25470 errdefer msg.destroy(zcu.gpa);
25472 Value.numberMin(elem_bounds[0], cur_bounds[0], zcu),25471 try sema.errNote(operand_src, msg, "vector operand here", .{});
25473 Value.numberMax(elem_bounds[1], cur_bounds[1], zcu),25472 break :msg msg;
25474 };25473 });
25474 }
25475 }
25476 break :vec_len null;
25477 }
25478 };
25479
25480 // Now we want to look at the scalar types. If any is a float, our result will be a float. This
25481 // union is in "priority" order: `float` overrides `comptime_float` overrides `int`.
25482 const TypeStrat = union(enum) {
25483 float: Type,
25484 comptime_float,
25485 int: struct {
25486 /// If this is still `true` at the end, we will just use a `comptime_int`.
25487 all_comptime_int: bool,
25488 // These two fields tells us about the *result* type, which is refined based on operand types.
25489 // e.g. `@max(u32, i64)` results in a `u63`, because the result is >=0 and <=maxInt(i64).
25490 result_min: Value,
25491 result_max: Value,
25492 // These two fields tell us the *intermediate* type to use for actually computing the min/max.
25493 // e.g. `@max(u32, i64)` uses an intermediate `i64`, because it can fit all our operands.
25494 operand_min: Value,
25495 operand_max: Value,
25496 },
25497 none,
25498 };
25499 var cur_strat: TypeStrat = .none;
25500 for (operands) |operand| {
25501 const operand_scalar_ty = sema.typeOf(operand).scalarType(zcu);
25502 const want_strat: TypeStrat = switch (operand_scalar_ty.zigTypeTag(zcu)) {
25503 .comptime_int => s: {
25504 const val = (try sema.resolveValueResolveLazy(operand)).?;
25505 if (val.isUndef(zcu)) break :s .none;
25506 break :s .{ .int = .{
25507 .all_comptime_int = true,
25508 .result_min = val,
25509 .result_max = val,
25510 .operand_min = val,
25511 .operand_max = val,
25512 } };
25513 },
25514 .comptime_float => .comptime_float,
25515 .float => .{ .float = operand_scalar_ty },
25516 .int => s: {
25517 // If the *value* is comptime-known, we will use that to get tighter bounds. If #3806
25518 // is accepted and implemented, so that integer literals have a tightly-bounded ranged
25519 // integer type (and `comptime_int` ceases to exist), this block should probably go away
25520 // (replaced with just the simple calls to `Type.minInt`/`Type.maxInt`) so that we only
25521 // use the input *types* to determine the result type.
25522 const min: Value, const max: Value = bounds: {
25523 if (try sema.resolveValueResolveLazy(operand)) |operand_val| {
25524 if (vector_len) |len| {
25525 var min = try operand_val.elemValue(pt, 0);
25526 var max = min;
25527 for (1..@intCast(len)) |elem_idx| {
25528 const elem_val = try operand_val.elemValue(pt, elem_idx);
25529 min = Value.numberMin(min, elem_val, zcu);
25530 max = Value.numberMax(max, elem_val, zcu);
25531 }
25532 if (!min.isUndef(zcu) and !max.isUndef(zcu)) {
25533 break :bounds .{ min, max };
25534 }
25535 } else {
25536 if (!operand_val.isUndef(zcu)) {
25537 break :bounds .{ operand_val, operand_val };
25538 }
25539 }
25475 }25540 }
25476 break :bounds cur_bounds;25541 break :bounds .{
25542 try operand_scalar_ty.minInt(pt, operand_scalar_ty),
25543 try operand_scalar_ty.maxInt(pt, operand_scalar_ty),
25544 };
25477 };25545 };
25478 if (scalar_bounds) |bounds| {25546 break :s .{ .int = .{
25479 if (bounds_status == .unknown) {25547 .all_comptime_int = false,
25480 cur_min_scalar = bounds[0];25548 .result_min = min,
25481 cur_max_scalar = bounds[1];25549 .result_max = max,
25482 bounds_status = .defined;25550 .operand_min = min,
25483 } else {25551 .operand_max = max,
25484 cur_min_scalar = opFunc(cur_min_scalar, bounds[0], zcu);25552 } };
25485 cur_max_scalar = opFunc(cur_max_scalar, bounds[1], zcu);
25486 }
25487 }
25488 },25553 },
25489 .non_integral => {},25554 else => unreachable,
25490 }
25491
25492 const cur = cur_minmax orelse {
25493 cur_minmax = operand;
25494 cur_minmax_src = operand_src;
25495 continue;
25496 };
25497
25498 const simd_op = try sema.checkSimdBinOp(block, src, cur, operand, cur_minmax_src, operand_src);
25499 const cur_val = try sema.resolveLazyValue(simd_op.lhs_val.?); // cur_minmax is comptime-known
25500 const operand_val = try sema.resolveLazyValue(simd_op.rhs_val.?); // we checked the operand was resolvable above
25501
25502 const vec_len = simd_op.len orelse {
25503 const result_val = opFunc(cur_val, operand_val, zcu);
25504 cur_minmax = Air.internedToRef(result_val.toIntern());
25505 continue;
25506 };25555 };
25507 const elems = try sema.arena.alloc(InternPool.Index, vec_len);25556 if (@intFromEnum(want_strat) < @intFromEnum(cur_strat)) {
25508 for (elems, 0..) |*elem, i| {25557 // `want_strat` overrides `cur_strat`.
25509 const lhs_elem_val = try cur_val.elemValue(pt, i);25558 cur_strat = want_strat;
25510 const rhs_elem_val = try operand_val.elemValue(pt, i);25559 } else if (@intFromEnum(want_strat) == @intFromEnum(cur_strat)) {
25511 const uncoerced_elem = opFunc(lhs_elem_val, rhs_elem_val, zcu);25560 // The behavior depends on the tag.
25512 elem.* = (try pt.getCoerced(uncoerced_elem, simd_op.scalar_ty)).toIntern();25561 switch (cur_strat) {
25562 .none, .comptime_float => {}, // no payload, so nop
25563 .float => |cur_float| {
25564 const want_float = want_strat.float;
25565 // Select the larger bit size. If the bit size is the same, select whichever is not c_longdouble.
25566 const cur_bits = cur_float.floatBits(zcu.getTarget());
25567 const want_bits = want_float.floatBits(zcu.getTarget());
25568 if (want_bits > cur_bits or
25569 (want_bits == cur_bits and
25570 cur_float.toIntern() == .c_longdouble_type and
25571 want_float.toIntern() != .c_longdouble_type))
25572 {
25573 cur_strat = want_strat;
25574 }
25575 },
25576 .int => |*cur_int| {
25577 const want_int = want_strat.int;
25578 if (!want_int.all_comptime_int) cur_int.all_comptime_int = false;
25579 cur_int.result_min = opFunc(cur_int.result_min, want_int.result_min, zcu);
25580 cur_int.result_max = opFunc(cur_int.result_max, want_int.result_max, zcu);
25581 cur_int.operand_min = Value.numberMin(cur_int.operand_min, want_int.operand_min, zcu);
25582 cur_int.operand_max = Value.numberMax(cur_int.operand_max, want_int.operand_max, zcu);
25583 },
25584 }
25513 }25585 }
25514 cur_minmax = Air.internedToRef((try pt.intern(.{ .aggregate = .{
25515 .ty = simd_op.result_ty.toIntern(),
25516 .storage = .{ .elems = elems },
25517 } })));
25518 }25586 }
2551925587
25520 const opt_runtime_idx = runtime_known.findFirstSet();25588 // Use `cur_strat` to actually resolve the result type (and intermediate type).
2552125589 const result_scalar_ty: Type, const intermediate_scalar_ty: Type = switch (cur_strat) {
25522 if (cur_minmax) |ct_minmax_ref| refine: {25590 .float => |ty| .{ ty, ty },
25523 // Refine the comptime-known result type based on the bounds. This isn't strictly necessary25591 .comptime_float => .{ .comptime_float, .comptime_float },
25524 // in the runtime case, since we'll refine the type again later, but keeping things as small25592 .int => |int| if (int.all_comptime_int) .{
25525 // as possible will allow us to emit more optimal AIR (if all the runtime operands have25593 .comptime_int,
25526 // smaller types than the non-refined comptime type).25594 .comptime_int,
2552725595 } else .{
25528 const val = (try sema.resolveValue(ct_minmax_ref)).?;25596 try pt.intFittingRange(int.result_min, int.result_max),
25529 const orig_ty = sema.typeOf(ct_minmax_ref);25597 try pt.intFittingRange(int.operand_min, int.operand_max),
2553025598 },
25531 if (opt_runtime_idx == null and orig_ty.scalarType(zcu).eql(Type.comptime_int, zcu)) {25599 .none => .{ .comptime_int, .comptime_int }, // all undef comptime ints
25532 // If all arguments were `comptime_int`, and there are no runtime args, we'll preserve that type25600 };
25533 break :refine;25601 const result_ty: Type = if (vector_len) |l| try pt.vectorType(.{
25602 .len = @intCast(l),
25603 .child = result_scalar_ty.toIntern(),
25604 }) else result_scalar_ty;
25605 const intermediate_ty: Type = if (vector_len) |l| try pt.vectorType(.{
25606 .len = @intCast(l),
25607 .child = intermediate_scalar_ty.toIntern(),
25608 }) else intermediate_scalar_ty;
25609
25610 // This value, if not `null`, will have type `intermediate_ty`.
25611 const comptime_part: ?Value = ct: {
25612 // Contains the comptime-known scalar result values.
25613 // Values are scalars with no particular type.
25614 // `elems.len` is `vector_len orelse 1`.
25615 const elems: []InternPool.Index = try sema.arena.alloc(
25616 InternPool.Index,
25617 try sema.usizeCast(block, src, vector_len orelse 1),
25618 );
25619 // If `false`, we've not seen any comptime-known operand yet, so `elems` contains `undefined`.
25620 // Otherwise, `elems` is populated with the comptime-known results so far.
25621 var elems_populated = false;
25622 // Populated when we see a runtime-known operand.
25623 var opt_runtime_src: ?LazySrcLoc = null;
25624
25625 for (operands, operand_srcs) |operand, operand_src| {
25626 const operand_val = try sema.resolveValueResolveLazy(operand) orelse {
25627 if (opt_runtime_src == null) opt_runtime_src = operand_src;
25628 continue;
25629 };
25630 if (vector_len) |len| {
25631 // Vector case; apply `opFunc` to each element.
25632 if (elems_populated) {
25633 for (elems, 0..@intCast(len)) |*elem, elem_idx| {
25634 const new_elem = try operand_val.elemValue(pt, elem_idx);
25635 elem.* = opFunc(.fromInterned(elem.*), new_elem, zcu).toIntern();
25636 }
25637 } else {
25638 elems_populated = true;
25639 for (elems, 0..@intCast(len)) |*elem_out, elem_idx| {
25640 elem_out.* = (try operand_val.elemValue(pt, elem_idx)).toIntern();
25641 }
25642 }
25643 } else {
25644 // Scalar case; just apply `opFunc`.
25645 if (elems_populated) {
25646 elems[0] = opFunc(.fromInterned(elems[0]), operand_val, zcu).toIntern();
25647 } else {
25648 elems_populated = true;
25649 elems[0] = operand_val.toIntern();
25650 }
25651 }
25534 }25652 }
2553525653 const runtime_src = opt_runtime_src orelse {
25536 // We can't refine float types25654 // The result is comptime-known. Coerce each element to its scalar type.
25537 if (orig_ty.scalarType(zcu).isAnyFloat()) break :refine;25655 assert(elems_populated);
2553825656 for (elems) |*elem| {
25539 assert(bounds_status == .defined); // there was a non-comptime-int integral comptime-known arg25657 if (Value.fromInterned(elem.*).isUndef(zcu)) {
2554025658 elem.* = (try pt.undefValue(result_scalar_ty)).toIntern();
25541 const refined_scalar_ty = try pt.intFittingRange(cur_min_scalar, cur_max_scalar);25659 } else {
25542 const refined_ty = if (orig_ty.isVector(zcu)) try pt.vectorType(.{25660 // This coercion will always succeed, because `result_scalar_ty` can definitely hold the result.
25543 .len = orig_ty.vectorLen(zcu),25661 const coerced_ref = try sema.coerce(block, result_scalar_ty, Air.internedToRef(elem.*), .unneeded);
25544 .child = refined_scalar_ty.toIntern(),25662 elem.* = coerced_ref.toInterned().?;
25545 }) else refined_scalar_ty;25663 }
2554625664 }
25547 // Apply the refined type to the current value25665 if (vector_len == null) return Air.internedToRef(elems[0]);
25548 if (std.debug.runtime_safety) {25666 return Air.internedToRef(try pt.intern(.{ .aggregate = .{
25549 assert(try sema.intFitsInType(val, refined_ty, null));25667 .ty = result_ty.toIntern(),
25668 .storage = .{ .elems = elems },
25669 } }));
25670 };
25671 _ = runtime_src;
25672 // The result is runtime-known.
25673 // Coerce each element to the intermediate scalar type, unless there were no comptime-known operands.
25674 if (!elems_populated) break :ct null;
25675 for (elems) |*elem| {
25676 if (Value.fromInterned(elem.*).isUndef(zcu)) {
25677 elem.* = (try pt.undefValue(intermediate_scalar_ty)).toIntern();
25678 } else {
25679 // This coercion will always succeed, because `intermediate_scalar_ty` can definitely hold all operands.
25680 const coerced_ref = try sema.coerce(block, intermediate_scalar_ty, Air.internedToRef(elem.*), .unneeded);
25681 elem.* = coerced_ref.toInterned().?;
25682 }
25550 }25683 }
25551 cur_minmax = try sema.coerceInMemory(val, refined_ty);25684 break :ct .fromInterned(if (vector_len != null) try pt.intern(.{ .aggregate = .{
25552 }25685 .ty = intermediate_ty.toIntern(),
2555325686 .storage = .{ .elems = elems },
25554 const runtime_idx = opt_runtime_idx orelse return cur_minmax.?;25687 } }) else elems[0]);
25555 const runtime_src = operand_srcs[runtime_idx];25688 };
25556 try sema.requireRuntimeBlock(block, src, runtime_src);
25557
25558 // Now, iterate over runtime operands, emitting a min/max instruction for each. We'll refine the
25559 // type again at the end, based on the comptime-known bound.
25560
25561 // If the comptime-known part is undef we can avoid emitting actual instructions later
25562 const known_undef = if (cur_minmax) |operand| blk: {
25563 const val = (try sema.resolveValue(operand)).?;
25564 break :blk val.isUndef(zcu);
25565 } else false;
2556625689
25567 if (cur_minmax == null) {25690 // Time to emit the runtime operations. All runtime-known peers are coerced to `intermediate_ty`, and we cast down to `result_ty` at the end.
25568 // No comptime operands - use the first operand as the starting value
25569 assert(bounds_status == .unknown);
25570 assert(runtime_idx == 0);
25571 cur_minmax = operands[0];
25572 cur_minmax_src = runtime_src;
25573 runtime_known.unset(0); // don't look at this operand in the loop below
25574 const scalar_ty = sema.typeOf(cur_minmax.?).scalarType(zcu);
25575 if (scalar_ty.isInt(zcu)) {
25576 cur_min_scalar = try scalar_ty.minInt(pt, scalar_ty);
25577 cur_max_scalar = try scalar_ty.maxInt(pt, scalar_ty);
25578 bounds_status = .defined;
25579 } else {
25580 bounds_status = .non_integral;
25581 }
25582 }
2558325691
25584 var it = runtime_known.iterator(.{});25692 // `.none` indicates no result so far.
25585 while (it.next()) |idx| {25693 var cur_result: Air.Inst.Ref = if (comptime_part) |val| Air.internedToRef(val.toIntern()) else .none;
25586 const lhs = cur_minmax.?;25694 for (operands, operand_srcs) |operand, operand_src| {
25587 const lhs_src = cur_minmax_src;25695 if (try sema.isComptimeKnown(operand)) continue; // already in `comptime_part`
25588 const rhs = operands[idx];25696 // This coercion could fail; e.g. coercing a runtime integer peer to a `comptime_float` in a case like `@min(runtime_int, 1.5)`.
25589 const rhs_src = operand_srcs[idx];25697 const operand_coerced = try sema.coerce(block, intermediate_ty, operand, operand_src);
25590 const simd_op = try sema.checkSimdBinOp(block, src, lhs, rhs, lhs_src, rhs_src);25698 if (cur_result == .none) {
25591 if (known_undef) {25699 cur_result = operand_coerced;
25592 cur_minmax = try pt.undefRef(simd_op.result_ty);
25593 } else {25700 } else {
25594 cur_minmax = try block.addBinOp(air_tag, simd_op.lhs, simd_op.rhs);25701 cur_result = try block.addBinOp(air_tag, cur_result, operand_coerced);
25595 }
25596 // Compute the bounds of this type
25597 switch (bounds_status) {
25598 .unknown, .defined => refine_bounds: {
25599 const scalar_ty = sema.typeOf(rhs).scalarType(zcu);
25600 if (scalar_ty.isAnyFloat()) {
25601 bounds_status = .non_integral;
25602 break :refine_bounds;
25603 }
25604 const scalar_min = try scalar_ty.minInt(pt, scalar_ty);
25605 const scalar_max = try scalar_ty.maxInt(pt, scalar_ty);
25606 if (bounds_status == .unknown) {
25607 cur_min_scalar = scalar_min;
25608 cur_max_scalar = scalar_max;
25609 bounds_status = .defined;
25610 } else {
25611 cur_min_scalar = opFunc(cur_min_scalar, scalar_min, zcu);
25612 cur_max_scalar = opFunc(cur_max_scalar, scalar_max, zcu);
25613 }
25614 },
25615 .non_integral => {},
25616 }25702 }
25617 }25703 }
2561825704
25619 // Finally, refine the type based on the known bounds.25705 assert(cur_result != .none);
25620 const unrefined_ty = sema.typeOf(cur_minmax.?);25706 assert(sema.typeOf(cur_result).toIntern() == intermediate_ty.toIntern());
25621 if (unrefined_ty.scalarType(zcu).isAnyFloat()) {
25622 // We can't refine floats, so we're done.
25623 return cur_minmax.?;
25624 }
25625 assert(bounds_status == .defined); // there were integral runtime operands
25626 const refined_scalar_ty = try pt.intFittingRange(cur_min_scalar, cur_max_scalar);
25627 const refined_ty = if (unrefined_ty.isVector(zcu)) try pt.vectorType(.{
25628 .len = unrefined_ty.vectorLen(zcu),
25629 .child = refined_scalar_ty.toIntern(),
25630 }) else refined_scalar_ty;
2563125707
25632 if (try sema.typeHasOnePossibleValue(refined_ty)) |opv| {25708 // If there is a comptime-known undef operand, we actually return comptime-known undef -- but we had to do the runtime stuff to check for coercion errors.
25633 return Air.internedToRef(opv.toIntern());25709 if (comptime_part) |val| {
25710 if (val.isUndefDeep(zcu)) {
25711 return pt.undefRef(result_ty);
25712 }
25634 }25713 }
2563525714
25636 if (!refined_ty.eql(unrefined_ty, zcu)) {25715 if (result_ty.toIntern() == intermediate_ty.toIntern()) {
25637 // We've reduced the type - cast the result down25716 // No final cast needed; we're all done.
25638 return block.addTyOp(.intcast, refined_ty, cur_minmax.?);25717 return cur_result;
25639 }25718 }
2564025719
25641 return cur_minmax.?;25720 // A final cast is needed. The only case where `intermediate_ty` is different is for integers,
25721 // where we have refined the range, so we should be doing an intcast.
25722 assert(intermediate_scalar_ty.zigTypeTag(zcu) == .int);
25723 assert(result_scalar_ty.zigTypeTag(zcu) == .int);
25724 return block.addTyOp(.intcast, result_ty, cur_result);
25642}25725}
2564325726
25644fn upgradeToArrayPtr(sema: *Sema, block: *Block, ptr: Air.Inst.Ref, len: u64) !Air.Inst.Ref {25727fn upgradeToArrayPtr(sema: *Sema, block: *Block, ptr: Air.Inst.Ref, len: u64) !Air.Inst.Ref {
test/behavior/maximum_minimum.zig+41
...@@ -351,3 +351,44 @@ test "@min resulting in u0" {...@@ -351,3 +351,44 @@ test "@min resulting in u0" {
351 const x = S.min(0, 1);351 const x = S.min(0, 1);
352 try expect(x == 0);352 try expect(x == 0);
353}353}
354
355test "@min/@max with runtime signed and unsigned integers of same size" {
356 const S = struct {
357 fn min(a: i32, b: u32) i32 {
358 return @min(a, b);
359 }
360 fn max(a: i32, b: u32) u32 {
361 return @max(a, b);
362 }
363 };
364
365 const min = S.min(std.math.minInt(i32), std.math.maxInt(u32));
366 try expect(min == std.math.minInt(i32));
367
368 const max = S.max(std.math.minInt(i32), std.math.maxInt(u32));
369 try expect(max == std.math.maxInt(u32));
370}
371
372test "@min/@max with runtime vectors of signed and unsigned integers of same size" {
373 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
374 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
375 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
376 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
377 if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
378 if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
379
380 const S = struct {
381 fn min(a: @Vector(2, i32), b: @Vector(2, u32)) @Vector(2, i32) {
382 return @min(a, b);
383 }
384 fn max(a: @Vector(2, i32), b: @Vector(2, u32)) @Vector(2, u32) {
385 return @max(a, b);
386 }
387 };
388
389 const a: @Vector(2, i32) = .{ std.math.minInt(i32), std.math.maxInt(i32) };
390 const b: @Vector(2, u32) = .{ std.math.maxInt(u32), std.math.minInt(u32) };
391
392 try expectEqual(@Vector(2, i32){ std.math.minInt(i32), std.math.minInt(u32) }, S.min(a, b));
393 try expectEqual(@Vector(2, u32){ std.math.maxInt(u32), std.math.maxInt(i32) }, S.max(a, b));
394}