[clang] [clang][bytecode] Initialize bases when bitcasting (PR #117179)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Nov 21 07:58:25 PST 2024
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>,
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>
Message-ID:
In-Reply-To: <llvm.org/llvm/llvm-project/pull/117179 at github.com>
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
Author: Timm Baeder (tbaederr)
<details>
<summary>Changes</summary>
---
Patch is 46.57 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/117179.diff
10 Files Affected:
- (added) clang/lib/AST/ByteCode/BitcastBuffer.cpp (+95)
- (added) clang/lib/AST/ByteCode/BitcastBuffer.h (+86)
- (modified) clang/lib/AST/ByteCode/Boolean.h (+1-3)
- (modified) clang/lib/AST/ByteCode/Integral.h (+1)
- (modified) clang/lib/AST/ByteCode/InterpBuiltinBitCast.cpp (+105-161)
- (modified) clang/lib/AST/CMakeLists.txt (+1)
- (added) clang/test/AST/ByteCode/builtin-bit-cast-bitfields.cpp (+437)
- (modified) clang/test/AST/ByteCode/builtin-bit-cast.cpp (+19-90)
- (added) clang/unittests/AST/ByteCode/BitcastBuffer.cpp (+87)
- (modified) clang/unittests/AST/ByteCode/CMakeLists.txt (+1)
``````````diff
diff --git a/clang/lib/AST/ByteCode/BitcastBuffer.cpp b/clang/lib/AST/ByteCode/BitcastBuffer.cpp
new file mode 100644
index 00000000000000..3793e0f4f2dbe2
--- /dev/null
+++ b/clang/lib/AST/ByteCode/BitcastBuffer.cpp
@@ -0,0 +1,95 @@
+//===-------------------- Bitcastbuffer.cpp ---------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#include "BitcastBuffer.h"
+
+using namespace clang;
+using namespace clang::interp;
+
+/// Returns the value of the bit in the given sequence of bytes.
+static inline bool bitof(const std::byte *B, Bits BitIndex) {
+ return (B[BitIndex.roundToBytes()] &
+ (std::byte{1} << (BitIndex.getOffsetInByte()))) != std::byte{0};
+}
+
+void BitcastBuffer::pushData(const std::byte *In, Bits BitOffset, Bits BitWidth,
+ Endian TargetEndianness) {
+ for (unsigned It = 0; It != BitWidth.getQuantity(); ++It) {
+ bool BitValue = bitof(In, Bits(It));
+ if (!BitValue)
+ continue;
+
+ Bits DstBit;
+ if (TargetEndianness == Endian::Little)
+ DstBit = BitOffset + Bits(It);
+ else
+ DstBit = size() - BitOffset - BitWidth + Bits(It);
+
+ size_t DstByte = DstBit.roundToBytes();
+ Data[DstByte] |= std::byte{1} << DstBit.getOffsetInByte();
+ }
+}
+
+std::unique_ptr<std::byte[]>
+BitcastBuffer::copyBits(Bits BitOffset, Bits BitWidth, Bits FullBitWidth,
+ Endian TargetEndianness) const {
+ assert(BitWidth.getQuantity() <= FullBitWidth.getQuantity());
+ assert(FullBitWidth.isFullByte());
+ auto Out = std::make_unique<std::byte[]>(FullBitWidth.roundToBytes());
+
+ for (unsigned It = 0; It != BitWidth.getQuantity(); ++It) {
+ Bits BitIndex;
+ if (TargetEndianness == Endian::Little)
+ BitIndex = BitOffset + Bits(It);
+ else
+ BitIndex = size() - BitWidth - BitOffset + Bits(It);
+
+ bool BitValue = bitof(Data.get(), BitIndex);
+ if (!BitValue)
+ continue;
+
+ Bits DstBit = Bits(It);
+ size_t DstByte = DstBit.roundToBytes();
+ Out[DstByte] |= std::byte{1} << DstBit.getOffsetInByte();
+ }
+
+ return Out;
+}
+
+#if 0
+ template<typename T>
+ static std::string hex(T t) {
+ std::stringstream stream;
+ stream << std::hex << (int)t;
+ return std::string(stream.str());
+ }
+
+
+ void BitcastBuffer::dump(bool AsHex = true) const {
+ llvm::errs() << "LSB\n ";
+ unsigned LineLength = 0;
+ for (unsigned I = 0; I != (FinalBitSize / 8); ++I) {
+ std::byte B = Data[I];
+ if (AsHex) {
+ std::stringstream stream;
+ stream << std::hex << (int)B;
+ llvm::errs() << stream.str();
+ LineLength += stream.str().size() + 1;
+ } else {
+ llvm::errs() << std::bitset<8>((int)B).to_string();
+ LineLength += 8 + 1;
+ // llvm::errs() << (int)B;
+ }
+ llvm::errs() << ' ';
+ }
+ llvm::errs() << '\n';
+
+ for (unsigned I = 0; I != LineLength; ++I)
+ llvm::errs() << ' ';
+ llvm::errs() << "MSB\n";
+ }
+#endif
diff --git a/clang/lib/AST/ByteCode/BitcastBuffer.h b/clang/lib/AST/ByteCode/BitcastBuffer.h
new file mode 100644
index 00000000000000..19a7e1151df4c3
--- /dev/null
+++ b/clang/lib/AST/ByteCode/BitcastBuffer.h
@@ -0,0 +1,86 @@
+//===--------------------- BitcastBuffer.h ----------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_AST_INTERP_BITCAST_BUFFER_H
+#define LLVM_CLANG_AST_INTERP_BITCAST_BUFFER_H
+
+#include <cassert>
+#include <cstddef>
+#include <memory>
+
+namespace clang {
+namespace interp {
+
+enum class Endian { Little, Big };
+
+/// A quantity in bits.
+struct Bits {
+ size_t N = 0;
+ Bits() = default;
+ static Bits zero() { return Bits(0); }
+ explicit Bits(size_t Quantity) : N(Quantity) {}
+ size_t getQuantity() const { return N; }
+ size_t roundToBytes() const { return N / 8; }
+ size_t getOffsetInByte() const { return N % 8; }
+ bool isFullByte() const { return N % 8 == 0; }
+ bool nonZero() const { return N != 0; }
+
+ Bits operator-(Bits Other) { return Bits(N - Other.N); }
+ Bits operator+(Bits Other) { return Bits(N + Other.N); }
+ Bits operator+=(size_t O) {
+ N += O;
+ return *this;
+ }
+};
+
+/// A quantity in bytes.
+struct Bytes {
+ size_t N;
+ explicit Bytes(size_t Quantity) : N(Quantity) {}
+ size_t getQuantity() const { return N; }
+ Bits toBits() const { return Bits(N * 8); }
+};
+
+/// Track what bits have been initialized to known values and which ones
+/// have indeterminate value.
+struct BitcastBuffer {
+ Bits FinalBitSize;
+ std::unique_ptr<std::byte[]> Data;
+
+ BitcastBuffer(Bits FinalBitSize) : FinalBitSize(FinalBitSize) {
+ assert(FinalBitSize.isFullByte());
+ unsigned ByteSize = FinalBitSize.roundToBytes();
+ Data = std::make_unique<std::byte[]>(ByteSize);
+ }
+
+ /// Returns the buffer size in bits.
+ Bits size() const { return FinalBitSize; }
+
+ /// Returns \c true if all bits in the buffer have been initialized.
+ bool allInitialized() const {
+ // FIXME: Implement.
+ return true;
+ }
+
+ /// Push \p BitWidth bits at \p BitOffset from \p In into the buffer.
+ /// \p TargetEndianness is the endianness of the target we're compiling for.
+ /// \p In must hold at least \p BitWidth many bits.
+ void pushData(const std::byte *In, Bits BitOffset, Bits BitWidth,
+ Endian TargetEndianness);
+
+ /// Copy \p BitWidth bits at offset \p BitOffset from the buffer.
+ /// \p TargetEndianness is the endianness of the target we're compiling for.
+ ///
+ /// The returned output holds exactly (\p FullBitWidth / 8) bytes.
+ std::unique_ptr<std::byte[]> copyBits(Bits BitOffset, Bits BitWidth,
+ Bits FullBitWidth,
+ Endian TargetEndianness) const;
+};
+
+} // namespace interp
+} // namespace clang
+#endif
diff --git a/clang/lib/AST/ByteCode/Boolean.h b/clang/lib/AST/ByteCode/Boolean.h
index 78d75e75c7531a..8380e85865ac55 100644
--- a/clang/lib/AST/ByteCode/Boolean.h
+++ b/clang/lib/AST/ByteCode/Boolean.h
@@ -82,9 +82,7 @@ class Boolean final {
Boolean truncate(unsigned TruncBits) const { return *this; }
static Boolean bitcastFromMemory(const std::byte *Buff, unsigned BitWidth) {
- // Boolean width is currently always 8 for all supported targets. If this
- // changes we need to get the bool width from the target info.
- assert(BitWidth == 8);
+ // Just load the first byte.
bool Val = static_cast<bool>(*Buff);
return Boolean(Val);
}
diff --git a/clang/lib/AST/ByteCode/Integral.h b/clang/lib/AST/ByteCode/Integral.h
index ca3674263aef4f..bb1688a8a7622c 100644
--- a/clang/lib/AST/ByteCode/Integral.h
+++ b/clang/lib/AST/ByteCode/Integral.h
@@ -181,6 +181,7 @@ template <unsigned Bits, bool Signed> class Integral final {
}
Integral truncate(unsigned TruncBits) const {
+ assert(TruncBits >= 1);
if (TruncBits >= Bits)
return *this;
const ReprT BitMask = (ReprT(1) << ReprT(TruncBits)) - 1;
diff --git a/clang/lib/AST/ByteCode/InterpBuiltinBitCast.cpp b/clang/lib/AST/ByteCode/InterpBuiltinBitCast.cpp
index b1230f92ddf1d4..2957b8a25ab958 100644
--- a/clang/lib/AST/ByteCode/InterpBuiltinBitCast.cpp
+++ b/clang/lib/AST/ByteCode/InterpBuiltinBitCast.cpp
@@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//
#include "InterpBuiltinBitCast.h"
+#include "BitcastBuffer.h"
#include "Boolean.h"
#include "Context.h"
#include "Floating.h"
@@ -21,9 +22,19 @@
using namespace clang;
using namespace clang::interp;
+/// Implement __builtin_bit_cast and related operations.
+/// Since our internal representation for data is more complex than
+/// something we can simply memcpy or memcmp, we first bitcast all the data
+/// into a buffer, which we then later use to copy the data into the target.
+
+// TODO:
+// - Try to minimize heap allocations.
+// - Optimize the common case of only pushing and pulling full
+// bytes to/from the buffer.
+
/// Used to iterate over pointer fields.
using DataFunc = llvm::function_ref<bool(const Pointer &P, PrimType Ty,
- size_t BitOffset, bool PackedBools)>;
+ Bits BitOffset, bool PackedBools)>;
#define BITCAST_TYPE_SWITCH(Expr, B) \
do { \
@@ -61,116 +72,44 @@ using DataFunc = llvm::function_ref<bool(const Pointer &P, PrimType Ty,
} \
} while (0)
-static bool bitof(std::byte B, unsigned BitIndex) {
- return (B & (std::byte{1} << BitIndex)) != std::byte{0};
-}
-
static void swapBytes(std::byte *M, size_t N) {
for (size_t I = 0; I != (N / 2); ++I)
std::swap(M[I], M[N - 1 - I]);
}
-/// Track what bits have been initialized to known values and which ones
-/// have indeterminate value.
-/// All offsets are in bits.
-struct BitcastBuffer {
- size_t SizeInBits = 0;
- llvm::SmallVector<std::byte> Data;
-
- BitcastBuffer() = default;
-
- size_t size() const { return SizeInBits; }
-
- const std::byte *data() const { return Data.data(); }
-
- std::byte *getBytes(unsigned BitOffset) const {
- assert(BitOffset % 8 == 0);
- assert(BitOffset < SizeInBits);
- return const_cast<std::byte *>(data() + (BitOffset / 8));
- }
-
- bool allInitialized() const {
- // FIXME: Implement.
- return true;
- }
-
- bool atByteBoundary() const { return (Data.size() * 8) == SizeInBits; }
-
- void pushBit(bool Value) {
- if (atByteBoundary())
- Data.push_back(std::byte{0});
-
- if (Value)
- Data.back() |= (std::byte{1} << (SizeInBits % 8));
- ++SizeInBits;
- }
-
- void pushData(const std::byte *data, size_t BitWidth, bool BigEndianTarget) {
- bool OnlyFullBytes = BitWidth % 8 == 0;
- unsigned NBytes = BitWidth / 8;
-
- size_t BitsHandled = 0;
- // Read all full bytes first
- for (size_t I = 0; I != NBytes; ++I) {
- std::byte B =
- BigEndianTarget ? data[NBytes - OnlyFullBytes - I] : data[I];
- for (unsigned X = 0; X != 8; ++X) {
- pushBit(bitof(B, X));
- ++BitsHandled;
- }
- }
-
- if (BitsHandled == BitWidth)
- return;
-
- // Rest of the bits.
- assert((BitWidth - BitsHandled) < 8);
- std::byte B = BigEndianTarget ? data[0] : data[NBytes];
- for (size_t I = 0, E = (BitWidth - BitsHandled); I != E; ++I) {
- pushBit(bitof(B, I));
- ++BitsHandled;
- }
-
- assert(BitsHandled == BitWidth);
- }
-};
-
-/// We use this to recursively iterate over all fields and elemends of a pointer
+/// We use this to recursively iterate over all fields and elements of a pointer
/// and extract relevant data for a bitcast.
-static bool enumerateData(const Pointer &P, const Context &Ctx, size_t Offset,
+static bool enumerateData(const Pointer &P, const Context &Ctx, Bits Offset,
DataFunc F) {
const Descriptor *FieldDesc = P.getFieldDesc();
assert(FieldDesc);
// Primitives.
if (FieldDesc->isPrimitive())
- return F(P, FieldDesc->getPrimType(), Offset, false);
+ return F(P, FieldDesc->getPrimType(), Offset, /*PackedBools=*/false);
// Primitive arrays.
if (FieldDesc->isPrimitiveArray()) {
- bool BigEndianTarget = Ctx.getASTContext().getTargetInfo().isBigEndian();
QualType ElemType = FieldDesc->getElemQualType();
size_t ElemSizeInBits = Ctx.getASTContext().getTypeSize(ElemType);
PrimType ElemT = *Ctx.classify(ElemType);
// Special case, since the bools here are packed.
bool PackedBools = FieldDesc->getType()->isExtVectorBoolType();
+ unsigned NumElems = FieldDesc->getNumElems();
bool Ok = true;
- for (unsigned I = 0; I != FieldDesc->getNumElems(); ++I) {
- unsigned Index = BigEndianTarget ? (FieldDesc->getNumElems() - 1 - I) : I;
- Ok = Ok && F(P.atIndex(Index), ElemT, Offset, PackedBools);
- Offset += ElemSizeInBits;
+ for (unsigned I = 0; I != NumElems; ++I) {
+ Ok = Ok && F(P.atIndex(I), ElemT, Offset, PackedBools);
+ Offset += PackedBools ? 1 : ElemSizeInBits;
}
return Ok;
}
// Composite arrays.
if (FieldDesc->isCompositeArray()) {
- bool BigEndianTarget = Ctx.getASTContext().getTargetInfo().isBigEndian();
QualType ElemType = FieldDesc->getElemQualType();
size_t ElemSizeInBits = Ctx.getASTContext().getTypeSize(ElemType);
for (unsigned I = 0; I != FieldDesc->getNumElems(); ++I) {
- unsigned Index = BigEndianTarget ? (FieldDesc->getNumElems() - 1 - I) : I;
- enumerateData(P.atIndex(Index).narrow(), Ctx, Offset, F);
+ enumerateData(P.atIndex(I).narrow(), Ctx, Offset, F);
Offset += ElemSizeInBits;
}
return true;
@@ -178,39 +117,27 @@ static bool enumerateData(const Pointer &P, const Context &Ctx, size_t Offset,
// Records.
if (FieldDesc->isRecord()) {
- bool BigEndianTarget = Ctx.getASTContext().getTargetInfo().isBigEndian();
const Record *R = FieldDesc->ElemRecord;
const ASTRecordLayout &Layout =
Ctx.getASTContext().getASTRecordLayout(R->getDecl());
bool Ok = true;
- auto enumerateFields = [&]() -> void {
- for (unsigned I = 0, N = R->getNumFields(); I != N; ++I) {
- const Record::Field *Fi =
- R->getField(BigEndianTarget ? (N - 1 - I) : I);
- Pointer Elem = P.atField(Fi->Offset);
- size_t BitOffset =
- Offset + Layout.getFieldOffset(Fi->Decl->getFieldIndex());
- Ok = Ok && enumerateData(Elem, Ctx, BitOffset, F);
- }
- };
- auto enumerateBases = [&]() -> void {
- for (unsigned I = 0, N = R->getNumBases(); I != N; ++I) {
- const Record::Base *B = R->getBase(BigEndianTarget ? (N - 1 - I) : I);
- Pointer Elem = P.atField(B->Offset);
- CharUnits ByteOffset =
- Layout.getBaseClassOffset(cast<CXXRecordDecl>(B->Decl));
- size_t BitOffset = Offset + Ctx.getASTContext().toBits(ByteOffset);
- Ok = Ok && enumerateData(Elem, Ctx, BitOffset, F);
- }
- };
-
- if (BigEndianTarget) {
- enumerateFields();
- enumerateBases();
- } else {
- enumerateBases();
- enumerateFields();
+ for (const Record::Field &Fi : R->fields()) {
+ Pointer Elem = P.atField(Fi.Offset);
+ Bits BitOffset =
+ Offset + Bits(Layout.getFieldOffset(Fi.Decl->getFieldIndex()));
+ Ok = Ok && enumerateData(Elem, Ctx, BitOffset, F);
+ }
+ for (const Record::Base &B : R->bases()) {
+ Pointer Elem = P.atField(B.Offset);
+ CharUnits ByteOffset =
+ Layout.getBaseClassOffset(cast<CXXRecordDecl>(B.Decl));
+ Bits BitOffset = Offset + Bits(Ctx.getASTContext().toBits(ByteOffset));
+ Ok = Ok && enumerateData(Elem, Ctx, BitOffset, F);
+ // FIXME: We should only (need to) do this when bitcasting OUT of the
+ // buffer, not when copying data into it.
+ if (Ok)
+ Elem.initialize();
}
return Ok;
@@ -221,7 +148,7 @@ static bool enumerateData(const Pointer &P, const Context &Ctx, size_t Offset,
static bool enumeratePointerFields(const Pointer &P, const Context &Ctx,
DataFunc F) {
- return enumerateData(P, Ctx, 0, F);
+ return enumerateData(P, Ctx, Bits::zero(), F);
}
// This function is constexpr if and only if To, From, and the types of
@@ -295,13 +222,12 @@ static bool CheckBitcastType(InterpState &S, CodePtr OpPC, QualType T,
static bool readPointerToBuffer(const Context &Ctx, const Pointer &FromPtr,
BitcastBuffer &Buffer, bool ReturnOnUninit) {
const ASTContext &ASTCtx = Ctx.getASTContext();
- bool SwapData = (ASTCtx.getTargetInfo().isLittleEndian() !=
- llvm::sys::IsLittleEndianHost);
- bool BigEndianTarget = ASTCtx.getTargetInfo().isBigEndian();
+ Endian TargetEndianness =
+ ASTCtx.getTargetInfo().isLittleEndian() ? Endian::Little : Endian::Big;
return enumeratePointerFields(
FromPtr, Ctx,
- [&](const Pointer &P, PrimType T, size_t BitOffset,
+ [&](const Pointer &P, PrimType T, Bits BitOffset,
bool PackedBools) -> bool {
if (!P.isInitialized()) {
assert(false && "Implement uninitialized value tracking");
@@ -314,43 +240,39 @@ static bool readPointerToBuffer(const Context &Ctx, const Pointer &FromPtr,
assert(false && "Implement casting to pointer types");
CharUnits ObjectReprChars = ASTCtx.getTypeSizeInChars(P.getType());
- unsigned BitWidth = ASTCtx.toBits(ObjectReprChars);
- llvm::SmallVector<std::byte> Buff(ObjectReprChars.getQuantity());
+ Bits BitWidth = Bits(ASTCtx.toBits(ObjectReprChars));
+ Bits FullBitWidth = BitWidth;
+ auto Buff =
+ std::make_unique<std::byte[]>(ObjectReprChars.getQuantity());
// Work around floating point types that contain unused padding bytes.
// This is really just `long double` on x86, which is the only
// fundamental type with padding bytes.
if (T == PT_Float) {
const Floating &F = P.deref<Floating>();
- unsigned NumBits =
- llvm::APFloatBase::getSizeInBits(F.getAPFloat().getSemantics());
- assert(NumBits % 8 == 0);
- assert(NumBits <= (ObjectReprChars.getQuantity() * 8));
- F.bitcastToMemory(Buff.data());
+ Bits NumBits = Bits(
+ llvm::APFloatBase::getSizeInBits(F.getAPFloat().getSemantics()));
+ assert(NumBits.isFullByte());
+ assert(NumBits.getQuantity() <= FullBitWidth.getQuantity());
+ F.bitcastToMemory(Buff.get());
// Now, only (maybe) swap the actual size of the float, excluding the
// padding bits.
- if (SwapData)
- swapBytes(Buff.data(), NumBits / 8);
+ if (llvm::sys::IsBigEndianHost)
+ swapBytes(Buff.get(), NumBits.roundToBytes());
} else {
if (const FieldDecl *FD = P.getField(); FD && FD->isBitField())
- BitWidth = FD->getBitWidthValue(ASTCtx);
+ BitWidth = Bits(std::min(FD->getBitWidthValue(ASTCtx),
+ (unsigned)FullBitWidth.getQuantity()));
else if (T == PT_Bool && PackedBools)
- BitWidth = 1;
-
- BITCAST_TYPE_SWITCH(T, {
- T Val = P.deref<T>();
- Val.bitcastToMemory(Buff.data());
- });
- if (SwapData)
- swapBytes(Buff.data(), ObjectReprChars.getQuantity());
- }
+ BitWidth = Bits(1);
- if (BitWidth != (Buff.size() * 8) && BigEndianTarget) {
- Buffer.pushData(Buff.data() + (Buff.size() - 1 - (BitWidth / 8)),
- BitWidth, BigEndianTarget);
- } else {
- Buffer.pushData(Buff.data(), BitWidth, BigEndianTarget);
+ BITCAST_TYPE_SWITCH(T, { P.deref<T>().bitcastToMemory(Buff.get()); });
+
+ if (llvm::sys::IsBigEndianHost)
+ swapBytes(Buff.get(), FullBitWidth.roundToBytes());
}
+
+ Buffer.pushData(Buff.get(), BitOffset, BitWidth, TargetEndianness);
return true;
});
}
@@ -362,16 +284,21 @@ bool clang::interp::DoBitCast(InterpState &S, CodePtr OpPC, const Pointer &Ptr,
assert(Ptr.isBlockPointer());
assert(Buff);
- BitcastBuffer Buffer;
+ Bits BitSize = Bytes(BuffSize).toBits();
+ BitcastBuffer Buffer(BitSize);
if (!CheckBitcastType(S, OpPC, Ptr.getType(), /*IsToType=*/false))
return false;
bool Success = readPointerToBuffer(S.getContext(), Ptr, Buffer,
/*ReturnOnUninit=*/false);
- assert(Buffer.size() == BuffSize * 8);
-
HasIndeterminateBits = !Buffer.allInitialized();
- std::memcpy(Buff, Buffer.data(), BuffSize);
+
+ const...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/117179
More information about the cfe-commits
mailing list