Skip to content

[clang][Interp] Implement __builtin_bit_cast #68288

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

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions clang/lib/AST/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ add_clang_library(clangAST
Interp/Frame.cpp
Interp/Function.cpp
Interp/InterpBuiltin.cpp
Interp/InterpBitcast.cpp
Interp/Floating.cpp
Interp/Interp.cpp
Interp/InterpBlock.cpp
Expand Down
17 changes: 15 additions & 2 deletions clang/lib/AST/Interp/Boolean.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
#ifndef LLVM_CLANG_AST_INTERP_BOOLEAN_H
#define LLVM_CLANG_AST_INTERP_BOOLEAN_H

#include <cstddef>
#include <cstdint>
#include "Integral.h"
#include "clang/AST/APValue.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ComparisonCategories.h"
#include "llvm/ADT/APSInt.h"
#include "llvm/Support/MathExtras.h"
Expand Down Expand Up @@ -66,6 +65,12 @@ class Boolean final {
Boolean toUnsigned() const { return *this; }

constexpr static unsigned bitWidth() { return 1; }
constexpr static unsigned objectReprBits() { return 8; }
constexpr static unsigned valueReprBytes(const ASTContext &Ctx) { return 1; }
constexpr static unsigned valueReprBits(const ASTContext &Ctx) {
return 8;
} // FIXME: Is this correct?
Copy link
Collaborator

Choose a reason for hiding this comment

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

https://eel.is/c++draft/basic#fundamental-10 -- so it's implementation-defined how many bits are in the value representation; we default to 8 (

BoolWidth = BoolAlign = 8;
) and I didn't see any targets that use a different value.

I think we may need to use TargetInfo for these at some point so we're matching the target when constant evaluating on the host. But I think that can happen later.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ctx.getTargetInfo().getBoolWidth() should do the trick


bool isZero() const { return !V; }
bool isMin() const { return isZero(); }

Expand Down Expand Up @@ -107,6 +112,14 @@ class Boolean final {
return Boolean(!Value.isZero());
}

static Boolean bitcastFromMemory(const std::byte *Buff, unsigned BitWidth) {
assert(BitWidth == 8);
bool Val = static_cast<bool>(*Buff);
return Boolean(Val);
}

void bitcastToMemory(std::byte *Buff) { std::memcpy(Buff, &V, sizeof(V)); }
Comment on lines +115 to +121
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a tradeoff to be made, here: booleans are among the primitive types that have padding bits, though I believe them to be by far the most commonly used (other contenders include long double and _BitInt(4)).

The wrinkle is something like this:

constexpr bool b() {
    return std::bit_cast<bool, uint8_t>(0x02);
}

int main() {
    if constexpr (b() == 0)
        return b();

    return 1;    
}

For that sample, gcc produces a binary that exits with code 2: https://compiler-explorer.com/z/jes71o85h , and current clang refuses to compile with a note: value 2 cannot be represented in type 'bool'. To preserve that behavior, I think that means here bitcastToMemory ought to be a fail-able operation that checks all bits above the LSB are zero.

There's another question about whether it's worth giving bool special treatment: should only Boolean::bitcastToMemory be a fail-able operation? In other words, given

constexpr auto a = std::bit_cast<uint8_t>(false);
constexpr auto b = std::bit_cast<__int128_t>((long double)0);

constexpr auto c = std::bit_cast<bool>('\x02');
constexpr auto d = std::bit_cast<long double>((__int128_t)~0);

Right now, a & d succeed. b & c fail to produce constant values for different reasons: c because the evaluator sees that it'd lose information by allowing the bit-cast, and b because it doesn't want to check.

I see two fully consistent interpretations, here (considering both implementation & semantics):

  1. a fails for the same reason as b, and then Interp is free to allow c & d as a simple memcpy + mask (to throw away any padding bits)
  2. a and b both succeed, but then c and d must fail for the same reason.

Both are backwards-incompatible changes, though: code that used to compile with the old constant interpreter would fail under the new one. The third choice preserves backwards compatibility by holding bool out as the only type whose values get checked for represent-ability. (NB: that's a link to the code from #74775 (the current implementation is subtly different, but the behavior is the same because the current evaluator doesn't [fully] support _BitInt(X))


static Boolean zero() { return from(false); }

template <typename T>
Expand Down
69 changes: 68 additions & 1 deletion clang/lib/AST/Interp/ByteCodeExprGen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include "Function.h"
#include "PrimType.h"
#include "Program.h"
#include "State.h"

using namespace clang;
using namespace clang::interp;
Expand Down Expand Up @@ -73,6 +72,71 @@ template <class Emitter> class OptionScope final {
} // namespace interp
} // namespace clang

// This function is constexpr if and only if To, From, and the types of
// all subobjects of To and From are types T such that...
// (3.1) - is_union_v<T> is false;
// (3.2) - is_pointer_v<T> is false;
// (3.3) - is_member_pointer_v<T> is false;
// (3.4) - is_volatile_v<T> is false; and
// (3.5) - T has no non-static data members of reference type
template <class Emitter>
bool ByteCodeExprGen<Emitter>::emitBuiltinBitCast(const CastExpr *E) {
const Expr *SubExpr = E->getSubExpr();
QualType FromType = SubExpr->getType();
QualType ToType = E->getType();
std::optional<PrimType> ToT = classify(ToType);

// FIXME: This is wrong. We need to do the bitcast and then
// throw away the result, so we still get the diagnostics.
if (DiscardResult)
return this->discard(SubExpr);

if (ToType->isNullPtrType()) {
if (!this->discard(SubExpr))
return false;

return this->emitNullPtr(E);
}

if (FromType->isNullPtrType() && ToT) {
if (!this->discard(SubExpr))
return false;

return visitZeroInitializer(*ToT, ToType, E);
}
assert(!ToType->isReferenceType());

// Get a pointer to the value-to-cast on the stack.
if (!this->visit(SubExpr))
return false;

if (!ToT || ToT == PT_Ptr) {
// Conversion to an array or record type.
return this->emitBitCastPtr(E);
}

assert(ToT);

const llvm::fltSemantics *TargetSemantics = nullptr;
if (ToT == PT_Float)
TargetSemantics = &Ctx.getFloatSemantics(ToType);

// Conversion to a primitive type. FromType can be another
// primitive type, or a record/array.
bool ToTypeIsUChar = (ToType->isSpecificBuiltinType(BuiltinType::UChar) ||
ToType->isSpecificBuiltinType(BuiltinType::Char_U));
uint32_t ResultBitWidth = std::max(Ctx.getBitWidth(ToType), 8u);

if (!this->emitBitCast(*ToT, ToTypeIsUChar || ToType->isStdByteType(),
ResultBitWidth, TargetSemantics, E))
return false;

if (DiscardResult)
return this->emitPop(*ToT, E);

return true;
}

template <class Emitter>
bool ByteCodeExprGen<Emitter>::VisitCastExpr(const CastExpr *CE) {
const Expr *SubExpr = CE->getSubExpr();
Expand All @@ -93,6 +157,9 @@ bool ByteCodeExprGen<Emitter>::VisitCastExpr(const CastExpr *CE) {
});
}

case CK_LValueToRValueBitCast:
return this->emitBuiltinBitCast(CE);

case CK_UncheckedDerivedToBase:
case CK_DerivedToBase: {
if (!this->visit(SubExpr))
Expand Down
1 change: 1 addition & 0 deletions clang/lib/AST/Interp/ByteCodeExprGen.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ class ByteCodeExprGen : public ConstStmtVisitor<ByteCodeExprGen<Emitter>, bool>,
bool emitRecordDestruction(const Descriptor *Desc);
unsigned collectBaseOffset(const RecordType *BaseType,
const RecordType *DerivedType);
bool emitBuiltinBitCast(const CastExpr *E);

protected:
/// Variable to storage mapping.
Expand Down
11 changes: 11 additions & 0 deletions clang/lib/AST/Interp/Floating.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "Primitives.h"
#include "clang/AST/APValue.h"
#include "clang/AST/ASTContext.h"
#include "llvm/ADT/APFloat.h"

namespace clang {
Expand Down Expand Up @@ -84,6 +85,12 @@ class Floating final {
}

unsigned bitWidth() const { return F.semanticsSizeInBits(F.getSemantics()); }
unsigned objectReprBits() { return F.semanticsSizeInBits(F.getSemantics()); }

unsigned valueReprBytes(const ASTContext &Ctx) {
return Ctx.toCharUnitsFromBits(F.semanticsSizeInBits(F.getSemantics()))
.getQuantity();
}

bool isSigned() const { return true; }
bool isNegative() const { return F.isNegative(); }
Expand Down Expand Up @@ -134,6 +141,10 @@ class Floating final {

return Floating(APFloat(Sem, API));
}
void bitcastToMemory(std::byte *Buff) {
llvm::APInt API = F.bitcastToAPInt();
llvm::StoreIntToMemory(API, (uint8_t *)Buff, bitWidth() / 8);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should that use getCharWidth() ?

}

// === Serialization support ===
size_t bytesToSerialize() const {
Expand Down
23 changes: 22 additions & 1 deletion clang/lib/AST/Interp/Integral.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
#ifndef LLVM_CLANG_AST_INTERP_INTEGRAL_H
#define LLVM_CLANG_AST_INTERP_INTEGRAL_H

#include "clang/AST/ComparisonCategories.h"
#include "clang/AST/APValue.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ComparisonCategories.h"
#include "llvm/ADT/APSInt.h"
#include "llvm/Support/MathExtras.h"
#include "llvm/Support/raw_ostream.h"
Expand Down Expand Up @@ -119,6 +120,13 @@ template <unsigned Bits, bool Signed> class Integral final {
}

constexpr static unsigned bitWidth() { return Bits; }
constexpr static unsigned objectReprBits() { return Bits; }
constexpr static unsigned valueReprBits(const ASTContext &Ctx) {
return Bits;
}
constexpr static unsigned valueReprBytes(const ASTContext &Ctx) {
return Ctx.toCharUnitsFromBits(Bits).getQuantity();
}

bool isZero() const { return !V; }

Expand Down Expand Up @@ -185,6 +193,19 @@ template <unsigned Bits, bool Signed> class Integral final {
return Integral(Value);
}

static Integral bitcastFromMemory(const std::byte *Buff, unsigned BitWidth) {
assert(BitWidth == sizeof(ReprT) * 8);
ReprT V;

std::memcpy(&V, Buff, sizeof(ReprT));
return Integral(V);
}

void bitcastToMemory(std::byte *Buff) const {
assert(Buff);
std::memcpy(Buff, &V, sizeof(ReprT));
}

static bool inRange(int64_t Value, unsigned NumBits) {
return CheckRange<ReprT, Min, Max>(Value);
}
Expand Down
19 changes: 18 additions & 1 deletion clang/lib/AST/Interp/IntegralAP.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "clang/AST/APValue.h"
#include "clang/AST/ComparisonCategories.h"
#include "llvm/ADT/APInt.h"
#include "llvm/ADT/APSInt.h"
#include "llvm/Support/MathExtras.h"
#include "llvm/Support/raw_ostream.h"
Expand Down Expand Up @@ -61,7 +62,7 @@ template <bool Signed> class IntegralAP final {

IntegralAP(APInt V) : V(V) {}
/// Arbitrary value for uninitialized variables.
IntegralAP() : IntegralAP(-1, 1024) {}
IntegralAP() : IntegralAP(-1, 17) {}

IntegralAP operator-() const { return IntegralAP(-V); }
IntegralAP operator-(const IntegralAP &Other) const {
Expand Down Expand Up @@ -123,6 +124,11 @@ template <bool Signed> class IntegralAP final {
}

constexpr unsigned bitWidth() const { return V.getBitWidth(); }
constexpr unsigned objectReprBits() { return bitWidth(); }
constexpr unsigned valueReprBits(const ASTContext &Ctx) { return bitWidth(); }
constexpr unsigned valueReprBytes(const ASTContext &Ctx) {
return Ctx.toCharUnitsFromBits(bitWidth()).getQuantity();
}

APSInt toAPSInt(unsigned Bits = 0) const {
if (Bits == 0)
Expand All @@ -145,6 +151,17 @@ template <bool Signed> class IntegralAP final {

unsigned countLeadingZeros() const { return V.countl_zero(); }

static IntegralAP bitcastFromMemory(const std::byte *Buff,
unsigned BitWidth) {
APInt V(BitWidth, static_cast<uint64_t>(0), Signed);
llvm::LoadIntFromMemory(V, (const uint8_t *)Buff, BitWidth / 8);
return IntegralAP(V);
}

void bitcastToMemory(std::byte *Buff) const {
llvm::StoreIntToMemory(V, (uint8_t *)Buff, bitWidth() / 8);
}

void print(llvm::raw_ostream &OS) const { OS << V; }
std::string toDiagnosticString(const ASTContext &Ctx) const {
std::string NameStr;
Expand Down
17 changes: 17 additions & 0 deletions clang/lib/AST/Interp/Interp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,24 @@ bool CheckDeclRef(InterpState &S, CodePtr OpPC, const DeclRefExpr *DR) {
return false;
}
}
return false;
}

bool CheckBitcast(InterpState &S, CodePtr OpPC, bool HasIndeterminateBits,
bool TargetIsUCharOrByte) {

// This is always fine.
if (!HasIndeterminateBits)
return true;

// Indeterminate bits can only be bitcast to unsigned char or std::byte.
if (TargetIsUCharOrByte)
return true;

const Expr *E = S.Current->getExpr(OpPC);
QualType ExprType = E->getType();
S.FFDiag(E, diag::note_constexpr_bit_cast_indet_dest)
<< ExprType << S.getLangOpts().CharIsSigned << E->getSourceRange();
return false;
}

Expand Down
56 changes: 56 additions & 0 deletions clang/lib/AST/Interp/Interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ bool CheckFloatResult(InterpState &S, CodePtr OpPC, const Floating &Result,
/// Checks why the given DeclRefExpr is invalid.
bool CheckDeclRef(InterpState &S, CodePtr OpPC, const DeclRefExpr *DR);

bool CheckBitcast(InterpState &S, CodePtr OpPC, bool HasIndeterminateBits,
bool TargetIsUCharOrByte);

/// Interpreter entry point.
bool Interpret(InterpState &S, APValue &Result);

Expand All @@ -198,6 +201,18 @@ bool InterpretBuiltin(InterpState &S, CodePtr OpPC, const Function *F,
bool InterpretOffsetOf(InterpState &S, CodePtr OpPC, const OffsetOfExpr *E,
llvm::ArrayRef<int64_t> ArrayIndices, int64_t &Result);

/// Perform a bitcast of all fields of P into Buff. This performs the
/// actions of a __builtin_bit_cast expression when the target type
/// is primitive.
bool DoBitCast(InterpState &S, CodePtr OpPC, const Pointer &P, std::byte *Buff,
size_t BuffSize, bool &HasIndeterminateBits);

/// Perform a bitcast of all fields of P into the fields of DestPtr.
/// This performs the actions of a __builtin_bit_cast expression when
/// the target type is a composite type.
bool DoBitCastToPtr(InterpState &S, const Pointer &P, Pointer &DestPtr,
CodePtr PC);

enum class ArithOp { Add, Sub };

//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -1561,6 +1576,47 @@ template <PrimType TIn, PrimType TOut> bool Cast(InterpState &S, CodePtr OpPC) {
return true;
}

template <PrimType Name, class ToT = typename PrimConv<Name>::T>
bool BitCast(InterpState &S, CodePtr OpPC, bool TargetIsUCharOrByte,
uint32_t ResultBitWidth, const llvm::fltSemantics *Sem) {
assert(ResultBitWidth > 0);
const Pointer &FromPtr = S.Stk.pop<Pointer>();

size_t BuffSize = ResultBitWidth / 8;
llvm::SmallVector<std::byte> Buff(BuffSize);
bool HasIndeterminateBits = false;

if (!DoBitCast(S, OpPC, FromPtr, Buff.data(), BuffSize, HasIndeterminateBits))
return false;

if (!CheckBitcast(S, OpPC, HasIndeterminateBits, TargetIsUCharOrByte))
return false;

if constexpr (std::is_same_v<ToT, Floating>) {
assert(Sem);
S.Stk.push<Floating>(Floating::bitcastFromMemory(Buff.data(), *Sem));
} else {
assert(!Sem);
S.Stk.push<ToT>(ToT::bitcastFromMemory(Buff.data(), ResultBitWidth));
}
return true;
}

/// 1) Pops a pointer from the stack
/// 2) Peeks a pointer
/// 3) Bitcasts the contents of the first pointer to the
/// fields of the second pointer.
inline bool BitCastPtr(InterpState &S, CodePtr OpPC) {
const Pointer &FromPtr = S.Stk.pop<Pointer>();
Pointer &ToPtr = S.Stk.peek<Pointer>();

// FIXME: We should CheckLoad() for FromPtr and ToPtr here, I think.
if (!DoBitCastToPtr(S, FromPtr, ToPtr, OpPC))
return false;

return true;
}

/// 1) Pops a Floating from the stack.
/// 2) Pushes a new floating on the stack that uses the given semantics.
inline bool CastFP(InterpState &S, CodePtr OpPC, const llvm::fltSemantics *Sem,
Expand Down
Loading