Skip to content

[clang-repl] Lay the basic infrastructure for pretty printing of types #148701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

vgvassilev
Copy link
Contributor

The idea is to store a type-value pair in clang::Value which is updated by the interpreter runtime. The class copies builtin types and boxes non-builtin types to provide some lifetime control.

The patch enables default printers for C and C++ using a very minimalistic approach. We handle enums, arrays and user types. Once we land this we can focus on enabling user-defined pretty-printers which take control over printing of types

The work started as part of https://reviews.llvm.org/D146809, then we created a giant in #84769

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jul 14, 2025
@vgvassilev vgvassilev requested a review from AaronBallman July 14, 2025 18:59
@llvmbot
Copy link
Member

llvmbot commented Jul 14, 2025

@llvm/pr-subscribers-clang

Author: Vassil Vassilev (vgvassilev)

Changes

The idea is to store a type-value pair in clang::Value which is updated by the interpreter runtime. The class copies builtin types and boxes non-builtin types to provide some lifetime control.

The patch enables default printers for C and C++ using a very minimalistic approach. We handle enums, arrays and user types. Once we land this we can focus on enabling user-defined pretty-printers which take control over printing of types

The work started as part of https://reviews.llvm.org/D146809, then we created a giant in #84769


Patch is 36.25 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/148701.diff

13 Files Affected:

  • (modified) clang/include/clang/AST/ASTContext.h (+2)
  • (modified) clang/include/clang/Interpreter/Interpreter.h (+22-12)
  • (modified) clang/include/clang/Interpreter/Value.h (+3-4)
  • (modified) clang/lib/Interpreter/CMakeLists.txt (+1)
  • (modified) clang/lib/Interpreter/Interpreter.cpp (+28-17)
  • (modified) clang/lib/Interpreter/InterpreterUtils.cpp (+5-3)
  • (modified) clang/lib/Interpreter/InterpreterUtils.h (+1-1)
  • (modified) clang/lib/Interpreter/InterpreterValuePrinter.cpp (+365-26)
  • (modified) clang/lib/Interpreter/Value.cpp (+29-7)
  • (modified) clang/lib/Parse/ParseStmt.cpp (+2-1)
  • (modified) clang/test/Interpreter/pretty-print.c (+73-2)
  • (added) clang/test/Interpreter/pretty-print.cpp (+59)
  • (modified) clang/unittests/Interpreter/InterpreterTest.cpp (+19)
diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index 2b9cd035623cc..f058239aabedc 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -1192,6 +1192,8 @@ class ASTContext : public RefCountedBase<ASTContext> {
   bool isInSameModule(const Module *M1, const Module *M2) const;
 
   TranslationUnitDecl *getTranslationUnitDecl() const {
+    assert(TUDecl->getMostRecentDecl() == TUDecl &&
+           "The active TU is not current one!");
     return TUDecl->getMostRecentDecl();
   }
   void addTranslationUnitDecl() {
diff --git a/clang/include/clang/Interpreter/Interpreter.h b/clang/include/clang/Interpreter/Interpreter.h
index 78dff1165dcf5..7ff5599a8a568 100644
--- a/clang/include/clang/Interpreter/Interpreter.h
+++ b/clang/include/clang/Interpreter/Interpreter.h
@@ -175,23 +175,15 @@ class Interpreter {
   llvm::Expected<llvm::orc::ExecutorAddr>
   getSymbolAddressFromLinkerName(llvm::StringRef LinkerName) const;
 
-  const llvm::SmallVectorImpl<Expr *> &getValuePrintingInfo() const {
-    return ValuePrintingInfo;
-  }
-
-  Expr *SynthesizeExpr(Expr *E);
+  std::unique_ptr<llvm::Module> GenModule(IncrementalAction *Action = nullptr);
+  PartialTranslationUnit &RegisterPTU(TranslationUnitDecl *TU,
+                                      std::unique_ptr<llvm::Module> M = {},
+                                      IncrementalAction *Action = nullptr);
 
 private:
   size_t getEffectivePTUSize() const;
   void markUserCodeStart();
   llvm::Expected<Expr *> ExtractValueFromExpr(Expr *E);
-  llvm::Expected<llvm::orc::ExecutorAddr> CompileDtorCall(CXXRecordDecl *CXXRD);
-
-  CodeGenerator *getCodeGen(IncrementalAction *Action = nullptr) const;
-  std::unique_ptr<llvm::Module> GenModule(IncrementalAction *Action = nullptr);
-  PartialTranslationUnit &RegisterPTU(TranslationUnitDecl *TU,
-                                      std::unique_ptr<llvm::Module> M = {},
-                                      IncrementalAction *Action = nullptr);
 
   // A cache for the compiled destructors used to for de-allocation of managed
   // clang::Values.
@@ -200,6 +192,24 @@ class Interpreter {
   llvm::SmallVector<Expr *, 4> ValuePrintingInfo;
 
   std::unique_ptr<llvm::orc::LLJITBuilder> JITBuilder;
+
+  /// @}
+  /// @name Value and pretty printing support
+  /// @{
+
+  std::string ValueDataToString(const Value &V);
+  std::string ValueTypeToString(const Value &V) const;
+
+  llvm::Expected<Expr *> convertExprToValue(Expr *E);
+
+  // When we deallocate clang::Value we need to run the destructor of the type.
+  // This function forces emission of the needed dtor.
+  llvm::Expected<llvm::orc::ExecutorAddr> CompileDtorCall(CXXRecordDecl *CXXRD);
+
+  /// @}
+  /// @name Code generation
+  /// @{
+  CodeGenerator *getCodeGen(IncrementalAction *Action = nullptr) const;
 };
 } // namespace clang
 
diff --git a/clang/include/clang/Interpreter/Value.h b/clang/include/clang/Interpreter/Value.h
index a93c0841915fc..e71e4e37e22f6 100644
--- a/clang/include/clang/Interpreter/Value.h
+++ b/clang/include/clang/Interpreter/Value.h
@@ -119,9 +119,9 @@ class REPL_EXTERNAL_VISIBILITY Value {
   ~Value();
 
   void printType(llvm::raw_ostream &Out) const;
-  void printData(llvm::raw_ostream &Out) const;
-  void print(llvm::raw_ostream &Out) const;
-  void dump() const;
+  void printData(llvm::raw_ostream &Out);
+  void print(llvm::raw_ostream &Out);
+  void dump();
   void clear();
 
   ASTContext &getASTContext();
@@ -205,6 +205,5 @@ template <> inline void *Value::as() const {
     return Data.m_Ptr;
   return (void *)as<uintptr_t>();
 }
-
 } // namespace clang
 #endif
diff --git a/clang/lib/Interpreter/CMakeLists.txt b/clang/lib/Interpreter/CMakeLists.txt
index 38cf139fa86a6..70de4a2aaa541 100644
--- a/clang/lib/Interpreter/CMakeLists.txt
+++ b/clang/lib/Interpreter/CMakeLists.txt
@@ -29,6 +29,7 @@ add_clang_library(clangInterpreter
   InterpreterUtils.cpp
   RemoteJITUtils.cpp
   Value.cpp
+  InterpreterValuePrinter.cpp
   ${WASM_SRC}
   PARTIAL_SOURCES_INTENDED
 
diff --git a/clang/lib/Interpreter/Interpreter.cpp b/clang/lib/Interpreter/Interpreter.cpp
index ed3bae59a144c..1bacf60627cee 100644
--- a/clang/lib/Interpreter/Interpreter.cpp
+++ b/clang/lib/Interpreter/Interpreter.cpp
@@ -264,7 +264,7 @@ class InProcessPrintingASTConsumer final : public MultiplexConsumer {
       if (auto *TLSD = llvm::dyn_cast<TopLevelStmtDecl>(D))
         if (TLSD && TLSD->isSemiMissing()) {
           auto ExprOrErr =
-              Interp.ExtractValueFromExpr(cast<Expr>(TLSD->getStmt()));
+              Interp.convertExprToValue(cast<Expr>(TLSD->getStmt()));
           if (llvm::Error E = ExprOrErr.takeError()) {
             llvm::logAllUnhandledErrors(std::move(E), llvm::errs(),
                                         "Value printing failed: ");
@@ -416,6 +416,8 @@ Interpreter::Interpreter(std::unique_ptr<CompilerInstance> Instance,
         return;
       }
   }
+
+  ValuePrintingInfo.resize(4);
 }
 
 Interpreter::~Interpreter() {
@@ -440,11 +442,10 @@ const char *const Runtimes = R"(
     #define __CLANG_REPL__ 1
 #ifdef __cplusplus
     #define EXTERN_C extern "C"
-    void *__clang_Interpreter_SetValueWithAlloc(void*, void*, void*);
     struct __clang_Interpreter_NewTag{} __ci_newtag;
     void* operator new(__SIZE_TYPE__, void* __p, __clang_Interpreter_NewTag) noexcept;
     template <class T, class = T (*)() /*disable for arrays*/>
-    void __clang_Interpreter_SetValueCopyArr(T* Src, void* Placement, unsigned long Size) {
+    void __clang_Interpreter_SetValueCopyArr(const T* Src, void* Placement, unsigned long Size) {
       for (auto Idx = 0; Idx < Size; ++Idx)
         new ((void*)(((T*)Placement) + Idx), __ci_newtag) T(Src[Idx]);
     }
@@ -454,8 +455,12 @@ const char *const Runtimes = R"(
     }
 #else
     #define EXTERN_C extern
+    EXTERN_C void *memcpy(void *restrict dst, const void *restrict src, __SIZE_TYPE__ n);
+    EXTERN_C inline void __clang_Interpreter_SetValueCopyArr(const void* Src, void* Placement, unsigned long Size) {
+      memcpy(Placement, Src, Size);
+    }
 #endif // __cplusplus
-
+  EXTERN_C void *__clang_Interpreter_SetValueWithAlloc(void*, void*, void*);
   EXTERN_C void __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType, ...);
 )";
 
@@ -470,12 +475,12 @@ Interpreter::create(std::unique_ptr<CompilerInstance> CI,
 
   // Add runtime code and set a marker to hide it from user code. Undo will not
   // go through that.
-  auto PTU = Interp->Parse(Runtimes);
-  if (!PTU)
-    return PTU.takeError();
+  Err = Interp->ParseAndExecute(Runtimes);
+  if (Err)
+    return std::move(Err);
+
   Interp->markUserCodeStart();
 
-  Interp->ValuePrintingInfo.resize(4);
   return std::move(Interp);
 }
 
@@ -524,12 +529,11 @@ Interpreter::createWithCUDA(std::unique_ptr<CompilerInstance> CI,
   return std::move(Interp);
 }
 
+CompilerInstance *Interpreter::getCompilerInstance() { return CI.get(); }
 const CompilerInstance *Interpreter::getCompilerInstance() const {
-  return CI.get();
+  return const_cast<Interpreter *>(this)->getCompilerInstance();
 }
 
-CompilerInstance *Interpreter::getCompilerInstance() { return CI.get(); }
-
 llvm::Expected<llvm::orc::LLJIT &> Interpreter::getExecutionEngine() {
   if (!IncrExecutor) {
     if (auto Err = CreateExecutor())
@@ -610,7 +614,14 @@ Interpreter::Parse(llvm::StringRef Code) {
   if (!TuOrErr)
     return TuOrErr.takeError();
 
-  return RegisterPTU(*TuOrErr);
+  PTUs.emplace_back(PartialTranslationUnit());
+  PartialTranslationUnit &LastPTU = PTUs.back();
+  LastPTU.TUPart = *TuOrErr;
+
+  if (std::unique_ptr<llvm::Module> M = GenModule())
+    LastPTU.TheModule = std::move(M);
+
+  return LastPTU;
 }
 
 static llvm::Expected<llvm::orc::JITTargetMachineBuilder>
@@ -806,13 +817,13 @@ Interpreter::GenModule(IncrementalAction *Action) {
     // of the module which does not map well to CodeGen's design. To work this
     // around we created an empty module to make CodeGen happy. We should make
     // sure it always stays empty.
-    assert(((!CachedInCodeGenModule ||
-             !getCompilerInstance()->getPreprocessorOpts().Includes.empty()) ||
-            (CachedInCodeGenModule->empty() &&
+    assert((!CachedInCodeGenModule ||
+            !getCompilerInstance()->getPreprocessorOpts().Includes.empty()) ||
+           ((CachedInCodeGenModule->empty() &&
              CachedInCodeGenModule->global_empty() &&
              CachedInCodeGenModule->alias_empty() &&
              CachedInCodeGenModule->ifunc_empty())) &&
-           "CodeGen wrote to a readonly module");
+               "CodeGen wrote to a readonly module");
     std::unique_ptr<llvm::Module> M(CG->ReleaseModule());
     CG->StartModule("incr_module_" + std::to_string(ID++), M->getContext());
     return M;
@@ -828,4 +839,4 @@ CodeGenerator *Interpreter::getCodeGen(IncrementalAction *Action) const {
     return nullptr;
   return static_cast<CodeGenAction *>(WrappedAct)->getCodeGenerator();
 }
-} // namespace clang
+} // end namespace clang
diff --git a/clang/lib/Interpreter/InterpreterUtils.cpp b/clang/lib/Interpreter/InterpreterUtils.cpp
index 45f6322b8461e..a19f96c80b94f 100644
--- a/clang/lib/Interpreter/InterpreterUtils.cpp
+++ b/clang/lib/Interpreter/InterpreterUtils.cpp
@@ -11,6 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "InterpreterUtils.h"
+#include "clang/AST/QualTypeNames.h"
 
 namespace clang {
 
@@ -81,7 +82,7 @@ NamedDecl *LookupNamed(Sema &S, llvm::StringRef Name,
   else {
     const DeclContext *PrimaryWithin = nullptr;
     if (const auto *TD = dyn_cast<TagDecl>(Within))
-      PrimaryWithin = llvm::dyn_cast_or_null<DeclContext>(TD->getDefinition());
+      PrimaryWithin = dyn_cast_if_present<DeclContext>(TD->getDefinition());
     else
       PrimaryWithin = Within->getPrimaryContext();
 
@@ -97,15 +98,16 @@ NamedDecl *LookupNamed(Sema &S, llvm::StringRef Name,
   R.resolveKind();
 
   if (R.isSingleResult())
-    return llvm::dyn_cast<NamedDecl>(R.getFoundDecl());
+    return dyn_cast<NamedDecl>(R.getFoundDecl());
 
   return nullptr;
 }
 
 std::string GetFullTypeName(ASTContext &Ctx, QualType QT) {
+  QualType FQT = TypeName::getFullyQualifiedType(QT, Ctx);
   PrintingPolicy Policy(Ctx.getPrintingPolicy());
   Policy.SuppressScope = false;
   Policy.AnonymousTagLocations = false;
-  return QT.getAsString(Policy);
+  return FQT.getAsString(Policy);
 }
 } // namespace clang
diff --git a/clang/lib/Interpreter/InterpreterUtils.h b/clang/lib/Interpreter/InterpreterUtils.h
index c7b405b486d93..fbf9814b0d4a7 100644
--- a/clang/lib/Interpreter/InterpreterUtils.h
+++ b/clang/lib/Interpreter/InterpreterUtils.h
@@ -45,7 +45,7 @@ NamespaceDecl *LookupNamespace(Sema &S, llvm::StringRef Name,
                                const DeclContext *Within = nullptr);
 
 NamedDecl *LookupNamed(Sema &S, llvm::StringRef Name,
-                       const DeclContext *Within);
+                       const DeclContext *Within = nullptr);
 
 std::string GetFullTypeName(ASTContext &Ctx, QualType QT);
 } // namespace clang
diff --git a/clang/lib/Interpreter/InterpreterValuePrinter.cpp b/clang/lib/Interpreter/InterpreterValuePrinter.cpp
index 3e7e32b2e8557..ab5edac9029dc 100644
--- a/clang/lib/Interpreter/InterpreterValuePrinter.cpp
+++ b/clang/lib/Interpreter/InterpreterValuePrinter.cpp
@@ -18,6 +18,7 @@
 #include "clang/Frontend/CompilerInstance.h"
 #include "clang/Interpreter/Interpreter.h"
 #include "clang/Interpreter/Value.h"
+#include "clang/Lex/Preprocessor.h"
 #include "clang/Sema/Lookup.h"
 #include "clang/Sema/Sema.h"
 
@@ -25,11 +26,333 @@
 #include "llvm/Support/raw_ostream.h"
 
 #include <cassert>
-
 #include <cstdarg>
+#include <sstream>
+#include <string>
+
+#define DEBUG_TYPE "interp-value"
+
+using namespace clang;
+
+static std::string DeclTypeToString(const QualType &QT, NamedDecl *D) {
+  std::string Str;
+  llvm::raw_string_ostream SS(Str);
+  if (QT.hasQualifiers())
+    SS << QT.getQualifiers().getAsString() << " ";
+  SS << D->getQualifiedNameAsString();
+  return Str;
+}
+
+static std::string QualTypeToString(ASTContext &Ctx, QualType QT) {
+  PrintingPolicy Policy(Ctx.getPrintingPolicy());
+  // Print the Allocator in STL containers, for instance.
+  Policy.SuppressDefaultTemplateArgs = false;
+  Policy.SuppressUnwrittenScope = true;
+  // Print 'a<b<c> >' rather than 'a<b<c>>'.
+  Policy.SplitTemplateClosers = true;
+
+  struct LocalPrintingPolicyRAII {
+    ASTContext &Context;
+    PrintingPolicy Policy;
+
+    LocalPrintingPolicyRAII(ASTContext &Ctx, PrintingPolicy &PP)
+        : Context(Ctx), Policy(Ctx.getPrintingPolicy()) {
+      Context.setPrintingPolicy(PP);
+    }
+    ~LocalPrintingPolicyRAII() { Context.setPrintingPolicy(Policy); }
+  } X(Ctx, Policy);
+
+  const QualType NonRefTy = QT.getNonReferenceType();
+
+  if (const auto *TTy = llvm::dyn_cast<TagType>(NonRefTy))
+    return DeclTypeToString(NonRefTy, TTy->getDecl());
+
+  if (const auto *TRy = dyn_cast<RecordType>(NonRefTy))
+    return DeclTypeToString(NonRefTy, TRy->getDecl());
+
+  const QualType Canon = NonRefTy.getCanonicalType();
+
+  // FIXME: How a builtin type can be a function pointer type?
+  if (Canon->isBuiltinType() && !NonRefTy->isFunctionPointerType() &&
+      !NonRefTy->isMemberPointerType())
+    return Canon.getAsString(Ctx.getPrintingPolicy());
+
+  if (const auto *TDTy = dyn_cast<TypedefType>(NonRefTy)) {
+    // FIXME: TemplateSpecializationType & SubstTemplateTypeParmType checks
+    // are predominately to get STL containers to print nicer and might be
+    // better handled in GetFullyQualifiedName.
+    //
+    // std::vector<Type>::iterator is a TemplateSpecializationType
+    // std::vector<Type>::value_type is a SubstTemplateTypeParmType
+    //
+    QualType SSDesugar = TDTy->getLocallyUnqualifiedSingleStepDesugaredType();
+    if (llvm::isa<SubstTemplateTypeParmType>(SSDesugar))
+      return GetFullTypeName(Ctx, Canon);
+    else if (llvm::isa<TemplateSpecializationType>(SSDesugar))
+      return GetFullTypeName(Ctx, NonRefTy);
+    return DeclTypeToString(NonRefTy, TDTy->getDecl());
+  }
+  return GetFullTypeName(Ctx, NonRefTy);
+}
+
+static std::string EnumToString(const Value &V) {
+  std::string Str;
+  llvm::raw_string_ostream SS(Str);
+  ASTContext &Ctx = const_cast<ASTContext &>(V.getASTContext());
+
+  QualType DesugaredTy = V.getType().getDesugaredType(Ctx);
+  const EnumType *EnumTy = DesugaredTy.getNonReferenceType()->getAs<EnumType>();
+  assert(EnumTy && "Fail to cast to enum type");
+
+  EnumDecl *ED = EnumTy->getDecl();
+  uint64_t Data = V.getULongLong();
+  bool IsFirst = true;
+  llvm::APSInt AP = Ctx.MakeIntValue(Data, DesugaredTy);
+
+  for (auto I = ED->enumerator_begin(), E = ED->enumerator_end(); I != E; ++I) {
+    if (I->getInitVal() == AP) {
+      if (!IsFirst)
+        SS << " ? ";
+      SS << "(" + I->getQualifiedNameAsString() << ")";
+      IsFirst = false;
+    }
+  }
+  llvm::SmallString<64> APStr;
+  AP.toString(APStr, /*Radix=*/10);
+  SS << " : " << QualTypeToString(Ctx, ED->getIntegerType()) << " " << APStr;
+  return Str;
+}
+
+static std::string FunctionToString(const Value &V, const void *Ptr) {
+  std::string Str;
+  llvm::raw_string_ostream SS(Str);
+  SS << "Function @" << Ptr;
+
+  const DeclContext *PTU = V.getASTContext().getTranslationUnitDecl();
+  // Find the last top-level-stmt-decl. This is a forward iterator but the
+  // partial translation unit should not be large.
+  const TopLevelStmtDecl *TLSD = nullptr;
+  for (const Decl *D : PTU->noload_decls())
+    if (isa<TopLevelStmtDecl>(D))
+      TLSD = cast<TopLevelStmtDecl>(D);
+
+  // Get __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void
+  // *OpaqueType, void *Val);
+  const FunctionDecl *FD = nullptr;
+  if (auto *InterfaceCall = llvm::dyn_cast<CallExpr>(TLSD->getStmt())) {
+    const auto *Arg = InterfaceCall->getArg(/*Val*/ 3);
+    // Get rid of cast nodes.
+    while (const CastExpr *CastE = llvm::dyn_cast<CastExpr>(Arg))
+      Arg = CastE->getSubExpr();
+    if (const DeclRefExpr *DeclRefExp = llvm::dyn_cast<DeclRefExpr>(Arg))
+      FD = llvm::dyn_cast<FunctionDecl>(DeclRefExp->getDecl());
+
+    if (FD) {
+      SS << '\n';
+      const clang::FunctionDecl *FDef;
+      if (FD->hasBody(FDef))
+        FDef->print(SS);
+    }
+  }
+  return Str;
+}
+
+static std::string AddressToString(const void *Ptr, char Prefix) {
+  std::string Str;
+  llvm::raw_string_ostream SS(Str);
+  if (!Ptr)
+    return Str;
+  SS << Prefix << Ptr;
+  return Str;
+}
+
+static std::string CharPtrToString(const char *Ptr) {
+  if (!Ptr)
+    return "0";
+
+  std::string result = "\"";
+  result += Ptr;
+  result += '"';
+  return result;
+}
 
 namespace clang {
 
+struct ValueRef : public Value {
+  ValueRef(Interpreter *In, void *Ty) : Value(In, Ty) {
+    // Tell the base class to not try to deallocate if it manages the value.
+    IsManuallyAlloc = false;
+  }
+  void setData(long double D) { Data.m_LongDouble = D; }
+};
+
+std::string Interpreter::ValueDataToString(const Value &V) {
+  Sema &S = getCompilerInstance()->getSema();
+  ASTContext &Ctx = S.getASTContext();
+
+  QualType QT = V.getType();
+
+  if (const ConstantArrayType *CAT = Ctx.getAsConstantArrayType(QT)) {
+    QualType ElemTy = CAT->getElementType();
+    size_t ElemCount = Ctx.getConstantArrayElementCount(CAT);
+    const Type *BaseTy = CAT->getBaseElementTypeUnsafe();
+    size_t ElemSize = Ctx.getTypeSizeInChars(BaseTy).getQuantity();
+
+    // Treat null terminated char arrays as strings basically.
+    if (ElemTy->isCharType()) {
+      char last = *(char *)(((uintptr_t)V.getPtr()) + ElemCount * ElemSize - 1);
+      if (last == '\0')
+        return CharPtrToString((char *)V.getPtr());
+    }
+
+    std::string result = "{ ";
+    for (unsigned Idx = 0, N = CAT->getZExtSize(); Idx < N; ++Idx) {
+      ValueRef InnerV = ValueRef(this, ElemTy.getAsOpaquePtr());
+      if (ElemTy->isBuiltinType()) {
+        // Single dim arrays, advancing.
+        uintptr_t offset = (uintptr_t)V.getPtr() + Idx * ElemSize;
+        InnerV.setData(*(long double *)offset);
+      } else {
+        // Multi dim arrays, position to the next dimension.
+        size_t Stride = ElemCount / N;
+        uintptr_t offset = ((uintptr_t)V.getPtr()) + Idx * Stride * ElemSize;
+        InnerV.setPtr((void *)offset);
+      }
+
+      result += ValueDataToString(InnerV);
+
+      // Skip the \0 if the char types
+      if (Idx < N - 1)
+        result += ", ";
+    }
+    result += " }";
+    return result;
+  }
+
+  QualType DesugaredTy = QT.getDesugaredType(Ctx);
+  QualType NonRefTy = DesugaredTy.getNonReferenceType();
+
+  // FIXME: Add support for user defined printers.
+  // LookupResult R = LookupUserDefined(S, QT);
+  // if (!R.empty())
+  //   return CallUserSpecifiedPrinter(R, V);
+
+  // If it is a builtin type dispatch to the builtin overloads.
+  if (auto *BT = DesugaredTy.getCanonicalType()->getAs<BuiltinType>()) {
+
+    auto formatFloating = [](auto val, char suffix = '\0') -> std::string {
+      std::string out;
+      llvm::raw_string_ostream ss(out);
+
+      if (std::isnan(val) || std::isinf(val)) {
+        ss << llvm::format("%g", val);
+        return ss.str();
+      }
+      if (val == static_cast<decltype(val)>(static_cast<int64_t>(val)))
+        ss << llvm::format("%.1f", val);
+      else if (std::abs(val) < 1e-4 || std::abs(val) > 1e6 || suffix == 'f')
+        ss << llvm::format("%#.6g", val);
+      else if (suffix == 'L')
+        ss << llvm::format("%#.12Lg", val);
+      else
+        ss << llvm::format("%#.8g", val);
+
+      if (suffix != '\0')
+        ss << suffix;
+      return ss.str();
+    };
+
+    std::string str;
+    llvm::raw_string_ostream ss(str);
+    switch (BT->getKind()) {
+    default:
+      return "{ error: unknown builtin type '" + std::to_string(BT->getKind()) +
+             " '}";
+    case clang::BuiltinType::Bool:
+      ss << ((V.getBool()) ? "true" : "false");
+      return str;
+    case clang::BuiltinType::Char_S:
+      ss << '\'' << V.getChar_S() << '\'';
+      return str;
+    case clang::BuiltinType::SChar:
+      ss << '\'' << V.getSChar() << '\'';
+      return str;
+    case clang::BuiltinType::Char_U:
+      ss << '\'' << V.getChar_U() << '\'';
+      return str;
+    case clang::Buil...
[truncated]

/// @name Value and pretty printing support
/// @{

std::string ValueDataToString(const Value &V);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ValueTypeToString const but not ValueDataToString?

Comment on lines 122 to 124
void printData(llvm::raw_ostream &Out);
void print(llvm::raw_ostream &Out);
void dump();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These APIs are often called from a debugger, I think we really want to leave them const so it's safe to use for that use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the problem is that for objects in C++ with non-trivial destructors we need to call the object destructor when the Value is destroyed. When creating the Value object we need to get the destructor symbol in CompileDtorCall. We store the destructor-symbol pair for performance reasons and we store that in a map. I've marked the map mutable and we should probably be good to go.

@@ -416,6 +416,8 @@ Interpreter::Interpreter(std::unique_ptr<CompilerInstance> Instance,
return;
}
}

ValuePrintingInfo.resize(4);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a std::vector of a predefined elements. I removed this by switching to a std::array. Good catch!

return CharPtrToString((char *)V.getPtr());
}

std::string result = "{ ";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should take a pass over things and make sure identifiers meet the usual naming conventions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I addressed this.

The idea is to store a type-value pair in clang::Value which is updated by the
interpreter runtime. The class copies builtin types and boxes non-builtin types
to provide some lifetime control.

The patch enables default printers for C and C++ using a very minimalistic
approach. We handle enums, arrays and user types. Once we land this we can focus
on enabling user-defined pretty-printers which take control over printing of types

The work started as part of https://reviews.llvm.org/D146809, then we created
a giant in llvm#84769
Copy link

github-actions bot commented Jul 15, 2025

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff HEAD~1 HEAD --extensions cpp,h,c -- clang/test/Interpreter/pretty-print.cpp clang/include/clang/AST/ASTContext.h clang/include/clang/Interpreter/Interpreter.h clang/include/clang/Interpreter/Value.h clang/lib/Interpreter/Interpreter.cpp clang/lib/Interpreter/InterpreterUtils.cpp clang/lib/Interpreter/InterpreterUtils.h clang/lib/Interpreter/InterpreterValuePrinter.cpp clang/lib/Interpreter/Value.cpp clang/lib/Parse/ParseStmt.cpp clang/test/Interpreter/pretty-print.c clang/unittests/Interpreter/InterpreterTest.cpp
View the diff from clang-format here.
diff --git a/clang/lib/Interpreter/InterpreterValuePrinter.cpp b/clang/lib/Interpreter/InterpreterValuePrinter.cpp
index 2c23acf22..727b1497c 100644
--- a/clang/lib/Interpreter/InterpreterValuePrinter.cpp
+++ b/clang/lib/Interpreter/InterpreterValuePrinter.cpp
@@ -640,17 +640,15 @@ llvm::Expected<Expr *> Interpreter::convertExprToValue(Expr *E) {
 using namespace clang;
 
 // Temporary rvalue struct that need special care.
-extern "C" void * REPL_EXTERNAL_VISIBILITY
-__clang_Interpreter_SetValueWithAlloc(void *This, void *OutVal,
-                                      void *OpaqueType) {
+extern "C" void *REPL_EXTERNAL_VISIBILITY __clang_Interpreter_SetValueWithAlloc(
+    void *This, void *OutVal, void *OpaqueType) {
   Value &VRef = *(Value *)OutVal;
   VRef = Value(static_cast<Interpreter *>(This), OpaqueType);
   return VRef.getPtr();
 }
 
-extern "C" void REPL_EXTERNAL_VISIBILITY
-__clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType,
-                                    ...) {
+extern "C" void REPL_EXTERNAL_VISIBILITY __clang_Interpreter_SetValueNoAlloc(
+    void *This, void *OutVal, void *OpaqueType, ...) {
   Value &VRef = *(Value *)OutVal;
   Interpreter *I = static_cast<Interpreter *>(This);
   VRef = Value(I, OpaqueType);
@@ -732,7 +730,7 @@ __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType,
 // definition in the interpreter runtime. We should move it in a runtime header
 // which gets included by the interpreter and here.
 struct __clang_Interpreter_NewTag {};
-void * REPL_EXTERNAL_VISIBILITY
+void *REPL_EXTERNAL_VISIBILITY
 operator new(size_t __sz, void *__p, __clang_Interpreter_NewTag) noexcept {
   // Just forward to the standard operator placement new.
   return operator new(__sz, __p);

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precommit CI found some crashing issues that will need to be addressed, and I had some more testing requests to make sure we handle basics properly.

Comment on lines +33 to +40
float f = 4.2f; f
// CHECK-NEXT: (float) 4.20000f

double d = 4.21; d
// CHECK-NEXT: (double) 4.2100000

long double tau = 6.2831853; tau
// CHECK-NEXT: (long double) 6.28318530000L
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these values be sensitive to the target architecture the test is run on? (I'm thinking about things like PPC double double or x87 long double.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this to print long double however I might be proven wrong by the llvm buildbot jungle. We'll see :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also expect it to print (long double) but what I was talking about was that I don't think 6.28318530000 is a value that all targets will print.

struct S5 { int foo() { return 42; }};
&S5::foo
// CHECK-NEXT: (int (S5::*)()) Function @0x{{[0-9a-f]+}}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to get a test for references as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand what you mean.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

int i = 12;
int &ref = i;
ref

int &&rref = 100;
rref

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems a bit of an issue right now. It seems that clang loses the correct type in the DeclRefExpr:

p E->dump()
DeclRefExpr 0x7fb5e5014678 'int' lvalue Var 0x7fb5e5014518 'iref' 'int &' non_odr_use_constant

p E->getType()->dump()
BuiltinType 0x7fb5e283f310 'int'

It seems we can do something like:

p ((clang::DeclRefExpr*) E)->getDecl()->getType().dump()
LValueReferenceType 0x7fb5e50144d0 'int &'
`-BuiltinType 0x7fb5e283f310 'int'

Is that what you have in mind?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's the output we'd like to see (int & instead of int). Is it dumping the DeclRefExpr after doing some implicit conversions perchance? (Maybe it's loading the value and thus losing the reference in an lvalue conversion?)

struct S5 { int foo() { return 42; }};
&S5::foo
// CHECK-NEXT: (int (S5::*)()) Function @0x{{[0-9a-f]+}}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems a bit of an issue right now. It seems that clang loses the correct type in the DeclRefExpr:

p E->dump()
DeclRefExpr 0x7fb5e5014678 'int' lvalue Var 0x7fb5e5014518 'iref' 'int &' non_odr_use_constant

p E->getType()->dump()
BuiltinType 0x7fb5e283f310 'int'

It seems we can do something like:

p ((clang::DeclRefExpr*) E)->getDecl()->getType().dump()
LValueReferenceType 0x7fb5e50144d0 'int &'
`-BuiltinType 0x7fb5e283f310 'int'

Is that what you have in mind?

@@ -300,16 +640,17 @@ llvm::Expected<Expr *> Interpreter::ExtractValueFromExpr(Expr *E) {
using namespace clang;

// Temporary rvalue struct that need special care.
REPL_EXTERNAL_VISIBILITY void *
REPL_EXTERNAL_VISIBILITY extern "C" void *
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AaronBallman I will need some help here. I think the windows issue we are seeing stems from here. If I move the REPL_EXTERNAL_VISIBILITY after extern "C" msvc errors out. If I keep it there I do not think it has an effect to export the symbol...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, I think this should be:

extern "C" void * REPL_EXTERNAL_VISIBILITY func();

CC @compnerd

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That didn't seem to work, but then why does this work already?

extern "C" void REPL_EXTERNAL_VISIBILITY __clang_Interpreter_SetValueNoAlloc(

But this one uses a different style:

REPL_EXTERNAL_VISIBILITY void *

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants