Преглед изворни кода

Add support for operators on `Core.IntLiteral`. (#4716)

Fixes integer builtins to produce the correct values (and not
CHECK-fail) when used on integer literals. Also adds impls to the
prelude to use the new builtins to perform operations on integer
literals.

Perhaps most importantly, this allows directly initializing `i32` values
with negative numbers, as the negation operation on integer literals now
works.

For testing I've added tests for use of literals with one operator in
each class (addition, multiplication, ordering, bitwise, etc) for which
there are distinct rules or overflow behavior, rather than exhaustively
testing all the combinations. This is aimed at finding a good tradeoff
between maintainability of the tests and thorough test coverage.

Also fixes lowering of heterogeneous shifts and comparisons. These are
currently disabled when one of the operands is an integer literal, but
we may want to allow that when the integer literal operand has a known
constant value.
Richard Smith пре 1 година
родитељ
комит
4a7aefefaa
59 измењених фајлова са 1838 додато и 419 уклоњено
  1. 1 2
      core/io.carbon
  2. 29 0
      core/prelude/operators/arithmetic.carbon
  3. 29 0
      core/prelude/operators/bitwise.carbon
  4. 17 0
      core/prelude/operators/comparison.carbon
  5. 8 3
      toolchain/base/int.h
  6. 238 104
      toolchain/check/eval.cpp
  7. 1 1
      toolchain/check/testdata/array/base.carbon
  8. 1 1
      toolchain/check/testdata/basics/builtin_types.carbon
  9. 1 1
      toolchain/check/testdata/basics/fail_numeric_literal_overflow.carbon
  10. 1 1
      toolchain/check/testdata/basics/numeric_literals.carbon
  11. 2 2
      toolchain/check/testdata/builtins/float/make_type.carbon
  12. 99 0
      toolchain/check/testdata/builtins/int/and.carbon
  13. 37 0
      toolchain/check/testdata/builtins/int/complement.carbon
  14. 70 1
      toolchain/check/testdata/builtins/int/eq.carbon
  15. 38 0
      toolchain/check/testdata/builtins/int/greater_eq.carbon
  16. 174 16
      toolchain/check/testdata/builtins/int/left_shift.carbon
  17. 72 0
      toolchain/check/testdata/builtins/int/less_eq.carbon
  18. 86 25
      toolchain/check/testdata/builtins/int/right_shift.carbon
  19. 57 5
      toolchain/check/testdata/builtins/int/sadd.carbon
  20. 35 7
      toolchain/check/testdata/builtins/int/sdiv.carbon
  21. 1 1
      toolchain/check/testdata/builtins/int/smod.carbon
  22. 42 4
      toolchain/check/testdata/builtins/int/smul.carbon
  23. 20 5
      toolchain/check/testdata/builtins/int/snegate.carbon
  24. 1 1
      toolchain/check/testdata/builtins/int/ssub.carbon
  25. 3 3
      toolchain/check/testdata/builtins/print/char.carbon
  26. 3 3
      toolchain/check/testdata/builtins/print/int.carbon
  27. 10 10
      toolchain/check/testdata/class/generic/import.carbon
  28. 19 19
      toolchain/check/testdata/class/import.carbon
  29. 1 1
      toolchain/check/testdata/function/builtin/method.carbon
  30. 1 1
      toolchain/check/testdata/if_expr/control_flow.carbon
  31. 1 1
      toolchain/check/testdata/if_expr/fail_not_in_function.carbon
  32. 29 9
      toolchain/check/testdata/index/fail_negative_indexing.carbon
  33. 7 6
      toolchain/check/testdata/operators/builtin/fail_type_mismatch_once.carbon
  34. 5 5
      toolchain/check/testdata/operators/builtin/fail_unimplemented_op.carbon
  35. 2 2
      toolchain/check/testdata/operators/overloaded/bit_complement.carbon
  36. 2 2
      toolchain/check/testdata/operators/overloaded/dec.carbon
  37. 1 1
      toolchain/check/testdata/operators/overloaded/eq.carbon
  38. 1 1
      toolchain/check/testdata/operators/overloaded/fail_error_recovery.carbon
  39. 3 3
      toolchain/check/testdata/operators/overloaded/fail_no_impl.carbon
  40. 1 1
      toolchain/check/testdata/operators/overloaded/fail_no_impl_for_arg.carbon
  41. 2 2
      toolchain/check/testdata/operators/overloaded/inc.carbon
  42. 2 2
      toolchain/check/testdata/operators/overloaded/negate.carbon
  43. 1 1
      toolchain/check/testdata/packages/implicit_imports_prelude.carbon
  44. 1 1
      toolchain/check/testdata/pointer/import.carbon
  45. 1 1
      toolchain/check/testdata/return/returned_var_scope.carbon
  46. 15 15
      toolchain/check/testdata/struct/import.carbon
  47. 14 3
      toolchain/check/testdata/tuple/access/fail_negative_indexing.carbon
  48. 1 1
      toolchain/check/testdata/tuple/access/index_not_literal.carbon
  49. 15 15
      toolchain/check/testdata/tuple/import.carbon
  50. 2 0
      toolchain/diagnostics/diagnostic_kind.def
  51. 8 3
      toolchain/lower/constant.cpp
  52. 6 0
      toolchain/lower/file_context.h
  53. 5 0
      toolchain/lower/function_context.h
  54. 84 17
      toolchain/lower/handle_call.cpp
  55. 382 78
      toolchain/lower/testdata/builtins/int.carbon
  56. 33 0
      toolchain/lower/testdata/builtins/int_literal.carbon
  57. 1 1
      toolchain/lower/testdata/builtins/print_read.carbon
  58. 114 30
      toolchain/sem_ir/builtin_function_kind.cpp
  59. 2 1
      toolchain/sem_ir/builtin_function_kind.h

+ 1 - 2
core/io.carbon

@@ -15,5 +15,4 @@ fn PrintChar(x: i32) -> i32 = "print.char";
 fn ReadChar() -> i32 = "read.char";
 
 // TODO: Change this to a global constant once they are fully supported.
-// TODO: Use simply -1 once we support negate on an IntLiteral.
-fn EOF() -> i32 { return -(1 as i32); }
+fn EOF() -> i32 { return -1; }

+ 29 - 0
core/prelude/operators/arithmetic.carbon

@@ -4,6 +4,8 @@
 
 package Core library "prelude/operators/arithmetic";
 
+import library "prelude/types/int_literal";
+
 // Addition: `a + b`.
 interface Add {
   fn Op[self: Self](other: Self) -> Self;
@@ -68,3 +70,30 @@ interface Mod {
 interface ModAssign {
   fn Op[addr self: Self*](other: Self);
 }
+
+
+// Operations for IntLiteral. These need to be here because IntLiteral has no
+// associated library of its own.
+impl IntLiteral() as Add {
+  fn Op[self: Self](other: Self) -> Self = "int.sadd";
+}
+
+impl IntLiteral() as Div {
+  fn Op[self: Self](other: Self) -> Self = "int.sdiv";
+}
+
+impl IntLiteral() as Mod {
+  fn Op[self: Self](other: Self) -> Self = "int.smod";
+}
+
+impl IntLiteral() as Mul {
+  fn Op[self: Self](other: Self) -> Self = "int.smul";
+}
+
+impl IntLiteral() as Negate {
+  fn Op[self: Self]() -> Self = "int.snegate";
+}
+
+impl IntLiteral() as Sub {
+  fn Op[self: Self](other: Self) -> Self = "int.ssub";
+}

+ 29 - 0
core/prelude/operators/bitwise.carbon

@@ -4,6 +4,8 @@
 
 package Core library "prelude/operators/bitwise";
 
+import library "prelude/types/int_literal";
+
 // Bit complement: `^a`.
 interface BitComplement {
   fn Op[self: Self]() -> Self;
@@ -58,3 +60,30 @@ interface RightShift {
 interface RightShiftAssign {
   fn Op[addr self: Self*](other: Self);
 }
+
+
+// Operations for IntLiteral. These need to be here because IntLiteral has no
+// associated library of its own.
+impl IntLiteral() as BitAnd {
+  fn Op[self: Self](other: Self) -> Self = "int.and";
+}
+
+impl IntLiteral() as BitComplement {
+  fn Op[self: Self]() -> Self = "int.complement";
+}
+
+impl IntLiteral() as BitOr {
+  fn Op[self: Self](other: Self) -> Self = "int.or";
+}
+
+impl IntLiteral() as BitXor {
+  fn Op[self: Self](other: Self) -> Self = "int.xor";
+}
+
+impl IntLiteral() as LeftShift {
+  fn Op[self: Self](other: Self) -> Self = "int.left_shift";
+}
+
+impl IntLiteral() as RightShift {
+  fn Op[self: Self](other: Self) -> Self = "int.right_shift";
+}

+ 17 - 0
core/prelude/operators/comparison.carbon

@@ -5,6 +5,7 @@
 package Core library "prelude/operators/comparison";
 
 export import library "prelude/types/bool";
+import library "prelude/types/int_literal";
 
 // Equality comparison: `a == b` and `a != b`.
 interface Eq {
@@ -28,3 +29,19 @@ impl bool as Eq {
   fn Equal[self: Self](other: Self) -> bool = "bool.eq";
   fn NotEqual[self: Self](other: Self) -> bool = "bool.neq";
 }
+
+
+// Operations for IntLiteral. These need to be here because IntLiteral has no
+// associated library of its own.
+impl IntLiteral() as Eq {
+  fn Equal[self: Self](other: Self) -> bool = "int.eq";
+  fn NotEqual[self: Self](other: Self) -> bool = "int.neq";
+}
+
+impl IntLiteral() as Ordered {
+  // TODO: fn Compare
+  fn Less[self: Self](other: Self) -> bool = "int.less";
+  fn LessOrEquivalent[self: Self](other: Self) -> bool = "int.less_eq";
+  fn Greater[self: Self](other: Self) -> bool = "int.greater";
+  fn GreaterOrEquivalent[self: Self](other: Self) -> bool = "int.greater_eq";
+}

+ 8 - 3
toolchain/base/int.h

@@ -232,6 +232,14 @@ constexpr int32_t IntId::InvalidIndex = Invalid.AsIndex();
 // an array of `APInt` values and represented as an index in the ID.
 class IntStore {
  public:
+  // The maximum supported bit width of an integer type.
+  // TODO: Pick a maximum size and document it in the design. For now
+  // we use 2^^23, because that's the largest size that LLVM supports.
+  static constexpr int MaxIntWidth = 1 << 23;
+
+  // Pick a canonical bit width for the provided number of significant bits.
+  static auto CanonicalBitWidth(int significant_bits) -> int;
+
   // Accepts a signed `int64_t` and uses the mathematical signed integer value
   // of it as the added integer value.
   //
@@ -395,9 +403,6 @@ class IntStore {
     return IntId::Invalid;
   }
 
-  // Pick a canonical bit width for the provided number of significant bits.
-  static auto CanonicalBitWidth(int significant_bits) -> int;
-
   // Canonicalize an incoming signed APInt to the correct bit width.
   static auto CanonicalizeSigned(llvm::APInt value) -> llvm::APInt;
 

+ 238 - 104
toolchain/check/eval.cpp

@@ -667,17 +667,14 @@ static auto ValidateIntType(Context& context, SemIRLoc loc,
         {.type = bit_width->type_id, .value = bit_width_val});
     return false;
   }
-  // TODO: Pick a maximum size and document it in the design. For now
-  // we use 2^^23, because that's the largest size that LLVM supports.
-  constexpr int MaxIntWidth = 1 << 23;
-  if (bit_width_val.ugt(MaxIntWidth)) {
+  if (bit_width_val.ugt(IntStore::MaxIntWidth)) {
     CARBON_DIAGNOSTIC(IntWidthTooLarge, Error,
                       "integer type width of {0} is greater than the "
                       "maximum supported width of {1}",
                       TypedInt, int);
     context.emitter().Emit(loc, IntWidthTooLarge,
                            {.type = bit_width->type_id, .value = bit_width_val},
-                           MaxIntWidth);
+                           IntStore::MaxIntWidth);
     return false;
   }
   return true;
@@ -769,6 +766,15 @@ static auto DiagnoseDivisionByZero(Context& context, SemIRLoc loc) -> void {
   context.emitter().Emit(loc, CompileTimeDivisionByZero);
 }
 
+// Get an integer at a suitable bit-width: either `bit_width_id` if it is valid,
+// or the canonical width from the value store if not.
+static auto GetIntAtSuitableWidth(Context& context, IntId int_id,
+                                  IntId bit_width_id) -> llvm::APInt {
+  return bit_width_id.is_valid()
+             ? context.ints().GetAtWidth(int_id, bit_width_id)
+             : context.ints().Get(int_id);
+}
+
 // Performs a builtin unary integer -> integer operation.
 static auto PerformBuiltinUnaryIntOp(Context& context, SemIRLoc loc,
                                      SemIR::BuiltinFunctionKind builtin_kind,
@@ -777,24 +783,34 @@ static auto PerformBuiltinUnaryIntOp(Context& context, SemIRLoc loc,
   auto op = context.insts().GetAs<SemIR::IntValue>(arg_id);
   auto [is_signed, bit_width_id] =
       context.sem_ir().types().GetIntTypeInfo(op.type_id);
-  CARBON_CHECK(bit_width_id != IntId::Invalid,
-               "Cannot evaluate a generic bit width integer: {0}", op);
-  llvm::APInt op_val = context.ints().GetAtWidth(op.int_id, bit_width_id);
+  llvm::APInt op_val = GetIntAtSuitableWidth(context, op.int_id, bit_width_id);
 
   switch (builtin_kind) {
     case SemIR::BuiltinFunctionKind::IntSNegate:
       if (op_val.isMinSignedValue()) {
-        CARBON_DIAGNOSTIC(CompileTimeIntegerNegateOverflow, Error,
-                          "integer overflow in negation of {0}", TypedInt);
-        context.emitter().Emit(loc, CompileTimeIntegerNegateOverflow,
-                               {.type = op.type_id, .value = op_val});
+        if (bit_width_id.is_valid()) {
+          CARBON_DIAGNOSTIC(CompileTimeIntegerNegateOverflow, Error,
+                            "integer overflow in negation of {0}", TypedInt);
+          context.emitter().Emit(loc, CompileTimeIntegerNegateOverflow,
+                                 {.type = op.type_id, .value = op_val});
+        } else {
+          // Widen the integer so we don't overflow into the sign bit.
+          op_val = op_val.sext(op_val.getBitWidth() +
+                               llvm::APInt::APINT_BITS_PER_WORD);
+        }
       }
       op_val.negate();
       break;
     case SemIR::BuiltinFunctionKind::IntUNegate:
+      CARBON_CHECK(bit_width_id.is_valid(), "Unsigned negate on unsized int");
       op_val.negate();
       break;
     case SemIR::BuiltinFunctionKind::IntComplement:
+      // TODO: Should we have separate builtins for signed and unsigned
+      // complement? Like with signed/unsigned negate, these operations do
+      // different things to the integer value, even though they do the same
+      // thing to the bits. We treat IntLiteral complement as signed complement,
+      // given that the result of unsigned complement depends on the bit width.
       op_val.flipAllBits();
       break;
     default:
@@ -804,80 +820,53 @@ static auto PerformBuiltinUnaryIntOp(Context& context, SemIRLoc loc,
   return MakeIntResult(context, op.type_id, is_signed, std::move(op_val));
 }
 
-// Performs a builtin binary integer -> integer operation.
-static auto PerformBuiltinBinaryIntOp(Context& context, SemIRLoc loc,
-                                      SemIR::BuiltinFunctionKind builtin_kind,
-                                      SemIR::InstId lhs_id,
-                                      SemIR::InstId rhs_id)
-    -> SemIR::ConstantId {
-  auto lhs = context.insts().GetAs<SemIR::IntValue>(lhs_id);
-  auto rhs = context.insts().GetAs<SemIR::IntValue>(rhs_id);
-
-  // Check for division by zero.
-  switch (builtin_kind) {
-    case SemIR::BuiltinFunctionKind::IntSDiv:
-    case SemIR::BuiltinFunctionKind::IntSMod:
-    case SemIR::BuiltinFunctionKind::IntUDiv:
-    case SemIR::BuiltinFunctionKind::IntUMod:
-      if (context.ints().Get(rhs.int_id).isZero()) {
-        DiagnoseDivisionByZero(context, loc);
-        return SemIR::ErrorInst::SingletonConstantId;
-      }
-      break;
-    default:
-      break;
-  }
-
-  auto [lhs_is_signed, lhs_bit_width_id] =
-      context.sem_ir().types().GetIntTypeInfo(lhs.type_id);
-  llvm::APInt lhs_val = context.ints().GetAtWidth(lhs.int_id, lhs_bit_width_id);
-
-  llvm::APInt result_val;
-
-  // First handle shift, which can directly use the canonical RHS and doesn't
-  // overflow.
-  switch (builtin_kind) {
-    // Bit shift.
-    case SemIR::BuiltinFunctionKind::IntLeftShift:
-    case SemIR::BuiltinFunctionKind::IntRightShift: {
-      const auto& rhs_orig_val = context.ints().Get(rhs.int_id);
-      if (rhs_orig_val.uge(lhs_val.getBitWidth()) ||
-          (rhs_orig_val.isNegative() && lhs_is_signed)) {
-        CARBON_DIAGNOSTIC(
-            CompileTimeShiftOutOfRange, Error,
-            "shift distance not in range [0, {0}) in {1} {2:<<|>>} {3}",
-            unsigned, TypedInt, BoolAsSelect, TypedInt);
-        context.emitter().Emit(
-            loc, CompileTimeShiftOutOfRange, lhs_val.getBitWidth(),
-            {.type = lhs.type_id, .value = lhs_val},
-            builtin_kind == SemIR::BuiltinFunctionKind::IntLeftShift,
-            {.type = rhs.type_id, .value = rhs_orig_val});
-        // TODO: Is it useful to recover by returning 0 or -1?
-        return SemIR::ErrorInst::SingletonConstantId;
-      }
+namespace {
+// A pair of APInts that are the operands of a binary operator. We use an
+// aggregate rather than `std::pair` to allow RVO of the individual ints.
+struct APIntBinaryOperands {
+  llvm::APInt lhs;
+  llvm::APInt rhs;
+};
+}  // namespace
 
-      if (builtin_kind == SemIR::BuiltinFunctionKind::IntLeftShift) {
-        result_val = lhs_val.shl(rhs_orig_val);
-      } else if (lhs_is_signed) {
-        result_val = lhs_val.ashr(rhs_orig_val);
+// Get a pair of integers at the same suitable bit-width: either their actual
+// width if they have a fixed width, or the smallest canonical width in which
+// they both fit otherwise.
+static auto GetIntsAtSuitableWidth(Context& context, IntId lhs_id, IntId rhs_id,
+                                   IntId bit_width_id) -> APIntBinaryOperands {
+  // Unsized operands: take the wider of the bit widths.
+  if (!bit_width_id.is_valid()) {
+    APIntBinaryOperands result = {.lhs = context.ints().Get(lhs_id),
+                                  .rhs = context.ints().Get(rhs_id)};
+    if (result.lhs.getBitWidth() != result.rhs.getBitWidth()) {
+      if (result.lhs.getBitWidth() > result.rhs.getBitWidth()) {
+        result.rhs = result.rhs.sext(result.lhs.getBitWidth());
       } else {
-        result_val = lhs_val.lshr(rhs_orig_val);
+        result.lhs = result.lhs.sext(result.rhs.getBitWidth());
       }
-      return MakeIntResult(context, lhs.type_id, lhs_is_signed,
-                           std::move(result_val));
     }
-
-    default:
-      // Break to do additional setup for other builtin kinds.
-      break;
+    return result;
   }
 
-  // Other operations are already checked to be homogeneous, so we can extend
-  // the RHS with the LHS bit width.
-  CARBON_CHECK(rhs.type_id == lhs.type_id, "Heterogeneous builtin integer op!");
-  llvm::APInt rhs_val = context.ints().GetAtWidth(rhs.int_id, lhs_bit_width_id);
+  return {.lhs = context.ints().GetAtWidth(lhs_id, bit_width_id),
+          .rhs = context.ints().GetAtWidth(rhs_id, bit_width_id)};
+}
+
+namespace {
+// The result of performing a binary int operation.
+struct BinaryIntOpResult {
+  llvm::APInt result_val;
+  bool overflow;
+  Lex::TokenKind op_token;
+};
+}  // namespace
 
-  // We may also need to diagnose overflow for these operations.
+// Computes the result of a homogeneous binary (int, int) -> int operation.
+static auto ComputeBinaryIntOpResult(SemIR::BuiltinFunctionKind builtin_kind,
+                                     const llvm::APInt& lhs_val,
+                                     const llvm::APInt& rhs_val)
+    -> BinaryIntOpResult {
+  llvm::APInt result_val;
   bool overflow = false;
   Lex::TokenKind op_token = Lex::TokenKind::Not;
 
@@ -943,23 +932,165 @@ static auto PerformBuiltinBinaryIntOp(Context& context, SemIRLoc loc,
 
     case SemIR::BuiltinFunctionKind::IntLeftShift:
     case SemIR::BuiltinFunctionKind::IntRightShift:
-      CARBON_FATAL("Handled specially above.");
+      CARBON_FATAL("Non-homogeneous operation handled separately.");
 
     default:
       CARBON_FATAL("Unexpected operation kind.");
   }
+  return {.result_val = std::move(result_val),
+          .overflow = overflow,
+          .op_token = op_token};
+}
+
+// Performs a builtin integer bit shift operation.
+static auto PerformBuiltinIntShiftOp(Context& context, SemIRLoc loc,
+                                     SemIR::BuiltinFunctionKind builtin_kind,
+                                     SemIR::InstId lhs_id, SemIR::InstId rhs_id)
+    -> SemIR::ConstantId {
+  auto lhs = context.insts().GetAs<SemIR::IntValue>(lhs_id);
+  auto rhs = context.insts().GetAs<SemIR::IntValue>(rhs_id);
+
+  auto [lhs_is_signed, lhs_bit_width_id] =
+      context.sem_ir().types().GetIntTypeInfo(lhs.type_id);
+
+  llvm::APInt lhs_val =
+      GetIntAtSuitableWidth(context, lhs.int_id, lhs_bit_width_id);
+  const auto& rhs_orig_val = context.ints().Get(rhs.int_id);
+  if (lhs_bit_width_id.is_valid() && rhs_orig_val.uge(lhs_val.getBitWidth())) {
+    CARBON_DIAGNOSTIC(
+        CompileTimeShiftOutOfRange, Error,
+        "shift distance >= type width of {0} in `{1} {2:<<|>>} {3}`", unsigned,
+        TypedInt, BoolAsSelect, TypedInt);
+    context.emitter().Emit(
+        loc, CompileTimeShiftOutOfRange, lhs_val.getBitWidth(),
+        {.type = lhs.type_id, .value = lhs_val},
+        builtin_kind == SemIR::BuiltinFunctionKind::IntLeftShift,
+        {.type = rhs.type_id, .value = rhs_orig_val});
+    // TODO: Is it useful to recover by returning 0 or -1?
+    return SemIR::ErrorInst::SingletonConstantId;
+  }
+
+  if (rhs_orig_val.isNegative() &&
+      context.sem_ir().types().IsSignedInt(rhs.type_id)) {
+    CARBON_DIAGNOSTIC(CompileTimeShiftNegative, Error,
+                      "shift distance negative in `{0} {1:<<|>>} {2}`",
+                      TypedInt, BoolAsSelect, TypedInt);
+    context.emitter().Emit(
+        loc, CompileTimeShiftNegative, {.type = lhs.type_id, .value = lhs_val},
+        builtin_kind == SemIR::BuiltinFunctionKind::IntLeftShift,
+        {.type = rhs.type_id, .value = rhs_orig_val});
+    // TODO: Is it useful to recover by returning 0 or -1?
+    return SemIR::ErrorInst::SingletonConstantId;
+  }
+
+  llvm::APInt result_val;
+  if (builtin_kind == SemIR::BuiltinFunctionKind::IntLeftShift) {
+    if (!lhs_bit_width_id.is_valid() && !lhs_val.isZero()) {
+      // Ensure we don't generate a ridiculously large integer through a bit
+      // shift.
+      auto width = rhs_orig_val.trySExtValue();
+      if (!width ||
+          *width > IntStore::MaxIntWidth - lhs_val.getSignificantBits()) {
+        CARBON_DIAGNOSTIC(CompileTimeUnsizedShiftOutOfRange, Error,
+                          "shift distance of {0} would result in an "
+                          "integer whose width is greater than the "
+                          "maximum supported width of {1}",
+                          TypedInt, int);
+        context.emitter().Emit(loc, CompileTimeUnsizedShiftOutOfRange,
+                               {.type = rhs.type_id, .value = rhs_orig_val},
+                               IntStore::MaxIntWidth);
+        return SemIR::ErrorInst::SingletonConstantId;
+      }
+      lhs_val = lhs_val.sext(
+          IntStore::CanonicalBitWidth(lhs_val.getSignificantBits() + *width));
+    }
+
+    result_val =
+        lhs_val.shl(rhs_orig_val.getLimitedValue(lhs_val.getBitWidth()));
+  } else if (lhs_is_signed) {
+    result_val =
+        lhs_val.ashr(rhs_orig_val.getLimitedValue(lhs_val.getBitWidth()));
+  } else {
+    CARBON_CHECK(lhs_bit_width_id.is_valid(), "Logical shift on unsized int");
+    result_val =
+        lhs_val.lshr(rhs_orig_val.getLimitedValue(lhs_val.getBitWidth()));
+  }
+  return MakeIntResult(context, lhs.type_id, lhs_is_signed,
+                       std::move(result_val));
+}
+
+// Performs a homogeneous builtin binary integer -> integer operation.
+static auto PerformBuiltinBinaryIntOp(Context& context, SemIRLoc loc,
+                                      SemIR::BuiltinFunctionKind builtin_kind,
+                                      SemIR::InstId lhs_id,
+                                      SemIR::InstId rhs_id)
+    -> SemIR::ConstantId {
+  auto lhs = context.insts().GetAs<SemIR::IntValue>(lhs_id);
+  auto rhs = context.insts().GetAs<SemIR::IntValue>(rhs_id);
+
+  CARBON_CHECK(rhs.type_id == lhs.type_id, "Heterogeneous builtin integer op!");
+  auto type_id = lhs.type_id;
+  auto [is_signed, bit_width_id] =
+      context.sem_ir().types().GetIntTypeInfo(type_id);
+  auto [lhs_val, rhs_val] =
+      GetIntsAtSuitableWidth(context, lhs.int_id, rhs.int_id, bit_width_id);
+
+  // Check for division by zero.
+  switch (builtin_kind) {
+    case SemIR::BuiltinFunctionKind::IntSDiv:
+    case SemIR::BuiltinFunctionKind::IntSMod:
+    case SemIR::BuiltinFunctionKind::IntUDiv:
+    case SemIR::BuiltinFunctionKind::IntUMod:
+      if (rhs_val.isZero()) {
+        DiagnoseDivisionByZero(context, loc);
+        return SemIR::ErrorInst::SingletonConstantId;
+      }
+      break;
+    default:
+      break;
+  }
+
+  BinaryIntOpResult result =
+      ComputeBinaryIntOpResult(builtin_kind, lhs_val, rhs_val);
+
+  if (result.overflow && !bit_width_id.is_valid()) {
+    // Retry with a larger bit width. Most operations can only overflow by one
+    // bit, but signed n-bit multiplication can overflow to 2n-1 bits. We don't
+    // need to handle unsigned multiplication here because it's not permitted
+    // for unsized integers.
+    //
+    // Note that we speculatively first perform the calculation in the width of
+    // the wider operand: smaller operations are faster and overflow to a wider
+    // integer is unlikely to be needed, especially given that the width will
+    // have been rounded up to a multiple of 64 bits by the int store.
+    CARBON_CHECK(builtin_kind != SemIR::BuiltinFunctionKind::IntUMul,
+                 "Unsigned arithmetic requires a fixed bitwidth");
+    int new_width =
+        builtin_kind == SemIR::BuiltinFunctionKind::IntSMul
+            ? lhs_val.getBitWidth() * 2
+            : IntStore::CanonicalBitWidth(lhs_val.getBitWidth() + 1);
+    new_width = std::min(new_width, IntStore::MaxIntWidth);
+    lhs_val = context.ints().GetAtWidth(lhs.int_id, new_width);
+    rhs_val = context.ints().GetAtWidth(rhs.int_id, new_width);
+
+    // Note that this can in theory still overflow if we limited `new_width` to
+    // `MaxIntWidth`. In that case we fall through to the signed overflow
+    // diagnostic below.
+    result = ComputeBinaryIntOpResult(builtin_kind, lhs_val, rhs_val);
+    CARBON_CHECK(!result.overflow || new_width == IntStore::MaxIntWidth);
+  }
 
-  if (overflow) {
+  if (result.overflow) {
     CARBON_DIAGNOSTIC(CompileTimeIntegerOverflow, Error,
-                      "integer overflow in calculation {0} {1} {2}", TypedInt,
+                      "integer overflow in calculation `{0} {1} {2}`", TypedInt,
                       Lex::TokenKind, TypedInt);
     context.emitter().Emit(loc, CompileTimeIntegerOverflow,
-                           {.type = lhs.type_id, .value = lhs_val}, op_token,
-                           {.type = rhs.type_id, .value = rhs_val});
+                           {.type = type_id, .value = lhs_val}, result.op_token,
+                           {.type = type_id, .value = rhs_val});
   }
 
-  return MakeIntResult(context, lhs.type_id, lhs_is_signed,
-                       std::move(result_val));
+  return MakeIntResult(context, type_id, is_signed,
+                       std::move(result.result_val));
 }
 
 // Performs a builtin integer comparison.
@@ -971,15 +1102,8 @@ static auto PerformBuiltinIntComparison(Context& context,
     -> SemIR::ConstantId {
   auto lhs = context.insts().GetAs<SemIR::IntValue>(lhs_id);
   auto rhs = context.insts().GetAs<SemIR::IntValue>(rhs_id);
-  CARBON_CHECK(lhs.type_id == rhs.type_id,
-               "Builtin comparison with mismatched types!");
-
-  auto [is_signed, bit_width_id] =
-      context.sem_ir().types().GetIntTypeInfo(lhs.type_id);
-  CARBON_CHECK(bit_width_id != IntId::Invalid,
-               "Cannot evaluate a generic bit width integer: {0}", lhs);
-  llvm::APInt lhs_val = context.ints().GetAtWidth(lhs.int_id, bit_width_id);
-  llvm::APInt rhs_val = context.ints().GetAtWidth(rhs.int_id, bit_width_id);
+  llvm::APInt lhs_val = context.ints().Get(lhs.int_id);
+  llvm::APInt rhs_val = context.ints().Get(rhs.int_id);
 
   bool result;
   switch (builtin_kind) {
@@ -990,16 +1114,16 @@ static auto PerformBuiltinIntComparison(Context& context,
       result = (lhs_val != rhs_val);
       break;
     case SemIR::BuiltinFunctionKind::IntLess:
-      result = is_signed ? lhs_val.slt(rhs_val) : lhs_val.ult(rhs_val);
+      result = lhs_val.slt(rhs_val);
       break;
     case SemIR::BuiltinFunctionKind::IntLessEq:
-      result = is_signed ? lhs_val.sle(rhs_val) : lhs_val.ule(rhs_val);
+      result = lhs_val.sle(rhs_val);
       break;
     case SemIR::BuiltinFunctionKind::IntGreater:
-      result = is_signed ? lhs_val.sgt(rhs_val) : lhs_val.sgt(rhs_val);
+      result = lhs_val.sgt(rhs_val);
       break;
     case SemIR::BuiltinFunctionKind::IntGreaterEq:
-      result = is_signed ? lhs_val.sge(rhs_val) : lhs_val.sge(rhs_val);
+      result = lhs_val.sge(rhs_val);
       break;
     default:
       CARBON_FATAL("Unexpected operation kind.");
@@ -1176,7 +1300,7 @@ static auto MakeConstantForBuiltinCall(Context& context, SemIRLoc loc,
       return PerformBuiltinUnaryIntOp(context, loc, builtin_kind, arg_ids[0]);
     }
 
-    // Binary integer -> integer operations.
+    // Homogeneous binary integer -> integer operations.
     case SemIR::BuiltinFunctionKind::IntSAdd:
     case SemIR::BuiltinFunctionKind::IntSSub:
     case SemIR::BuiltinFunctionKind::IntSMul:
@@ -1189,9 +1313,7 @@ static auto MakeConstantForBuiltinCall(Context& context, SemIRLoc loc,
     case SemIR::BuiltinFunctionKind::IntUMod:
     case SemIR::BuiltinFunctionKind::IntAnd:
     case SemIR::BuiltinFunctionKind::IntOr:
-    case SemIR::BuiltinFunctionKind::IntXor:
-    case SemIR::BuiltinFunctionKind::IntLeftShift:
-    case SemIR::BuiltinFunctionKind::IntRightShift: {
+    case SemIR::BuiltinFunctionKind::IntXor: {
       if (phase != Phase::Template) {
         break;
       }
@@ -1199,6 +1321,16 @@ static auto MakeConstantForBuiltinCall(Context& context, SemIRLoc loc,
                                        arg_ids[1]);
     }
 
+    // Bit shift operations.
+    case SemIR::BuiltinFunctionKind::IntLeftShift:
+    case SemIR::BuiltinFunctionKind::IntRightShift: {
+      if (phase != Phase::Template) {
+        break;
+      }
+      return PerformBuiltinIntShiftOp(context, loc, builtin_kind, arg_ids[0],
+                                      arg_ids[1]);
+    }
+
     // Integer comparisons.
     case SemIR::BuiltinFunctionKind::IntEq:
     case SemIR::BuiltinFunctionKind::IntNeq:
@@ -1311,7 +1443,9 @@ static auto MakeConstantForCall(EvalContext& eval_context, SemIRLoc loc,
   // If any operand of the call is non-constant, the call is non-constant.
   // TODO: Some builtin calls might allow some operands to be non-constant.
   if (!has_constant_operands) {
-    if (builtin_kind.IsCompTimeOnly()) {
+    if (builtin_kind.IsCompTimeOnly(
+            eval_context.sem_ir(), eval_context.inst_blocks().Get(call.args_id),
+            call.type_id)) {
       CARBON_DIAGNOSTIC(NonConstantCallToCompTimeOnlyFunction, Error,
                         "non-constant call to compile-time-only function");
       CARBON_DIAGNOSTIC(CompTimeOnlyFunctionHere, Note,

+ 1 - 1
toolchain/check/testdata/array/base.carbon

@@ -49,7 +49,7 @@ var c: [(); 5] = ((), (), (), (), (),);
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Float = %import_ref.193
+// CHECK:STDOUT:     .Float = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/basics/builtin_types.carbon

@@ -34,7 +34,7 @@ var test_type: type = i32;
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Float = %import_ref.193
+// CHECK:STDOUT:     .Float = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/basics/fail_numeric_literal_overflow.carbon

@@ -60,7 +60,7 @@ let e: f64 = 5.0e39999999999999999993;
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Float = %import_ref.193
+// CHECK:STDOUT:     .Float = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/basics/numeric_literals.carbon

@@ -77,7 +77,7 @@ fn F() {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Float = %import_ref.193
+// CHECK:STDOUT:     .Float = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 2 - 2
toolchain/check/testdata/builtins/float/make_type.carbon

@@ -102,7 +102,7 @@ var dyn: Float(dyn_size);
 // CHECK:STDOUT:   %import_ref.1: %Float.type = import_ref Main//types, Float, loaded [template = constants.%Float]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Int = %import_ref.193
+// CHECK:STDOUT:     .Int = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
@@ -176,7 +176,7 @@ var dyn: Float(dyn_size);
 // CHECK:STDOUT:   %import_ref.1: %Float.type = import_ref Main//types, Float, loaded [template = constants.%Float]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Int = %import_ref.193
+// CHECK:STDOUT:     .Int = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 99 - 0
toolchain/check/testdata/builtins/int/and.carbon

@@ -12,6 +12,8 @@
 
 // --- int_and.carbon
 
+library "[[@TEST_NAME]]";
+
 fn And(a: i32, b: i32) -> i32 = "int.and";
 
 var arr: [i32; And(12, 10)];
@@ -20,3 +22,100 @@ let arr_p: [i32; 8]* = &arr;
 fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return And(a, b);
 }
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn And(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(And(1, 2)) as Expect(0);
+  Test(And(12, 10)) as Expect(8);
+
+  Test(And(1, -1)) as Expect(1);
+  Test(And(-2, -3)) as Expect(-4);
+  // Ensure the sign bit is treated properly even for 64-bit numbers.
+  Test(And(0x7FFF_FFFF_FFFF_FFFF, -3)) as Expect(0x7FFF_FFFF_FFFF_FFFD);
+  Test(And(0x8000_0000_0000_0000, -1)) as Expect(0x8000_0000_0000_0000);
+}
+
+// --- fail_literal_runtime.carbon
+
+library "[[@TEST_NAME]]";
+
+fn AndLit(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+
+fn F(a: Core.IntLiteral()) -> Core.IntLiteral() {
+  // CHECK:STDERR: fail_literal_runtime.carbon:[[@LINE+7]]:10: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   return AndLit(5, a);
+  // CHECK:STDERR:          ^~~~~~~~~~~~
+  // CHECK:STDERR: fail_literal_runtime.carbon:[[@LINE-6]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn AndLit(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  return AndLit(5, a);
+}
+
+// --- fail_bad_decl.carbon
+
+library "[[@TEST_NAME]]";
+
+// Heterogeneous "and" is not supported.
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.and" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAnd1(a: i32, b: Core.IntLiteral()) -> i32 = "int.and";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAnd1(a: i32, b: Core.IntLiteral()) -> i32 = "int.and";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.and" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAnd2(a: Core.IntLiteral(), b: i32) -> i32 = "int.and";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAnd2(a: Core.IntLiteral(), b: i32) -> i32 = "int.and";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.and" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAnd3(a: i32, b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAnd3(a: i32, b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.and" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAnd4(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.and";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAnd4(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.and";
+
+// --- fail_runtime_literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn And(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+
+fn Test(n: Core.IntLiteral()) {
+  // OK
+  And(1, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   And(n, 1);
+  // CHECK:STDERR:   ^~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-8]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn And(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  And(n, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   And(1, n);
+  // CHECK:STDERR:   ^~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-16]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn And(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  And(1, n);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+6]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   And(n, n);
+  // CHECK:STDERR:   ^~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-24]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn And(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.and";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  And(n, n);
+}

+ 37 - 0
toolchain/check/testdata/builtins/int/complement.carbon

@@ -12,6 +12,8 @@
 
 // --- int_complement.carbon
 
+library "[[@TEST_NAME]]";
+
 fn Complement(a: i32) -> i32 = "int.complement";
 fn And(a: i32, b: i32) -> i32 = "int.and";
 
@@ -21,3 +23,38 @@ let arr_p: [i32; 0xEDCBA9]* = &arr;
 fn RuntimeCallIsValid(a: i32) -> i32 {
   return Complement(a);
 }
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Complement(a: Core.IntLiteral()) -> Core.IntLiteral() = "int.complement";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Complement(0)) as Expect(-1);
+  Test(Complement(1)) as Expect(-2);
+  Test(Complement(-1)) as Expect(0);
+  Test(Complement(-0x7FFF_FFFF_FFFF_FFFF)) as Expect(0x7FFF_FFFF_FFFF_FFFE);
+  Test(Complement(-0x8000_0000_0000_0000)) as Expect(0x7FFF_FFFF_FFFF_FFFF);
+  Test(Complement(0x7FFF_FFFF_FFFF_FFFF)) as Expect(-0x8000_0000_0000_0000);
+  Test(Complement(0x8000_0000_0000_0000)) as Expect(-0x8000_0000_0000_0001);
+}
+
+// --- fail_literal_runtime.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Complement(a: Core.IntLiteral()) -> Core.IntLiteral() = "int.complement";
+
+fn F(a: Core.IntLiteral()) -> Core.IntLiteral() {
+  // CHECK:STDERR: fail_literal_runtime.carbon:[[@LINE+6]]:10: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   return Complement(a);
+  // CHECK:STDERR:          ^~~~~~~~~~~~~
+  // CHECK:STDERR: fail_literal_runtime.carbon:[[@LINE-6]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn Complement(a: Core.IntLiteral()) -> Core.IntLiteral() = "int.complement";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  return Complement(a);
+}

+ 70 - 1
toolchain/check/testdata/builtins/int/eq.carbon

@@ -12,6 +12,8 @@
 
 // --- int_eq.carbon
 
+library "[[@TEST_NAME]]";
+
 fn Eq(a: i32, b: i32) -> bool = "int.eq";
 
 class True {}
@@ -30,7 +32,74 @@ fn RuntimeCallIsValid(a: i32, b: i32) -> bool {
 
 package FailBadDecl;
 
-// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+3]]:1: error: invalid signature for builtin function "int.eq" [InvalidBuiltinSignature]
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.eq" [InvalidBuiltinSignature]
 // CHECK:STDERR: fn WrongResult(a: i32, b: i32) -> i32 = "int.eq";
 // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
 fn WrongResult(a: i32, b: i32) -> i32 = "int.eq";
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Eq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(Eq(5, 5)) as Expect(true);
+  Test(Eq(5, 6)) as Expect(false);
+  Test(Eq(-1, -1)) as Expect(true);
+  Test(Eq(-1, 1)) as Expect(false);
+}
+
+// --- mixed.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Eq(a: Core.IntLiteral(), b: i32) -> bool = "int.eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(Eq(5, 5)) as Expect(true);
+  Test(Eq(5, 6)) as Expect(false);
+  Test(Eq(-1, -1)) as Expect(true);
+  Test(Eq(-1, 1)) as Expect(false);
+}
+
+// --- fail_runtime_literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Eq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.eq";
+
+fn Test(n: Core.IntLiteral()) {
+  // OK
+  Eq(1, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   Eq(n, 1);
+  // CHECK:STDERR:   ^~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-8]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn Eq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  Eq(n, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   Eq(1, n);
+  // CHECK:STDERR:   ^~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-16]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn Eq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  Eq(1, n);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+6]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   Eq(n, n);
+  // CHECK:STDERR:   ^~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-24]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn Eq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  Eq(n, n);
+}

+ 38 - 0
toolchain/check/testdata/builtins/int/greater_eq.carbon

@@ -12,6 +12,8 @@
 
 // --- int_greater_eq.carbon
 
+library "[[@TEST_NAME]]";
+
 fn GreaterEq(a: i32, b: i32) -> bool = "int.greater_eq";
 fn Negate(a: i32) -> i32 = "int.snegate";
 
@@ -29,3 +31,39 @@ fn F(true_: True, false_: False) {
 fn RuntimeCallIsValid(a: i32, b: i32) -> bool {
   return GreaterEq(a, b);
 }
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn GreaterEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.greater_eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(GreaterEq(5, 5)) as Expect(true);
+  Test(GreaterEq(5, 6)) as Expect(false);
+  Test(GreaterEq(6, 5)) as Expect(true);
+  Test(GreaterEq(-1, -1)) as Expect(true);
+  Test(GreaterEq(-1, 1)) as Expect(false);
+  Test(GreaterEq(1, -1)) as Expect(true);
+}
+
+// --- mixed.carbon
+
+library "[[@TEST_NAME]]";
+
+fn GreaterEq(a: Core.IntLiteral(), b: i32) -> bool = "int.greater_eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(GreaterEq(5, 5)) as Expect(true);
+  Test(GreaterEq(5, 6)) as Expect(false);
+  Test(GreaterEq(6, 5)) as Expect(true);
+  Test(GreaterEq(-1, -1)) as Expect(true);
+  Test(GreaterEq(-1, 1)) as Expect(false);
+  Test(GreaterEq(1, -1)) as Expect(true);
+}

+ 174 - 16
toolchain/check/testdata/builtins/int/left_shift.carbon

@@ -10,34 +10,109 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/int/left_shift.carbon
 
-// --- int_left_shift.carbon
+// --- i32.carbon
+
+library "[[@TEST_NAME]]";
+
+class Expect(N:! i32) {}
+fn Test(N:! i32) -> Expect(N) { return {}; }
 
 fn LeftShift(a: i32, b: i32) -> i32 = "int.left_shift";
 
-var arr: [i32; LeftShift(5, 2)];
-let arr_p: [i32; 20]* = &arr;
+fn F() {
+  Test(LeftShift(0, 0)) as Expect(0);
+  Test(LeftShift(0, 1)) as Expect(0);
+  Test(LeftShift(0, 30)) as Expect(0);
+  Test(LeftShift(1, 30)) as Expect(0x4000_0000);
+  Test(LeftShift(5, 2)) as Expect(20);
+  Test(LeftShift(-1, 1)) as Expect(-2);
+  Test(LeftShift(-2, 1)) as Expect(-4);
+  Test(LeftShift(-3, 1)) as Expect(-6);
+}
 
 fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return LeftShift(a, b);
 }
 
-// TODO: Test mixed types for LHS and RHS.
+// --- u32.carbon
+
+library "[[@TEST_NAME]]";
+
+class Expect(N:! u32) {}
+fn Test(N:! u32) -> Expect(N) { return {}; }
+
+fn LeftShift(a: u32, b: i32) -> u32 = "int.left_shift";
+
+fn F() {
+  Test(LeftShift(0, 0)) as Expect(0);
+  Test(LeftShift(0, 1)) as Expect(0);
+  Test(LeftShift(0, 30)) as Expect(0);
+  Test(LeftShift(1, 30)) as Expect(0x4000_0000);
+  Test(LeftShift(5, 2)) as Expect(20);
+  Test(LeftShift(0xFFFF_FFFF, 1)) as Expect(0xFFFF_FFFE);
+  Test(LeftShift(0xFFFF_FFFE, 1)) as Expect(0xFFFF_FFFC);
+  Test(LeftShift(0xABCD_EF01, 8)) as Expect(0xCDEF_0100);
+}
+
+fn RuntimeCall(a: u32, b: i32) -> u32 {
+  return LeftShift(a, b);
+}
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LeftShift(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.left_shift";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  // Zero can be shifted by any amount.
+  Test(LeftShift(0, 0)) as Expect(0);
+  Test(LeftShift(0, 0)) as Expect(0);
+  Test(LeftShift(0, 1)) as Expect(0);
+  Test(LeftShift(0, 30)) as Expect(0);
+  Test(LeftShift(0, 1_000_000_000)) as Expect(0);
+
+  // Positive numbers can be shifted.
+  Test(LeftShift(1, 0)) as Expect(1);
+  Test(LeftShift(1, 1)) as Expect(2);
+  Test(LeftShift(2, 1)) as Expect(4);
+  Test(LeftShift(1, 2)) as Expect(4);
+  Test(LeftShift(3, 2)) as Expect(12);
+  Test(LeftShift(1, 30)) as Expect(0x4000_0000);
+  Test(LeftShift(5, 2)) as Expect(20);
+
+  // Negative numbers can be shifted too.
+  Test(LeftShift(-1, 0)) as Expect(-1);
+  Test(LeftShift(-1, 1)) as Expect(-2);
+  Test(LeftShift(-2, 1)) as Expect(-4);
+  Test(LeftShift(-3, 1)) as Expect(-6);
+
+  // Large numbers can be shifted losslessly.
+  Test(LeftShift(0xFFFF_FFFF, 1)) as Expect(0x1_FFFF_FFFE);
+  Test(LeftShift(0xFFFF_FFFE, 1)) as Expect(0x1_FFFF_FFFC);
+  Test(LeftShift(0xABCD_EF01, 8)) as Expect(0xAB_CDEF_0100);
+  Test(LeftShift(0x7FFF_FFFF_FFFF_FFFF, 1)) as Expect(0xFFFF_FFFF_FFFF_FFFE);
+  Test(LeftShift(0xFFFF_FFFF_FFFF_FFFF, 1)) as Expect(0x1_FFFF_FFFF_FFFF_FFFE);
+}
 
 // --- fail_bad_shift.carbon
 
-package BadShift;
+library "[[@TEST_NAME]]";
 
 fn LeftShift(a: i32, b: i32) -> i32 = "int.left_shift";
-fn Negate(a: i32) -> i32 = "int.snegate";
+fn LeftShiftLit(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.left_shift";
 
-// Shift greater than size is disallowed.
+// Shift greater than size is disallowed for sized types.
 let size_1: i32 = LeftShift(1, 31);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance not in range [0, 32) in 1 << 32 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance >= type width of 32 in `1 << 32` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let size_2: i32 = LeftShift(1, 32);
 // CHECK:STDERR:                   ^~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 let size_2: i32 = LeftShift(1, 32);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance not in range [0, 32) in 1 << 33 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance >= type width of 32 in `1 << 33` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let size_3: i32 = LeftShift(1, 33);
 // CHECK:STDERR:                   ^~~~~~~~~~~~~~~~
 // CHECK:STDERR:
@@ -45,7 +120,7 @@ let size_3: i32 = LeftShift(1, 33);
 
 // Overflow is allowed if the shift distance is in bounds.
 let overflow_1: i32 = LeftShift(1000, 31);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:23: error: shift distance not in range [0, 32) in 1000 << 32 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:23: error: shift distance >= type width of 32 in `1000 << 32` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let overflow_2: i32 = LeftShift(1000, 32);
 // CHECK:STDERR:                       ^~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
@@ -53,14 +128,97 @@ let overflow_2: i32 = LeftShift(1000, 32);
 
 // Oversize shifts aren't allowed even if there's no overflow.
 let no_overflow_1: i32 = LeftShift(0, 31);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:26: error: shift distance not in range [0, 32) in 0 << 32 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:26: error: shift distance >= type width of 32 in `0 << 32` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let no_overflow_2: i32 = LeftShift(0, 32);
 // CHECK:STDERR:                          ^~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 let no_overflow_2: i32 = LeftShift(0, 32);
 
-// Negative shifts aren't allowed either.
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+3]]:21: error: shift distance not in range [0, 32) in 1 << -1 [CompileTimeShiftOutOfRange]
-// CHECK:STDERR: let negative: i32 = LeftShift(1, Negate(1));
-// CHECK:STDERR:                     ^~~~~~~~~~~~~~~~~~~~~~~
-let negative: i32 = LeftShift(1, Negate(1));
+// Negative shifts aren't allowed either, even for literals, even if the lhs is zero.
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:21: error: shift distance >= type width of 32 in `1 << -1` [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: let negative: i32 = LeftShift(1, -1);
+// CHECK:STDERR:                     ^~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative: i32 = LeftShift(1, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:26: error: shift distance >= type width of 32 in `0 << -1` [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: let negative_zero: i32 = LeftShift(0, -1);
+// CHECK:STDERR:                          ^~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative_zero: i32 = LeftShift(0, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:39: error: shift distance negative in `1 << -1` [CompileTimeShiftNegative]
+// CHECK:STDERR: let negative_lit: Core.IntLiteral() = LeftShiftLit(1, -1);
+// CHECK:STDERR:                                       ^~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative_lit: Core.IntLiteral() = LeftShiftLit(1, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:44: error: shift distance negative in `0 << -1` [CompileTimeShiftNegative]
+// CHECK:STDERR: let negative_lit_zero: Core.IntLiteral() = LeftShiftLit(0, -1);
+// CHECK:STDERR:                                            ^~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative_lit_zero: Core.IntLiteral() = LeftShiftLit(0, -1);
+
+// --- fail_literal_overflow.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LeftShift(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.left_shift";
+
+// CHECK:STDERR: fail_literal_overflow.carbon:[[@LINE+4]]:16: error: shift distance of 1000000000 would result in an integer whose width is greater than the maximum supported width of 8388608 [CompileTimeUnsizedShiftOutOfRange]
+// CHECK:STDERR: let bad: i32 = LeftShift(1, 1_000_000_000);
+// CHECK:STDERR:                ^~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let bad: i32 = LeftShift(1, 1_000_000_000);
+
+// CHECK:STDERR: fail_literal_overflow.carbon:[[@LINE+4]]:25: error: shift distance of 1000000000 would result in an integer whose width is greater than the maximum supported width of 8388608 [CompileTimeUnsizedShiftOutOfRange]
+// CHECK:STDERR: let bad_negative: i32 = LeftShift(-1, 1_000_000_000);
+// CHECK:STDERR:                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let bad_negative: i32 = LeftShift(-1, 1_000_000_000);
+
+// --- fail_comp_time_only_shift.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LeftShiftByLit(a: i32, b: Core.IntLiteral()) -> i32 = "int.left_shift";
+fn LeftShiftOfLit(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.left_shift";
+
+var a_lit: Core.IntLiteral() = 12;
+var an_i32: i32 = 34;
+
+// This can't be valid: we don't have a compile-time or runtime integer value for `a_lit`.
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE+7]]:17: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+// CHECK:STDERR: let bad1: i32 = LeftShiftByLit(an_i32, a_lit);
+// CHECK:STDERR:                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE-10]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+// CHECK:STDERR: fn LeftShiftByLit(a: i32, b: Core.IntLiteral()) -> i32 = "int.left_shift";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let bad1: i32 = LeftShiftByLit(an_i32, a_lit);
+
+// TODO: This could be valid because we don't actually need the return value at runtime.
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE+7]]:31: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+// CHECK:STDERR: let bad2: Core.IntLiteral() = LeftShiftOfLit(a_lit, an_i32);
+// CHECK:STDERR:                               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE-19]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+// CHECK:STDERR: fn LeftShiftOfLit(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.left_shift";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let bad2: Core.IntLiteral() = LeftShiftOfLit(a_lit, an_i32);
+
+// TODO: This could be valid because the literal argument has a constant value.
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE+7]]:17: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+// CHECK:STDERR: let bad3: i32 = LeftShiftByLit(an_i32, 12);
+// CHECK:STDERR:                 ^~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE-30]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+// CHECK:STDERR: fn LeftShiftByLit(a: i32, b: Core.IntLiteral()) -> i32 = "int.left_shift";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let bad3: i32 = LeftShiftByLit(an_i32, 12);
+
+// TODO: This could be valid because we don't actually need the return value at runtime.
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE+6]]:31: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+// CHECK:STDERR: let bad4: Core.IntLiteral() = LeftShiftOfLit(12, an_i32);
+// CHECK:STDERR:                               ^~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_comp_time_only_shift.carbon:[[@LINE-39]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+// CHECK:STDERR: fn LeftShiftOfLit(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.left_shift";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+let bad4: Core.IntLiteral() = LeftShiftOfLit(12, an_i32);

+ 72 - 0
toolchain/check/testdata/builtins/int/less_eq.carbon

@@ -12,6 +12,8 @@
 
 // --- int_less_eq.carbon
 
+library "[[@TEST_NAME]]";
+
 fn LessEq(a: i32, b: i32) -> bool = "int.less_eq";
 fn Negate(a: i32) -> i32 = "int.snegate";
 
@@ -29,3 +31,73 @@ fn F(true_: True, false_: False) {
 fn RuntimeCallIsValid(a: i32, b: i32) -> bool {
   return LessEq(a, b);
 }
+
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LessEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.less_eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(LessEq(5, 5)) as Expect(true);
+  Test(LessEq(5, 6)) as Expect(true);
+  Test(LessEq(6, 5)) as Expect(false);
+  Test(LessEq(-1, -1)) as Expect(true);
+  Test(LessEq(-1, 1)) as Expect(true);
+  Test(LessEq(1, -1)) as Expect(false);
+}
+
+// --- mixed.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LessEq(a: Core.IntLiteral(), b: i32) -> bool = "int.less_eq";
+
+class Expect(B:! bool) {}
+fn Test(B:! bool) -> Expect(B) { return {}; }
+
+fn F() {
+  Test(LessEq(5, 5)) as Expect(true);
+  Test(LessEq(5, 6)) as Expect(true);
+  Test(LessEq(6, 5)) as Expect(false);
+  Test(LessEq(-1, -1)) as Expect(true);
+  Test(LessEq(-1, 1)) as Expect(true);
+  Test(LessEq(1, -1)) as Expect(false);
+}
+
+// --- fail_runtime_literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LessEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.less_eq";
+
+fn Test(n: Core.IntLiteral()) {
+  // OK
+  LessEq(1, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   LessEq(n, 1);
+  // CHECK:STDERR:   ^~~~~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-8]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn LessEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.less_eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  LessEq(n, 1);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+7]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   LessEq(1, n);
+  // CHECK:STDERR:   ^~~~~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-16]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn LessEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.less_eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  LessEq(1, n);
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE+6]]:3: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+  // CHECK:STDERR:   LessEq(n, n);
+  // CHECK:STDERR:   ^~~~~~~~~~~~
+  // CHECK:STDERR: fail_runtime_literal.carbon:[[@LINE-24]]:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+  // CHECK:STDERR: fn LessEq(a: Core.IntLiteral(), b: Core.IntLiteral()) -> bool = "int.less_eq";
+  // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  LessEq(n, n);
+}

+ 86 - 25
toolchain/check/testdata/builtins/int/right_shift.carbon

@@ -10,58 +10,119 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/int/right_shift.carbon
 
-// --- int_right_shift.carbon
+// --- i32.carbon
+
+library "[[@TEST_NAME]]";
+
+class Expect(N:! i32) {}
+fn Test(N:! i32) -> Expect(N) { return {}; }
 
 fn RightShift(a: i32, b: i32) -> i32 = "int.right_shift";
 
-var arr: [i32; RightShift(22, 2)];
-let arr_p: [i32; 5]* = &arr;
+fn F() {
+  Test(RightShift(0, 31)) as Expect(0);
+  Test(RightShift(1, 31)) as Expect(0);
+  Test(RightShift(1, 0)) as Expect(1);
+  Test(RightShift(1, 2)) as Expect(0);
+  Test(RightShift(22, 2)) as Expect(5);
+  Test(RightShift(-1, 1)) as Expect(-1);
+  Test(RightShift(-2, 1)) as Expect(-1);
+  Test(RightShift(-10, 2)) as Expect(-3);
+}
 
 fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return RightShift(a, b);
 }
 
-// TODO: Test mixed types for LHS and RHS.
+// --- u32.carbon
 
-// --- arith_shift.carbon
+library "[[@TEST_NAME]]";
 
-// TODO: Also test unsigned / logical right shift.
+class Expect(N:! u32) {}
+fn Test(N:! u32) -> Expect(N) { return {}; }
 
-package ArithShift;
+fn RightShift(a: u32, b: i32) -> u32 = "int.right_shift";
 
-fn RightShift(a: i32, b: i32) -> i32 = "int.right_shift";
-fn Negate(a: i32) -> i32 = "int.snegate";
+fn F() {
+  Test(RightShift(0, 31)) as Expect(0);
+  Test(RightShift(1, 31)) as Expect(0);
+  Test(RightShift(1, 0)) as Expect(1);
+  Test(RightShift(1, 2)) as Expect(0);
+  Test(RightShift(22, 2)) as Expect(5);
+  Test(RightShift(0xFFFF_FFFF, 1)) as Expect(0x7FFF_FFFF);
+  Test(RightShift(0xABCD_EF01, 8)) as Expect(0xAB_CDEF);
+}
 
-// -1 >> 1 is -1.
-var arr1: [i32; Negate(RightShift(Negate(1), 1))];
-let arr1_p: [i32; 1]* = &arr1;
+fn RuntimeCall(a: u32, b: i32) -> u32 {
+  return RightShift(a, b);
+}
 
-// -10 >> 2 is -3.
-var arr2: [i32; Negate(RightShift(Negate(10), 2))];
-let arr2_p: [i32; 3]* = &arr2;
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn RightShift(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.right_shift";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(RightShift(0, 31)) as Expect(0);
+  Test(RightShift(1, 31)) as Expect(0);
+  Test(RightShift(1, 0)) as Expect(1);
+  Test(RightShift(1, 2)) as Expect(0);
+  Test(RightShift(22, 2)) as Expect(5);
+  Test(RightShift(-1, 1)) as Expect(-1);
+  Test(RightShift(-2, 1)) as Expect(-1);
+  Test(RightShift(-10, 2)) as Expect(-3);
+  Test(RightShift(0xFFFF_FFFF, 1)) as Expect(0x7FFF_FFFF);
+  Test(RightShift(0xABCD_EF01, 8)) as Expect(0xAB_CDEF);
+
+  Test(RightShift(0x1234_5678, 1_000_000_000)) as Expect(0);
+  Test(RightShift(-0x1234_5678, 1_000_000_000)) as Expect(-1);
+  Test(RightShift(0xFFFF_FFFF_FFFF_FFFF, 1_000_000_000)) as Expect(0);
+  Test(RightShift(0x7FFF_FFFF_FFFF_FFFF, 1_000_000_000)) as Expect(0);
+  Test(RightShift(-0x7FFF_FFFF_FFFF_FFFF, 1_000_000_000)) as Expect(-1);
+  Test(RightShift(-0x8000_0000_0000_0000, 1_000_000_000)) as Expect(-1);
+}
 
 // --- fail_bad_shift.carbon
 
-package BadShift;
+library "[[@TEST_NAME]]";
 
 fn RightShift(a: i32, b: i32) -> i32 = "int.right_shift";
-fn Negate(a: i32) -> i32 = "int.snegate";
+fn RightShiftLit(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.right_shift";
 
-// Shift greater than size is disallowed.
+// Shift greater than size is disallowed for sized types.
 let size_1: i32 = RightShift(1, 31);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance not in range [0, 32) in 1 >> 32 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance >= type width of 32 in `1 >> 32` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let size_2: i32 = RightShift(1, 32);
 // CHECK:STDERR:                   ^~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 let size_2: i32 = RightShift(1, 32);
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance not in range [0, 32) in 1 >> 33 [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:19: error: shift distance >= type width of 32 in `1 >> 33` [CompileTimeShiftOutOfRange]
 // CHECK:STDERR: let size_3: i32 = RightShift(1, 33);
 // CHECK:STDERR:                   ^~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 let size_3: i32 = RightShift(1, 33);
 
-// Negative shifts aren't allowed either.
-// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+3]]:21: error: shift distance not in range [0, 32) in 1 >> -1 [CompileTimeShiftOutOfRange]
-// CHECK:STDERR: let negative: i32 = RightShift(1, Negate(1));
-// CHECK:STDERR:                     ^~~~~~~~~~~~~~~~~~~~~~~~
-let negative: i32 = RightShift(1, Negate(1));
+// Negative shifts aren't allowed either, even for literals, even if the lhs is zero.
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:21: error: shift distance >= type width of 32 in `1 >> -1` [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: let negative: i32 = RightShift(1, -1);
+// CHECK:STDERR:                     ^~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative: i32 = RightShift(1, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:26: error: shift distance >= type width of 32 in `0 >> -1` [CompileTimeShiftOutOfRange]
+// CHECK:STDERR: let negative_zero: i32 = RightShift(0, -1);
+// CHECK:STDERR:                          ^~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative_zero: i32 = RightShift(0, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+4]]:39: error: shift distance negative in `1 >> -1` [CompileTimeShiftNegative]
+// CHECK:STDERR: let negative_lit: Core.IntLiteral() = RightShiftLit(1, -1);
+// CHECK:STDERR:                                       ^~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+let negative_lit: Core.IntLiteral() = RightShiftLit(1, -1);
+// CHECK:STDERR: fail_bad_shift.carbon:[[@LINE+3]]:44: error: shift distance negative in `0 >> -1` [CompileTimeShiftNegative]
+// CHECK:STDERR: let negative_lit_zero: Core.IntLiteral() = RightShiftLit(0, -1);
+// CHECK:STDERR:                                            ^~~~~~~~~~~~~~~~~~~~
+let negative_lit_zero: Core.IntLiteral() = RightShiftLit(0, -1);

+ 57 - 5
toolchain/check/testdata/builtins/int/sadd.carbon

@@ -10,17 +10,47 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/int/sadd.carbon
 
-// --- int_add.carbon
+// --- i32.carbon
+
+library "[[@TEST_NAME]]";
 
 fn Add(a: i32, b: i32) -> i32 = "int.sadd";
 
-var arr: [i32; Add(1, 2)];
-let arr_p: [i32; 3]* = &arr;
+class Expect(N:! i32) {}
+fn Test(N:! i32) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Add(0, 0)) as Expect(0);
+  Test(Add(1, 2)) as Expect(3);
+  Test(Add(0x7FFF_FFFE, 1)) as Expect(0x7FFF_FFFF);
+}
 
 fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return Add(a, b);
 }
 
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Add(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.sadd";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Add(0, 0)) as Expect(0);
+  Test(Add(1, 2)) as Expect(3);
+
+  // Test some cases that might -- but shouldn't -- overflow.
+  Test(Add(0x7FFF_FFFE, 1)) as Expect(0x7FFF_FFFF);
+  Test(Add(0x7FFF_FFFF, 1)) as Expect(0x8000_0000);
+  Test(Add(0x7FFF_FFFF_FFFF_FFFF, 1)) as Expect(0x8000_0000_0000_0000);
+  Test(Add(0xFFFF_FFFF_FFFF_FFFF, 1)) as Expect(0x1_0000_0000_0000_0000);
+  Test(Add(-0x8000_0000_0000_0000, -1)) as Expect(-0x8000_0000_0000_0001);
+  Test(Add(-0x8000_0000_0000_0000, -0x8000_0000_0000_0000)) as Expect(-0x1_0000_0000_0000_0000);
+}
+
 // --- fail_bad_decl.carbon
 
 package FailBadDecl;
@@ -42,6 +72,28 @@ fn TooMany(a: i32, b: i32, c: i32) -> i32 = "int.sadd";
 fn BadReturnType(a: i32, b: i32) -> bool = "int.sadd";
 fn JustRight(a: i32, b: i32) -> i32 = "int.sadd";
 
+// Heterogeneous "add" is not supported.
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.sadd" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAdd1(a: i32, b: Core.IntLiteral()) -> i32 = "int.sadd";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAdd1(a: i32, b: Core.IntLiteral()) -> i32 = "int.sadd";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.sadd" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAdd2(a: Core.IntLiteral(), b: i32) -> i32 = "int.sadd";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAdd2(a: Core.IntLiteral(), b: i32) -> i32 = "int.sadd";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.sadd" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAdd3(a: i32, b: Core.IntLiteral()) -> Core.IntLiteral() = "int.sadd";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAdd3(a: i32, b: Core.IntLiteral()) -> Core.IntLiteral() = "int.sadd";
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "int.sadd" [InvalidBuiltinSignature]
+// CHECK:STDERR: fn MixedAdd4(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.sadd";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn MixedAdd4(a: Core.IntLiteral(), b: i32) -> Core.IntLiteral() = "int.sadd";
+
 // CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+4]]:20: error: array bound is not a constant [InvalidArrayExpr]
 // CHECK:STDERR: var too_few: [i32; TooFew(1)];
 // CHECK:STDERR:                    ^~~~~~~~~
@@ -61,7 +113,7 @@ var bad_return_type: [i32; BadReturnType(1, 2)];
 // CHECK:STDERR: fail_bad_decl.carbon:[[@LINE+7]]:21: error: 3 arguments passed to function expecting 2 arguments [CallArgCountMismatch]
 // CHECK:STDERR: var bad_call: [i32; JustRight(1, 2, 3)];
 // CHECK:STDERR:                     ^~~~~~~~~~~~~~~~~~
-// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE-21]]:1: note: calling function declared here [InCallToEntity]
+// CHECK:STDERR: fail_bad_decl.carbon:[[@LINE-43]]:1: note: calling function declared here [InCallToEntity]
 // CHECK:STDERR: fn JustRight(a: i32, b: i32) -> i32 = "int.sadd";
 // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
@@ -86,7 +138,7 @@ package FailOverflow;
 fn Add(a: i32, b: i32) -> i32 = "int.sadd";
 
 let a: i32 = Add(0x7FFFFFFF, 0);
-// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation 2147483647 + 1 [CompileTimeIntegerOverflow]
+// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation `2147483647 + 1` [CompileTimeIntegerOverflow]
 // CHECK:STDERR: let b: i32 = Add(0x7FFFFFFF, 1);
 // CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~
 let b: i32 = Add(0x7FFFFFFF, 1);

+ 35 - 7
toolchain/check/testdata/builtins/int/sdiv.carbon

@@ -30,23 +30,38 @@ fn Sub(a: i32, b: i32) -> i32 = "int.ssub";
 fn Negate(a: i32) -> i32 = "int.snegate";
 
 // -0x7FFF_FFFF / -1 is OK.
-let a: i32 = Div(Negate(0x7FFF_FFFF), Negate(1));
+let a: i32 = Div(-0x7FFF_FFFF, -1);
 
 // -0x8000_0000 / 1 is OK.
-let b: i32 = Div(Sub(Negate(0x7FFF_FFFF), 1), 1);
+let b: i32 = Div(-0x8000_0000, 1);
 
 // -0x8000_0000 / -1 overflows.
-// CHECK:STDERR: fail_overflow.carbon:[[@LINE+4]]:14: error: integer overflow in calculation -2147483648 / -1 [CompileTimeIntegerOverflow]
-// CHECK:STDERR: let c: i32 = Div(Sub(Negate(0x7FFF_FFFF), 1), Negate(1));
-// CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_overflow.carbon:[[@LINE+4]]:14: error: integer overflow in calculation `-2147483648 / -1` [CompileTimeIntegerOverflow]
+// CHECK:STDERR: let c: i32 = Div(-0x8000_0000, -1);
+// CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
-let c: i32 = Div(Sub(Negate(0x7FFF_FFFF), 1), Negate(1));
+let c: i32 = Div(-0x8000_0000, -1);
+
+// --- literal_no_overflow.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Div(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.sdiv";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Div(-0x8000_0000, -1)) as Expect(0x8000_0000);
+  Test(Div(-0x8000_0000_0000_0000, -1)) as Expect(0x8000_0000_0000_0000);
+}
 
 // --- fail_div_by_zero.carbon
 
 package FailDivByZero;
 
 fn Div(a: i32, b: i32) -> i32 = "int.sdiv";
+fn DivLit(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.sdiv";
 
 // CHECK:STDERR: fail_div_by_zero.carbon:[[@LINE+4]]:14: error: division by zero [CompileTimeDivisionByZero]
 // CHECK:STDERR: let a: i32 = Div(1, 0);
@@ -54,7 +69,20 @@ fn Div(a: i32, b: i32) -> i32 = "int.sdiv";
 // CHECK:STDERR:
 let a: i32 = Div(1, 0);
 
-// CHECK:STDERR: fail_div_by_zero.carbon:[[@LINE+3]]:14: error: division by zero [CompileTimeDivisionByZero]
+// CHECK:STDERR: fail_div_by_zero.carbon:[[@LINE+4]]:14: error: division by zero [CompileTimeDivisionByZero]
 // CHECK:STDERR: let b: i32 = Div(0, 0);
 // CHECK:STDERR:              ^~~~~~~~~
+// CHECK:STDERR:
 let b: i32 = Div(0, 0);
+
+// IntLiteral allows "overflow" by widening its representation, but not overflow to infinity.
+// CHECK:STDERR: fail_div_by_zero.carbon:[[@LINE+4]]:28: error: division by zero [CompileTimeDivisionByZero]
+// CHECK:STDERR: let c: Core.IntLiteral() = DivLit(1, 0);
+// CHECK:STDERR:                            ^~~~~~~~~~~~
+// CHECK:STDERR:
+let c: Core.IntLiteral() = DivLit(1, 0);
+
+// CHECK:STDERR: fail_div_by_zero.carbon:[[@LINE+3]]:28: error: division by zero [CompileTimeDivisionByZero]
+// CHECK:STDERR: let d: Core.IntLiteral() = DivLit(0, 0);
+// CHECK:STDERR:                            ^~~~~~~~~~~~
+let d: Core.IntLiteral() = DivLit(0, 0);

+ 1 - 1
toolchain/check/testdata/builtins/int/smod.carbon

@@ -37,7 +37,7 @@ let b: i32 = Mod(Sub(Negate(0x7FFF_FFFF), 1), 1);
 
 // -0x8000_0000 / -1 overflows, so -0x8000_0000 % -1 is disallowed, even though
 // its result is representable.
-// CHECK:STDERR: fail_overflow.carbon:[[@LINE+4]]:14: error: integer overflow in calculation -2147483648 % -1 [CompileTimeIntegerOverflow]
+// CHECK:STDERR: fail_overflow.carbon:[[@LINE+4]]:14: error: integer overflow in calculation `-2147483648 % -1` [CompileTimeIntegerOverflow]
 // CHECK:STDERR: let c: i32 = Mod(Sub(Negate(0x7FFF_FFFF), 1), Negate(1));
 // CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:

+ 42 - 4
toolchain/check/testdata/builtins/int/smul.carbon

@@ -10,17 +10,55 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/int/smul.carbon
 
-// --- int_mul.carbon
+// --- i32.carbon
+
+library "[[@TEST_NAME]]";
 
 fn Mul(a: i32, b: i32) -> i32 = "int.smul";
 
-var arr: [i32; Mul(3, 2)];
-let arr_p: [i32; 6]* = &arr;
+class Expect(N:! i32) {}
+fn Test(N:! i32) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Mul(0, 0)) as Expect(0);
+  Test(Mul(0, 3)) as Expect(0);
+  Test(Mul(1, 2)) as Expect(2);
+  Test(Mul(3, 2)) as Expect(6);
+  Test(Mul(0x7FFF_FFFF, 1)) as Expect(0x7FFF_FFFF);
+  Test(Mul(0x7FFF_FFFF, -1)) as Expect(-0x7FFF_FFFF);
+}
 
 fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return Mul(a, b);
 }
 
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Mul(a: Core.IntLiteral(), b: Core.IntLiteral()) -> Core.IntLiteral() = "int.smul";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Mul(0, 0)) as Expect(0);
+  Test(Mul(0, 3)) as Expect(0);
+  Test(Mul(1, 2)) as Expect(2);
+  Test(Mul(3, 2)) as Expect(6);
+  Test(Mul(0x7FFF_FFFF, 1)) as Expect(0x7FFF_FFFF);
+  Test(Mul(0x7FFF_FFFF, -1)) as Expect(-0x7FFF_FFFF);
+
+  // Test some cases that might -- but shouldn't -- overflow.
+  Test(Mul(0x8000_0000_0000_0000, 1)) as Expect(0x8000_0000_0000_0000);
+  Test(Mul(0x8000_0000_0000_0000, -1)) as Expect(-0x8000_0000_0000_0000);
+  Test(Mul(-0x8000_0000_0000_0000, 1)) as Expect(-0x8000_0000_0000_0000);
+  Test(Mul(-0x8000_0000_0000_0000, -1)) as Expect(0x8000_0000_0000_0000);
+  Test(Mul(-0x8000_0000_0000_0000, -1)) as Expect(0x8000_0000_0000_0000);
+  Test(Mul(-0x8000_0000_0000_0000, -0x8000_0000_0000_0000))
+    as Expect(0x4000_0000_0000_0000_0000_0000_0000_0000);
+}
+
 // --- fail_overflow.carbon
 
 package FailOverflow;
@@ -28,7 +66,7 @@ package FailOverflow;
 fn Mul(a: i32, b: i32) -> i32 = "int.smul";
 
 let a: i32 = Mul(0x7FFF, 0x10000);
-// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation 32768 * 65536 [CompileTimeIntegerOverflow]
+// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation `32768 * 65536` [CompileTimeIntegerOverflow]
 // CHECK:STDERR: let b: i32 = Mul(0x8000, 0x10000);
 // CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~
 let b: i32 = Mul(0x8000, 0x10000);

+ 20 - 5
toolchain/check/testdata/builtins/int/snegate.carbon

@@ -23,6 +23,21 @@ fn RuntimeCallIsValid(a: i32, b: i32) -> i32 {
   return Negate(a);
 }
 
+// --- literal.carbon
+
+library "[[@TEST_NAME]]";
+
+fn Negate(a: Core.IntLiteral()) -> Core.IntLiteral() = "int.snegate";
+
+class Expect(N:! Core.IntLiteral()) {}
+fn Test(N:! Core.IntLiteral()) -> Expect(N) { return {}; }
+
+fn F() {
+  Test(Negate(0)) as Expect(0);
+  Test(Negate(1)) as Expect(0 - 1);
+  Test(Negate(0 - 0x8000_0000_0000_0000)) as Expect(0x8000_0000_0000_0000);
+}
+
 // --- fail_bad_decl.carbon
 
 package FailBadDecl;
@@ -110,10 +125,10 @@ fn Negate(a: i32) -> i32 = "int.snegate";
 fn Sub(a: i32, b: i32) -> i32 = "int.ssub";
 
 // -(-INT_MAX) is INT_MAX.
-let a: i32 = Negate(Negate(0x7FFFFFFF));
+let a: i32 = Negate(Negate(0x7FFF_FFFF));
 
-// -(-INT_MAX - 1) is too large for i32.
+// -INT_MIN is too large for i32.
 // CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in negation of -2147483648 [CompileTimeIntegerNegateOverflow]
-// CHECK:STDERR: let b: i32 = Negate(Sub(Negate(0x7FFFFFFF), 1));
-// CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-let b: i32 = Negate(Sub(Negate(0x7FFFFFFF), 1));
+// CHECK:STDERR: let b: i32 = Negate(-0x8000_0000);
+// CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~
+let b: i32 = Negate(-0x8000_0000);

+ 1 - 1
toolchain/check/testdata/builtins/int/ssub.carbon

@@ -29,7 +29,7 @@ fn Sub(a: i32, b: i32) -> i32 = "int.ssub";
 
 let a: i32 = Sub(0, 0x7FFFFFFF);
 let b: i32 = Sub(Sub(0, 0x7FFFFFFF), 1);
-// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation -2147483647 - 2 [CompileTimeIntegerOverflow]
+// CHECK:STDERR: fail_overflow.carbon:[[@LINE+3]]:14: error: integer overflow in calculation `-2147483647 - 2` [CompileTimeIntegerOverflow]
 // CHECK:STDERR: let c: i32 = Sub(Sub(0, 0x7FFFFFFF), 2);
 // CHECK:STDERR:              ^~~~~~~~~~~~~~~~~~~~~~~~~~
 let c: i32 = Sub(Sub(0, 0x7FFFFFFF), 2);

+ 3 - 3
toolchain/check/testdata/builtins/print/char.carbon

@@ -46,12 +46,12 @@ fn Main() {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .PrintChar = %import_ref.193
+// CHECK:STDOUT:     .PrintChar = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//io
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.193: %PrintChar.type.2 = import_ref Core//io, PrintChar, loaded [template = constants.%PrintChar.2]
+// CHECK:STDOUT:   %import_ref.229: %PrintChar.type.2 = import_ref Core//io, PrintChar, loaded [template = constants.%PrintChar.2]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -95,7 +95,7 @@ fn Main() {
 // CHECK:STDOUT:   %.loc16_13.2: %i32 = converted %int_1, %.loc16_13.1 [template = constants.%int_1.2]
 // CHECK:STDOUT:   %print.char.loc16: init %i32 = call %PrintChar.ref.loc16(%.loc16_13.2)
 // CHECK:STDOUT:   %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
-// CHECK:STDOUT:   %PrintChar.ref.loc17: %PrintChar.type.2 = name_ref PrintChar, imports.%import_ref.193 [template = constants.%PrintChar.2]
+// CHECK:STDOUT:   %PrintChar.ref.loc17: %PrintChar.type.2 = name_ref PrintChar, imports.%import_ref.229 [template = constants.%PrintChar.2]
 // CHECK:STDOUT:   %int_2: Core.IntLiteral = int_value 2 [template = constants.%int_2.1]
 // CHECK:STDOUT:   %impl.elem0.loc17: %Convert.type.2 = interface_witness_access constants.%interface.19, element0 [template = constants.%Convert.10]
 // CHECK:STDOUT:   %Convert.bound.loc17: <bound method> = bound_method %int_2, %impl.elem0.loc17 [template = constants.%Convert.bound.2]

+ 3 - 3
toolchain/check/testdata/builtins/print/int.carbon

@@ -48,12 +48,12 @@ fn Main() {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Print = %import_ref.193
+// CHECK:STDOUT:     .Print = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//io
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.193: %Print.type.2 = import_ref Core//io, Print, loaded [template = constants.%Print.2]
+// CHECK:STDOUT:   %import_ref.229: %Print.type.2 = import_ref Core//io, Print, loaded [template = constants.%Print.2]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -91,7 +91,7 @@ fn Main() {
 // CHECK:STDOUT:   %.loc16_9.2: %i32 = converted %int_1, %.loc16_9.1 [template = constants.%int_1.2]
 // CHECK:STDOUT:   %print.int.loc16: init %empty_tuple.type = call %Print.ref.loc16(%.loc16_9.2)
 // CHECK:STDOUT:   %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
-// CHECK:STDOUT:   %Print.ref.loc18: %Print.type.2 = name_ref Print, imports.%import_ref.193 [template = constants.%Print.2]
+// CHECK:STDOUT:   %Print.ref.loc18: %Print.type.2 = name_ref Print, imports.%import_ref.229 [template = constants.%Print.2]
 // CHECK:STDOUT:   %int_2: Core.IntLiteral = int_value 2 [template = constants.%int_2.1]
 // CHECK:STDOUT:   %impl.elem0.loc18: %Convert.type.2 = interface_witness_access constants.%interface.19, element0 [template = constants.%Convert.10]
 // CHECK:STDOUT:   %Convert.bound.loc18: <bound method> = bound_method %int_2, %impl.elem0.loc18 [template = constants.%Convert.bound.2]

+ 10 - 10
toolchain/check/testdata/class/generic/import.carbon

@@ -282,15 +282,15 @@ class Class(U:! type) {
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %import_ref.2: %CompleteClass.type = import_ref Main//foo, CompleteClass, loaded [template = constants.%CompleteClass.generic]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .Int = %import_ref.197
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.198
+// CHECK:STDOUT:     .Int = %import_ref.233
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.234
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.193: <witness> = import_ref Main//foo, loc9_1, loaded [template = constants.%complete_type.4]
-// CHECK:STDOUT:   %import_ref.194 = import_ref Main//foo, inst37 [no loc], unloaded
-// CHECK:STDOUT:   %import_ref.195 = import_ref Main//foo, loc7_8, unloaded
-// CHECK:STDOUT:   %import_ref.196 = import_ref Main//foo, loc8_17, unloaded
+// CHECK:STDOUT:   %import_ref.229: <witness> = import_ref Main//foo, loc9_1, loaded [template = constants.%complete_type.4]
+// CHECK:STDOUT:   %import_ref.230 = import_ref Main//foo, inst37 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.231 = import_ref Main//foo, loc7_8, unloaded
+// CHECK:STDOUT:   %import_ref.232 = import_ref Main//foo, loc8_17, unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -357,10 +357,10 @@ class Class(U:! type) {
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.194
-// CHECK:STDOUT:     .n = imports.%import_ref.195
-// CHECK:STDOUT:     .F = imports.%import_ref.196
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.193
+// CHECK:STDOUT:     .Self = imports.%import_ref.230
+// CHECK:STDOUT:     .n = imports.%import_ref.231
+// CHECK:STDOUT:     .F = imports.%import_ref.232
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.229
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 19 - 19
toolchain/check/testdata/class/import.carbon

@@ -204,14 +204,14 @@ fn Run() {
 // CHECK:STDOUT:   %import_ref.10: <witness> = import_ref Main//a, loc9_1, loaded [template = constants.%complete_type.3]
 // CHECK:STDOUT:   %import_ref.11 = import_ref Main//a, inst21 [no loc], unloaded
 // CHECK:STDOUT:   %import_ref.12: %Field.elem = import_ref Main//a, loc8_8, loaded [template = %.1]
-// CHECK:STDOUT:   %import_ref.201: <witness> = import_ref Main//a, loc16_1, loaded [template = constants.%complete_type.1]
-// CHECK:STDOUT:   %import_ref.202 = import_ref Main//a, inst56 [no loc], unloaded
-// CHECK:STDOUT:   %import_ref.203: %F.type = import_ref Main//a, loc14_21, loaded [template = constants.%F]
-// CHECK:STDOUT:   %import_ref.204: %G.type = import_ref Main//a, loc15_27, loaded [template = constants.%G]
-// CHECK:STDOUT:   %import_ref.205: <witness> = import_ref Main//a, loc16_1, loaded [template = constants.%complete_type.1]
-// CHECK:STDOUT:   %import_ref.206 = import_ref Main//a, inst56 [no loc], unloaded
-// CHECK:STDOUT:   %import_ref.207 = import_ref Main//a, loc14_21, unloaded
-// CHECK:STDOUT:   %import_ref.208 = import_ref Main//a, loc15_27, unloaded
+// CHECK:STDOUT:   %import_ref.237: <witness> = import_ref Main//a, loc16_1, loaded [template = constants.%complete_type.1]
+// CHECK:STDOUT:   %import_ref.238 = import_ref Main//a, inst56 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.239: %F.type = import_ref Main//a, loc14_21, loaded [template = constants.%F]
+// CHECK:STDOUT:   %import_ref.240: %G.type = import_ref Main//a, loc15_27, loaded [template = constants.%G]
+// CHECK:STDOUT:   %import_ref.241: <witness> = import_ref Main//a, loc16_1, loaded [template = constants.%complete_type.1]
+// CHECK:STDOUT:   %import_ref.242 = import_ref Main//a, inst56 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.243 = import_ref Main//a, loc14_21, unloaded
+// CHECK:STDOUT:   %import_ref.244 = import_ref Main//a, loc15_27, unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -243,18 +243,18 @@ fn Run() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @ForwardDeclared.1 [from "a.carbon"] {
 // CHECK:STDOUT: !members:
-// CHECK:STDOUT:   .Self = imports.%import_ref.202
-// CHECK:STDOUT:   .F = imports.%import_ref.203
-// CHECK:STDOUT:   .G = imports.%import_ref.204
-// CHECK:STDOUT:   complete_type_witness = imports.%import_ref.201
+// CHECK:STDOUT:   .Self = imports.%import_ref.238
+// CHECK:STDOUT:   .F = imports.%import_ref.239
+// CHECK:STDOUT:   .G = imports.%import_ref.240
+// CHECK:STDOUT:   complete_type_witness = imports.%import_ref.237
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @ForwardDeclared.2 [from "a.carbon"] {
 // CHECK:STDOUT: !members:
-// CHECK:STDOUT:   .Self = imports.%import_ref.206
-// CHECK:STDOUT:   .F = imports.%import_ref.207
-// CHECK:STDOUT:   .G = imports.%import_ref.208
-// CHECK:STDOUT:   complete_type_witness = imports.%import_ref.205
+// CHECK:STDOUT:   .Self = imports.%import_ref.242
+// CHECK:STDOUT:   .F = imports.%import_ref.243
+// CHECK:STDOUT:   .G = imports.%import_ref.244
+// CHECK:STDOUT:   complete_type_witness = imports.%import_ref.241
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @Incomplete [from "a.carbon"];
@@ -298,12 +298,12 @@ fn Run() {
 // CHECK:STDOUT:   %.loc12_30: init %ForwardDeclared.1 = converted %.loc12_29.1, %.loc12_29.2 [template = constants.%ForwardDeclared.val]
 // CHECK:STDOUT:   assign %c.var, %.loc12_30
 // CHECK:STDOUT:   %c.ref.loc13: ref %ForwardDeclared.1 = name_ref c, %c
-// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, imports.%import_ref.203 [template = constants.%F]
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, imports.%import_ref.239 [template = constants.%F]
 // CHECK:STDOUT:   %F.bound: <bound method> = bound_method %c.ref.loc13, %F.ref
 // CHECK:STDOUT:   %.loc13: %ForwardDeclared.1 = bind_value %c.ref.loc13
 // CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.bound(%.loc13)
 // CHECK:STDOUT:   %c.ref.loc14: ref %ForwardDeclared.1 = name_ref c, %c
-// CHECK:STDOUT:   %G.ref: %G.type = name_ref G, imports.%import_ref.204 [template = constants.%G]
+// CHECK:STDOUT:   %G.ref: %G.type = name_ref G, imports.%import_ref.240 [template = constants.%G]
 // CHECK:STDOUT:   %G.bound: <bound method> = bound_method %c.ref.loc14, %G.ref
 // CHECK:STDOUT:   %addr.loc14: %ptr.3 = addr_of %c.ref.loc14
 // CHECK:STDOUT:   %G.call: init %empty_tuple.type = call %G.bound(%addr.loc14)
@@ -319,5 +319,5 @@ fn Run() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F[%self.param_patt: %ForwardDeclared.1]() [from "a.carbon"];
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @G[addr <unexpected>.inst942: %ptr.3]() [from "a.carbon"];
+// CHECK:STDOUT: fn @G[addr <unexpected>.inst990: %ptr.3]() [from "a.carbon"];
 // CHECK:STDOUT:

+ 1 - 1
toolchain/check/testdata/function/builtin/method.carbon

@@ -42,7 +42,7 @@ var arr: [i32; (1 as i32).(I.F)(2)];
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .As = %import_ref.5
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.193
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/if_expr/control_flow.carbon

@@ -46,7 +46,7 @@ fn F(b: bool) -> i32 {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Bool = %import_ref.193
+// CHECK:STDOUT:     .Bool = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/if_expr/fail_not_in_function.carbon

@@ -67,7 +67,7 @@ class C {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Float = %import_ref.193
+// CHECK:STDOUT:     .Float = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 29 - 9
toolchain/check/testdata/index/fail_negative_indexing.carbon

@@ -9,7 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/index/fail_negative_indexing.carbon
 
 var c: [i32; 2] = (42, 42);
-// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:16: error: cannot access member of interface `Core.Negate` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess]
+// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:16: error: array index `-10` is past the end of type `[i32; 2]` [ArrayIndexOutOfBounds]
 // CHECK:STDERR: var d: i32 = c[-10];
 // CHECK:STDERR:                ^~~
 var d: i32 = c[-10];
@@ -29,19 +29,28 @@ var d: i32 = c[-10];
 // CHECK:STDOUT:   %Convert.type.10: type = fn_type @Convert.2, @impl.1(%int_32) [template]
 // CHECK:STDOUT:   %Convert.10: %Convert.type.10 = struct_value () [template]
 // CHECK:STDOUT:   %interface.19: <witness> = interface_witness (%Convert.10) [template]
-// CHECK:STDOUT:   %Convert.bound: <bound method> = bound_method %int_42.1, %Convert.10 [template]
-// CHECK:STDOUT:   %Convert.specific_fn: <specific function> = specific_function %Convert.bound, @Convert.2(%int_32) [template]
+// CHECK:STDOUT:   %Convert.bound.1: <bound method> = bound_method %int_42.1, %Convert.10 [template]
+// CHECK:STDOUT:   %Convert.specific_fn.1: <specific function> = specific_function %Convert.bound.1, @Convert.2(%int_32) [template]
 // CHECK:STDOUT:   %int_42.2: %i32 = int_value 42 [template]
 // CHECK:STDOUT:   %int_1: Core.IntLiteral = int_value 1 [template]
 // CHECK:STDOUT:   %array: %array_type = tuple_value (%int_42.2, %int_42.2) [template]
 // CHECK:STDOUT:   %int_10: Core.IntLiteral = int_value 10 [template]
+// CHECK:STDOUT:   %Op.type.13: type = fn_type @Op.13 [template]
+// CHECK:STDOUT:   %Op.type.14: type = fn_type @Op.14 [template]
+// CHECK:STDOUT:   %Op.14: %Op.type.14 = struct_value () [template]
+// CHECK:STDOUT:   %interface.20: <witness> = interface_witness (%Op.14) [template]
+// CHECK:STDOUT:   %Op.bound: <bound method> = bound_method %int_10, %Op.14 [template]
+// CHECK:STDOUT:   %int_-10.1: Core.IntLiteral = int_value -10 [template]
+// CHECK:STDOUT:   %Convert.bound.2: <bound method> = bound_method %int_-10.1, %Convert.10 [template]
+// CHECK:STDOUT:   %Convert.specific_fn.2: <specific function> = specific_function %Convert.bound.2, @Convert.2(%int_32) [template]
+// CHECK:STDOUT:   %int_-10.2: %i32 = int_value -10 [template]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Negate = %import_ref.193
+// CHECK:STDOUT:     .Negate = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
@@ -66,16 +75,16 @@ var d: i32 = c[-10];
 // CHECK:STDOUT:   %int_42.loc11_24: Core.IntLiteral = int_value 42 [template = constants.%int_42.1]
 // CHECK:STDOUT:   %.loc11_26.1: %tuple.type = tuple_literal (%int_42.loc11_20, %int_42.loc11_24)
 // CHECK:STDOUT:   %impl.elem0.loc11_26.1: %Convert.type.2 = interface_witness_access constants.%interface.19, element0 [template = constants.%Convert.10]
-// CHECK:STDOUT:   %Convert.bound.loc11_26.1: <bound method> = bound_method %int_42.loc11_20, %impl.elem0.loc11_26.1 [template = constants.%Convert.bound]
-// CHECK:STDOUT:   %Convert.specific_fn.loc11_26.1: <specific function> = specific_function %Convert.bound.loc11_26.1, @Convert.2(constants.%int_32) [template = constants.%Convert.specific_fn]
+// CHECK:STDOUT:   %Convert.bound.loc11_26.1: <bound method> = bound_method %int_42.loc11_20, %impl.elem0.loc11_26.1 [template = constants.%Convert.bound.1]
+// CHECK:STDOUT:   %Convert.specific_fn.loc11_26.1: <specific function> = specific_function %Convert.bound.loc11_26.1, @Convert.2(constants.%int_32) [template = constants.%Convert.specific_fn.1]
 // CHECK:STDOUT:   %int.convert_checked.loc11_26.1: init %i32 = call %Convert.specific_fn.loc11_26.1(%int_42.loc11_20) [template = constants.%int_42.2]
 // CHECK:STDOUT:   %.loc11_26.2: init %i32 = converted %int_42.loc11_20, %int.convert_checked.loc11_26.1 [template = constants.%int_42.2]
 // CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [template = constants.%int_0]
 // CHECK:STDOUT:   %.loc11_26.3: ref %i32 = array_index file.%c.var, %int_0
 // CHECK:STDOUT:   %.loc11_26.4: init %i32 = initialize_from %.loc11_26.2 to %.loc11_26.3 [template = constants.%int_42.2]
 // CHECK:STDOUT:   %impl.elem0.loc11_26.2: %Convert.type.2 = interface_witness_access constants.%interface.19, element0 [template = constants.%Convert.10]
-// CHECK:STDOUT:   %Convert.bound.loc11_26.2: <bound method> = bound_method %int_42.loc11_24, %impl.elem0.loc11_26.2 [template = constants.%Convert.bound]
-// CHECK:STDOUT:   %Convert.specific_fn.loc11_26.2: <specific function> = specific_function %Convert.bound.loc11_26.2, @Convert.2(constants.%int_32) [template = constants.%Convert.specific_fn]
+// CHECK:STDOUT:   %Convert.bound.loc11_26.2: <bound method> = bound_method %int_42.loc11_24, %impl.elem0.loc11_26.2 [template = constants.%Convert.bound.1]
+// CHECK:STDOUT:   %Convert.specific_fn.loc11_26.2: <specific function> = specific_function %Convert.bound.loc11_26.2, @Convert.2(constants.%int_32) [template = constants.%Convert.specific_fn.1]
 // CHECK:STDOUT:   %int.convert_checked.loc11_26.2: init %i32 = call %Convert.specific_fn.loc11_26.2(%int_42.loc11_24) [template = constants.%int_42.2]
 // CHECK:STDOUT:   %.loc11_26.5: init %i32 = converted %int_42.loc11_24, %int.convert_checked.loc11_26.2 [template = constants.%int_42.2]
 // CHECK:STDOUT:   %int_1: Core.IntLiteral = int_value 1 [template = constants.%int_1]
@@ -86,9 +95,20 @@ var d: i32 = c[-10];
 // CHECK:STDOUT:   assign file.%c.var, %.loc11_27
 // CHECK:STDOUT:   %c.ref: ref %array_type = name_ref c, file.%c
 // CHECK:STDOUT:   %int_10: Core.IntLiteral = int_value 10 [template = constants.%int_10]
+// CHECK:STDOUT:   %impl.elem0.loc15_16.1: %Op.type.13 = interface_witness_access constants.%interface.20, element0 [template = constants.%Op.14]
+// CHECK:STDOUT:   %Op.bound: <bound method> = bound_method %int_10, %impl.elem0.loc15_16.1 [template = constants.%Op.bound]
+// CHECK:STDOUT:   %int.snegate: init Core.IntLiteral = call %Op.bound(%int_10) [template = constants.%int_-10.1]
 // CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [template = constants.%int_32]
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(constants.%int_32) [template = constants.%i32]
-// CHECK:STDOUT:   %.loc15_19.1: ref %i32 = array_index %c.ref, <error> [template = <error>]
+// CHECK:STDOUT:   %impl.elem0.loc15_16.2: %Convert.type.2 = interface_witness_access constants.%interface.19, element0 [template = constants.%Convert.10]
+// CHECK:STDOUT:   %Convert.bound.loc15: <bound method> = bound_method %int.snegate, %impl.elem0.loc15_16.2 [template = constants.%Convert.bound.2]
+// CHECK:STDOUT:   %Convert.specific_fn.loc15: <specific function> = specific_function %Convert.bound.loc15, @Convert.2(constants.%int_32) [template = constants.%Convert.specific_fn.2]
+// CHECK:STDOUT:   %.loc15_16.1: Core.IntLiteral = value_of_initializer %int.snegate [template = constants.%int_-10.1]
+// CHECK:STDOUT:   %.loc15_16.2: Core.IntLiteral = converted %int.snegate, %.loc15_16.1 [template = constants.%int_-10.1]
+// CHECK:STDOUT:   %int.convert_checked.loc15: init %i32 = call %Convert.specific_fn.loc15(%.loc15_16.2) [template = constants.%int_-10.2]
+// CHECK:STDOUT:   %.loc15_16.3: %i32 = value_of_initializer %int.convert_checked.loc15 [template = constants.%int_-10.2]
+// CHECK:STDOUT:   %.loc15_16.4: %i32 = converted %int.snegate, %.loc15_16.3 [template = constants.%int_-10.2]
+// CHECK:STDOUT:   %.loc15_19.1: ref %i32 = array_index %c.ref, %.loc15_16.4 [template = <error>]
 // CHECK:STDOUT:   %.loc15_19.2: %i32 = bind_value %.loc15_19.1
 // CHECK:STDOUT:   assign file.%d.var, %.loc15_19.2
 // CHECK:STDOUT:   return

+ 7 - 6
toolchain/check/testdata/operators/builtin/fail_type_mismatch_once.carbon

@@ -11,10 +11,10 @@
 fn Main() -> i32 {
   // The following line has two mismatches, but after the first, it shouldn't
   // keep erroring.
-  // CHECK:STDERR: fail_type_mismatch_once.carbon:[[@LINE+3]]:10: error: cannot access member of interface `Core.Add` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess]
-  // CHECK:STDERR:   return 12 + 3.4 + 12;
+  // CHECK:STDERR: fail_type_mismatch_once.carbon:[[@LINE+3]]:10: error: cannot access member of interface `Core.Add` in type `{}` that does not implement that interface [MissingImplInMemberAccess]
+  // CHECK:STDERR:   return {} + 3.4 + 12;
   // CHECK:STDERR:          ^~~~~~~~
-  return 12 + 3.4 + 12;
+  return {} + 3.4 + 12;
 }
 
 // CHECK:STDOUT: --- fail_type_mismatch_once.carbon
@@ -24,9 +24,10 @@ fn Main() -> i32 {
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [template]
 // CHECK:STDOUT:   %Main.type: type = fn_type @Main [template]
 // CHECK:STDOUT:   %Main: %Main.type = struct_value () [template]
-// CHECK:STDOUT:   %int_12: Core.IntLiteral = int_value 12 [template]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [template]
 // CHECK:STDOUT:   %float: f64 = float_literal 3.4000000000000004 [template]
 // CHECK:STDOUT:   %Op.type: type = fn_type @Op [template]
+// CHECK:STDOUT:   %int_12: Core.IntLiteral = int_value 12 [template]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -57,9 +58,9 @@ fn Main() -> i32 {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @Main() -> %i32 {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %int_12.loc17_10: Core.IntLiteral = int_value 12 [template = constants.%int_12]
+// CHECK:STDOUT:   %.loc17: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %float: f64 = float_literal 3.4000000000000004 [template = constants.%float]
-// CHECK:STDOUT:   %int_12.loc17_21: Core.IntLiteral = int_value 12 [template = constants.%int_12]
+// CHECK:STDOUT:   %int_12: Core.IntLiteral = int_value 12 [template = constants.%int_12]
 // CHECK:STDOUT:   %impl.elem0: %Op.type = interface_witness_access <error>, element0 [template = <error>]
 // CHECK:STDOUT:   %Op.bound: <bound method> = bound_method <error>, %impl.elem0 [template = <error>]
 // CHECK:STDOUT:   return <error>

+ 5 - 5
toolchain/check/testdata/operators/builtin/fail_unimplemented_op.carbon

@@ -9,10 +9,10 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/operators/builtin/fail_unimplemented_op.carbon
 
 fn Main() -> i32 {
-  // CHECK:STDERR: fail_unimplemented_op.carbon:[[@LINE+3]]:10: error: cannot access member of interface `Core.Add` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess]
-  // CHECK:STDERR:   return 12 + 34;
+  // CHECK:STDERR: fail_unimplemented_op.carbon:[[@LINE+3]]:10: error: cannot access member of interface `Core.Add` in type `{}` that does not implement that interface [MissingImplInMemberAccess]
+  // CHECK:STDERR:   return {} + 34;
   // CHECK:STDERR:          ^~~~~~~
-  return 12 + 34;
+  return {} + 34;
 }
 
 // CHECK:STDOUT: --- fail_unimplemented_op.carbon
@@ -22,7 +22,7 @@ fn Main() -> i32 {
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [template]
 // CHECK:STDOUT:   %Main.type: type = fn_type @Main [template]
 // CHECK:STDOUT:   %Main: %Main.type = struct_value () [template]
-// CHECK:STDOUT:   %int_12: Core.IntLiteral = int_value 12 [template]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [template]
 // CHECK:STDOUT:   %int_34: Core.IntLiteral = int_value 34 [template]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -54,7 +54,7 @@ fn Main() -> i32 {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @Main() -> %i32 {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %int_12: Core.IntLiteral = int_value 12 [template = constants.%int_12]
+// CHECK:STDOUT:   %.loc15: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %int_34: Core.IntLiteral = int_value 34 [template = constants.%int_34]
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/operators/overloaded/bit_complement.carbon

@@ -57,7 +57,7 @@ fn TestOp(a: C) -> C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %C.decl: type = class_decl @C [template = constants.%C] {} {}
-// CHECK:STDOUT:   impl_decl @impl [template] {} {
+// CHECK:STDOUT:   impl_decl @impl.1 [template] {} {
 // CHECK:STDOUT:     %C.ref: type = name_ref C, file.%C.decl [template = constants.%C]
 // CHECK:STDOUT:     %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
 // CHECK:STDOUT:     %BitComplement.ref: type = name_ref BitComplement, imports.%import_ref.1 [template = constants.%BitComplement.type]
@@ -77,7 +77,7 @@ fn TestOp(a: C) -> C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: impl @impl: %C.ref as %BitComplement.ref {
+// CHECK:STDOUT: impl @impl.1: %C.ref as %BitComplement.ref {
 // CHECK:STDOUT:   %Op.decl: %Op.type.1 = fn_decl @Op.1 [template = constants.%Op.1] {
 // CHECK:STDOUT:     %self.patt: %C = binding_pattern self
 // CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, runtime_param0

+ 2 - 2
toolchain/check/testdata/operators/overloaded/dec.carbon

@@ -58,7 +58,7 @@ fn TestOp() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %C.decl: type = class_decl @C [template = constants.%C] {} {}
-// CHECK:STDOUT:   impl_decl @impl [template] {} {
+// CHECK:STDOUT:   impl_decl @impl.1 [template] {} {
 // CHECK:STDOUT:     %C.ref: type = name_ref C, file.%C.decl [template = constants.%C]
 // CHECK:STDOUT:     %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
 // CHECK:STDOUT:     %Dec.ref: type = name_ref Dec, imports.%import_ref.1 [template = constants.%Dec.type]
@@ -66,7 +66,7 @@ fn TestOp() {
 // CHECK:STDOUT:   %TestOp.decl: %TestOp.type = fn_decl @TestOp [template = constants.%TestOp] {} {}
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: impl @impl: %C.ref as %Dec.ref {
+// CHECK:STDOUT: impl @impl.1: %C.ref as %Dec.ref {
 // CHECK:STDOUT:   %Op.decl: %Op.type.1 = fn_decl @Op.1 [template = constants.%Op.1] {
 // CHECK:STDOUT:     %self.patt: %ptr.1 = binding_pattern self
 // CHECK:STDOUT:     %self.param_patt: %ptr.1 = value_param_pattern %self.patt, runtime_param0

+ 1 - 1
toolchain/check/testdata/operators/overloaded/eq.carbon

@@ -377,7 +377,7 @@ fn TestLhsBad(a: D, b: C) -> bool {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Eq = %import_ref.1
 // CHECK:STDOUT:     .Bool = %import_ref.7
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.12
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.27
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/operators/overloaded/fail_error_recovery.carbon

@@ -37,7 +37,7 @@ fn G(n: i32) {
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [template]
 // CHECK:STDOUT:   %G.type: type = fn_type @G [template]
 // CHECK:STDOUT:   %G: %G.type = struct_value () [template]
-// CHECK:STDOUT:   %Op.type.14: type = fn_type @Op.2, @impl.7(%int_32) [template]
+// CHECK:STDOUT:   %Op.type.14: type = fn_type @Op.2, @impl.13(%int_32) [template]
 // CHECK:STDOUT:   %Op.14: %Op.type.14 = struct_value () [template]
 // CHECK:STDOUT:   %interface.19: <witness> = interface_witness (%Op.14) [template]
 // CHECK:STDOUT: }

+ 3 - 3
toolchain/check/testdata/operators/overloaded/fail_no_impl.carbon

@@ -59,9 +59,9 @@ fn TestRef(b: C) {
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Negate = %import_ref.1
-// CHECK:STDOUT:     .Add = %import_ref.6
-// CHECK:STDOUT:     .AddAssign = %import_ref.11
-// CHECK:STDOUT:     .Inc = %import_ref.16
+// CHECK:STDOUT:     .Add = %import_ref.39
+// CHECK:STDOUT:     .AddAssign = %import_ref.41
+// CHECK:STDOUT:     .Inc = %import_ref.46
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/operators/overloaded/fail_no_impl_for_arg.carbon

@@ -78,7 +78,7 @@ fn TestAssign(b: D) {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Add = %import_ref.1
 // CHECK:STDOUT:     .AddAssign = %import_ref.5
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.10
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.43
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 2 - 2
toolchain/check/testdata/operators/overloaded/inc.carbon

@@ -58,7 +58,7 @@ fn TestOp() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %C.decl: type = class_decl @C [template = constants.%C] {} {}
-// CHECK:STDOUT:   impl_decl @impl [template] {} {
+// CHECK:STDOUT:   impl_decl @impl.1 [template] {} {
 // CHECK:STDOUT:     %C.ref: type = name_ref C, file.%C.decl [template = constants.%C]
 // CHECK:STDOUT:     %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
 // CHECK:STDOUT:     %Inc.ref: type = name_ref Inc, imports.%import_ref.1 [template = constants.%Inc.type]
@@ -66,7 +66,7 @@ fn TestOp() {
 // CHECK:STDOUT:   %TestOp.decl: %TestOp.type = fn_decl @TestOp [template = constants.%TestOp] {} {}
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: impl @impl: %C.ref as %Inc.ref {
+// CHECK:STDOUT: impl @impl.1: %C.ref as %Inc.ref {
 // CHECK:STDOUT:   %Op.decl: %Op.type.1 = fn_decl @Op.1 [template = constants.%Op.1] {
 // CHECK:STDOUT:     %self.patt: %ptr.1 = binding_pattern self
 // CHECK:STDOUT:     %self.param_patt: %ptr.1 = value_param_pattern %self.patt, runtime_param0

+ 2 - 2
toolchain/check/testdata/operators/overloaded/negate.carbon

@@ -57,7 +57,7 @@ fn TestOp(a: C) -> C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %C.decl: type = class_decl @C [template = constants.%C] {} {}
-// CHECK:STDOUT:   impl_decl @impl [template] {} {
+// CHECK:STDOUT:   impl_decl @impl.1 [template] {} {
 // CHECK:STDOUT:     %C.ref: type = name_ref C, file.%C.decl [template = constants.%C]
 // CHECK:STDOUT:     %Core.ref: <namespace> = name_ref Core, imports.%Core [template = imports.%Core]
 // CHECK:STDOUT:     %Negate.ref: type = name_ref Negate, imports.%import_ref.1 [template = constants.%Negate.type]
@@ -77,7 +77,7 @@ fn TestOp(a: C) -> C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: impl @impl: %C.ref as %Negate.ref {
+// CHECK:STDOUT: impl @impl.1: %C.ref as %Negate.ref {
 // CHECK:STDOUT:   %Op.decl: %Op.type.1 = fn_decl @Op.1 [template = constants.%Op.1] {
 // CHECK:STDOUT:     %self.patt: %C = binding_pattern self
 // CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, runtime_param0

+ 1 - 1
toolchain/check/testdata/packages/implicit_imports_prelude.carbon

@@ -78,7 +78,7 @@ var b: i32 = a;
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %import_ref.1: ref %i32 = import_ref Main//lib, a, loaded
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .Int = %import_ref.191
+// CHECK:STDOUT:     .Int = %import_ref.227
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/pointer/import.carbon

@@ -86,7 +86,7 @@ var a: i32* = a_ref;
 // CHECK:STDOUT:   %import_ref.1 = import_ref Implicit//default, a_orig, unloaded
 // CHECK:STDOUT:   %import_ref.2: ref %ptr = import_ref Implicit//default, a_ref, loaded
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .Int = %import_ref.192
+// CHECK:STDOUT:     .Int = %import_ref.228
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 1 - 1
toolchain/check/testdata/return/returned_var_scope.carbon

@@ -57,7 +57,7 @@ fn EnclosingButAfter(b: bool) -> i32 {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Bool = %import_ref.193
+// CHECK:STDOUT:     .Bool = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 15 - 15
toolchain/check/testdata/struct/import.carbon

@@ -270,13 +270,13 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:   %import_ref.3: %C.type = import_ref Implicit//default, C, loaded [template = constants.%C.generic]
 // CHECK:STDOUT:   %import_ref.4: %F.type = import_ref Implicit//default, F, loaded [template = constants.%F]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .Int = %import_ref.194
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.197
+// CHECK:STDOUT:     .Int = %import_ref.230
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.233
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.195: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.196 = import_ref Implicit//default, inst940 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.231: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.232 = import_ref Implicit//default, inst988 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -309,8 +309,8 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.196
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.195
+// CHECK:STDOUT:     .Self = imports.%import_ref.232
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.231
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -395,8 +395,8 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.194: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.195 = import_ref Implicit//default, inst940 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.230: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.231 = import_ref Implicit//default, inst988 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -423,8 +423,8 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.195
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.194
+// CHECK:STDOUT:     .Self = imports.%import_ref.231
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.230
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -481,12 +481,12 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:   %import_ref.3: %C.type = import_ref Implicit//default, C, loaded [template = constants.%C.generic]
 // CHECK:STDOUT:   %import_ref.4: %F.type = import_ref Implicit//default, F, loaded [template = constants.%F]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.196
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.232
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.194: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.195 = import_ref Implicit//default, inst940 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.230: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.231 = import_ref Implicit//default, inst988 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -513,8 +513,8 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.195
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.194
+// CHECK:STDOUT:     .Self = imports.%import_ref.231
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.230
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 14 - 3
toolchain/check/testdata/tuple/access/fail_negative_indexing.carbon

@@ -9,9 +9,9 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/tuple/access/fail_negative_indexing.carbon
 
 var a: (i32, i32) = (12, 6);
-// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:17: error: cannot access member of interface `Core.Negate` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess]
+// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:14: error: tuple element index `-10` is past the end of type `(i32, i32)` [TupleIndexOutOfBounds]
 // CHECK:STDERR: var b: i32 = a.(-10);
-// CHECK:STDERR:                 ^~~
+// CHECK:STDERR:              ^~~~~~~
 var b: i32 = a.(-10);
 
 // CHECK:STDOUT: --- fail_negative_indexing.carbon
@@ -35,13 +35,19 @@ var b: i32 = a.(-10);
 // CHECK:STDOUT:   %int_6.2: %i32 = int_value 6 [template]
 // CHECK:STDOUT:   %tuple: %tuple.type.2 = tuple_value (%int_12.2, %int_6.2) [template]
 // CHECK:STDOUT:   %int_10: Core.IntLiteral = int_value 10 [template]
+// CHECK:STDOUT:   %Op.type.13: type = fn_type @Op.13 [template]
+// CHECK:STDOUT:   %Op.type.14: type = fn_type @Op.14 [template]
+// CHECK:STDOUT:   %Op.14: %Op.type.14 = struct_value () [template]
+// CHECK:STDOUT:   %interface.20: <witness> = interface_witness (%Op.14) [template]
+// CHECK:STDOUT:   %Op.bound: <bound method> = bound_method %int_10, %Op.14 [template]
+// CHECK:STDOUT:   %int_-10: Core.IntLiteral = int_value -10 [template]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
 // CHECK:STDOUT:     .Int = %import_ref.1
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.5
-// CHECK:STDOUT:     .Negate = %import_ref.193
+// CHECK:STDOUT:     .Negate = %import_ref.229
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
@@ -84,6 +90,11 @@ var b: i32 = a.(-10);
 // CHECK:STDOUT:   assign file.%a.var, %.loc11_28
 // CHECK:STDOUT:   %a.ref: ref %tuple.type.2 = name_ref a, file.%a
 // CHECK:STDOUT:   %int_10: Core.IntLiteral = int_value 10 [template = constants.%int_10]
+// CHECK:STDOUT:   %impl.elem0.loc15: %Op.type.13 = interface_witness_access constants.%interface.20, element0 [template = constants.%Op.14]
+// CHECK:STDOUT:   %Op.bound: <bound method> = bound_method %int_10, %impl.elem0.loc15 [template = constants.%Op.bound]
+// CHECK:STDOUT:   %int.snegate: init Core.IntLiteral = call %Op.bound(%int_10) [template = constants.%int_-10]
+// CHECK:STDOUT:   %.loc15_17.1: Core.IntLiteral = value_of_initializer %int.snegate [template = constants.%int_-10]
+// CHECK:STDOUT:   %.loc15_17.2: Core.IntLiteral = converted %int.snegate, %.loc15_17.1 [template = constants.%int_-10]
 // CHECK:STDOUT:   assign file.%b.var, <error>
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/tuple/access/index_not_literal.carbon

@@ -61,7 +61,7 @@ var d: i32 = a.({.index = 1 as i32}.index);
 // CHECK:STDOUT:     .Bool = %import_ref.1
 // CHECK:STDOUT:     .Int = %import_ref.2
 // CHECK:STDOUT:     .ImplicitAs = %import_ref.6
-// CHECK:STDOUT:     .As = %import_ref.194
+// CHECK:STDOUT:     .As = %import_ref.230
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }

+ 15 - 15
toolchain/check/testdata/tuple/import.carbon

@@ -287,13 +287,13 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:   %import_ref.3: %C.type = import_ref Implicit//default, C, loaded [template = constants.%C.generic]
 // CHECK:STDOUT:   %import_ref.4: %F.type = import_ref Implicit//default, F, loaded [template = constants.%F]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .Int = %import_ref.194
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.197
+// CHECK:STDOUT:     .Int = %import_ref.230
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.233
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.195: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.196 = import_ref Implicit//default, inst975 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.231: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.232 = import_ref Implicit//default, inst1023 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -326,8 +326,8 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.196
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.195
+// CHECK:STDOUT:     .Self = imports.%import_ref.232
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.231
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -420,8 +420,8 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.194: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.195 = import_ref Implicit//default, inst975 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.230: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.231 = import_ref Implicit//default, inst1023 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -448,8 +448,8 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.195
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.194
+// CHECK:STDOUT:     .Self = imports.%import_ref.231
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.230
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -506,12 +506,12 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:   %import_ref.3: %C.type = import_ref Implicit//default, C, loaded [template = constants.%C.generic]
 // CHECK:STDOUT:   %import_ref.4: %F.type = import_ref Implicit//default, F, loaded [template = constants.%F]
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [template] {
-// CHECK:STDOUT:     .ImplicitAs = %import_ref.196
+// CHECK:STDOUT:     .ImplicitAs = %import_ref.232
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %import_ref.194: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
-// CHECK:STDOUT:   %import_ref.195 = import_ref Implicit//default, inst975 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.230: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.3]
+// CHECK:STDOUT:   %import_ref.231 = import_ref Implicit//default, inst1023 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -538,8 +538,8 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:
 // CHECK:STDOUT:   class {
 // CHECK:STDOUT:   !members:
-// CHECK:STDOUT:     .Self = imports.%import_ref.195
-// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.194
+// CHECK:STDOUT:     .Self = imports.%import_ref.231
+// CHECK:STDOUT:     complete_type_witness = imports.%import_ref.230
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 2 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -306,7 +306,9 @@ CARBON_DIAGNOSTIC_KIND(CompileTimeDivisionByZero)
 CARBON_DIAGNOSTIC_KIND(CompileTimeIntegerOverflow)
 CARBON_DIAGNOSTIC_KIND(CompileTimeIntegerNegateOverflow)
 CARBON_DIAGNOSTIC_KIND(CompileTimeFloatBitWidth)
+CARBON_DIAGNOSTIC_KIND(CompileTimeShiftNegative)
 CARBON_DIAGNOSTIC_KIND(CompileTimeShiftOutOfRange)
+CARBON_DIAGNOSTIC_KIND(CompileTimeUnsizedShiftOutOfRange)
 CARBON_DIAGNOSTIC_KIND(ContinueOutsideLoop)
 CARBON_DIAGNOSTIC_KIND(CopyOfUncopyableType)
 CARBON_DIAGNOSTIC_KIND(CoreNameNotFound)

+ 8 - 3
toolchain/lower/constant.cpp

@@ -48,6 +48,11 @@ class ConstantContext {
     return nullptr;
   }
 
+  // Gets the value to use for an integer literal.
+  auto GetIntLiteralAsValue() const -> llvm::Constant* {
+    return file_context_->GetIntLiteralAsValue();
+  }
+
   // Gets a callable's function. Returns nullptr for a builtin.
   auto GetFunction(SemIR::FunctionId function_id) -> llvm::Function* {
     return file_context_->GetFunction(function_id);
@@ -187,9 +192,9 @@ static auto EmitAsConstant(ConstantContext& context, SemIR::IntValue inst)
   // represented as an LLVM integer type.
   auto* int_type = llvm::dyn_cast<llvm::IntegerType>(type);
   if (!int_type) {
-    auto* struct_type = llvm::dyn_cast<llvm::StructType>(type);
-    CARBON_CHECK(struct_type && struct_type->getNumElements() == 0);
-    return llvm::ConstantStruct::get(struct_type);
+    auto* int_literal_value = context.GetIntLiteralAsValue();
+    CARBON_CHECK(int_literal_value->getType() == type);
+    return int_literal_value;
   }
 
   auto val = context.sem_ir().ints().Get(inst.int_id);

+ 6 - 0
toolchain/lower/file_context.h

@@ -68,6 +68,12 @@ class FileContext {
     return llvm::ConstantStruct::get(GetTypeType());
   }
 
+  // Returns a lowered value to use for a value of int literal type.
+  auto GetIntLiteralAsValue() -> llvm::Constant* {
+    // TODO: Consider adding a named struct type for integer literals.
+    return llvm::ConstantStruct::get(llvm::StructType::get(llvm_context()));
+  }
+
   // Returns a global value for the given instruction.
   auto GetGlobal(SemIR::InstId inst_id) -> llvm::Value*;
 

+ 5 - 0
toolchain/lower/function_context.h

@@ -89,6 +89,11 @@ class FunctionContext {
     return file_context_->GetTypeAsValue();
   }
 
+  // Returns a lowered value to use for a value of int literal type.
+  auto GetIntLiteralAsValue() -> llvm::Constant* {
+    return file_context_->GetIntLiteralAsValue();
+  }
+
   // Returns the instruction immediately after all the existing static allocas.
   // This is the insert point for future static allocas.
   auto GetInstructionAfterAllocas() const -> llvm::Instruction* {

+ 84 - 17
toolchain/lower/handle_call.cpp

@@ -65,6 +65,79 @@ static auto IsSignedInt(FunctionContext& context, SemIR::InstId int_id)
       context.sem_ir().insts().Get(int_id).type_id());
 }
 
+// Creates a zext or sext instruction depending on the signedness of the
+// operand.
+static auto CreateZExtOrSExt(FunctionContext& context, llvm::Value* value,
+                             llvm::Type* type, bool is_signed,
+                             const llvm::Twine& name = "") -> llvm::Value* {
+  return is_signed ? context.builder().CreateSExt(value, type, name)
+                   : context.builder().CreateZExt(value, type, name);
+}
+
+// Handles a call to a builtin integer bit shift operator.
+static auto HandleIntShift(FunctionContext& context, SemIR::InstId inst_id,
+                           llvm::Instruction::BinaryOps bin_op,
+                           SemIR::InstId lhs_id, SemIR::InstId rhs_id) -> void {
+  llvm::Value* lhs = context.GetValue(lhs_id);
+  llvm::Value* rhs = context.GetValue(rhs_id);
+
+  // Weirdly, LLVM requires the operands of bit shift operators to be of the
+  // same type. We can always use the width of the LHS, because if the RHS
+  // doesn't fit in that then the cast is out of range anyway. Zero-extending is
+  // always fine because it's an error for the RHS to be negative.
+  //
+  // TODO: In a development build we should trap if the RHS is signed and
+  // negative or greater than or equal to the number of bits in the left-hand
+  // type.
+  rhs = context.builder().CreateZExtOrTrunc(rhs, lhs->getType(), "rhs");
+
+  context.SetLocal(inst_id, context.builder().CreateBinOp(bin_op, lhs, rhs));
+}
+
+// Handles a call to a builtin integer comparison operator.
+static auto HandleIntComparison(FunctionContext& context, SemIR::InstId inst_id,
+                                SemIR::BuiltinFunctionKind builtin_kind,
+                                SemIR::InstId lhs_id, SemIR::InstId rhs_id)
+    -> void {
+  llvm::Value* lhs = context.GetValue(lhs_id);
+  llvm::Value* rhs = context.GetValue(rhs_id);
+  const auto* lhs_type = cast<llvm::IntegerType>(lhs->getType());
+  const auto* rhs_type = cast<llvm::IntegerType>(rhs->getType());
+
+  // We perform a signed comparison if either operand is signed.
+  bool lhs_signed = IsSignedInt(context, lhs_id);
+  bool rhs_signed = IsSignedInt(context, rhs_id);
+  bool cmp_signed = lhs_signed || rhs_signed;
+
+  // Compute the width for the comparison. This is the smallest width that
+  // fits both types, after widening them to include a sign bit if
+  // necessary.
+  auto width_for_cmp = [&](const llvm::IntegerType* type, bool is_signed) {
+    unsigned width = type->getBitWidth();
+    if (!is_signed && cmp_signed) {
+      // We're performing a signed comparison but this input is unsigned.
+      // Widen it by at least one bit to provide a sign bit.
+      ++width;
+    }
+    return width;
+  };
+  // TODO: This might be an awkward size, such as 33 or 65 bits, for a
+  // signed/unsigned comparison. Would it be better to round this up to a
+  // "nicer" bit width?
+  unsigned cmp_width = std::max(width_for_cmp(lhs_type, lhs_signed),
+                                width_for_cmp(rhs_type, rhs_signed));
+  auto* cmp_type = llvm::IntegerType::get(context.llvm_context(), cmp_width);
+
+  // Widen the operands as needed.
+  lhs = CreateZExtOrSExt(context, lhs, cmp_type, lhs_signed, "lhs");
+  rhs = CreateZExtOrSExt(context, rhs, cmp_type, rhs_signed, "rhs");
+
+  context.SetLocal(
+      inst_id,
+      context.builder().CreateICmp(
+          GetBuiltinICmpPredicate(builtin_kind, cmp_signed), lhs, rhs));
+}
+
 // Handles a call to a builtin function.
 static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
                               SemIR::BuiltinFunctionKind builtin_kind,
@@ -245,19 +318,15 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
       return;
     }
     case SemIR::BuiltinFunctionKind::IntLeftShift: {
-      context.SetLocal(
-          inst_id, context.builder().CreateShl(context.GetValue(arg_ids[0]),
-                                               context.GetValue(arg_ids[1])));
+      HandleIntShift(context, inst_id, llvm::Instruction::Shl, arg_ids[0],
+                     arg_ids[1]);
       return;
     }
     case SemIR::BuiltinFunctionKind::IntRightShift: {
-      context.SetLocal(
-          inst_id,
-          IsSignedInt(context, inst_id)
-              ? context.builder().CreateAShr(context.GetValue(arg_ids[0]),
-                                             context.GetValue(arg_ids[1]))
-              : context.builder().CreateLShr(context.GetValue(arg_ids[0]),
-                                             context.GetValue(arg_ids[1])));
+      HandleIntShift(context, inst_id,
+                     IsSignedInt(context, inst_id) ? llvm::Instruction::AShr
+                                                   : llvm::Instruction::LShr,
+                     arg_ids[0], arg_ids[1]);
       return;
     }
     case SemIR::BuiltinFunctionKind::IntEq:
@@ -268,12 +337,8 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
     case SemIR::BuiltinFunctionKind::IntGreaterEq:
     case SemIR::BuiltinFunctionKind::BoolEq:
     case SemIR::BuiltinFunctionKind::BoolNeq: {
-      context.SetLocal(
-          inst_id,
-          context.builder().CreateICmp(
-              GetBuiltinICmpPredicate(builtin_kind,
-                                      IsSignedInt(context, arg_ids[0])),
-              context.GetValue(arg_ids[0]), context.GetValue(arg_ids[1])));
+      HandleIntComparison(context, inst_id, builtin_kind, arg_ids[0],
+                          arg_ids[1]);
       return;
     }
     case SemIR::BuiltinFunctionKind::FloatNegate: {
@@ -320,7 +385,9 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
 
     case SemIR::BuiltinFunctionKind::IntConvertChecked: {
       // TODO: Check this statically.
-      CARBON_CHECK(builtin_kind.IsCompTimeOnly());
+      CARBON_CHECK(builtin_kind.IsCompTimeOnly(
+          context.sem_ir(), arg_ids,
+          context.sem_ir().insts().Get(inst_id).type_id()));
       CARBON_FATAL("Missing constant value for call to comptime-only function");
     }
   }

+ 382 - 78
toolchain/lower/testdata/builtins/int.carbon

@@ -8,6 +8,10 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/builtins/int.carbon
 
+// --- basic.carbon
+
+library "[[@TEST_NAME]]";
+
 fn Negate(a: i32) -> i32 = "int.snegate";
 fn TestNegate(a: i32) -> i32 { return Negate(a); }
 
@@ -41,8 +45,11 @@ fn TestXor(a: i32, b: i32) -> i32 { return Xor(a, b); }
 fn LeftShift(a: i32, b: i32) -> i32 = "int.left_shift";
 fn TestLeftShift(a: i32, b: i32) -> i32 { return LeftShift(a, b); }
 
-fn RightShift(a: i32, b: i32) -> i32 = "int.right_shift";
-fn TestRightShift(a: i32, b: i32) -> i32 { return RightShift(a, b); }
+fn ArithmeticRightShift(a: i32, b: i32) -> i32 = "int.right_shift";
+fn TestArithmeticRightShift(a: i32, b: i32) -> i32 { return ArithmeticRightShift(a, b); }
+
+fn LogicalRightShift(a: u32, b: u32) -> u32 = "int.right_shift";
+fn TestLogicalRightShift(a: u32, b: u32) -> u32 { return LogicalRightShift(a, b); }
 
 fn Eq(a: i32, b: i32) -> bool = "int.eq";
 fn TestEq(a: i32, b: i32) -> bool { return Eq(a, b); }
@@ -62,8 +69,68 @@ fn TestGreater(a: i32, b: i32) -> bool { return Greater(a, b); }
 fn GreaterEq(a: i32, b: i32) -> bool = "int.greater_eq";
 fn TestGreaterEq(a: i32, b: i32) -> bool { return GreaterEq(a, b); }
 
-// CHECK:STDOUT: ; ModuleID = 'int.carbon'
-// CHECK:STDOUT: source_filename = "int.carbon"
+// --- mixed_shift.carbon
+
+library "[[@TEST_NAME]]";
+
+fn LeftShiftSmaller(a: i32, b: i16) -> i32 = "int.left_shift";
+fn TestLeftShiftSmaller(a: i32, b: i16) -> i32 { return LeftShiftSmaller(a, b); }
+
+fn RightShiftSmaller(a: i32, b: i16) -> i32 = "int.right_shift";
+fn TestRightShiftSmaller(a: i32, b: i16) -> i32 { return RightShiftSmaller(a, b); }
+
+fn LeftShiftLargerII(a: i16, b: i32) -> i16 = "int.left_shift";
+fn TestLeftShiftLargerII(a: i16, b: i32) -> i16 { return LeftShiftLargerII(a, b); }
+
+fn RightShiftLargerII(a: i16, b: i32) -> i16 = "int.right_shift";
+fn TestRightShiftLargerII(a: i16, b: i32) -> i16 { return RightShiftLargerII(a, b); }
+
+fn LeftShiftLargerIU(a: i16, b: u32) -> i16 = "int.left_shift";
+fn TestLeftShiftLargerIU(a: i16, b: u32) -> i16 { return LeftShiftLargerIU(a, b); }
+
+fn RightShiftLargerIU(a: i16, b: u32) -> i16 = "int.right_shift";
+fn TestRightShiftLargerIU(a: i16, b: u32) -> i16 { return RightShiftLargerIU(a, b); }
+
+fn LeftShiftLargerUI(a: u16, b: i32) -> u16 = "int.left_shift";
+fn TestLeftShiftLargerUI(a: u16, b: i32) -> u16 { return LeftShiftLargerUI(a, b); }
+
+fn RightShiftLargerUI(a: u16, b: i32) -> u16 = "int.right_shift";
+fn TestRightShiftLargerUI(a: u16, b: i32) -> u16 { return RightShiftLargerUI(a, b); }
+
+fn LeftShiftLargerUU(a: u16, b: u32) -> u16 = "int.left_shift";
+fn TestLeftShiftLargerUU(a: u16, b: u32) -> u16 { return LeftShiftLargerUU(a, b); }
+
+fn RightShiftLargerUU(a: u16, b: u32) -> u16 = "int.right_shift";
+fn TestRightShiftLargerUU(a: u16, b: u32) -> u16 { return RightShiftLargerUU(a, b); }
+
+// --- mixed_compare.carbon
+
+fn Eq_u16_u32(a: u16, b: u32) -> bool = "int.eq";
+fn Eq_i16_u32(a: i16, b: u32) -> bool = "int.eq";
+fn Eq_u16_i32(a: u16, b: i32) -> bool = "int.eq";
+fn Eq_i16_i32(a: i16, b: i32) -> bool = "int.eq";
+fn Eq_i32_u32(a: i32, b: u32) -> bool = "int.eq";
+
+fn TestEq_u16_u32(a: u16, b: u32) -> bool { return Eq_u16_u32(a, b); }
+fn TestEq_i16_u32(a: i16, b: u32) -> bool { return Eq_i16_u32(a, b); }
+fn TestEq_u16_i32(a: u16, b: i32) -> bool { return Eq_u16_i32(a, b); }
+fn TestEq_i16_i32(a: i16, b: i32) -> bool { return Eq_i16_i32(a, b); }
+fn TestEq_i32_u32(a: i32, b: u32) -> bool { return Eq_i32_u32(a, b); }
+
+fn Less_u16_u32(a: u16, b: u32) -> bool = "int.less";
+fn Less_i16_u32(a: i16, b: u32) -> bool = "int.less";
+fn Less_u16_i32(a: u16, b: i32) -> bool = "int.less";
+fn Less_i16_i32(a: i16, b: i32) -> bool = "int.less";
+fn Less_i32_u32(a: i32, b: u32) -> bool = "int.less";
+
+fn TestLess_u16_u32(a: u16, b: u32) -> bool { return Less_u16_u32(a, b); }
+fn TestLess_i16_u32(a: i16, b: u32) -> bool { return Less_i16_u32(a, b); }
+fn TestLess_u16_i32(a: u16, b: i32) -> bool { return Less_u16_i32(a, b); }
+fn TestLess_i16_i32(a: i16, b: i32) -> bool { return Less_i16_i32(a, b); }
+fn TestLess_i32_u32(a: i32, b: u32) -> bool { return Less_i32_u32(a, b); }
+
+// CHECK:STDOUT: ; ModuleID = 'basic.carbon'
+// CHECK:STDOUT: source_filename = "basic.carbon"
 // CHECK:STDOUT:
 // CHECK:STDOUT: define i32 @_CTestNegate.Main(i32 %a) !dbg !4 {
 // CHECK:STDOUT: entry:
@@ -131,46 +198,307 @@ fn TestGreaterEq(a: i32, b: i32) -> bool { return GreaterEq(a, b); }
 // CHECK:STDOUT:   ret i32 %int.left_shift, !dbg !38
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i32 @_CTestRightShift.Main(i32 %a, i32 %b) !dbg !39 {
+// CHECK:STDOUT: define i32 @_CTestArithmeticRightShift.Main(i32 %a, i32 %b) !dbg !39 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %int.right_shift = ashr i32 %a, %b, !dbg !40
 // CHECK:STDOUT:   ret i32 %int.right_shift, !dbg !41
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestEq.Main(i32 %a, i32 %b) !dbg !42 {
+// CHECK:STDOUT: define i32 @_CTestLogicalRightShift.Main(i32 %a, i32 %b) !dbg !42 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift = lshr i32 %a, %b, !dbg !43
+// CHECK:STDOUT:   ret i32 %int.right_shift, !dbg !44
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq.Main(i32 %a, i32 %b) !dbg !45 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.eq = icmp eq i32 %a, %b, !dbg !46
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !47
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestNeq.Main(i32 %a, i32 %b) !dbg !48 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.neq = icmp ne i32 %a, %b, !dbg !49
+// CHECK:STDOUT:   ret i1 %int.neq, !dbg !50
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestLess.Main(i32 %a, i32 %b) !dbg !51 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.less = icmp slt i32 %a, %b, !dbg !52
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !53
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestLessEq.Main(i32 %a, i32 %b) !dbg !54 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.less_eq = icmp sle i32 %a, %b, !dbg !55
+// CHECK:STDOUT:   ret i1 %int.less_eq, !dbg !56
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestGreater.Main(i32 %a, i32 %b) !dbg !57 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.greater = icmp sgt i32 %a, %b, !dbg !58
+// CHECK:STDOUT:   ret i1 %int.greater, !dbg !59
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestGreaterEq.Main(i32 %a, i32 %b) !dbg !60 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.greater_eq = icmp sge i32 %a, %b, !dbg !61
+// CHECK:STDOUT:   ret i1 %int.greater_eq, !dbg !62
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "basic.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "TestNegate", linkageName: "_CTestNegate.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{}
+// CHECK:STDOUT: !7 = !DILocation(line: 5, column: 39, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 5, column: 32, scope: !4)
+// CHECK:STDOUT: !9 = distinct !DISubprogram(name: "TestAdd", linkageName: "_CTestAdd.Main", scope: null, file: !3, line: 8, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !10 = !DILocation(line: 8, column: 44, scope: !9)
+// CHECK:STDOUT: !11 = !DILocation(line: 8, column: 37, scope: !9)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestSub", linkageName: "_CTestSub.Main", scope: null, file: !3, line: 11, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 11, column: 44, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 11, column: 37, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "TestMul", linkageName: "_CTestMul.Main", scope: null, file: !3, line: 14, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !16 = !DILocation(line: 14, column: 44, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 14, column: 37, scope: !15)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "TestDiv", linkageName: "_CTestDiv.Main", scope: null, file: !3, line: 17, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !19 = !DILocation(line: 17, column: 44, scope: !18)
+// CHECK:STDOUT: !20 = !DILocation(line: 17, column: 37, scope: !18)
+// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "TestMod", linkageName: "_CTestMod.Main", scope: null, file: !3, line: 20, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !22 = !DILocation(line: 20, column: 44, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 20, column: 37, scope: !21)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "TestComplement", linkageName: "_CTestComplement.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !25 = !DILocation(line: 23, column: 43, scope: !24)
+// CHECK:STDOUT: !26 = !DILocation(line: 23, column: 36, scope: !24)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "TestAnd", linkageName: "_CTestAnd.Main", scope: null, file: !3, line: 26, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !28 = !DILocation(line: 26, column: 44, scope: !27)
+// CHECK:STDOUT: !29 = !DILocation(line: 26, column: 37, scope: !27)
+// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "TestOr", linkageName: "_CTestOr.Main", scope: null, file: !3, line: 29, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !31 = !DILocation(line: 29, column: 43, scope: !30)
+// CHECK:STDOUT: !32 = !DILocation(line: 29, column: 36, scope: !30)
+// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "TestXor", linkageName: "_CTestXor.Main", scope: null, file: !3, line: 32, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !34 = !DILocation(line: 32, column: 44, scope: !33)
+// CHECK:STDOUT: !35 = !DILocation(line: 32, column: 37, scope: !33)
+// CHECK:STDOUT: !36 = distinct !DISubprogram(name: "TestLeftShift", linkageName: "_CTestLeftShift.Main", scope: null, file: !3, line: 35, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !37 = !DILocation(line: 35, column: 50, scope: !36)
+// CHECK:STDOUT: !38 = !DILocation(line: 35, column: 43, scope: !36)
+// CHECK:STDOUT: !39 = distinct !DISubprogram(name: "TestArithmeticRightShift", linkageName: "_CTestArithmeticRightShift.Main", scope: null, file: !3, line: 38, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !40 = !DILocation(line: 38, column: 61, scope: !39)
+// CHECK:STDOUT: !41 = !DILocation(line: 38, column: 54, scope: !39)
+// CHECK:STDOUT: !42 = distinct !DISubprogram(name: "TestLogicalRightShift", linkageName: "_CTestLogicalRightShift.Main", scope: null, file: !3, line: 41, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !43 = !DILocation(line: 41, column: 58, scope: !42)
+// CHECK:STDOUT: !44 = !DILocation(line: 41, column: 51, scope: !42)
+// CHECK:STDOUT: !45 = distinct !DISubprogram(name: "TestEq", linkageName: "_CTestEq.Main", scope: null, file: !3, line: 44, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !46 = !DILocation(line: 44, column: 44, scope: !45)
+// CHECK:STDOUT: !47 = !DILocation(line: 44, column: 37, scope: !45)
+// CHECK:STDOUT: !48 = distinct !DISubprogram(name: "TestNeq", linkageName: "_CTestNeq.Main", scope: null, file: !3, line: 47, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !49 = !DILocation(line: 47, column: 45, scope: !48)
+// CHECK:STDOUT: !50 = !DILocation(line: 47, column: 38, scope: !48)
+// CHECK:STDOUT: !51 = distinct !DISubprogram(name: "TestLess", linkageName: "_CTestLess.Main", scope: null, file: !3, line: 50, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !52 = !DILocation(line: 50, column: 46, scope: !51)
+// CHECK:STDOUT: !53 = !DILocation(line: 50, column: 39, scope: !51)
+// CHECK:STDOUT: !54 = distinct !DISubprogram(name: "TestLessEq", linkageName: "_CTestLessEq.Main", scope: null, file: !3, line: 53, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !55 = !DILocation(line: 53, column: 48, scope: !54)
+// CHECK:STDOUT: !56 = !DILocation(line: 53, column: 41, scope: !54)
+// CHECK:STDOUT: !57 = distinct !DISubprogram(name: "TestGreater", linkageName: "_CTestGreater.Main", scope: null, file: !3, line: 56, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !58 = !DILocation(line: 56, column: 49, scope: !57)
+// CHECK:STDOUT: !59 = !DILocation(line: 56, column: 42, scope: !57)
+// CHECK:STDOUT: !60 = distinct !DISubprogram(name: "TestGreaterEq", linkageName: "_CTestGreaterEq.Main", scope: null, file: !3, line: 59, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !61 = !DILocation(line: 59, column: 51, scope: !60)
+// CHECK:STDOUT: !62 = !DILocation(line: 59, column: 44, scope: !60)
+// CHECK:STDOUT: ; ModuleID = 'mixed_shift.carbon'
+// CHECK:STDOUT: source_filename = "mixed_shift.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestLeftShiftSmaller.Main(i32 %a, i16 %b) !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.left_shift.rhs = zext i16 %b to i32, !dbg !7
+// CHECK:STDOUT:   %int.left_shift = shl i32 %a, %int.left_shift.rhs, !dbg !7
+// CHECK:STDOUT:   ret i32 %int.left_shift, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestRightShiftSmaller.Main(i32 %a, i16 %b) !dbg !9 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift.rhs = zext i16 %b to i32, !dbg !10
+// CHECK:STDOUT:   %int.right_shift = ashr i32 %a, %int.right_shift.rhs, !dbg !10
+// CHECK:STDOUT:   ret i32 %int.right_shift, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestLeftShiftLargerII.Main(i16 %a, i32 %b) !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.left_shift.rhs = trunc i32 %b to i16, !dbg !13
+// CHECK:STDOUT:   %int.left_shift = shl i16 %a, %int.left_shift.rhs, !dbg !13
+// CHECK:STDOUT:   ret i16 %int.left_shift, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestRightShiftLargerII.Main(i16 %a, i32 %b) !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift.rhs = trunc i32 %b to i16, !dbg !16
+// CHECK:STDOUT:   %int.right_shift = ashr i16 %a, %int.right_shift.rhs, !dbg !16
+// CHECK:STDOUT:   ret i16 %int.right_shift, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestLeftShiftLargerIU.Main(i16 %a, i32 %b) !dbg !18 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.left_shift.rhs = trunc i32 %b to i16, !dbg !19
+// CHECK:STDOUT:   %int.left_shift = shl i16 %a, %int.left_shift.rhs, !dbg !19
+// CHECK:STDOUT:   ret i16 %int.left_shift, !dbg !20
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestRightShiftLargerIU.Main(i16 %a, i32 %b) !dbg !21 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift.rhs = trunc i32 %b to i16, !dbg !22
+// CHECK:STDOUT:   %int.right_shift = ashr i16 %a, %int.right_shift.rhs, !dbg !22
+// CHECK:STDOUT:   ret i16 %int.right_shift, !dbg !23
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestLeftShiftLargerUI.Main(i16 %a, i32 %b) !dbg !24 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.left_shift.rhs = trunc i32 %b to i16, !dbg !25
+// CHECK:STDOUT:   %int.left_shift = shl i16 %a, %int.left_shift.rhs, !dbg !25
+// CHECK:STDOUT:   ret i16 %int.left_shift, !dbg !26
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestRightShiftLargerUI.Main(i16 %a, i32 %b) !dbg !27 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift.rhs = trunc i32 %b to i16, !dbg !28
+// CHECK:STDOUT:   %int.right_shift = lshr i16 %a, %int.right_shift.rhs, !dbg !28
+// CHECK:STDOUT:   ret i16 %int.right_shift, !dbg !29
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestLeftShiftLargerUU.Main(i16 %a, i32 %b) !dbg !30 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.left_shift.rhs = trunc i32 %b to i16, !dbg !31
+// CHECK:STDOUT:   %int.left_shift = shl i16 %a, %int.left_shift.rhs, !dbg !31
+// CHECK:STDOUT:   ret i16 %int.left_shift, !dbg !32
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestRightShiftLargerUU.Main(i16 %a, i32 %b) !dbg !33 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.right_shift.rhs = trunc i32 %b to i16, !dbg !34
+// CHECK:STDOUT:   %int.right_shift = lshr i16 %a, %int.right_shift.rhs, !dbg !34
+// CHECK:STDOUT:   ret i16 %int.right_shift, !dbg !35
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "mixed_shift.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "TestLeftShiftSmaller", linkageName: "_CTestLeftShiftSmaller.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{}
+// CHECK:STDOUT: !7 = !DILocation(line: 5, column: 57, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 5, column: 50, scope: !4)
+// CHECK:STDOUT: !9 = distinct !DISubprogram(name: "TestRightShiftSmaller", linkageName: "_CTestRightShiftSmaller.Main", scope: null, file: !3, line: 8, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !10 = !DILocation(line: 8, column: 58, scope: !9)
+// CHECK:STDOUT: !11 = !DILocation(line: 8, column: 51, scope: !9)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestLeftShiftLargerII", linkageName: "_CTestLeftShiftLargerII.Main", scope: null, file: !3, line: 11, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 11, column: 58, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 11, column: 51, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "TestRightShiftLargerII", linkageName: "_CTestRightShiftLargerII.Main", scope: null, file: !3, line: 14, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !16 = !DILocation(line: 14, column: 59, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 14, column: 52, scope: !15)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "TestLeftShiftLargerIU", linkageName: "_CTestLeftShiftLargerIU.Main", scope: null, file: !3, line: 17, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !19 = !DILocation(line: 17, column: 58, scope: !18)
+// CHECK:STDOUT: !20 = !DILocation(line: 17, column: 51, scope: !18)
+// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "TestRightShiftLargerIU", linkageName: "_CTestRightShiftLargerIU.Main", scope: null, file: !3, line: 20, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !22 = !DILocation(line: 20, column: 59, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 20, column: 52, scope: !21)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "TestLeftShiftLargerUI", linkageName: "_CTestLeftShiftLargerUI.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !25 = !DILocation(line: 23, column: 58, scope: !24)
+// CHECK:STDOUT: !26 = !DILocation(line: 23, column: 51, scope: !24)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "TestRightShiftLargerUI", linkageName: "_CTestRightShiftLargerUI.Main", scope: null, file: !3, line: 26, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !28 = !DILocation(line: 26, column: 59, scope: !27)
+// CHECK:STDOUT: !29 = !DILocation(line: 26, column: 52, scope: !27)
+// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "TestLeftShiftLargerUU", linkageName: "_CTestLeftShiftLargerUU.Main", scope: null, file: !3, line: 29, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !31 = !DILocation(line: 29, column: 58, scope: !30)
+// CHECK:STDOUT: !32 = !DILocation(line: 29, column: 51, scope: !30)
+// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "TestRightShiftLargerUU", linkageName: "_CTestRightShiftLargerUU.Main", scope: null, file: !3, line: 32, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !34 = !DILocation(line: 32, column: 59, scope: !33)
+// CHECK:STDOUT: !35 = !DILocation(line: 32, column: 52, scope: !33)
+// CHECK:STDOUT: ; ModuleID = 'mixed_compare.carbon'
+// CHECK:STDOUT: source_filename = "mixed_compare.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq_u16_u32.Main(i16 %a, i32 %b) !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.eq.lhs = zext i16 %a to i32, !dbg !7
+// CHECK:STDOUT:   %int.eq = icmp eq i32 %int.eq.lhs, %b, !dbg !7
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq_i16_u32.Main(i16 %a, i32 %b) !dbg !9 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.eq.lhs = sext i16 %a to i33, !dbg !10
+// CHECK:STDOUT:   %int.eq.rhs = zext i32 %b to i33, !dbg !10
+// CHECK:STDOUT:   %int.eq = icmp eq i33 %int.eq.lhs, %int.eq.rhs, !dbg !10
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq_u16_i32.Main(i16 %a, i32 %b) !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.eq.lhs = zext i16 %a to i32, !dbg !13
+// CHECK:STDOUT:   %int.eq = icmp eq i32 %int.eq.lhs, %b, !dbg !13
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq_i16_i32.Main(i16 %a, i32 %b) !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.eq.lhs = sext i16 %a to i32, !dbg !16
+// CHECK:STDOUT:   %int.eq = icmp eq i32 %int.eq.lhs, %b, !dbg !16
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i1 @_CTestEq_i32_u32.Main(i32 %a, i32 %b) !dbg !18 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.eq = icmp eq i32 %a, %b, !dbg !43
-// CHECK:STDOUT:   ret i1 %int.eq, !dbg !44
+// CHECK:STDOUT:   %int.eq.lhs = sext i32 %a to i33, !dbg !19
+// CHECK:STDOUT:   %int.eq.rhs = zext i32 %b to i33, !dbg !19
+// CHECK:STDOUT:   %int.eq = icmp eq i33 %int.eq.lhs, %int.eq.rhs, !dbg !19
+// CHECK:STDOUT:   ret i1 %int.eq, !dbg !20
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestNeq.Main(i32 %a, i32 %b) !dbg !45 {
+// CHECK:STDOUT: define i1 @_CTestLess_u16_u32.Main(i16 %a, i32 %b) !dbg !21 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.neq = icmp ne i32 %a, %b, !dbg !46
-// CHECK:STDOUT:   ret i1 %int.neq, !dbg !47
+// CHECK:STDOUT:   %int.less.lhs = zext i16 %a to i32, !dbg !22
+// CHECK:STDOUT:   %int.less = icmp ult i32 %int.less.lhs, %b, !dbg !22
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !23
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestLess.Main(i32 %a, i32 %b) !dbg !48 {
+// CHECK:STDOUT: define i1 @_CTestLess_i16_u32.Main(i16 %a, i32 %b) !dbg !24 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.less = icmp slt i32 %a, %b, !dbg !49
-// CHECK:STDOUT:   ret i1 %int.less, !dbg !50
+// CHECK:STDOUT:   %int.less.lhs = sext i16 %a to i33, !dbg !25
+// CHECK:STDOUT:   %int.less.rhs = zext i32 %b to i33, !dbg !25
+// CHECK:STDOUT:   %int.less = icmp slt i33 %int.less.lhs, %int.less.rhs, !dbg !25
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !26
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestLessEq.Main(i32 %a, i32 %b) !dbg !51 {
+// CHECK:STDOUT: define i1 @_CTestLess_u16_i32.Main(i16 %a, i32 %b) !dbg !27 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.less_eq = icmp sle i32 %a, %b, !dbg !52
-// CHECK:STDOUT:   ret i1 %int.less_eq, !dbg !53
+// CHECK:STDOUT:   %int.less.lhs = zext i16 %a to i32, !dbg !28
+// CHECK:STDOUT:   %int.less = icmp slt i32 %int.less.lhs, %b, !dbg !28
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !29
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestGreater.Main(i32 %a, i32 %b) !dbg !54 {
+// CHECK:STDOUT: define i1 @_CTestLess_i16_i32.Main(i16 %a, i32 %b) !dbg !30 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.greater = icmp sgt i32 %a, %b, !dbg !55
-// CHECK:STDOUT:   ret i1 %int.greater, !dbg !56
+// CHECK:STDOUT:   %int.less.lhs = sext i16 %a to i32, !dbg !31
+// CHECK:STDOUT:   %int.less = icmp slt i32 %int.less.lhs, %b, !dbg !31
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !32
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define i1 @_CTestGreaterEq.Main(i32 %a, i32 %b) !dbg !57 {
+// CHECK:STDOUT: define i1 @_CTestLess_i32_u32.Main(i32 %a, i32 %b) !dbg !33 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %int.greater_eq = icmp sge i32 %a, %b, !dbg !58
-// CHECK:STDOUT:   ret i1 %int.greater_eq, !dbg !59
+// CHECK:STDOUT:   %int.less.lhs = sext i32 %a to i33, !dbg !34
+// CHECK:STDOUT:   %int.less.rhs = zext i32 %b to i33, !dbg !34
+// CHECK:STDOUT:   %int.less = icmp slt i33 %int.less.lhs, %int.less.rhs, !dbg !34
+// CHECK:STDOUT:   ret i1 %int.less, !dbg !35
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
@@ -179,60 +507,36 @@ fn TestGreaterEq(a: i32, b: i32) -> bool { return GreaterEq(a, b); }
 // CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
 // CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
 // CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
-// CHECK:STDOUT: !3 = !DIFile(filename: "int.carbon", directory: "")
-// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "TestNegate", linkageName: "_CTestNegate.Main", scope: null, file: !3, line: 12, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !3 = !DIFile(filename: "mixed_compare.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "TestEq_u16_u32", linkageName: "_CTestEq_u16_u32.Main", scope: null, file: !3, line: 8, type: !5, spFlags: DISPFlagDefinition, unit: !2)
 // CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
 // CHECK:STDOUT: !6 = !{}
-// CHECK:STDOUT: !7 = !DILocation(line: 12, column: 39, scope: !4)
-// CHECK:STDOUT: !8 = !DILocation(line: 12, column: 32, scope: !4)
-// CHECK:STDOUT: !9 = distinct !DISubprogram(name: "TestAdd", linkageName: "_CTestAdd.Main", scope: null, file: !3, line: 15, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !10 = !DILocation(line: 15, column: 44, scope: !9)
-// CHECK:STDOUT: !11 = !DILocation(line: 15, column: 37, scope: !9)
-// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestSub", linkageName: "_CTestSub.Main", scope: null, file: !3, line: 18, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !13 = !DILocation(line: 18, column: 44, scope: !12)
-// CHECK:STDOUT: !14 = !DILocation(line: 18, column: 37, scope: !12)
-// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "TestMul", linkageName: "_CTestMul.Main", scope: null, file: !3, line: 21, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !16 = !DILocation(line: 21, column: 44, scope: !15)
-// CHECK:STDOUT: !17 = !DILocation(line: 21, column: 37, scope: !15)
-// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "TestDiv", linkageName: "_CTestDiv.Main", scope: null, file: !3, line: 24, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !19 = !DILocation(line: 24, column: 44, scope: !18)
-// CHECK:STDOUT: !20 = !DILocation(line: 24, column: 37, scope: !18)
-// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "TestMod", linkageName: "_CTestMod.Main", scope: null, file: !3, line: 27, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !22 = !DILocation(line: 27, column: 44, scope: !21)
-// CHECK:STDOUT: !23 = !DILocation(line: 27, column: 37, scope: !21)
-// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "TestComplement", linkageName: "_CTestComplement.Main", scope: null, file: !3, line: 30, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !25 = !DILocation(line: 30, column: 43, scope: !24)
-// CHECK:STDOUT: !26 = !DILocation(line: 30, column: 36, scope: !24)
-// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "TestAnd", linkageName: "_CTestAnd.Main", scope: null, file: !3, line: 33, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !28 = !DILocation(line: 33, column: 44, scope: !27)
-// CHECK:STDOUT: !29 = !DILocation(line: 33, column: 37, scope: !27)
-// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "TestOr", linkageName: "_CTestOr.Main", scope: null, file: !3, line: 36, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !31 = !DILocation(line: 36, column: 43, scope: !30)
-// CHECK:STDOUT: !32 = !DILocation(line: 36, column: 36, scope: !30)
-// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "TestXor", linkageName: "_CTestXor.Main", scope: null, file: !3, line: 39, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !34 = !DILocation(line: 39, column: 44, scope: !33)
-// CHECK:STDOUT: !35 = !DILocation(line: 39, column: 37, scope: !33)
-// CHECK:STDOUT: !36 = distinct !DISubprogram(name: "TestLeftShift", linkageName: "_CTestLeftShift.Main", scope: null, file: !3, line: 42, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !37 = !DILocation(line: 42, column: 50, scope: !36)
-// CHECK:STDOUT: !38 = !DILocation(line: 42, column: 43, scope: !36)
-// CHECK:STDOUT: !39 = distinct !DISubprogram(name: "TestRightShift", linkageName: "_CTestRightShift.Main", scope: null, file: !3, line: 45, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !40 = !DILocation(line: 45, column: 51, scope: !39)
-// CHECK:STDOUT: !41 = !DILocation(line: 45, column: 44, scope: !39)
-// CHECK:STDOUT: !42 = distinct !DISubprogram(name: "TestEq", linkageName: "_CTestEq.Main", scope: null, file: !3, line: 48, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !43 = !DILocation(line: 48, column: 44, scope: !42)
-// CHECK:STDOUT: !44 = !DILocation(line: 48, column: 37, scope: !42)
-// CHECK:STDOUT: !45 = distinct !DISubprogram(name: "TestNeq", linkageName: "_CTestNeq.Main", scope: null, file: !3, line: 51, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !46 = !DILocation(line: 51, column: 45, scope: !45)
-// CHECK:STDOUT: !47 = !DILocation(line: 51, column: 38, scope: !45)
-// CHECK:STDOUT: !48 = distinct !DISubprogram(name: "TestLess", linkageName: "_CTestLess.Main", scope: null, file: !3, line: 54, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !49 = !DILocation(line: 54, column: 46, scope: !48)
-// CHECK:STDOUT: !50 = !DILocation(line: 54, column: 39, scope: !48)
-// CHECK:STDOUT: !51 = distinct !DISubprogram(name: "TestLessEq", linkageName: "_CTestLessEq.Main", scope: null, file: !3, line: 57, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !52 = !DILocation(line: 57, column: 48, scope: !51)
-// CHECK:STDOUT: !53 = !DILocation(line: 57, column: 41, scope: !51)
-// CHECK:STDOUT: !54 = distinct !DISubprogram(name: "TestGreater", linkageName: "_CTestGreater.Main", scope: null, file: !3, line: 60, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !55 = !DILocation(line: 60, column: 49, scope: !54)
-// CHECK:STDOUT: !56 = !DILocation(line: 60, column: 42, scope: !54)
-// CHECK:STDOUT: !57 = distinct !DISubprogram(name: "TestGreaterEq", linkageName: "_CTestGreaterEq.Main", scope: null, file: !3, line: 63, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !58 = !DILocation(line: 63, column: 51, scope: !57)
-// CHECK:STDOUT: !59 = !DILocation(line: 63, column: 44, scope: !57)
+// CHECK:STDOUT: !7 = !DILocation(line: 8, column: 52, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 8, column: 45, scope: !4)
+// CHECK:STDOUT: !9 = distinct !DISubprogram(name: "TestEq_i16_u32", linkageName: "_CTestEq_i16_u32.Main", scope: null, file: !3, line: 9, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !10 = !DILocation(line: 9, column: 52, scope: !9)
+// CHECK:STDOUT: !11 = !DILocation(line: 9, column: 45, scope: !9)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestEq_u16_i32", linkageName: "_CTestEq_u16_i32.Main", scope: null, file: !3, line: 10, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 10, column: 52, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 10, column: 45, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "TestEq_i16_i32", linkageName: "_CTestEq_i16_i32.Main", scope: null, file: !3, line: 11, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !16 = !DILocation(line: 11, column: 52, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 11, column: 45, scope: !15)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "TestEq_i32_u32", linkageName: "_CTestEq_i32_u32.Main", scope: null, file: !3, line: 12, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !19 = !DILocation(line: 12, column: 52, scope: !18)
+// CHECK:STDOUT: !20 = !DILocation(line: 12, column: 45, scope: !18)
+// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "TestLess_u16_u32", linkageName: "_CTestLess_u16_u32.Main", scope: null, file: !3, line: 20, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !22 = !DILocation(line: 20, column: 54, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 20, column: 47, scope: !21)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "TestLess_i16_u32", linkageName: "_CTestLess_i16_u32.Main", scope: null, file: !3, line: 21, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !25 = !DILocation(line: 21, column: 54, scope: !24)
+// CHECK:STDOUT: !26 = !DILocation(line: 21, column: 47, scope: !24)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "TestLess_u16_i32", linkageName: "_CTestLess_u16_i32.Main", scope: null, file: !3, line: 22, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !28 = !DILocation(line: 22, column: 54, scope: !27)
+// CHECK:STDOUT: !29 = !DILocation(line: 22, column: 47, scope: !27)
+// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "TestLess_i16_i32", linkageName: "_CTestLess_i16_i32.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !31 = !DILocation(line: 23, column: 54, scope: !30)
+// CHECK:STDOUT: !32 = !DILocation(line: 23, column: 47, scope: !30)
+// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "TestLess_i32_u32", linkageName: "_CTestLess_i32_u32.Main", scope: null, file: !3, line: 24, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !34 = !DILocation(line: 24, column: 54, scope: !33)
+// CHECK:STDOUT: !35 = !DILocation(line: 24, column: 47, scope: !33)

+ 33 - 0
toolchain/lower/testdata/builtins/int_literal.carbon

@@ -14,6 +14,18 @@ fn Copy(x: Make()) -> Make() {
   return x;
 }
 
+fn MinusOne() -> i32 {
+  return -1;
+}
+
+fn IntMax() -> i32 {
+  return 0x1_0000_0000_0000_0000 / 0x2_0000_0000 - 1;
+}
+
+fn IntMin() -> i32 {
+  return -0x8000_0000;
+}
+
 // CHECK:STDOUT: ; ModuleID = 'int_literal.carbon'
 // CHECK:STDOUT: source_filename = "int_literal.carbon"
 // CHECK:STDOUT:
@@ -22,6 +34,21 @@ fn Copy(x: Make()) -> Make() {
 // CHECK:STDOUT:   ret {} %x, !dbg !7
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CMinusOne.Main() !dbg !8 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 -1, !dbg !9
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CIntMax.Main() !dbg !10 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 2147483647, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CIntMin.Main() !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 -2147483648, !dbg !13
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!2}
 // CHECK:STDOUT:
@@ -33,3 +60,9 @@ fn Copy(x: Make()) -> Make() {
 // CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
 // CHECK:STDOUT: !6 = !{}
 // CHECK:STDOUT: !7 = !DILocation(line: 14, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = distinct !DISubprogram(name: "MinusOne", linkageName: "_CMinusOne.Main", scope: null, file: !3, line: 17, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !9 = !DILocation(line: 18, column: 3, scope: !8)
+// CHECK:STDOUT: !10 = distinct !DISubprogram(name: "IntMax", linkageName: "_CIntMax.Main", scope: null, file: !3, line: 21, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !11 = !DILocation(line: 22, column: 3, scope: !10)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "IntMin", linkageName: "_CIntMin.Main", scope: null, file: !3, line: 25, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 26, column: 3, scope: !12)

+ 1 - 1
toolchain/lower/testdata/builtins/print_read.carbon

@@ -16,7 +16,7 @@ fn ReadChar() -> i32 = "read.char";
 fn Main() {
   Core.Print(1);
 
-  let EOF: i32 = -(1 as i32);
+  let EOF: i32 = -1;
   while (ReadChar() != EOF) {
     // "Hi"
     if (PrintChar(0x48) != EOF) {

+ 114 - 30
toolchain/sem_ir/builtin_function_kind.cpp

@@ -79,15 +79,21 @@ struct NoReturn {
 // Constraint that a type is `bool`.
 using Bool = BuiltinType<BoolType::SingletonInstId>;
 
+// Constraint that requires the type to be a sized integer type.
+struct AnySizedInt {
+  static auto Check(const File& sem_ir, ValidateState& /*state*/,
+                    TypeId type_id) -> bool {
+    return sem_ir.types().Is<IntType>(type_id);
+  }
+};
+
 // Constraint that requires the type to be an integer type.
 struct AnyInt {
   static auto Check(const File& sem_ir, ValidateState& state, TypeId type_id)
       -> bool {
-    if (BuiltinType<IntLiteralType::SingletonInstId>::Check(sem_ir, state,
-                                                            type_id)) {
-      return true;
-    }
-    return sem_ir.types().Is<IntType>(type_id);
+    return AnySizedInt::Check(sem_ir, state, type_id) ||
+           BuiltinType<IntLiteralType::SingletonInstId>::Check(sem_ir, state,
+                                                               type_id);
   }
 };
 
@@ -188,6 +194,10 @@ using IntT = TypeParam<0, AnyInt>;
 // generic type parameter that is constrained to be an integer type.
 using IntU = TypeParam<1, AnyInt>;
 
+// Convenience name used in the builtin type signatures below for a first
+// generic type parameter that is constrained to be a sized integer type.
+using SizedIntT = TypeParam<0, AnySizedInt>;
+
 // Convenience name used in the builtin type signatures below for a first
 // generic type parameter that is constrained to be an float type.
 using FloatT = TypeParam<0, AnyFloat>;
@@ -196,16 +206,16 @@ using FloatT = TypeParam<0, AnyFloat>;
 constexpr BuiltinInfo None = {"", nullptr};
 
 // Prints a single character.
-constexpr BuiltinInfo PrintChar = {"print.char",
-                                   ValidateSignature<auto(AnyInt)->AnyInt>};
+constexpr BuiltinInfo PrintChar = {
+    "print.char", ValidateSignature<auto(AnySizedInt)->AnySizedInt>};
 
 // Prints an integer.
-constexpr BuiltinInfo PrintInt = {"print.int",
-                                  ValidateSignature<auto(AnyInt)->NoReturn>};
+constexpr BuiltinInfo PrintInt = {
+    "print.int", ValidateSignature<auto(AnySizedInt)->NoReturn>};
 
 // Reads a single character from stdin.
 constexpr BuiltinInfo ReadChar = {"read.char",
-                                  ValidateSignature<auto()->AnyInt>};
+                                  ValidateSignature<auto()->AnySizedInt>};
 
 // Returns the `Core.IntLiteral` type.
 constexpr BuiltinInfo IntLiteralMakeType = {"int_literal.make_type",
@@ -257,28 +267,28 @@ constexpr BuiltinInfo IntSMod = {"int.smod",
                                  ValidateSignature<auto(IntT, IntT)->IntT>};
 
 // "int.unegate": unsigned integer negation.
-constexpr BuiltinInfo IntUNegate = {"int.unegate",
-                                    ValidateSignature<auto(IntT)->IntT>};
+constexpr BuiltinInfo IntUNegate = {
+    "int.unegate", ValidateSignature<auto(SizedIntT)->SizedIntT>};
 
 // "int.uadd": unsigned integer addition.
-constexpr BuiltinInfo IntUAdd = {"int.uadd",
-                                 ValidateSignature<auto(IntT, IntT)->IntT>};
+constexpr BuiltinInfo IntUAdd = {
+    "int.uadd", ValidateSignature<auto(SizedIntT, SizedIntT)->SizedIntT>};
 
 // "int.usub": unsigned integer subtraction.
-constexpr BuiltinInfo IntUSub = {"int.usub",
-                                 ValidateSignature<auto(IntT, IntT)->IntT>};
+constexpr BuiltinInfo IntUSub = {
+    "int.usub", ValidateSignature<auto(SizedIntT, SizedIntT)->SizedIntT>};
 
 // "int.umul": unsigned integer multiplication.
-constexpr BuiltinInfo IntUMul = {"int.umul",
-                                 ValidateSignature<auto(IntT, IntT)->IntT>};
+constexpr BuiltinInfo IntUMul = {
+    "int.umul", ValidateSignature<auto(SizedIntT, SizedIntT)->SizedIntT>};
 
 // "int.udiv": unsigned integer division.
-constexpr BuiltinInfo IntUDiv = {"int.udiv",
-                                 ValidateSignature<auto(IntT, IntT)->IntT>};
+constexpr BuiltinInfo IntUDiv = {
+    "int.udiv", ValidateSignature<auto(SizedIntT, SizedIntT)->SizedIntT>};
 
 // "int.mod": integer modulo.
-constexpr BuiltinInfo IntUMod = {"int.umod",
-                                 ValidateSignature<auto(IntT, IntT)->IntT>};
+constexpr BuiltinInfo IntUMod = {
+    "int.umod", ValidateSignature<auto(SizedIntT, SizedIntT)->SizedIntT>};
 
 // "int.complement": integer bitwise complement.
 constexpr BuiltinInfo IntComplement = {"int.complement",
@@ -306,27 +316,27 @@ constexpr BuiltinInfo IntRightShift = {
 
 // "int.eq": integer equality comparison.
 constexpr BuiltinInfo IntEq = {"int.eq",
-                               ValidateSignature<auto(IntT, IntT)->Bool>};
+                               ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "int.neq": integer non-equality comparison.
 constexpr BuiltinInfo IntNeq = {"int.neq",
-                                ValidateSignature<auto(IntT, IntT)->Bool>};
+                                ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "int.less": integer less than comparison.
 constexpr BuiltinInfo IntLess = {"int.less",
-                                 ValidateSignature<auto(IntT, IntT)->Bool>};
+                                 ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "int.less_eq": integer less than or equal comparison.
 constexpr BuiltinInfo IntLessEq = {"int.less_eq",
-                                   ValidateSignature<auto(IntT, IntT)->Bool>};
+                                   ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "int.greater": integer greater than comparison.
 constexpr BuiltinInfo IntGreater = {"int.greater",
-                                    ValidateSignature<auto(IntT, IntT)->Bool>};
+                                    ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "int.greater_eq": integer greater than or equal comparison.
 constexpr BuiltinInfo IntGreaterEq = {
-    "int.greater_eq", ValidateSignature<auto(IntT, IntT)->Bool>};
+    "int.greater_eq", ValidateSignature<auto(IntT, IntU)->Bool>};
 
 // "float.negate": float negation.
 constexpr BuiltinInfo FloatNegate = {"float.negate",
@@ -411,8 +421,82 @@ auto BuiltinFunctionKind::IsValidType(const File& sem_ir,
   return ValidateFns[AsInt()](sem_ir, arg_types, return_type);
 }
 
-auto BuiltinFunctionKind::IsCompTimeOnly() const -> bool {
-  return *this == BuiltinFunctionKind::IntConvertChecked;
+auto BuiltinFunctionKind::IsCompTimeOnly(const File& sem_ir,
+                                         llvm::ArrayRef<InstId> arg_ids,
+                                         TypeId return_type_id) const -> bool {
+  // Some builtin functions are unconditionally compile-time-only, or
+  // unconditionally usable at runtime. However, we need to take extra care for
+  // builtins operating on an arbitrary integer type, because `Core.IntLiteral`
+  // has an empty runtime representation and a value of that type isn't
+  // necessarily a compile-time constant. For example, given:
+  //
+  // var n: Core.IntLiteral() = 123;
+  //
+  // we would be unable to lower a runtime operation such as `(1 as i32) << n`
+  // because the runtime representation of `n` doesn't track its value at all.
+  // So we treat operations involving `Core.IntLiteral` as being
+  // compile-time-only.
+  switch (*this) {
+    case IntConvertChecked:
+      // Checked integer conversions are compile-time only.
+      return true;
+
+    case IntSNegate:
+    case IntComplement:
+    case IntSAdd:
+    case IntSSub:
+    case IntSMul:
+    case IntSDiv:
+    case IntSMod:
+    case IntAnd:
+    case IntOr:
+    case IntXor:
+      // Integer builtins producing an IntLiteral are compile-time only.
+      // TODO: We could allow these at runtime and just produce an empty struct
+      // result. Should we?
+      return sem_ir.types().Is<SemIR::IntLiteralType>(return_type_id);
+
+    case IntLeftShift:
+    case IntRightShift:
+      // Shifts by an integer literal amount are compile-time only. We don't
+      // have a value for the shift amount at runtime in general.
+      // TODO: Decide how shifting a non-literal by a literal amount should
+      // work. We could support these with a builtin in the case where the shift
+      // amount has a compile-time value, or we could perform a conversion in
+      // the prelude.
+      if (sem_ir.types().Is<SemIR::IntLiteralType>(
+              sem_ir.insts().Get(arg_ids[1]).type_id())) {
+        return true;
+      }
+
+      // Integer builtins producing an IntLiteral are compile-time only.
+      // TODO: We could allow these at runtime and just produce an empty struct
+      // result. Should we?
+      return sem_ir.types().Is<SemIR::IntLiteralType>(return_type_id);
+
+    case IntEq:
+    case IntNeq:
+    case IntLess:
+    case IntLessEq:
+    case IntGreater:
+    case IntGreaterEq:
+      // Comparisons involving an integer literal operand are compile-time only.
+      // We don't have a value for an integer literal operand argument at
+      // runtime in general.
+      // TODO: Figure out how mixed literal / non-literal comparisons should
+      // work. We could support these with builtins in the case where the
+      // operand has a compile-time value, or we could perform a conversion in
+      // the prelude.
+      return sem_ir.types().Is<SemIR::IntLiteralType>(
+                 sem_ir.insts().Get(arg_ids[0]).type_id()) ||
+             sem_ir.types().Is<SemIR::IntLiteralType>(
+                 sem_ir.insts().Get(arg_ids[1]).type_id());
+
+    default:
+      // TODO: Should the sized MakeType functions be compile-time only? We
+      // can't produce diagnostics for bad sizes at runtime.
+      return false;
+  }
 }
 
 }  // namespace Carbon::SemIR

+ 2 - 1
toolchain/sem_ir/builtin_function_kind.h

@@ -37,7 +37,8 @@ class BuiltinFunctionKind : public CARBON_ENUM_BASE(BuiltinFunctionKind) {
                    TypeId return_type) const -> bool;
 
   // Returns whether this is a compile-time-only function.
-  auto IsCompTimeOnly() const -> bool;
+  auto IsCompTimeOnly(const File& sem_ir, llvm::ArrayRef<InstId> arg_ids,
+                      TypeId return_type_id) const -> bool;
 };
 
 #define CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(Name) \