[clang] [Clang][POC] __builtin_to_chars / __builtin_from_chars for constexpr integer charconv (PR #204606)
Xavier Roche via cfe-commits
cfe-commits at lists.llvm.org
Thu Jun 18 08:02:28 PDT 2026
https://github.com/xroche updated https://github.com/llvm/llvm-project/pull/204606
>From 9770a9ee4abf4275a27635fa45bdf1d9da21b326 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Thu, 18 Jun 2026 15:50:59 +0200
Subject: [PATCH 1/3] [Clang] POC: __builtin_to_chars / __builtin_from_chars
Proof of concept for constexpr and runtime integer-to-text conversion as a
compiler builtin, including _BitInt wider than 128 bits. Motivated by the
P3666 _BitInt library-support discussion and P3652's constexpr <charconv>: a
builtin folds the conversion in the constant evaluator without spending the
program's -fconstexpr-steps budget, which the library division loop cannot do
for very wide values.
Both builtins are usable in a constant expression (classic evaluator) and at
run time. Sema sets the result to the buffer pointer type; constant evaluation
lives in PointerExprEvaluator using host APInt arithmetic; runtime codegen is
an inline loop with no new runtime-library symbol. The bytecode interpreter
(-fexperimental-new-constant-interpreter) is not implemented.
Local proof of concept, not intended for upstream review.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply at anthropic.com>
---
clang/include/clang/Basic/Builtins.td | 14 +
.../clang/Basic/DiagnosticSemaKinds.td | 10 +
clang/lib/AST/ExprConstant.cpp | 205 ++++++++++++++
clang/lib/CodeGen/CGBuiltin.cpp | 268 ++++++++++++++++++
clang/lib/Sema/SemaChecking.cpp | 138 +++++++++
clang/test/CodeGen/builtin-charconv.c | 21 ++
clang/test/Sema/builtin-charconv.c | 40 +++
.../SemaCXX/builtin-charconv-constexpr.cpp | 115 ++++++++
clang/test/SemaCXX/builtin-charconv-steps.cpp | 25 ++
9 files changed, 836 insertions(+)
create mode 100644 clang/test/CodeGen/builtin-charconv.c
create mode 100644 clang/test/Sema/builtin-charconv.c
create mode 100644 clang/test/SemaCXX/builtin-charconv-constexpr.cpp
create mode 100644 clang/test/SemaCXX/builtin-charconv-steps.cpp
diff --git a/clang/include/clang/Basic/Builtins.td b/clang/include/clang/Basic/Builtins.td
index 053a257ba6d4a..5e527a2400993 100644
--- a/clang/include/clang/Basic/Builtins.td
+++ b/clang/include/clang/Basic/Builtins.td
@@ -4750,6 +4750,20 @@ def MulOverflow : Builtin {
let Prototype = "bool(...)";
}
+// POC: constexpr-capable charconv primitives for integers of any width,
+// including _BitInt(N > 128). Return type (char* / const char*) is set in Sema.
+def ToChars : Builtin {
+ let Spellings = ["__builtin_to_chars"];
+ let Attributes = [NoThrow, CustomTypeChecking, Constexpr];
+ let Prototype = "int(...)";
+}
+
+def FromChars : Builtin {
+ let Spellings = ["__builtin_from_chars"];
+ let Attributes = [NoThrow, CustomTypeChecking, Constexpr];
+ let Prototype = "int(...)";
+}
+
class UOverflowTemplate :
Template<["unsigned int", "unsigned long int", "unsigned long long int"],
["_overflow", "l_overflow", "ll_overflow"]>;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index f84cd8dca6d4c..a4307b6db6ab9 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -9704,6 +9704,16 @@ def err_overflow_builtin_must_be_ptr_int : Error<
def err_overflow_builtin_bit_int_max_size : Error<
"__builtin_mul_overflow does not support 'signed _BitInt' operands of more "
"than %0 bits">;
+def err_charconv_builtin_must_be_char_ptr : Error<
+ "argument %0 to %1 must be a pointer to "
+ "%select{|non-const }2'char' (%3 invalid)">;
+def err_charconv_builtin_must_be_int_ptr : Error<
+ "value argument to %0 must be a pointer to a non-const integer type "
+ "(%1 invalid)">;
+def err_charconv_builtin_must_be_int : Error<
+ "value argument to %0 must be an integer type (%1 invalid)">;
+def err_charconv_builtin_invalid_base : Error<
+ "base argument to %0 must be between 2 and 36 (%1 invalid)">;
def err_expected_struct_pointer_argument : Error<
"expected pointer to struct as %ordinal0 argument to %1, found %2">;
def err_expected_callable_argument : Error<
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 6ac16c2b831d2..0d918b5679156 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -10708,6 +10708,211 @@ bool PointerExprEvaluator::VisitBuiltinCallExpr(const CallExpr *E,
}
}
+ case Builtin::BI__builtin_to_chars: {
+ // (char *first, char *last, T value, int base) -> char *.
+ LValue First, Last;
+ if (!EvaluatePointer(E->getArg(0), First, Info) ||
+ !EvaluatePointer(E->getArg(1), Last, Info))
+ return false;
+ APSInt Value, Base;
+ if (!EvaluateInteger(E->getArg(2), Value, Info) ||
+ !EvaluateInteger(E->getArg(3), Base, Info))
+ return false;
+
+ uint64_t B = Base.getZExtValue();
+ if (B < 2 || B > 36) {
+ Info.FFDiag(E->getArg(3));
+ return false;
+ }
+
+ // Sign and magnitude. Widen by one bit so negating the most-negative
+ // value cannot overflow.
+ bool Neg = Value.isSigned() && Value.isNegative();
+ APInt Mag = Value.isSigned() ? Value.sext(Value.getBitWidth() + 1)
+ : Value.zext(Value.getBitWidth() + 1);
+ if (Neg)
+ Mag.negate();
+
+ static const char Table[] = "0123456789abcdefghijklmnopqrstuvwxyz";
+ SmallVector<char, 64> Rev; // least-significant digit first
+ if (Mag.isZero()) {
+ Rev.push_back('0');
+ } else {
+ APInt BaseAP(Mag.getBitWidth(), B);
+ while (!Mag.isZero()) {
+ APInt Q, R;
+ APInt::udivrem(Mag, BaseAP, Q, R);
+ Rev.push_back(Table[R.getZExtValue()]);
+ Mag = std::move(Q);
+ }
+ }
+ uint64_t Len = Rev.size() + (Neg ? 1 : 0);
+
+ // Capacity check against [first, last). char is one byte.
+ if (!HasSameBase(First, Last)) {
+ Info.FFDiag(E);
+ return false;
+ }
+ int64_t Cap =
+ (Last.getLValueOffset() - First.getLValueOffset()).getQuantity();
+ if (Cap < 0 || (uint64_t)Cap < Len)
+ return ZeroInitialization(E); // buffer too small -> nullptr
+
+ QualType CharTy = E->getArg(0)->getType()->getPointeeType();
+ unsigned CW = Info.Ctx.getCharWidth();
+ bool CharUnsigned = !CharTy->isSignedIntegerType();
+ auto WriteChar = [&](LValue &Dest, char C) -> bool {
+ APValue V{APSInt(APInt(CW, (uint64_t)(unsigned char)C), CharUnsigned)};
+ return handleAssignment(Info, E, Dest, CharTy, V) &&
+ HandleLValueArrayAdjustment(Info, E, Dest, CharTy, 1);
+ };
+
+ LValue Dest = First;
+ if (Neg && !WriteChar(Dest, '-'))
+ return false;
+ for (char C : llvm::reverse(Rev))
+ if (!WriteChar(Dest, C))
+ return false;
+
+ // Return the past-the-end write pointer.
+ Result = First;
+ return HandleLValueArrayAdjustment(Info, E, Result, CharTy, Len);
+ }
+
+ case Builtin::BI__builtin_from_chars: {
+ // (const char *first, const char *last, T *value, int base, int *ec)
+ // -> const char *.
+ LValue First, Last, ValPtr, EcPtr;
+ if (!EvaluatePointer(E->getArg(0), First, Info) ||
+ !EvaluatePointer(E->getArg(1), Last, Info) ||
+ !EvaluatePointer(E->getArg(2), ValPtr, Info) ||
+ !EvaluatePointer(E->getArg(4), EcPtr, Info))
+ return false;
+ APSInt Base;
+ if (!EvaluateInteger(E->getArg(3), Base, Info))
+ return false;
+ uint64_t B = Base.getZExtValue();
+ if (B < 2 || B > 36) {
+ Info.FFDiag(E->getArg(3));
+ return false;
+ }
+
+ if (!HasSameBase(First, Last)) {
+ Info.FFDiag(E);
+ return false;
+ }
+ int64_t Len =
+ (Last.getLValueOffset() - First.getLValueOffset()).getQuantity();
+ if (Len < 0)
+ Len = 0;
+
+ QualType CharTy = E->getArg(0)->getType()->getPointeeType();
+ QualType ValTy = E->getArg(2)->getType()->getPointeeType();
+ QualType EcTy = E->getArg(4)->getType()->getPointeeType();
+ unsigned VW = Info.Ctx.getIntWidth(ValTy);
+ bool VSigned = ValTy->isSignedIntegerType();
+
+ auto DigitVal = [&](char C) -> int {
+ int D;
+ if (C >= '0' && C <= '9')
+ D = C - '0';
+ else if (C >= 'a' && C <= 'z')
+ D = C - 'a' + 10;
+ else if (C >= 'A' && C <= 'Z')
+ D = C - 'A' + 10;
+ else
+ return -1;
+ return (uint64_t)D < B ? D : -1;
+ };
+
+ LValue Cur = First;
+ int64_t Pos = 0;
+ auto ReadChar = [&](char &C) -> bool {
+ APValue V;
+ if (!handleLValueToRValueConversion(Info, E, CharTy, Cur, V) ||
+ !V.isInt())
+ return false;
+ C = (char)V.getInt().getZExtValue();
+ return true;
+ };
+ auto Advance = [&]() -> bool {
+ ++Pos;
+ return HandleLValueArrayAdjustment(Info, E, Cur, CharTy, 1);
+ };
+
+ bool Neg = false;
+ if (Pos < Len) {
+ char C;
+ if (!ReadChar(C))
+ return false;
+ if (C == '-' && VSigned) {
+ Neg = true;
+ if (!Advance())
+ return false;
+ }
+ }
+
+ // Accumulate magnitude in VW+1 bits with overflow detection.
+ unsigned AW = VW + 1;
+ APInt Acc(AW, 0), BaseAP(AW, B);
+ bool AnyDigit = false, Ovf = false;
+ while (Pos < Len) {
+ char C;
+ if (!ReadChar(C))
+ return false;
+ int D = DigitVal(C);
+ if (D < 0)
+ break;
+ AnyDigit = true;
+ bool O1 = false, O2 = false;
+ APInt T = Acc.umul_ov(BaseAP, O1);
+ T = T.uadd_ov(APInt(AW, (uint64_t)D), O2);
+ if (O1 || O2)
+ Ovf = true;
+ else
+ Acc = std::move(T);
+ if (!Advance())
+ return false;
+ }
+
+ auto WriteInt = [&](LValue &Dest, QualType Ty, const APInt &Val,
+ bool Unsigned) -> bool {
+ APValue V{APSInt(Val, Unsigned)};
+ return handleAssignment(Info, E, Dest, Ty, V);
+ };
+ unsigned EW = Info.Ctx.getIntWidth(EcTy);
+ bool EUnsigned = !EcTy->isSignedIntegerType();
+ auto WriteEc = [&](unsigned Code) -> bool {
+ return WriteInt(EcPtr, EcTy, APInt(EW, Code), EUnsigned);
+ };
+
+ if (!AnyDigit) {
+ // invalid_argument: ptr == first, value untouched.
+ if (!WriteEc(1))
+ return false;
+ Result = First;
+ return true;
+ }
+
+ // Range check against the target's representable bounds.
+ APInt MaxMag = VSigned ? (Neg ? APInt::getOneBitSet(AW, VW - 1)
+ : APInt::getOneBitSet(AW, VW - 1) - 1)
+ : APInt::getLowBitsSet(AW, VW);
+ if (Ovf || Acc.ugt(MaxMag)) {
+ // result_out_of_range: ptr past consumed digits, value untouched.
+ if (!WriteEc(2))
+ return false;
+ Result = Cur;
+ return true;
+ }
+
+ APInt Stored = (Neg ? (-Acc) : Acc).trunc(VW);
+ if (!WriteInt(ValPtr, ValTy, Stored, !VSigned) || !WriteEc(0))
+ return false;
+ Result = Cur;
+ return true;
+ }
+
default:
return false;
}
diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp
index 509ab4245d99a..9b2f83f6cbed0 100644
--- a/clang/lib/CodeGen/CGBuiltin.cpp
+++ b/clang/lib/CodeGen/CGBuiltin.cpp
@@ -5184,6 +5184,274 @@ RValue CodeGenFunction::EmitBuiltinExpr(const GlobalDecl GD, unsigned BuiltinID,
addInstToNewSourceAtom(I, nullptr);
return RValue::get(Dest, *this);
}
+ case Builtin::BI__builtin_to_chars: {
+ // (char *first, char *last, T value, int base) -> char *. Inline loop; no
+ // runtime symbol. Precondition base in [2,36] (UB otherwise, like the std).
+ Value *First = EmitScalarExpr(E->getArg(0));
+ Value *Last = EmitScalarExpr(E->getArg(1));
+ Value *Val = EmitScalarExpr(E->getArg(2));
+ Value *Base = EmitScalarExpr(E->getArg(3));
+ bool VSigned = E->getArg(2)->getType()->isSignedIntegerType();
+ unsigned N = cast<llvm::IntegerType>(Val->getType())->getBitWidth();
+ // Calc width must hold the magnitude and the base.
+ auto *CalcTy = llvm::IntegerType::get(getLLVMContext(), std::max(N, 8u));
+
+ Value *Neg = VSigned ? Builder.CreateICmpSLT(
+ Val, llvm::ConstantInt::get(Val->getType(), 0))
+ : Builder.getFalse();
+ // Unsigned negate yields the correct magnitude for the most-negative value.
+ Value *Mag =
+ VSigned ? Builder.CreateSelect(Neg, Builder.CreateNeg(Val), Val) : Val;
+ Mag = Builder.CreateZExtOrTrunc(Mag, CalcTy);
+ Value *BaseW = Builder.CreateIntCast(Base, CalcTy, /*isSigned=*/false);
+
+ Address QAddr = CreateDefaultAlignTempAlloca(CalcTy, "tc.q");
+ Address CntAddr = CreateDefaultAlignTempAlloca(Int64Ty, "tc.cnt");
+ Builder.CreateStore(Mag, QAddr);
+ Builder.CreateStore(llvm::ConstantInt::get(Int64Ty, 0), CntAddr);
+
+ BasicBlock *CountBB = createBasicBlock("tc.count");
+ BasicBlock *CountDone = createBasicBlock("tc.count.done");
+ Builder.CreateBr(CountBB);
+ EmitBlock(CountBB);
+ {
+ Value *Q = Builder.CreateLoad(QAddr);
+ Value *QN = Builder.CreateUDiv(Q, BaseW);
+ Builder.CreateStore(QN, QAddr);
+ Value *Cnt = Builder.CreateLoad(CntAddr);
+ Builder.CreateStore(
+ Builder.CreateAdd(Cnt, llvm::ConstantInt::get(Int64Ty, 1)), CntAddr);
+ Builder.CreateCondBr(
+ Builder.CreateICmpEQ(QN, llvm::ConstantInt::get(CalcTy, 0)),
+ CountDone, CountBB);
+ }
+ EmitBlock(CountDone);
+ Value *Nd = Builder.CreateLoad(CntAddr);
+ Value *NegZ = Builder.CreateZExt(Neg, Int64Ty);
+ Value *Len = Builder.CreateAdd(Nd, NegZ);
+ Value *Cap = Builder.CreatePtrDiff(Int8Ty, Last, First);
+ Cap = Builder.CreateSExtOrTrunc(Cap, Int64Ty);
+ Value *Fits = Builder.CreateICmpULE(Len, Cap);
+
+ BasicBlock *WriteBB = createBasicBlock("tc.write");
+ BasicBlock *WriteLoop = createBasicBlock("tc.write.loop");
+ BasicBlock *WriteDone = createBasicBlock("tc.write.done");
+ BasicBlock *TooSmall = createBasicBlock("tc.toosmall");
+ BasicBlock *Exit = createBasicBlock("tc.exit");
+ Builder.CreateCondBr(Fits, WriteBB, TooSmall);
+
+ EmitBlock(WriteBB);
+ if (VSigned) {
+ BasicBlock *SignBB = createBasicBlock("tc.sign");
+ BasicBlock *NoSign = createBasicBlock("tc.nosign");
+ Builder.CreateCondBr(Neg, SignBB, NoSign);
+ EmitBlock(SignBB);
+ Builder.CreateAlignedStore(Builder.getInt8('-'), First, llvm::Align(1));
+ Builder.CreateBr(NoSign);
+ EmitBlock(NoSign);
+ }
+ Value *WriteBase = Builder.CreateGEP(Int8Ty, First, NegZ);
+ Builder.CreateStore(Mag, QAddr); // reset for the write pass
+ Address PosAddr = CreateDefaultAlignTempAlloca(Int64Ty, "tc.pos");
+ Builder.CreateStore(
+ Builder.CreateSub(Nd, llvm::ConstantInt::get(Int64Ty, 1)), PosAddr);
+ Builder.CreateBr(WriteLoop);
+ EmitBlock(WriteLoop);
+ {
+ Value *Q = Builder.CreateLoad(QAddr);
+ Value *D = Builder.CreateURem(Q, BaseW);
+ Value *QN = Builder.CreateUDiv(Q, BaseW);
+ Builder.CreateStore(QN, QAddr);
+ Value *DT = Builder.CreateZExtOrTrunc(D, Int8Ty);
+ Value *IsDig = Builder.CreateICmpULT(DT, Builder.getInt8(10));
+ Value *Ch = Builder.CreateSelect(
+ IsDig, Builder.CreateAdd(DT, Builder.getInt8('0')),
+ Builder.CreateAdd(DT, Builder.getInt8('a' - 10)));
+ Value *Pos = Builder.CreateLoad(PosAddr);
+ Builder.CreateAlignedStore(Ch, Builder.CreateGEP(Int8Ty, WriteBase, Pos),
+ llvm::Align(1));
+ Builder.CreateStore(
+ Builder.CreateSub(Pos, llvm::ConstantInt::get(Int64Ty, 1)), PosAddr);
+ Builder.CreateCondBr(
+ Builder.CreateICmpEQ(QN, llvm::ConstantInt::get(CalcTy, 0)),
+ WriteDone, WriteLoop);
+ }
+ EmitBlock(WriteDone);
+ Value *RetOk = Builder.CreateGEP(Int8Ty, First, Len);
+ Builder.CreateBr(Exit);
+ EmitBlock(TooSmall);
+ Builder.CreateBr(Exit);
+ EmitBlock(Exit);
+ PHINode *Ret = Builder.CreatePHI(First->getType(), 2);
+ Ret->addIncoming(RetOk, WriteDone);
+ Ret->addIncoming(llvm::Constant::getNullValue(First->getType()), TooSmall);
+ return RValue::get(Ret);
+ }
+ case Builtin::BI__builtin_from_chars: {
+ // (const char *first, const char *last, T *value, int base, int *ec)
+ // -> const char *.
+ Value *First = EmitScalarExpr(E->getArg(0));
+ Value *Last = EmitScalarExpr(E->getArg(1));
+ Value *ValPtr = EmitScalarExpr(E->getArg(2));
+ Value *Base = EmitScalarExpr(E->getArg(3));
+ Value *EcPtr = EmitScalarExpr(E->getArg(4));
+
+ QualType ValPointee = E->getArg(2)->getType()->getPointeeType();
+ QualType EcPointee = E->getArg(4)->getType()->getPointeeType();
+ bool VSigned = ValPointee->isSignedIntegerType();
+ auto *ValTy = cast<llvm::IntegerType>(ConvertType(ValPointee));
+ llvm::Type *EcTy = ConvertType(EcPointee);
+ unsigned N = ValTy->getBitWidth();
+ auto *AccTy = llvm::IntegerType::get(getLLVMContext(), std::max(N + 1, 8u));
+ llvm::Align ValAlign =
+ getContext().getTypeAlignInChars(ValPointee).getAsAlign();
+ llvm::Align EcAlign =
+ getContext().getTypeAlignInChars(EcPointee).getAsAlign();
+ Value *BaseA = Builder.CreateIntCast(Base, AccTy, /*isSigned=*/false);
+
+ Address CurAddr = CreateDefaultAlignTempAlloca(First->getType(), "fc.cur");
+ Address AccAddr = CreateDefaultAlignTempAlloca(AccTy, "fc.acc");
+ Address OvfAddr = CreateDefaultAlignTempAlloca(Int8Ty, "fc.ovf");
+ Address AnyAddr = CreateDefaultAlignTempAlloca(Int8Ty, "fc.any");
+ Address NegAddr = CreateDefaultAlignTempAlloca(Int8Ty, "fc.neg");
+ Builder.CreateStore(First, CurAddr);
+ Builder.CreateStore(llvm::ConstantInt::get(AccTy, 0), AccAddr);
+ Builder.CreateStore(Builder.getInt8(0), OvfAddr);
+ Builder.CreateStore(Builder.getInt8(0), AnyAddr);
+ Builder.CreateStore(Builder.getInt8(0), NegAddr);
+
+ if (VSigned) {
+ // Optional leading '-' (no '+', per [charconv.from.chars]).
+ BasicBlock *SignChk = createBasicBlock("fc.signchk");
+ BasicBlock *SignRead = createBasicBlock("fc.signread");
+ BasicBlock *DoNeg = createBasicBlock("fc.doneg");
+ BasicBlock *AfterSign = createBasicBlock("fc.aftersign");
+ Builder.CreateBr(SignChk);
+ EmitBlock(SignChk);
+ Value *Cur0 = Builder.CreateLoad(CurAddr);
+ Builder.CreateCondBr(Builder.CreateICmpNE(Cur0, Last), SignRead,
+ AfterSign);
+ EmitBlock(SignRead);
+ Value *C0 = Builder.CreateAlignedLoad(Int8Ty, Cur0, llvm::Align(1));
+ Builder.CreateCondBr(Builder.CreateICmpEQ(C0, Builder.getInt8('-')),
+ DoNeg, AfterSign);
+ EmitBlock(DoNeg);
+ Builder.CreateStore(Builder.getInt8(1), NegAddr);
+ Builder.CreateStore(Builder.CreateGEP(Int8Ty, Cur0, Builder.getInt64(1)),
+ CurAddr);
+ Builder.CreateBr(AfterSign);
+ EmitBlock(AfterSign);
+ }
+
+ BasicBlock *Loop = createBasicBlock("fc.loop");
+ BasicBlock *Body = createBasicBlock("fc.body");
+ BasicBlock *Acc = createBasicBlock("fc.acc");
+ BasicBlock *AfterLoop = createBasicBlock("fc.after");
+ Builder.CreateBr(Loop);
+ EmitBlock(Loop);
+ Value *Cur = Builder.CreateLoad(CurAddr);
+ Builder.CreateCondBr(Builder.CreateICmpNE(Cur, Last), Body, AfterLoop);
+
+ EmitBlock(Body);
+ Value *Ch = Builder.CreateAlignedLoad(Int8Ty, Cur, llvm::Align(1));
+ auto InRange = [&](char Lo, char Hi) {
+ return Builder.CreateAnd(Builder.CreateICmpUGE(Ch, Builder.getInt8(Lo)),
+ Builder.CreateICmpULE(Ch, Builder.getInt8(Hi)));
+ };
+ Value *IsDig = InRange('0', '9');
+ Value *IsLow = InRange('a', 'z');
+ Value *IsUp = InRange('A', 'Z');
+ Value *DVal = Builder.CreateSelect(
+ IsDig, Builder.CreateSub(Ch, Builder.getInt8('0')),
+ Builder.CreateSelect(
+ IsLow, Builder.CreateSub(Ch, Builder.getInt8('a' - 10)),
+ Builder.CreateSelect(
+ IsUp, Builder.CreateSub(Ch, Builder.getInt8('A' - 10)),
+ Builder.getInt8(0))));
+ Value *DW = Builder.CreateZExtOrTrunc(DVal, AccTy);
+ Value *IsChar = Builder.CreateOr(Builder.CreateOr(IsDig, IsLow), IsUp);
+ Value *Valid = Builder.CreateAnd(IsChar, Builder.CreateICmpULT(DW, BaseA));
+ Builder.CreateCondBr(Valid, Acc, AfterLoop);
+
+ EmitBlock(Acc);
+ Builder.CreateStore(Builder.getInt8(1), AnyAddr);
+ Value *AccV = Builder.CreateLoad(AccAddr);
+ Value *Mul = Builder.CreateBinaryIntrinsic(
+ llvm::Intrinsic::umul_with_overflow, AccV, BaseA);
+ Value *O1 = Builder.CreateExtractValue(Mul, 1);
+ Value *Sum =
+ Builder.CreateBinaryIntrinsic(llvm::Intrinsic::uadd_with_overflow,
+ Builder.CreateExtractValue(Mul, 0), DW);
+ Value *O2 = Builder.CreateExtractValue(Sum, 1);
+ Builder.CreateStore(Builder.CreateExtractValue(Sum, 0), AccAddr);
+ Value *OvfNow = Builder.CreateOr(O1, O2);
+ Value *OldOvf =
+ Builder.CreateICmpNE(Builder.CreateLoad(OvfAddr), Builder.getInt8(0));
+ Builder.CreateStore(
+ Builder.CreateZExt(Builder.CreateOr(OvfNow, OldOvf), Int8Ty), OvfAddr);
+ Builder.CreateStore(Builder.CreateGEP(Int8Ty, Cur, Builder.getInt64(1)),
+ CurAddr);
+ Builder.CreateBr(Loop);
+
+ EmitBlock(AfterLoop);
+ Value *CurEnd = Builder.CreateLoad(CurAddr);
+ Value *AnyB =
+ Builder.CreateICmpNE(Builder.CreateLoad(AnyAddr), Builder.getInt8(0));
+ Value *NegB =
+ Builder.CreateICmpNE(Builder.CreateLoad(NegAddr), Builder.getInt8(0));
+ Value *OvfB =
+ Builder.CreateICmpNE(Builder.CreateLoad(OvfAddr), Builder.getInt8(0));
+ Value *AccFin = Builder.CreateLoad(AccAddr);
+ // Range bound: signed uses 2^(N-1)-1 (pos) / 2^(N-1) (neg); unsigned 2^N-1.
+ Value *MaxMag;
+ if (VSigned) {
+ Value *MaxPos = llvm::ConstantInt::get(
+ AccTy, llvm::APInt::getOneBitSet(AccTy->getBitWidth(), N - 1) - 1);
+ Value *MaxNeg = llvm::ConstantInt::get(
+ AccTy, llvm::APInt::getOneBitSet(AccTy->getBitWidth(), N - 1));
+ MaxMag = Builder.CreateSelect(NegB, MaxNeg, MaxPos);
+ } else {
+ MaxMag = llvm::ConstantInt::get(
+ AccTy, llvm::APInt::getLowBitsSet(AccTy->getBitWidth(), N));
+ }
+ Value *TooBig =
+ Builder.CreateOr(OvfB, Builder.CreateICmpUGT(AccFin, MaxMag));
+ Value *AccTrunc = Builder.CreateTrunc(AccFin, ValTy);
+ Value *Final =
+ VSigned
+ ? Builder.CreateSelect(NegB, Builder.CreateNeg(AccTrunc), AccTrunc)
+ : AccTrunc;
+
+ BasicBlock *Invalid = createBasicBlock("fc.invalid");
+ BasicBlock *Check = createBasicBlock("fc.check");
+ BasicBlock *Oor = createBasicBlock("fc.oor");
+ BasicBlock *Ok = createBasicBlock("fc.ok");
+ BasicBlock *Done = createBasicBlock("fc.done");
+ Builder.CreateCondBr(AnyB, Check, Invalid);
+
+ EmitBlock(Invalid);
+ Builder.CreateAlignedStore(llvm::ConstantInt::get(EcTy, 1), EcPtr, EcAlign);
+ Builder.CreateBr(Done);
+
+ EmitBlock(Check);
+ Builder.CreateCondBr(TooBig, Oor, Ok);
+
+ EmitBlock(Oor);
+ Builder.CreateAlignedStore(llvm::ConstantInt::get(EcTy, 2), EcPtr, EcAlign);
+ Builder.CreateBr(Done);
+
+ EmitBlock(Ok);
+ Builder.CreateAlignedStore(Final, ValPtr, ValAlign);
+ Builder.CreateAlignedStore(llvm::ConstantInt::get(EcTy, 0), EcPtr, EcAlign);
+ Builder.CreateBr(Done);
+
+ EmitBlock(Done);
+ PHINode *Ret = Builder.CreatePHI(First->getType(), 3);
+ Ret->addIncoming(First, Invalid);
+ Ret->addIncoming(CurEnd, Oor);
+ Ret->addIncoming(CurEnd, Ok);
+ return RValue::get(Ret);
+ }
case Builtin::BI__builtin_wmemchr: {
// The MSVC runtime library does not provide a definition of wmemchr, so we
// need an inline implementation.
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index b8a3f48a32f24..499bd03dea30c 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -487,6 +487,136 @@ static bool BuiltinOverflow(Sema &S, CallExpr *TheCall, unsigned BuiltinID) {
return false;
}
+// Shared checking for __builtin_to_chars / __builtin_from_chars buffer
+// arguments: a pointer to 'char', optionally required to be non-const.
+static bool checkCharconvCharPtr(Sema &S, CallExpr *TheCall, unsigned ArgIdx,
+ unsigned BuiltinID, bool RequireNonConst) {
+ ExprResult Arg =
+ S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(ArgIdx));
+ if (Arg.isInvalid())
+ return true;
+ TheCall->setArg(ArgIdx, Arg.get());
+
+ QualType Ty = Arg.get()->getType();
+ const auto *PtrTy = Ty->getAs<PointerType>();
+ if (!PtrTy || !PtrTy->getPointeeType()->isCharType() ||
+ (RequireNonConst && PtrTy->getPointeeType().isConstQualified())) {
+ S.Diag(Arg.get()->getBeginLoc(),
+ diag::err_charconv_builtin_must_be_char_ptr)
+ << (ArgIdx + 1) << S.Context.BuiltinInfo.getQuotedName(BuiltinID)
+ << RequireNonConst << Ty << Arg.get()->getSourceRange();
+ return true;
+ }
+ return false;
+}
+
+// Shared base-argument checking: an integer, in [2, 36] when constant.
+static bool checkCharconvBase(Sema &S, CallExpr *TheCall, unsigned ArgIdx,
+ unsigned BuiltinID) {
+ ExprResult Arg =
+ S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(ArgIdx));
+ if (Arg.isInvalid())
+ return true;
+ TheCall->setArg(ArgIdx, Arg.get());
+
+ if (!Arg.get()->getType()->isIntegerType()) {
+ S.Diag(Arg.get()->getBeginLoc(), diag::err_charconv_builtin_invalid_base)
+ << S.Context.BuiltinInfo.getQuotedName(BuiltinID)
+ << Arg.get()->getType() << Arg.get()->getSourceRange();
+ return true;
+ }
+ if (std::optional<llvm::APSInt> Base =
+ Arg.get()->getIntegerConstantExpr(S.Context)) {
+ bool Bad = Base->isNegative() || Base->getActiveBits() > 6;
+ if (!Bad) {
+ uint64_t B = Base->getZExtValue();
+ Bad = B < 2 || B > 36;
+ }
+ if (Bad) {
+ S.Diag(Arg.get()->getBeginLoc(), diag::err_charconv_builtin_invalid_base)
+ << S.Context.BuiltinInfo.getQuotedName(BuiltinID)
+ << toString(*Base, 10, Base->isSigned())
+ << Arg.get()->getSourceRange();
+ return true;
+ }
+ }
+ return false;
+}
+
+// __builtin_to_chars(char *first, char *last, T value, int base) -> char *.
+static bool BuiltinToChars(Sema &S, CallExpr *TheCall, unsigned BuiltinID) {
+ if (S.checkArgCount(TheCall, 4))
+ return true;
+ if (checkCharconvCharPtr(S, TheCall, 0, BuiltinID,
+ /*RequireNonConst=*/true) ||
+ checkCharconvCharPtr(S, TheCall, 1, BuiltinID, /*RequireNonConst=*/true))
+ return true;
+
+ ExprResult Value = S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(2));
+ if (Value.isInvalid())
+ return true;
+ TheCall->setArg(2, Value.get());
+ if (!Value.get()->getType()->isIntegerType()) {
+ S.Diag(Value.get()->getBeginLoc(), diag::err_charconv_builtin_must_be_int)
+ << S.Context.BuiltinInfo.getQuotedName(BuiltinID)
+ << Value.get()->getType() << Value.get()->getSourceRange();
+ return true;
+ }
+
+ if (checkCharconvBase(S, TheCall, 3, BuiltinID))
+ return true;
+
+ // Result is the past-the-end write pointer, of the buffer's type.
+ TheCall->setType(TheCall->getArg(0)->getType());
+ return false;
+}
+
+// __builtin_from_chars(const char *first, const char *last, T *value,
+// int base, int *ec) -> const char *.
+static bool BuiltinFromChars(Sema &S, CallExpr *TheCall, unsigned BuiltinID) {
+ if (S.checkArgCount(TheCall, 5))
+ return true;
+ if (checkCharconvCharPtr(S, TheCall, 0, BuiltinID,
+ /*RequireNonConst=*/false) ||
+ checkCharconvCharPtr(S, TheCall, 1, BuiltinID, /*RequireNonConst=*/false))
+ return true;
+
+ ExprResult Value = S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(2));
+ if (Value.isInvalid())
+ return true;
+ TheCall->setArg(2, Value.get());
+ QualType ValTy = Value.get()->getType();
+ const auto *ValPtr = ValTy->getAs<PointerType>();
+ if (!ValPtr || !ValPtr->getPointeeType()->isIntegerType() ||
+ ValPtr->getPointeeType().isConstQualified()) {
+ S.Diag(Value.get()->getBeginLoc(),
+ diag::err_charconv_builtin_must_be_int_ptr)
+ << S.Context.BuiltinInfo.getQuotedName(BuiltinID) << ValTy
+ << Value.get()->getSourceRange();
+ return true;
+ }
+
+ if (checkCharconvBase(S, TheCall, 3, BuiltinID))
+ return true;
+
+ ExprResult Ec = S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(4));
+ if (Ec.isInvalid())
+ return true;
+ TheCall->setArg(4, Ec.get());
+ const auto *EcPtr = Ec.get()->getType()->getAs<PointerType>();
+ if (!EcPtr || !EcPtr->getPointeeType()->isIntegerType() ||
+ EcPtr->getPointeeType().isConstQualified()) {
+ S.Diag(Ec.get()->getBeginLoc(), diag::err_charconv_builtin_must_be_int_ptr)
+ << S.Context.BuiltinInfo.getQuotedName(BuiltinID) << Ec.get()->getType()
+ << Ec.get()->getSourceRange();
+ return true;
+ }
+
+ // Result is the past-the-consumed-input pointer, of the input's type.
+ TheCall->setType(TheCall->getArg(0)->getType());
+ return false;
+}
+
namespace {
struct BuiltinDumpStructGenerator {
Sema &S;
@@ -3384,6 +3514,14 @@ Sema::CheckBuiltinFunctionCall(FunctionDecl *FDecl, unsigned BuiltinID,
if (BuiltinOverflow(*this, TheCall, BuiltinID))
return ExprError();
break;
+ case Builtin::BI__builtin_to_chars:
+ if (BuiltinToChars(*this, TheCall, BuiltinID))
+ return ExprError();
+ break;
+ case Builtin::BI__builtin_from_chars:
+ if (BuiltinFromChars(*this, TheCall, BuiltinID))
+ return ExprError();
+ break;
case Builtin::BI__builtin_operator_new:
case Builtin::BI__builtin_operator_delete: {
bool IsDelete = BuiltinID == Builtin::BI__builtin_operator_delete;
diff --git a/clang/test/CodeGen/builtin-charconv.c b/clang/test/CodeGen/builtin-charconv.c
new file mode 100644
index 0000000000000..09fb0b6bec3ce
--- /dev/null
+++ b/clang/test/CodeGen/builtin-charconv.c
@@ -0,0 +1,21 @@
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -O0 -emit-llvm %s -o - | FileCheck %s
+
+// POC runtime codegen: an inline loop, no external charconv runtime symbol.
+
+int gec;
+
+// CHECK-LABEL: define{{.*}} ptr @do_to_chars
+char *do_to_chars(char *first, char *last, int v, int base) {
+ return __builtin_to_chars(first, last, v, base);
+}
+// CHECK: udiv
+// CHECK: urem
+// CHECK-NOT: call{{.*}}@{{.*}}chars
+
+// CHECK-LABEL: define{{.*}} ptr @do_from_chars
+const char *do_from_chars(const char *first, const char *last, int *out, int base) {
+ return __builtin_from_chars(first, last, out, base, &gec);
+}
+// CHECK: @llvm.umul.with.overflow
+// CHECK: @llvm.uadd.with.overflow
+// CHECK-NOT: call{{.*}}@{{.*}}chars
diff --git a/clang/test/Sema/builtin-charconv.c b/clang/test/Sema/builtin-charconv.c
new file mode 100644
index 0000000000000..3370031d70009
--- /dev/null
+++ b/clang/test/Sema/builtin-charconv.c
@@ -0,0 +1,40 @@
+// RUN: %clang_cc1 -fsyntax-only -verify %s
+
+// POC diagnostics for __builtin_to_chars / __builtin_from_chars.
+
+void to_chars_checks(char *p, const char *cp, int v, int base) {
+ (void)__builtin_to_chars(p, p, v, 10); // ok
+ (void)__builtin_to_chars(p, p, v, base); // ok, runtime base
+
+ __builtin_to_chars(p, p, v); // expected-error {{too few arguments}}
+ __builtin_to_chars(p, p, v, 10, p); // expected-error {{too many arguments}}
+
+ // Buffer must be a pointer to non-const char.
+ (void)__builtin_to_chars(v, p, v, 10); // expected-error {{must be a pointer to non-const 'char'}}
+ (void)__builtin_to_chars(cp, p, v, 10); // expected-error {{must be a pointer to non-const 'char'}}
+
+ // Value must be an integer.
+ (void)__builtin_to_chars(p, p, p, 10); // expected-error {{value argument to '__builtin_to_chars' must be an integer type}}
+
+ // Constant base must be in [2, 36].
+ (void)__builtin_to_chars(p, p, v, 1); // expected-error {{base argument to '__builtin_to_chars' must be between 2 and 36}}
+ (void)__builtin_to_chars(p, p, v, 37); // expected-error {{base argument to '__builtin_to_chars' must be between 2 and 36}}
+}
+
+void from_chars_checks(const char *cp, char *p, int *out, const int *cout, int base) {
+ int ec;
+ (void)__builtin_from_chars(cp, cp, out, 10, &ec); // ok
+ (void)__builtin_from_chars(p, p, out, base, &ec); // ok, non-const buffer is fine
+
+ __builtin_from_chars(cp, cp, out, 10); // expected-error {{too few arguments}}
+
+ // Value must be a pointer to a non-const integer.
+ (void)__builtin_from_chars(cp, cp, cout, 10, &ec); // expected-error {{value argument to '__builtin_from_chars' must be a pointer to a non-const integer type}}
+ (void)__builtin_from_chars(cp, cp, base, 10, &ec); // expected-error {{value argument to '__builtin_from_chars' must be a pointer to a non-const integer type}}
+
+ // ec must be a pointer to a non-const integer.
+ (void)__builtin_from_chars(cp, cp, out, 10, base); // expected-error {{must be a pointer to a non-const integer type}}
+
+ // Constant base range.
+ (void)__builtin_from_chars(cp, cp, out, 0, &ec); // expected-error {{base argument to '__builtin_from_chars' must be between 2 and 36}}
+}
diff --git a/clang/test/SemaCXX/builtin-charconv-constexpr.cpp b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
new file mode 100644
index 0000000000000..9437ee86d147e
--- /dev/null
+++ b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
@@ -0,0 +1,115 @@
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// expected-no-diagnostics
+
+// POC scope: the conversion is implemented in the classic constant evaluator
+// (ExprConstant.cpp). The experimental bytecode interpreter
+// (-fexperimental-new-constant-interpreter) would need a parallel
+// implementation in clang/lib/AST/ByteCode and is left as follow-up.
+
+// POC: __builtin_to_chars / __builtin_from_chars are usable in constant
+// expressions, for integers of any width including _BitInt(N > 128).
+
+template <class T> constexpr bool rt(T v, int base) {
+ char buf[2100] = {};
+ char *end = __builtin_to_chars(buf, buf + sizeof(buf), v, base);
+ if (!end)
+ return false;
+ T out = 0;
+ int ec = 0;
+ const char *p = __builtin_from_chars(buf, end, &out, base, &ec);
+ return ec == 0 && p == end && out == v;
+}
+
+template <class T> constexpr bool rt_all_bases(T v) {
+ return rt(v, 2) && rt(v, 8) && rt(v, 10) && rt(v, 16) && rt(v, 36) &&
+ rt(v, 3) && rt(v, 7);
+}
+
+// Standard integer types, including edge values.
+static_assert(rt_all_bases<int>(0));
+static_assert(rt_all_bases<int>(1));
+static_assert(rt_all_bases<int>(-1));
+static_assert(rt_all_bases<int>(2147483647));
+static_assert(rt_all_bases<int>(-2147483647 - 1)); // most-negative
+static_assert(rt_all_bases<unsigned>(0u));
+static_assert(rt_all_bases<unsigned>(4294967295u));
+static_assert(rt_all_bases<long long>(-9223372036854775807LL - 1));
+static_assert(rt_all_bases<unsigned long long>(18446744073709551615ULL));
+
+#ifdef __SIZEOF_INT128__
+static_assert(rt_all_bases<__int128>(0));
+static_assert(rt_all_bases<__int128>((__int128)-1));
+static_assert(rt_all_bases<unsigned __int128>(~(unsigned __int128)0));
+#endif
+
+// _BitInt wider than 128 bits: the motivating case.
+using s256 = signed _BitInt(256);
+using u256 = unsigned _BitInt(256);
+using s1024 = signed _BitInt(1024);
+
+constexpr s256 mk256() {
+ s256 v = 1;
+ for (int i = 0; i < 150; ++i) // 3^150 < 2^255, stays in range
+ v = v * 3 + 1;
+ return v;
+}
+
+static_assert(rt_all_bases<u256>(0));
+static_assert(rt_all_bases<u256>(~(u256)0)); // all ones
+static_assert(rt_all_bases<s256>(mk256()));
+static_assert(rt_all_bases<s256>(-mk256()));
+// Most-negative via unsigned cast; a signed shift into the sign bit is UB.
+static_assert(rt_all_bases<s256>((s256)(((u256)1) << 255)));
+static_assert(rt(((s1024)1) << 1000, 10));
+static_assert(rt(((u256)1) << 200, 16));
+
+// Buffer too small -> null return.
+constexpr bool too_small() {
+ char buf[2] = {};
+ return __builtin_to_chars(buf, buf + 2, 12345, 10) == nullptr;
+}
+static_assert(too_small());
+
+// Exact-fit buffer succeeds.
+constexpr bool exact_fit() {
+ char buf[3] = {};
+ char *end = __builtin_to_chars(buf, buf + 3, -42, 10);
+ return end == buf + 3 && buf[0] == '-' && buf[1] == '4' && buf[2] == '2';
+}
+static_assert(exact_fit());
+
+// from_chars error classes and parsing edges.
+constexpr int parse_ec(const char *s, unsigned long n, int base) {
+ long long out = 0;
+ int ec = 0;
+ __builtin_from_chars(s, s + n, &out, base, &ec);
+ return ec;
+}
+static_assert(parse_ec("", 0, 10) == 1); // invalid_argument
+static_assert(parse_ec("xyz", 3, 10) == 1); // invalid_argument
+static_assert(parse_ec("+5", 2, 10) == 1); // '+' not accepted
+static_assert(parse_ec("123", 3, 10) == 0); // ok
+static_assert(parse_ec("99999999999999999999999999999", 29, 10) == 2); // range
+
+// from_chars stops at the first non-digit; no base prefix is consumed.
+constexpr bool partial() {
+ long long out = 0;
+ int ec = 0;
+ const char *s = "0x1f";
+ const char *p = __builtin_from_chars(s, s + 4, &out, 16, &ec);
+ return ec == 0 && out == 0 && p == s + 1; // parsed only "0"
+}
+static_assert(partial());
+
+// '8' is not a base-8 digit; case-insensitive letters in base 16.
+static_assert(parse_ec("8", 1, 8) == 1);
+constexpr bool hex_case() {
+ const char upper[] = "FF";
+ const char lower[] = "ff";
+ long long a = 0, b = 0;
+ int ea = 0, eb = 0;
+ __builtin_from_chars(upper, upper + 2, &a, 16, &ea);
+ __builtin_from_chars(lower, lower + 2, &b, 16, &eb);
+ return ea == 0 && eb == 0 && a == 255 && b == 255;
+}
+static_assert(hex_case());
diff --git a/clang/test/SemaCXX/builtin-charconv-steps.cpp b/clang/test/SemaCXX/builtin-charconv-steps.cpp
new file mode 100644
index 0000000000000..b0e07731e52eb
--- /dev/null
+++ b/clang/test/SemaCXX/builtin-charconv-steps.cpp
@@ -0,0 +1,25 @@
+// RUN: %clang_cc1 -std=c++20 -fconstexpr-steps=100 -fsyntax-only -verify %s
+// expected-no-diagnostics
+
+// The builtin does not spend constexpr steps proportional to the operand width.
+// Parsing the 302-digit decimal of 2^1000 into a 1024-bit _BitInt under a
+// 100-step budget succeeds; a hand-written Horner loop over 302 digits would
+// exceed it. to_chars cannot be shown the same way, because its output buffer
+// is a constexpr array whose bound is itself limited by -fconstexpr-steps.
+
+using u1024 = unsigned _BitInt(1024);
+
+constexpr u1024 parse(const char *s, int n) {
+ u1024 out = 0;
+ int ec = 0;
+ __builtin_from_chars(s, s + n, &out, 10, &ec);
+ return ec == 0 ? out : 0;
+}
+
+static_assert(
+ parse("107150860718626732094842504906000181056140481170553360744375038837"
+ "035105112493612249319837881569585812759467291755314682518714528569"
+ "231404359845775746985748039345677748242309854210746050623711418779"
+ "541821530464749835819412673987675591655439460770629145711964776865"
+ "42167660429831652624386837205668069376",
+ 302) == ((u1024)1) << 1000);
>From 31648ee017647ab6f7acf6241b4294936c0c6dfb Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Thu, 18 Jun 2026 15:59:14 +0200
Subject: [PATCH 2/3] [Clang] POC: add independent-oracle anchors to charconv
constexpr test
The round-trip checks verify from_chars(to_chars(v)) == v, which a symmetric
bug in both directions would survive, and bases 2/3/7/8/36 had no independent
oracle. Add fixed expected strings for to_chars per base and fixed expected
values for from_chars, so a digit-table or base-offset error is caught. The
runtime engine matches the constexpr engine byte-for-byte (review INV-1), so
these constexpr anchors also cover the runtime codegen path; clang/test does
not execute compiled binaries, and a runnable runtime probe is kept in the
project sandbox.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply at anthropic.com>
---
.../SemaCXX/builtin-charconv-constexpr.cpp | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/clang/test/SemaCXX/builtin-charconv-constexpr.cpp b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
index 9437ee86d147e..ad2703194d887 100644
--- a/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
+++ b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
@@ -113,3 +113,41 @@ constexpr bool hex_case() {
return ea == 0 && eb == 0 && a == 255 && b == 255;
}
static_assert(hex_case());
+
+// Independent-oracle anchors. The round-trip checks above pass even if a
+// symmetric bug hits both directions; these compare to_chars output and
+// from_chars values against fixed expected results, per base. INV-1 of the
+// review establishes the runtime engine matches the constexpr engine
+// byte-for-byte, so these constexpr anchors cover the runtime path too.
+constexpr bool to_eq(auto v, int base, const char *expect) {
+ char buf[300] = {};
+ char *e = __builtin_to_chars(buf, buf + sizeof(buf), v, base);
+ if (!e)
+ return false;
+ const char *p = buf;
+ for (; *expect; ++p, ++expect)
+ if (p == e || *p != *expect)
+ return false;
+ return p == e;
+}
+static_assert(to_eq(5u, 2, "101"));
+static_assert(to_eq(8u, 8, "10"));
+static_assert(to_eq(255u, 16, "ff"));
+static_assert(to_eq(35u, 36, "z"));
+static_assert(to_eq(1000000u, 10, "1000000"));
+static_assert(to_eq(-255, 16, "-ff"));
+static_assert(to_eq((s256)(((u256)1) << 200), 10,
+ "16069380442589902755419620923411626025222029937827928353"
+ "01376"));
+
+constexpr long long parse_val(const char *s, unsigned n, int base) {
+ long long out = -999;
+ int ec = 0;
+ __builtin_from_chars(s, s + n, &out, base, &ec);
+ return out;
+}
+static_assert(parse_val("101", 3, 2) == 5);
+static_assert(parse_val("777", 3, 8) == 511);
+static_assert(parse_val("7f", 2, 16) == 127);
+static_assert(parse_val("z", 1, 36) == 35);
+static_assert(parse_val("-123", 4, 10) == -123);
>From dca4f629c6d7e04be6c90a57951bc19754f688e0 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Thu, 18 Jun 2026 17:02:14 +0200
Subject: [PATCH 3/3] [Clang][POC] Pin x86_64 triple in the wide-_BitInt
charconv tests
The two tests use _BitInt(256) and _BitInt(1024) without a target triple, so
they pick up the host target and fail on AArch64, whose default
__BITINT_MAXWIDTH__ is 128. Pin -triple x86_64-linux-gnu so the tests do not
depend on the host. The builtin implementation is target-independent; only the
tests assumed a large maximum width.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply at anthropic.com>
---
clang/test/SemaCXX/builtin-charconv-constexpr.cpp | 5 ++++-
clang/test/SemaCXX/builtin-charconv-steps.cpp | 5 ++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/clang/test/SemaCXX/builtin-charconv-constexpr.cpp b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
index ad2703194d887..ff3ae325fae53 100644
--- a/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
+++ b/clang/test/SemaCXX/builtin-charconv-constexpr.cpp
@@ -1,6 +1,9 @@
-// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -std=c++20 -fsyntax-only -verify %s
// expected-no-diagnostics
+// A wide _BitInt needs a target with a large __BITINT_MAXWIDTH__; pin x86_64 so
+// the test does not depend on the host target (e.g. AArch64 caps it at 128).
+
// POC scope: the conversion is implemented in the classic constant evaluator
// (ExprConstant.cpp). The experimental bytecode interpreter
// (-fexperimental-new-constant-interpreter) would need a parallel
diff --git a/clang/test/SemaCXX/builtin-charconv-steps.cpp b/clang/test/SemaCXX/builtin-charconv-steps.cpp
index b0e07731e52eb..0d56442f0bbaa 100644
--- a/clang/test/SemaCXX/builtin-charconv-steps.cpp
+++ b/clang/test/SemaCXX/builtin-charconv-steps.cpp
@@ -1,6 +1,9 @@
-// RUN: %clang_cc1 -std=c++20 -fconstexpr-steps=100 -fsyntax-only -verify %s
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -std=c++20 -fconstexpr-steps=100 -fsyntax-only -verify %s
// expected-no-diagnostics
+// Pin x86_64 so the wide _BitInt does not depend on the host target's
+// __BITINT_MAXWIDTH__ (e.g. AArch64 caps it at 128).
+
// The builtin does not spend constexpr steps proportional to the operand width.
// Parsing the 302-digit decimal of 2^1000 into a 1024-bit _BitInt under a
// 100-step budget succeeds; a hand-written Horner loop over 302 digits would
More information about the cfe-commits
mailing list