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 <[email protected]>
Date: Thu, 18 Jun 2026 15:50:59 +0200
Subject: [PATCH 1/4] [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) <[email protected]>
---
 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 <[email protected]>
Date: Thu, 18 Jun 2026 15:59:14 +0200
Subject: [PATCH 2/4] [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) <[email protected]>
---
 .../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 <[email protected]>
Date: Thu, 18 Jun 2026 17:02:14 +0200
Subject: [PATCH 3/4] [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) <[email protected]>
---
 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

>From 22e75cd66e21548230ef8bc8c4c7374e9a4f6f8e Mon Sep 17 00:00:00 2001
From: Xavier Roche <[email protected]>
Date: Thu, 18 Jun 2026 17:44:56 +0200
Subject: [PATCH 4/4] [Clang][POC] Reuse EmitOverflowIntrinsic; guard invalid
 designators in charconv

Two cleanups from a reuse audit:
- from_chars runtime codegen now uses the EmitOverflowIntrinsic helper instead
  of open-coding umul/uadd with_overflow plus the extractvalue calls.
- The constexpr to_chars/from_chars buffer guards also reject an invalid pointer
  designator before walking, matching the memchr/strcmp neighbors.

APInt::toString and Sema::BuiltinConstantArgRange were considered and rejected:
APInt::toString supports only radix 2/8/10/16/36 while charconv needs every base
in [2, 36], and BuiltinConstantArgRange requires a constant argument while the
base may be a runtime value. A comment in to_chars records the radix limit.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 clang/lib/AST/ExprConstant.cpp  |  9 +++++----
 clang/lib/CodeGen/CGBuiltin.cpp | 14 ++++++--------
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 0d918b5679156..0179c534fdeb8 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -10725,8 +10725,9 @@ bool PointerExprEvaluator::VisitBuiltinCallExpr(const 
CallExpr *E,
       return false;
     }
 
-    // Sign and magnitude. Widen by one bit so negating the most-negative
-    // value cannot overflow.
+    // Sign and magnitude. Widen by one bit so negating the most-negative value
+    // cannot overflow. APInt::toString is not usable here: it only supports
+    // radix 2/8/10/16/36, whereas charconv covers every base in [2, 36].
     bool Neg = Value.isSigned() && Value.isNegative();
     APInt Mag = Value.isSigned() ? Value.sext(Value.getBitWidth() + 1)
                                  : Value.zext(Value.getBitWidth() + 1);
@@ -10749,7 +10750,7 @@ bool PointerExprEvaluator::VisitBuiltinCallExpr(const 
CallExpr *E,
     uint64_t Len = Rev.size() + (Neg ? 1 : 0);
 
     // Capacity check against [first, last). char is one byte.
-    if (!HasSameBase(First, Last)) {
+    if (!HasSameBase(First, Last) || First.Designator.Invalid) {
       Info.FFDiag(E);
       return false;
     }
@@ -10797,7 +10798,7 @@ bool PointerExprEvaluator::VisitBuiltinCallExpr(const 
CallExpr *E,
       return false;
     }
 
-    if (!HasSameBase(First, Last)) {
+    if (!HasSameBase(First, Last) || First.Designator.Invalid) {
       Info.FFDiag(E);
       return false;
     }
diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp
index 9b2f83f6cbed0..d6d90353ee4b8 100644
--- a/clang/lib/CodeGen/CGBuiltin.cpp
+++ b/clang/lib/CodeGen/CGBuiltin.cpp
@@ -5376,14 +5376,12 @@ RValue CodeGenFunction::EmitBuiltinExpr(const 
GlobalDecl GD, unsigned BuiltinID,
     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 *O1, *O2;
+    Value *Mul = EmitOverflowIntrinsic(
+        *this, llvm::Intrinsic::umul_with_overflow, AccV, BaseA, O1);
+    Value *Sum = EmitOverflowIntrinsic(
+        *this, llvm::Intrinsic::uadd_with_overflow, Mul, DW, O2);
+    Builder.CreateStore(Sum, AccAddr);
     Value *OvfNow = Builder.CreateOr(O1, O2);
     Value *OldOvf =
         Builder.CreateICmpNE(Builder.CreateLoad(OvfAddr), Builder.getInt8(0));

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to