Просмотр исходного кода

Add support for converting between integer types (#4753)

Add a builtin `"int.convert"` supporting unchecked conversions between
different integer types. This performs a truncation, zero-extension, or
sign-extension, depending on the widths of the operands and the
signedness of the source type. Add explicit `As` support to the prelude.
No implicit conversions are supported yet as we don't have a way to
express the constraint that we can only implicitly convert to wider
types.
Richard Smith 1 год назад
Родитель
Сommit
246ec785df

+ 5 - 0
core/prelude/types/int.carbon

@@ -32,6 +32,11 @@ impl forall [N:! IntLiteral()] Int(N) as As(IntLiteral()) {
   fn Convert[self: Self]() -> IntLiteral() = "int.convert_checked";
 }
 
+// TODO: Allow as an implicit conversion if N > M.
+impl forall [M:! IntLiteral(), N:! IntLiteral()] Int(M) as As(Int(N)) {
+  fn Convert[self: Self]() -> Int(N) = "int.convert";
+}
+
 // Comparisons.
 
 impl forall [N:! IntLiteral()] Int(N) as Eq {

+ 16 - 0
core/prelude/types/uint.carbon

@@ -5,6 +5,7 @@
 package Core library "prelude/types/uint";
 
 import library "prelude/types/int_literal";
+import library "prelude/types/int";
 import library "prelude/operators";
 
 private fn MakeUInt(size: IntLiteral()) -> type = "int.make_type_unsigned";
@@ -32,6 +33,21 @@ impl forall [N:! IntLiteral()] UInt(N) as As(IntLiteral()) {
   fn Convert[self: Self]() -> IntLiteral() = "int.convert_checked";
 }
 
+// TODO: Allow as an implicit conversion if N > M.
+impl forall [M:! IntLiteral(), N:! IntLiteral()] UInt(M) as As(UInt(N)) {
+  fn Convert[self: Self]() -> UInt(N) = "int.convert";
+}
+
+// TODO: Allow as an implicit conversion if N > M.
+impl forall [M:! IntLiteral(), N:! IntLiteral()] UInt(M) as As(Int(N)) {
+  fn Convert[self: Self]() -> Int(N) = "int.convert";
+}
+
+// Never implicit.
+impl forall [M:! IntLiteral(), N:! IntLiteral()] Int(M) as As(UInt(N)) {
+  fn Convert[self: Self]() -> UInt(N) = "int.convert";
+}
+
 // Comparisons.
 
 impl forall [N:! IntLiteral()] UInt(N) as Eq {

+ 26 - 0
toolchain/check/eval.cpp

@@ -721,6 +721,26 @@ static auto ValidateFloatType(Context& context, SemIRLoc loc,
   return ValidateFloatBitWidth(context, loc, result.bit_width_id);
 }
 
+// Performs a conversion between integer types, truncating if the value doesn't
+// fit in the destination type.
+static auto PerformIntConvert(Context& context, SemIR::InstId arg_id,
+                              SemIR::TypeId dest_type_id) -> SemIR::ConstantId {
+  auto arg_val =
+      context.ints().Get(context.insts().GetAs<SemIR::IntValue>(arg_id).int_id);
+  auto [dest_is_signed, bit_width_id] =
+      context.sem_ir().types().GetIntTypeInfo(dest_type_id);
+  if (bit_width_id.is_valid()) {
+    // TODO: If the value fits in the destination type, reuse the existing
+    // int_id rather than recomputing it. This is probably the most common case.
+    bool src_is_signed = context.sem_ir().types().IsSignedInt(
+        context.insts().Get(arg_id).type_id());
+    unsigned width = context.ints().Get(bit_width_id).getZExtValue();
+    arg_val =
+        src_is_signed ? arg_val.sextOrTrunc(width) : arg_val.zextOrTrunc(width);
+  }
+  return MakeIntResult(context, dest_type_id, dest_is_signed, arg_val);
+}
+
 // Performs a conversion between integer types, diagnosing if the value doesn't
 // fit in the destination type.
 static auto PerformCheckedIntConvert(Context& context, SemIRLoc loc,
@@ -1284,6 +1304,12 @@ static auto MakeConstantForBuiltinCall(Context& context, SemIRLoc loc,
     }
 
     // Integer conversions.
+    case SemIR::BuiltinFunctionKind::IntConvert: {
+      if (phase == Phase::Symbolic) {
+        return MakeConstantResult(context, call, phase);
+      }
+      return PerformIntConvert(context, arg_ids[0], call.type_id);
+    }
     case SemIR::BuiltinFunctionKind::IntConvertChecked: {
       if (phase == Phase::Symbolic) {
         return MakeConstantResult(context, call, phase);

+ 226 - 0
toolchain/check/testdata/builtins/int/convert.carbon

@@ -0,0 +1,226 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// EXTRA-ARGS: --no-dump-sem-ir
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/builtins/int/convert.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/int/convert.carbon
+
+// --- int_ops.carbon
+
+library "[[@TEST_NAME]]";
+
+// Size preserving
+fn Int32ToInt32(a: i32) -> i32 = "int.convert";
+fn Int32ToUint32(a: i32) -> u32 = "int.convert";
+fn Uint32ToInt32(a: u32) -> i32 = "int.convert";
+fn Uint32ToUint32(a: u32) -> u32 = "int.convert";
+fn IntLiteralToIntLiteral(a: Core.IntLiteral()) -> Core.IntLiteral() = "int.convert";
+
+// Narrowing
+fn Int32ToInt16(a: i32) -> i16 = "int.convert";
+fn Int32ToUint16(a: i32) -> u16 = "int.convert";
+fn Uint32ToInt16(a: u32) -> i16 = "int.convert";
+fn Uint32ToUint16(a: u32) -> u16 = "int.convert";
+fn IntLiteralToInt16(a: Core.IntLiteral()) -> i16 = "int.convert";
+fn IntLiteralToUint16(a: Core.IntLiteral()) -> u16 = "int.convert";
+
+// Widening
+fn Int32ToInt64(a: i32) -> i64 = "int.convert";
+fn Int32ToUint64(a: i32) -> u64 = "int.convert";
+fn Uint32ToInt64(a: u32) -> i64 = "int.convert";
+fn Uint32ToUint64(a: u32) -> u64 = "int.convert";
+fn Int32ToIntLiteral(a: i32) -> Core.IntLiteral() = "int.convert";
+fn Uint32ToIntLiteral(a: u32) -> Core.IntLiteral() = "int.convert";
+
+class Expect[T:! type](N:! T) {}
+fn Test[T:! type](N:! T) -> Expect(N) { return {}; }
+
+// --- fail_self_test.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  // Ensure our testing machinery works.
+  // CHECK:STDERR: fail_self_test.carbon:[[@LINE+7]]:3: error: cannot convert from `Expect(0)` to `Expect(1)` with `as` [ExplicitAsConversionFailure]
+  // CHECK:STDERR:   Test(Int32ToInt32(0)) as Expect(1 as i32);
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR: fail_self_test.carbon:[[@LINE+4]]:3: note: type `Expect(0)` does not implement interface `Core.As(Expect(1))` [MissingImplInMemberAccessNote]
+  // CHECK:STDERR:   Test(Int32ToInt32(0)) as Expect(1 as i32);
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  Test(Int32ToInt32(0)) as Expect(1 as i32);
+}
+
+// --- identity.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  Test(Int32ToInt32(-0x8000_0000)) as Expect(-0x8000_0000 as i32);
+  Test(Int32ToInt32(-1)) as Expect(-1 as i32);
+  Test(Int32ToInt32(0)) as Expect(0 as i32);
+  Test(Int32ToInt32(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as i32);
+
+  Test(Uint32ToUint32(0)) as Expect(0 as u32);
+  Test(Uint32ToUint32(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as u32);
+  Test(Uint32ToUint32(0x8000_0000)) as Expect(0x8000_0000 as u32);
+  Test(Uint32ToUint32(0xFFFF_FFFF)) as Expect(0xFFFF_FFFF as u32);
+
+  Test(IntLiteralToIntLiteral(0x1_0000_0000_0000_0000)) as
+      Expect(0x1_0000_0000_0000_0000);
+  Test(IntLiteralToIntLiteral(-1)) as Expect(-1);
+}
+
+// --- same_size.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  Test(Int32ToUint32(-0x8000_0000)) as Expect(0x8000_0000 as u32);
+  Test(Int32ToUint32(-1)) as Expect(0xFFFF_FFFF as u32);
+  Test(Int32ToUint32(0)) as Expect(0 as u32);
+  Test(Int32ToUint32(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as u32);
+
+  Test(Uint32ToInt32(0)) as Expect(0 as i32);
+  Test(Uint32ToInt32(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as i32);
+  Test(Uint32ToInt32(0x8000_0000)) as Expect(-0x8000_0000 as i32);
+  Test(Uint32ToInt32(0xFFFF_FFFF)) as Expect(-1 as i32);
+}
+
+// --- truncate.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  Test(Int32ToInt16(-0x8000_0000)) as Expect(0 as i16);
+  Test(Int32ToInt16(-0x7FFF_EDCC)) as Expect(0x1234 as i16);
+  Test(Int32ToInt16(-0x7FFF_1234)) as Expect(-0x1234 as i16);
+  Test(Int32ToInt16(-0x8000)) as Expect(-0x8000 as i16);
+  Test(Int32ToInt16(-1)) as Expect(-1 as i16);
+  Test(Int32ToInt16(0)) as Expect(0 as i16);
+  Test(Int32ToInt16(0x7FFF)) as Expect(0x7FFF as i16);
+  Test(Int32ToInt16(0xFFFF)) as Expect(-1 as i16);
+  Test(Int32ToInt16(0x7FFF_1234)) as Expect(0x1234 as i16);
+  Test(Int32ToInt16(0x7FFF_EDCC)) as Expect(-0x1234 as i16);
+  Test(Int32ToInt16(0x7FFF_FFFF)) as Expect(-1 as i16);
+
+  Test(Int32ToUint16(-0x8000_0000)) as Expect(0 as u16);
+  Test(Int32ToUint16(-0x7FFF_EDCC)) as Expect(0x1234 as u16);
+  Test(Int32ToUint16(-0x7FFF_1234)) as Expect(0xEDCC as u16);
+  Test(Int32ToUint16(-0x8000)) as Expect(0x8000 as u16);
+  Test(Int32ToUint16(-1)) as Expect(0xFFFF as u16);
+  Test(Int32ToUint16(0)) as Expect(0 as u16);
+  Test(Int32ToUint16(0x7FFF)) as Expect(0x7FFF as u16);
+  Test(Int32ToUint16(0xFFFF)) as Expect(0xFFFF as u16);
+  Test(Int32ToUint16(0x7FFF_1234)) as Expect(0x1234 as u16);
+  Test(Int32ToUint16(0x7FFF_EDCC)) as Expect(0xEDCC as u16);
+  Test(Int32ToUint16(0x7FFF_FFFF)) as Expect(0xFFFF as u16);
+
+  Test(Uint32ToInt16(0x8000_0000)) as Expect(0 as i16);
+  Test(Uint32ToInt16(0xFFFF_1234)) as Expect(0x1234 as i16);
+  Test(Uint32ToInt16(0xFFFF_EDCC)) as Expect(-0x1234 as i16);
+  Test(Uint32ToInt16(0xFFFF_8000)) as Expect(-0x8000 as i16);
+  Test(Uint32ToInt16(0xFFFF_FFFF)) as Expect(-1 as i16);
+  Test(Uint32ToInt16(0)) as Expect(0 as i16);
+  Test(Uint32ToInt16(0x7FFF)) as Expect(0x7FFF as i16);
+  Test(Uint32ToInt16(0xFFFF)) as Expect(-1 as i16);
+  Test(Uint32ToInt16(0x7FFF_1234)) as Expect(0x1234 as i16);
+  Test(Uint32ToInt16(0x7FFF_EDCC)) as Expect(-0x1234 as i16);
+  Test(Uint32ToInt16(0x7FFF_FFFF)) as Expect(-1 as i16);
+
+  Test(Uint32ToUint16(0x8000_0000)) as Expect(0 as u16);
+  Test(Uint32ToUint16(0xFFFF_1234)) as Expect(0x1234 as u16);
+  Test(Uint32ToUint16(0xFFFF_EDCC)) as Expect(0xEDCC as u16);
+  Test(Uint32ToUint16(0xFFFF_8000)) as Expect(0x8000 as u16);
+  Test(Uint32ToUint16(0xFFFF_FFFF)) as Expect(0xFFFF as u16);
+  Test(Uint32ToUint16(0)) as Expect(0 as u16);
+  Test(Uint32ToUint16(0x7FFF)) as Expect(0x7FFF as u16);
+  Test(Uint32ToUint16(0xFFFF)) as Expect(0xFFFF as u16);
+  Test(Uint32ToUint16(0x7FFF_1234)) as Expect(0x1234 as u16);
+  Test(Uint32ToUint16(0x7FFF_EDCC)) as Expect(0xEDCC as u16);
+  Test(Uint32ToUint16(0x7FFF_FFFF)) as Expect(0xFFFF as u16);
+
+  Test(IntLiteralToInt16(0)) as Expect(0 as i16);
+  Test(IntLiteralToInt16(0x7FFF)) as Expect(0x7FFF as i16);
+  Test(IntLiteralToInt16(0x8000)) as Expect(-0x8000 as i16);
+  Test(IntLiteralToInt16(0xFFFF)) as Expect(-1 as i16);
+  Test(IntLiteralToInt16(0x1_2345)) as Expect(0x2345 as i16);
+  Test(IntLiteralToInt16(-1)) as Expect(-1 as i16);
+
+  Test(IntLiteralToUint16(0)) as Expect(0 as u16);
+  Test(IntLiteralToUint16(0x7FFF)) as Expect(0x7FFF as u16);
+  Test(IntLiteralToUint16(0x8000)) as Expect(0x8000 as u16);
+  Test(IntLiteralToUint16(0xFFFF)) as Expect(0xFFFF as u16);
+  Test(IntLiteralToUint16(0x1_2345)) as Expect(0x2345 as u16);
+  Test(IntLiteralToUint16(-1)) as Expect(0xFFFF as u16);
+}
+
+// --- zero_extend.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  Test(Uint32ToInt64(0)) as Expect(0 as i64);
+  Test(Uint32ToInt64(0x1234_5678)) as Expect(0x1234_5678 as i64);
+  Test(Uint32ToInt64(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as i64);
+  Test(Uint32ToInt64(0x8000_0000)) as Expect(0x8000_0000 as i64);
+  Test(Uint32ToInt64(0xFFFF_FFFF)) as Expect(0xFFFF_FFFF as i64);
+
+  Test(Uint32ToUint64(0)) as Expect(0 as u64);
+  Test(Uint32ToUint64(0x1234_5678)) as Expect(0x1234_5678 as u64);
+  Test(Uint32ToUint64(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as u64);
+  Test(Uint32ToUint64(0x8000_0000)) as Expect(0x8000_0000 as u64);
+  Test(Uint32ToUint64(0xFFFF_FFFF)) as Expect(0xFFFF_FFFF as u64);
+
+  Test(Uint32ToIntLiteral(0x1234_5678)) as Expect(0x1234_5678);
+  Test(Uint32ToIntLiteral(0x8765_4321)) as Expect(0x8765_4321);
+  Test(Uint32ToIntLiteral(0xFFFF_FFFF)) as Expect(0xFFFF_FFFF);
+}
+
+// --- sign_extend.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+fn F() {
+  Test(Int32ToInt64(0)) as Expect(0 as i64);
+  Test(Int32ToInt64(0x1234_5678)) as Expect(0x1234_5678 as i64);
+  Test(Int32ToInt64(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as i64);
+  Test(Int32ToInt64(-1)) as Expect(-1 as i64);
+
+  Test(Int32ToUint64(0)) as Expect(0 as u64);
+  Test(Int32ToUint64(0x1234_5678)) as Expect(0x1234_5678 as u64);
+  Test(Int32ToUint64(0x7FFF_FFFF)) as Expect(0x7FFF_FFFF as u64);
+  Test(Int32ToUint64(-1)) as Expect(0xFFFF_FFFF_FFFF_FFFF as u64);
+  Test(Int32ToUint64(-0x8000_0000)) as Expect(0xFFFF_FFFF_8000_0000 as u64);
+
+  Test(Int32ToIntLiteral(0x1234_5678)) as Expect(0x1234_5678);
+  Test(Int32ToIntLiteral(-0x1234_5678)) as Expect(-0x1234_5678);
+  Test(Int32ToIntLiteral(-1)) as Expect(-1);
+}
+
+// --- fail_not_constant.carbon
+
+library "[[@TEST_NAME]]";
+import library "int_ops";
+
+let not_constant: Core.IntLiteral() = 0;
+
+// CHECK:STDERR: fail_not_constant.carbon:[[@LINE+7]]:33: error: non-constant call to compile-time-only function [NonConstantCallToCompTimeOnlyFunction]
+// CHECK:STDERR: let convert_not_constant: i16 = IntLiteralToInt16(not_constant);
+// CHECK:STDERR:                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_not_constant.carbon:[[@LINE-7]]:1: in import [InImport]
+// CHECK:STDERR: int_ops.carbon:16:1: note: compile-time-only function declared here [CompTimeOnlyFunctionHere]
+// CHECK:STDERR: fn IntLiteralToInt16(a: Core.IntLiteral()) -> i16 = "int.convert";
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+let convert_not_constant: i16 = IntLiteralToInt16(not_constant);

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

@@ -319,5 +319,5 @@ fn Run() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F[%self.param_patt: %ForwardDeclared.7b34f2.1]() [from "a.carbon"];
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @G[addr <unexpected>.inst1019: %ptr.6cf]() [from "a.carbon"];
+// CHECK:STDOUT: fn @G[addr <unexpected>.inst1063: %ptr.6cf]() [from "a.carbon"];
 // CHECK:STDOUT:

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

@@ -37,8 +37,8 @@ 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:   %impl_witness.a03: <witness> = impl_witness (imports.%import_ref.c16), @impl.13(%int_32) [template]
-// CHECK:STDOUT:   %Op.type.16e: type = fn_type @Op.2, @impl.13(%int_32) [template]
+// CHECK:STDOUT:   %impl_witness.a03: <witness> = impl_witness (imports.%import_ref.c16), @impl.14(%int_32) [template]
+// CHECK:STDOUT:   %Op.type.16e: type = fn_type @Op.2, @impl.14(%int_32) [template]
 // CHECK:STDOUT:   %Op.ceb: %Op.type.16e = struct_value () [template]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

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

@@ -276,7 +276,7 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1017 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1061 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -396,7 +396,7 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1017 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1061 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -486,7 +486,7 @@ var c_bad: C({.a = 3, .b = 4}) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc8_34, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1017 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.5c7 = import_ref Implicit//default, inst1061 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {

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

@@ -293,7 +293,7 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1052 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1096 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -421,7 +421,7 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1052 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1096 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -511,7 +511,7 @@ var c_bad: C((3, 4)) = F();
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %import_ref.8f2: <witness> = import_ref Implicit//default, loc7_26, loaded [template = constants.%complete_type.357]
-// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1052 [no loc], unloaded
+// CHECK:STDOUT:   %import_ref.2e0 = import_ref Implicit//default, inst1096 [no loc], unloaded
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {

+ 24 - 5
toolchain/lower/handle_call.cpp

@@ -67,13 +67,22 @@ static auto IsSignedInt(FunctionContext& context, SemIR::InstId int_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* {
+static auto CreateExt(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);
 }
 
+// Creates a zext, sext, or trunc instruction depending on the signedness of the
+// operand.
+static auto CreateExtOrTrunc(FunctionContext& context, llvm::Value* value,
+                             llvm::Type* type, bool is_signed,
+                             const llvm::Twine& name = "") -> llvm::Value* {
+  return is_signed ? context.builder().CreateSExtOrTrunc(value, type, name)
+                   : context.builder().CreateZExtOrTrunc(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,
@@ -129,8 +138,8 @@ static auto HandleIntComparison(FunctionContext& context, SemIR::InstId inst_id,
   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");
+  lhs = CreateExt(context, lhs, cmp_type, lhs_signed, "lhs");
+  rhs = CreateExt(context, rhs, cmp_type, rhs_signed, "rhs");
 
   context.SetLocal(
       inst_id,
@@ -204,6 +213,16 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
       context.SetLocal(inst_id, context.GetTypeAsValue());
       return;
 
+    case SemIR::BuiltinFunctionKind::IntConvert: {
+      context.SetLocal(
+          inst_id,
+          CreateExtOrTrunc(
+              context, context.GetValue(arg_ids[0]),
+              context.GetType(context.sem_ir().insts().Get(inst_id).type_id()),
+              IsSignedInt(context, arg_ids[0])));
+      return;
+    }
+
     case SemIR::BuiltinFunctionKind::IntSNegate: {
       // Lower `-x` as `0 - x`.
       auto* operand = context.GetValue(arg_ids[0]);

+ 181 - 30
toolchain/lower/testdata/builtins/int.carbon

@@ -105,6 +105,8 @@ fn TestRightShiftLargerUU(a: u16, b: u32) -> u16 { return RightShiftLargerUU(a,
 
 // --- mixed_compare.carbon
 
+library "[[@TEST_NAME]]";
+
 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";
@@ -129,6 +131,43 @@ 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); }
 
+// --- convert.carbon
+
+library "[[@TEST_NAME]]";
+
+// Size preserving
+fn Int32ToInt32(a: i32) -> i32 = "int.convert";
+fn Int32ToUint32(a: i32) -> u32 = "int.convert";
+fn Uint32ToInt32(a: u32) -> i32 = "int.convert";
+fn Uint32ToUint32(a: u32) -> u32 = "int.convert";
+
+fn TestInt32ToInt32(a: i32) -> i32 { return Int32ToInt32(a); }
+fn TestInt32ToUint32(a: i32) -> u32 { return Int32ToUint32(a); }
+fn TestUint32ToInt32(a: u32) -> i32 { return Uint32ToInt32(a); }
+fn TestUint32ToUint32(a: u32) -> u32 { return Uint32ToUint32(a); }
+
+// Narrowing
+fn Int32ToInt16(a: i32) -> i16 = "int.convert";
+fn Int32ToUint16(a: i32) -> u16 = "int.convert";
+fn Uint32ToInt16(a: u32) -> i16 = "int.convert";
+fn Uint32ToUint16(a: u32) -> u16 = "int.convert";
+
+fn TestInt32ToInt16(a: i32) -> i16 { return Int32ToInt16(a); }
+fn TestInt32ToUint16(a: i32) -> u16 { return Int32ToUint16(a); }
+fn TestUint32ToInt16(a: u32) -> i16 { return Uint32ToInt16(a); }
+fn TestUint32ToUint16(a: u32) -> u16 { return Uint32ToUint16(a); }
+
+// Widening
+fn Int32ToInt64(a: i32) -> i64 = "int.convert";
+fn Int32ToUint64(a: i32) -> u64 = "int.convert";
+fn Uint32ToInt64(a: u32) -> i64 = "int.convert";
+fn Uint32ToUint64(a: u32) -> u64 = "int.convert";
+
+fn TestInt32ToInt64(a: i32) -> i64 { return Int32ToInt64(a); }
+fn TestInt32ToUint64(a: i32) -> u64 { return Int32ToUint64(a); }
+fn TestUint32ToInt64(a: u32) -> i64 { return Uint32ToInt64(a); }
+fn TestUint32ToUint64(a: u32) -> u64 { return Uint32ToUint64(a); }
+
 // CHECK:STDOUT: ; ModuleID = 'basic.carbon'
 // CHECK:STDOUT: source_filename = "basic.carbon"
 // CHECK:STDOUT:
@@ -508,35 +547,147 @@ fn TestLess_i32_u32(a: i32, b: u32) -> bool { return Less_i32_u32(a, b); }
 // 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_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: !4 = distinct !DISubprogram(name: "TestEq_u16_u32", linkageName: "_CTestEq_u16_u32.Main", scope: null, file: !3, line: 10, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{}
+// CHECK:STDOUT: !7 = !DILocation(line: 10, column: 52, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 10, column: 45, scope: !4)
+// CHECK:STDOUT: !9 = distinct !DISubprogram(name: "TestEq_i16_u32", linkageName: "_CTestEq_i16_u32.Main", scope: null, file: !3, line: 11, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !10 = !DILocation(line: 11, column: 52, scope: !9)
+// CHECK:STDOUT: !11 = !DILocation(line: 11, column: 45, scope: !9)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestEq_u16_i32", linkageName: "_CTestEq_u16_i32.Main", scope: null, file: !3, line: 12, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 12, column: 52, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 12, column: 45, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "TestEq_i16_i32", linkageName: "_CTestEq_i16_i32.Main", scope: null, file: !3, line: 13, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !16 = !DILocation(line: 13, column: 52, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 13, column: 45, scope: !15)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "TestEq_i32_u32", linkageName: "_CTestEq_i32_u32.Main", scope: null, file: !3, line: 14, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !19 = !DILocation(line: 14, column: 52, scope: !18)
+// CHECK:STDOUT: !20 = !DILocation(line: 14, column: 45, scope: !18)
+// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "TestLess_u16_u32", linkageName: "_CTestLess_u16_u32.Main", scope: null, file: !3, line: 22, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !22 = !DILocation(line: 22, column: 54, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 22, column: 47, scope: !21)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "TestLess_i16_u32", linkageName: "_CTestLess_i16_u32.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !25 = !DILocation(line: 23, column: 54, scope: !24)
+// CHECK:STDOUT: !26 = !DILocation(line: 23, column: 47, scope: !24)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "TestLess_u16_i32", linkageName: "_CTestLess_u16_i32.Main", scope: null, file: !3, line: 24, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !28 = !DILocation(line: 24, column: 54, scope: !27)
+// CHECK:STDOUT: !29 = !DILocation(line: 24, column: 47, scope: !27)
+// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "TestLess_i16_i32", linkageName: "_CTestLess_i16_i32.Main", scope: null, file: !3, line: 25, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !31 = !DILocation(line: 25, column: 54, scope: !30)
+// CHECK:STDOUT: !32 = !DILocation(line: 25, column: 47, scope: !30)
+// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "TestLess_i32_u32", linkageName: "_CTestLess_i32_u32.Main", scope: null, file: !3, line: 26, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !34 = !DILocation(line: 26, column: 54, scope: !33)
+// CHECK:STDOUT: !35 = !DILocation(line: 26, column: 47, scope: !33)
+// CHECK:STDOUT: ; ModuleID = 'convert.carbon'
+// CHECK:STDOUT: source_filename = "convert.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestInt32ToInt32.Main(i32 %a) !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 %a, !dbg !7
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestInt32ToUint32.Main(i32 %a) !dbg !8 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 %a, !dbg !9
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestUint32ToInt32.Main(i32 %a) !dbg !10 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 %a, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CTestUint32ToUint32.Main(i32 %a) !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret i32 %a, !dbg !13
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestInt32ToInt16.Main(i32 %a) !dbg !14 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = trunc i32 %a to i16, !dbg !15
+// CHECK:STDOUT:   ret i16 %int.convert, !dbg !16
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestInt32ToUint16.Main(i32 %a) !dbg !17 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = trunc i32 %a to i16, !dbg !18
+// CHECK:STDOUT:   ret i16 %int.convert, !dbg !19
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestUint32ToInt16.Main(i32 %a) !dbg !20 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = trunc i32 %a to i16, !dbg !21
+// CHECK:STDOUT:   ret i16 %int.convert, !dbg !22
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CTestUint32ToUint16.Main(i32 %a) !dbg !23 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = trunc i32 %a to i16, !dbg !24
+// CHECK:STDOUT:   ret i16 %int.convert, !dbg !25
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CTestInt32ToInt64.Main(i32 %a) !dbg !26 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = sext i32 %a to i64, !dbg !27
+// CHECK:STDOUT:   ret i64 %int.convert, !dbg !28
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CTestInt32ToUint64.Main(i32 %a) !dbg !29 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = sext i32 %a to i64, !dbg !30
+// CHECK:STDOUT:   ret i64 %int.convert, !dbg !31
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CTestUint32ToInt64.Main(i32 %a) !dbg !32 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = zext i32 %a to i64, !dbg !33
+// CHECK:STDOUT:   ret i64 %int.convert, !dbg !34
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CTestUint32ToUint64.Main(i32 %a) !dbg !35 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %int.convert = zext i32 %a to i64, !dbg !36
+// CHECK:STDOUT:   ret i64 %int.convert, !dbg !37
+// 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: "convert.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "TestInt32ToInt32", linkageName: "_CTestInt32ToInt32.Main", scope: null, file: !3, line: 10, type: !5, spFlags: DISPFlagDefinition, unit: !2)
 // CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
 // CHECK:STDOUT: !6 = !{}
-// 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)
+// CHECK:STDOUT: !7 = !DILocation(line: 10, column: 38, scope: !4)
+// CHECK:STDOUT: !8 = distinct !DISubprogram(name: "TestInt32ToUint32", linkageName: "_CTestInt32ToUint32.Main", scope: null, file: !3, line: 11, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !9 = !DILocation(line: 11, column: 39, scope: !8)
+// CHECK:STDOUT: !10 = distinct !DISubprogram(name: "TestUint32ToInt32", linkageName: "_CTestUint32ToInt32.Main", scope: null, file: !3, line: 12, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !11 = !DILocation(line: 12, column: 39, scope: !10)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "TestUint32ToUint32", linkageName: "_CTestUint32ToUint32.Main", scope: null, file: !3, line: 13, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 13, column: 40, scope: !12)
+// CHECK:STDOUT: !14 = distinct !DISubprogram(name: "TestInt32ToInt16", linkageName: "_CTestInt32ToInt16.Main", scope: null, file: !3, line: 21, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !15 = !DILocation(line: 21, column: 45, scope: !14)
+// CHECK:STDOUT: !16 = !DILocation(line: 21, column: 38, scope: !14)
+// CHECK:STDOUT: !17 = distinct !DISubprogram(name: "TestInt32ToUint16", linkageName: "_CTestInt32ToUint16.Main", scope: null, file: !3, line: 22, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !18 = !DILocation(line: 22, column: 46, scope: !17)
+// CHECK:STDOUT: !19 = !DILocation(line: 22, column: 39, scope: !17)
+// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "TestUint32ToInt16", linkageName: "_CTestUint32ToInt16.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !21 = !DILocation(line: 23, column: 46, scope: !20)
+// CHECK:STDOUT: !22 = !DILocation(line: 23, column: 39, scope: !20)
+// CHECK:STDOUT: !23 = distinct !DISubprogram(name: "TestUint32ToUint16", linkageName: "_CTestUint32ToUint16.Main", scope: null, file: !3, line: 24, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !24 = !DILocation(line: 24, column: 47, scope: !23)
+// CHECK:STDOUT: !25 = !DILocation(line: 24, column: 40, scope: !23)
+// CHECK:STDOUT: !26 = distinct !DISubprogram(name: "TestInt32ToInt64", linkageName: "_CTestInt32ToInt64.Main", scope: null, file: !3, line: 32, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !27 = !DILocation(line: 32, column: 45, scope: !26)
+// CHECK:STDOUT: !28 = !DILocation(line: 32, column: 38, scope: !26)
+// CHECK:STDOUT: !29 = distinct !DISubprogram(name: "TestInt32ToUint64", linkageName: "_CTestInt32ToUint64.Main", scope: null, file: !3, line: 33, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !30 = !DILocation(line: 33, column: 46, scope: !29)
+// CHECK:STDOUT: !31 = !DILocation(line: 33, column: 39, scope: !29)
+// CHECK:STDOUT: !32 = distinct !DISubprogram(name: "TestUint32ToInt64", linkageName: "_CTestUint32ToInt64.Main", scope: null, file: !3, line: 34, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !33 = !DILocation(line: 34, column: 46, scope: !32)
+// CHECK:STDOUT: !34 = !DILocation(line: 34, column: 39, scope: !32)
+// CHECK:STDOUT: !35 = distinct !DISubprogram(name: "TestUint32ToUint64", linkageName: "_CTestUint32ToUint64.Main", scope: null, file: !3, line: 35, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !36 = !DILocation(line: 35, column: 47, scope: !35)
+// CHECK:STDOUT: !37 = !DILocation(line: 35, column: 40, scope: !35)

+ 8 - 8
toolchain/lower/testdata/function/generic/call.carbon

@@ -44,11 +44,11 @@ fn G() {
 // CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %d.var, ptr align 1 @D.val.loc19_16, i64 0, i1 false), !dbg !9
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(i64 4, ptr %n.var), !dbg !7
 // CHECK:STDOUT:   store i32 0, ptr %n.var, align 4, !dbg !10
-// CHECK:STDOUT:   call void @_CF.Main.118(ptr %c.var), !dbg !11
-// CHECK:STDOUT:   call void @_CF.Main.119(ptr %d.var), !dbg !12
+// CHECK:STDOUT:   call void @_CF.Main.129(ptr %c.var), !dbg !11
+// CHECK:STDOUT:   call void @_CF.Main.130(ptr %d.var), !dbg !12
 // CHECK:STDOUT:   %.loc24 = load i32, ptr %n.var, align 4, !dbg !13
-// CHECK:STDOUT:   call void @_CF.Main.120(i32 %.loc24), !dbg !14
-// CHECK:STDOUT:   call void @_CF.Main.121(%type zeroinitializer), !dbg !15
+// CHECK:STDOUT:   call void @_CF.Main.131(i32 %.loc24), !dbg !14
+// CHECK:STDOUT:   call void @_CF.Main.132(%type zeroinitializer), !dbg !15
 // CHECK:STDOUT:   ret void, !dbg !16
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -58,13 +58,13 @@ fn G() {
 // CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
 // CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg) #1
 // CHECK:STDOUT:
-// CHECK:STDOUT: declare void @_CF.Main.118(ptr)
+// CHECK:STDOUT: declare void @_CF.Main.129(ptr)
 // CHECK:STDOUT:
-// CHECK:STDOUT: declare void @_CF.Main.119(ptr)
+// CHECK:STDOUT: declare void @_CF.Main.130(ptr)
 // CHECK:STDOUT:
-// CHECK:STDOUT: declare void @_CF.Main.120(i32)
+// CHECK:STDOUT: declare void @_CF.Main.131(i32)
 // CHECK:STDOUT:
-// CHECK:STDOUT: declare void @_CF.Main.121(%type)
+// CHECK:STDOUT: declare void @_CF.Main.132(%type)
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; uselistorder directives
 // CHECK:STDOUT: uselistorder ptr @llvm.lifetime.start.p0, { 2, 1, 0 }

+ 2 - 2
toolchain/lower/testdata/function/generic/call_method.carbon

@@ -34,7 +34,7 @@ fn CallF() -> i32 {
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(i64 4, ptr %n.var), !dbg !7
 // CHECK:STDOUT:   store i32 0, ptr %n.var, align 4, !dbg !9
 // CHECK:STDOUT:   %.loc20_14 = load i32, ptr %n.var, align 4, !dbg !10
-// CHECK:STDOUT:   %F.call = call i32 @_CF.C.Main.118(ptr %c.var, i32 %.loc20_14), !dbg !11
+// CHECK:STDOUT:   %F.call = call i32 @_CF.C.Main.129(ptr %c.var, i32 %.loc20_14), !dbg !11
 // CHECK:STDOUT:   ret i32 %F.call, !dbg !12
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -44,7 +44,7 @@ fn CallF() -> i32 {
 // CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
 // CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg) #1
 // CHECK:STDOUT:
-// CHECK:STDOUT: declare i32 @_CF.C.Main.118(ptr, i32)
+// CHECK:STDOUT: declare i32 @_CF.C.Main.129(ptr, i32)
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; uselistorder directives
 // CHECK:STDOUT: uselistorder ptr @llvm.lifetime.start.p0, { 1, 0 }

+ 47 - 44
toolchain/sem_ir/builtin_function_kind.cpp

@@ -238,6 +238,10 @@ constexpr BuiltinInfo FloatMakeType = {"float.make_type",
 constexpr BuiltinInfo BoolMakeType = {"bool.make_type",
                                       ValidateSignature<auto()->Type>};
 
+// Converts between integer types, truncating if necessary.
+constexpr BuiltinInfo IntConvert = {"int.convert",
+                                    ValidateSignature<auto(AnyInt)->AnyInt>};
+
 // Converts between integer types, with a diagnostic if the value doesn't fit.
 constexpr BuiltinInfo IntConvertChecked = {
     "int.convert_checked", ValidateSignature<auto(AnyInt)->AnyInt>};
@@ -421,26 +425,54 @@ auto BuiltinFunctionKind::IsValidType(const File& sem_ir,
   return ValidateFns[AsInt()](sem_ir, arg_types, return_type);
 }
 
+// Determines whether a builtin call involves an integer literal in its
+// arguments or return type. If so, for many builtins we want to treat the call
+// as being compile-time-only. This is because `Core.IntLiteral` has an empty
+// runtime representation, and a value of that type isn't necessarily a
+// compile-time constant, so an arbitrary runtime value of type
+// `Core.IntLiteral` may not have a value available for the builtin to use. 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.
+//
+// For now, we treat all operations involving `Core.IntLiteral` as being
+// compile-time-only.
+//
+// TODO: We will need to accept things like `some_i32 << 5` eventually. We could
+// allow builtin calls at runtime if all the IntLiteral arguments have constant
+// values, or add logic to the prelude to promote the `IntLiteral` operand to a
+// different type in such cases.
+//
+// TODO: For now, we also treat builtins *returning* `Core.IntLiteral` as being
+// compile-time-only. This is mostly done for simplicity, but should probably be
+// revisited.
+static auto AnyIntLiteralTypes(const File& sem_ir,
+                               llvm::ArrayRef<InstId> arg_ids,
+                               TypeId return_type_id) -> bool {
+  if (sem_ir.types().Is<SemIR::IntLiteralType>(return_type_id)) {
+    return true;
+  }
+  for (auto arg_id : arg_ids) {
+    if (sem_ir.types().Is<SemIR::IntLiteralType>(
+            sem_ir.insts().Get(arg_id).type_id())) {
+      return true;
+    }
+  }
+  return false;
+}
+
 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 IntConvert:
     case IntSNegate:
     case IntComplement:
     case IntSAdd:
@@ -451,46 +483,17 @@ auto BuiltinFunctionKind::IsCompTimeOnly(const File& sem_ir,
     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());
+      // Integer operations are compile-time-only if they involve integer
+      // literal types. See AnyIntLiteralTypes comment for explanation.
+      return AnyIntLiteralTypes(sem_ir, arg_ids, return_type_id);
 
     default:
       // TODO: Should the sized MakeType functions be compile-time only? We

+ 1 - 0
toolchain/sem_ir/builtin_function_kind.def

@@ -31,6 +31,7 @@ CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(FloatMakeType)
 CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(BoolMakeType)
 
 // Integer conversion.
+CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvert)
 CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvertChecked)
 
 // Integer arithmetic.