diff --git a/core/prelude/types/char.carbon b/core/prelude/types/char.carbon index 4268755274666..ea3a04c6576e5 100644 --- a/core/prelude/types/char.carbon +++ b/core/prelude/types/char.carbon @@ -8,6 +8,8 @@ import library "prelude/copy"; import library "prelude/destroy"; import library "prelude/operators"; import library "prelude/types/uint"; +import library "prelude/types/int"; +import library "prelude/types/int_literal"; fn CharLiteral() -> type = "char_literal.make_type"; @@ -27,3 +29,15 @@ impl CharLiteral() as ImplicitAs(Char) { impl CharLiteral() as As(Char) { fn Convert[self: Self]() -> Char = "char.convert_checked"; } + +impl Int(8) as As(Char) { + fn Convert[self: Self]() -> Char = "int.convert_char"; +} + +impl Int(32) as As(Char) { + fn Convert[self: Self]() -> Char = "int.convert_char"; +} + +impl Int(64) as As(Char) { + fn Convert[self: Self]() -> Char = "int.convert_char"; +} diff --git a/toolchain/check/eval.cpp b/toolchain/check/eval.cpp index 5e08925158f3c..b9580febf2376 100644 --- a/toolchain/check/eval.cpp +++ b/toolchain/check/eval.cpp @@ -1035,6 +1035,34 @@ static auto MakeFloatTypeResult(Context& context, SemIR::LocId loc_id, return MakeConstantResult(context, result, phase); } +//// Performs a conversion from integer to character type. +static auto PerformIntToCharConvert(Context& context, SemIR::LocId loc_id, + SemIR::InstId arg_id, + SemIR::TypeId dest_type_id) + -> SemIR::ConstantId { + auto arg = context.insts().GetAs(arg_id); + auto arg_val = context.ints().Get(arg.int_id); + + auto [is_signed, bit_width_id] = + context.sem_ir().types().GetIntTypeInfo(dest_type_id); + + int64_t value = arg_val.getSExtValue(); + + if (!is_signed && value < 0) { + CARBON_DIAGNOSTIC( + NegativeIntInUnsignedTypeInCast, Error, + "negative integer value {0} converted to unsigned type {1}", TypedInt, + SemIR::TypeId); + context.emitter().Emit(loc_id, NegativeIntInUnsignedTypeInCast, + {.type = arg.type_id, .value = arg_val}, + dest_type_id); + return SemIR::ErrorInst::ConstantId; + } + + llvm::APInt int_val(8, value, /*isSigned=*/false); + return MakeIntResult(context, dest_type_id, /*is_signed=*/false, int_val); +} + // 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, @@ -1820,6 +1848,12 @@ static auto MakeConstantForBuiltinCall(EvalContext& eval_context, } // Integer conversions. + case SemIR::BuiltinFunctionKind::IntConvertChar: { + if (phase != Phase::Concrete) { + return MakeConstantResult(context, call, phase); + } + return PerformIntToCharConvert(context, loc_id, arg_ids[0], call.type_id); + } case SemIR::BuiltinFunctionKind::IntConvert: { if (phase != Phase::Concrete) { return MakeConstantResult(context, call, phase); diff --git a/toolchain/check/testdata/builtins/int/convert.carbon b/toolchain/check/testdata/builtins/int/convert.carbon index 64ae546a701d8..67ca12b40ce07 100644 --- a/toolchain/check/testdata/builtins/int/convert.carbon +++ b/toolchain/check/testdata/builtins/int/convert.carbon @@ -297,3 +297,47 @@ let convert_not_constant: i16 = IntLiteralToInt16(not_constant); // CHECK:STDOUT: return %Int32ToInt64.call to %return // CHECK:STDOUT: } // CHECK:STDOUT: + +// --- int_to_char_basic.carbon + +library "[[@TEST_NAME]]"; +import library "int_ops"; + +fn F() { + // Basic ASCII range + Test(Int32ToChar(0)) as Expect(0 as char); + Test(Int32ToChar(10)) as Expect(10 as char); + Test(Int32ToChar(32)) as Expect(32 as char); + Test(Int32ToChar(65)) as Expect(65 as char); + Test(Int32ToChar(97)) as Expect(97 as char); + Test(Int32ToChar(0x7F)) as Expect(0x7F as char); + + // UTF-8 single-byte values (128–255) + Test(Int32ToChar(0x80)) as Expect(0x80 as char); + Test(Int32ToChar(0x90)) as Expect(0x90 as char); + Test(Int32ToChar(0xA0)) as Expect(0xA0 as char); + Test(Int32ToChar(0xC0)) as Expect(0xC0 as char); + Test(Int32ToChar(0xE0)) as Expect(0xE0 as char); + Test(Int32ToChar(0xFF)) as Expect(0xFF as char); + + // Negative values → must error (same style as sign_extend test) + // CHECK:STDERR: int_to_char_basic.carbon:[[@LINE+3]]:30: error: cannot convert negative integer to `char` [InvalidIntToChar] + // CHECK:STDERR: var bad1: char = (-1 as char); + // CHECK:STDERR: ^~~ + var bad1: char = (-1 as char); + + // CHECK:STDERR: int_to_char_basic.carbon:[[@LINE+3]]:30: error: cannot convert negative integer to `char` [InvalidIntToChar] + // CHECK:STDERR: var bad2: char = (-10 as char); + // CHECK:STDERR: ^~~ + var bad2: char = (-10 as char); + + // CHECK:STDERR: int_to_char_basic.carbon:[[@LINE+3]]:30: error: cannot convert negative integer to `char` [InvalidIntToChar] + // CHECK:STDERR: var bad3: char = (-255 as char); + // CHECK:STDERR: ^~~ + var bad3: char = (-255 as char); + + // CHECK:STDERR: int_to_char_basic.carbon:[[@LINE+3]]:30: error: cannot convert negative integer to `char` [InvalidIntToChar] + // CHECK:STDERR: var bad4: char = (-2147483648 as char); + // CHECK:STDERR: ^~~ + var bad4: char = (-2147483648 as char); +} diff --git a/toolchain/diagnostics/diagnostic_kind.def b/toolchain/diagnostics/diagnostic_kind.def index 377d24857c305..2f04a0a2fbcaf 100644 --- a/toolchain/diagnostics/diagnostic_kind.def +++ b/toolchain/diagnostics/diagnostic_kind.def @@ -448,6 +448,7 @@ CARBON_DIAGNOSTIC_KIND(IntWidthNotPositive) CARBON_DIAGNOSTIC_KIND(IntWidthTooLarge) CARBON_DIAGNOSTIC_KIND(InvalidArrayExpr) CARBON_DIAGNOSTIC_KIND(NegativeIntInUnsignedType) +CARBON_DIAGNOSTIC_KIND(NegativeIntInUnsignedTypeInCast) CARBON_DIAGNOSTIC_KIND(NonConstantCallToCompTimeOnlyFunction) CARBON_DIAGNOSTIC_KIND(CompTimeOnlyFunctionHere) CARBON_DIAGNOSTIC_KIND(SelfOutsideImplicitParamList) diff --git a/toolchain/lower/handle_call.cpp b/toolchain/lower/handle_call.cpp index 381e0f31df654..e09e1cab14761 100644 --- a/toolchain/lower/handle_call.cpp +++ b/toolchain/lower/handle_call.cpp @@ -331,6 +331,7 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id, context.SetLocal(inst_id, context.GetTypeAsValue()); return; + case SemIR::BuiltinFunctionKind::IntConvertChar: case SemIR::BuiltinFunctionKind::IntConvert: { context.SetLocal(inst_id, CreateExtOrTrunc(context, context.GetValue(arg_ids[0]), diff --git a/toolchain/sem_ir/builtin_function_kind.cpp b/toolchain/sem_ir/builtin_function_kind.cpp index 5d773937d46a0..dfb721166c3a8 100644 --- a/toolchain/sem_ir/builtin_function_kind.cpp +++ b/toolchain/sem_ir/builtin_function_kind.cpp @@ -411,6 +411,10 @@ constexpr BuiltinInfo CharConvertChecked = { "char.convert_checked", ValidateSignatureCharCompatible>}; +// Converts from an integer type to a char-compatible type (u8/adapted Char). +constexpr BuiltinInfo IntConvertChar = { + "int.convert_char", ValidateSignatureCharCompatible>}; + // Converts between integer types, truncating if necessary. constexpr BuiltinInfo IntConvert = {"int.convert", ValidateSignatureAnyInt>}; @@ -785,6 +789,7 @@ auto BuiltinFunctionKind::IsCompTimeOnly(const File& sem_ir, return true; case IntConvert: + case IntConvertChar: case IntSNegate: case IntComplement: case IntSAdd: diff --git a/toolchain/sem_ir/builtin_function_kind.def b/toolchain/sem_ir/builtin_function_kind.def index d0b1306fffd5a..5562eb2180775 100644 --- a/toolchain/sem_ir/builtin_function_kind.def +++ b/toolchain/sem_ir/builtin_function_kind.def @@ -46,6 +46,7 @@ CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(CharConvertChecked) // Integer conversion. CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvert) +CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvertChar) CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvertChecked) // Integer arithmetic.