| //===--- Pointer.h - Types for the constexpr VM -----------------*- 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 |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Defines the classes responsible for pointer tracking. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #ifndef LLVM_CLANG_AST_INTERP_POINTER_H |
| #define LLVM_CLANG_AST_INTERP_POINTER_H |
| |
| #include "Descriptor.h" |
| #include "InterpBlock.h" |
| #include "clang/AST/ComparisonCategories.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/AST/DeclCXX.h" |
| #include "clang/AST/Expr.h" |
| #include "llvm/ADT/PointerUnion.h" |
| #include "llvm/Support/raw_ostream.h" |
| |
| namespace clang { |
| namespace interp { |
| class Block; |
| class DeadBlock; |
| class Pointer; |
| class Context; |
| template <unsigned A, bool B> class Integral; |
| enum PrimType : unsigned; |
| |
| class Pointer; |
| inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Pointer &P); |
| |
| struct BlockPointer { |
| /// The block the pointer is pointing to. |
| Block *Pointee; |
| /// Start of the current subfield. |
| unsigned Base; |
| }; |
| |
| struct IntPointer { |
| const Descriptor *Desc; |
| uint64_t Value; |
| }; |
| |
| enum class Storage { Block, Int }; |
| |
| /// A pointer to a memory block, live or dead. |
| /// |
| /// This object can be allocated into interpreter stack frames. If pointing to |
| /// a live block, it is a link in the chain of pointers pointing to the block. |
| /// |
| /// In the simplest form, a Pointer has a Block* (the pointee) and both Base |
| /// and Offset are 0, which means it will point to raw data. |
| /// |
| /// The Base field is used to access metadata about the data. For primitive |
| /// arrays, the Base is followed by an InitMap. In a variety of cases, the |
| /// Base is preceded by an InlineDescriptor, which is used to track the |
| /// initialization state, among other things. |
| /// |
| /// The Offset field is used to access the actual data. In other words, the |
| /// data the pointer decribes can be found at |
| /// Pointee->rawData() + Pointer.Offset. |
| /// |
| /// |
| /// Pointee Offset |
| /// │ │ |
| /// │ │ |
| /// ▼ ▼ |
| /// ┌───────┬────────────┬─────────┬────────────────────────────┐ |
| /// │ Block │ InlineDesc │ InitMap │ Actual Data │ |
| /// └───────┴────────────┴─────────┴────────────────────────────┘ |
| /// ▲ |
| /// │ |
| /// │ |
| /// Base |
| class Pointer { |
| private: |
| static constexpr unsigned PastEndMark = ~0u; |
| static constexpr unsigned RootPtrMark = ~0u; |
| |
| public: |
| Pointer() { |
| StorageKind = Storage::Int; |
| PointeeStorage.Int.Value = 0; |
| PointeeStorage.Int.Desc = nullptr; |
| } |
| Pointer(Block *B); |
| Pointer(Block *B, uint64_t BaseAndOffset); |
| Pointer(const Pointer &P); |
| Pointer(Pointer &&P); |
| Pointer(uint64_t Address, const Descriptor *Desc, uint64_t Offset = 0) |
| : Offset(Offset), StorageKind(Storage::Int) { |
| PointeeStorage.Int.Value = Address; |
| PointeeStorage.Int.Desc = Desc; |
| } |
| ~Pointer(); |
| |
| void operator=(const Pointer &P); |
| void operator=(Pointer &&P); |
| |
| /// Equality operators are just for tests. |
| bool operator==(const Pointer &P) const { |
| if (P.StorageKind != StorageKind) |
| return false; |
| if (isIntegralPointer()) |
| return P.asIntPointer().Value == asIntPointer().Value && |
| Offset == P.Offset; |
| |
| assert(isBlockPointer()); |
| return P.asBlockPointer().Pointee == asBlockPointer().Pointee && |
| P.asBlockPointer().Base == asBlockPointer().Base && |
| Offset == P.Offset; |
| } |
| |
| bool operator!=(const Pointer &P) const { return !(P == *this); } |
| |
| /// Converts the pointer to an APValue. |
| APValue toAPValue() const; |
| |
| /// Converts the pointer to a string usable in diagnostics. |
| std::string toDiagnosticString(const ASTContext &Ctx) const; |
| |
| uint64_t getIntegerRepresentation() const { |
| if (isIntegralPointer()) |
| return asIntPointer().Value + (Offset * elemSize()); |
| return reinterpret_cast<uint64_t>(asBlockPointer().Pointee) + Offset; |
| } |
| |
| /// Converts the pointer to an APValue that is an rvalue. |
| std::optional<APValue> toRValue(const Context &Ctx) const; |
| |
| /// Offsets a pointer inside an array. |
| [[nodiscard]] Pointer atIndex(uint64_t Idx) const { |
| if (isIntegralPointer()) |
| return Pointer(asIntPointer().Value, asIntPointer().Desc, Idx); |
| |
| if (asBlockPointer().Base == RootPtrMark) |
| return Pointer(asBlockPointer().Pointee, RootPtrMark, |
| getDeclDesc()->getSize()); |
| uint64_t Off = Idx * elemSize(); |
| if (getFieldDesc()->ElemDesc) |
| Off += sizeof(InlineDescriptor); |
| else |
| Off += sizeof(InitMapPtr); |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, |
| asBlockPointer().Base + Off); |
| } |
| |
| /// Creates a pointer to a field. |
| [[nodiscard]] Pointer atField(unsigned Off) const { |
| unsigned Field = Offset + Off; |
| if (isIntegralPointer()) |
| return Pointer(asIntPointer().Value + Field, asIntPointer().Desc); |
| return Pointer(asBlockPointer().Pointee, Field, Field); |
| } |
| |
| /// Subtract the given offset from the current Base and Offset |
| /// of the pointer. |
| [[nodiscard]] Pointer atFieldSub(unsigned Off) const { |
| assert(Offset >= Off); |
| unsigned O = Offset - Off; |
| return Pointer(asBlockPointer().Pointee, O, O); |
| } |
| |
| /// Restricts the scope of an array element pointer. |
| [[nodiscard]] Pointer narrow() const { |
| if (!isBlockPointer()) |
| return *this; |
| assert(isBlockPointer()); |
| // Null pointers cannot be narrowed. |
| if (isZero() || isUnknownSizeArray()) |
| return *this; |
| |
| // Pointer to an array of base types - enter block. |
| if (asBlockPointer().Base == RootPtrMark) |
| return Pointer(asBlockPointer().Pointee, sizeof(InlineDescriptor), |
| Offset == 0 ? Offset : PastEndMark); |
| |
| // Pointer is one past end - magic offset marks that. |
| if (isOnePastEnd()) |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, |
| PastEndMark); |
| |
| // Primitive arrays are a bit special since they do not have inline |
| // descriptors. If Offset != Base, then the pointer already points to |
| // an element and there is nothing to do. Otherwise, the pointer is |
| // adjusted to the first element of the array. |
| if (inPrimitiveArray()) { |
| if (Offset != asBlockPointer().Base) |
| return *this; |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, |
| Offset + sizeof(InitMapPtr)); |
| } |
| |
| // Pointer is to a field or array element - enter it. |
| if (Offset != asBlockPointer().Base) |
| return Pointer(asBlockPointer().Pointee, Offset, Offset); |
| |
| // Enter the first element of an array. |
| if (!getFieldDesc()->isArray()) |
| return *this; |
| |
| const unsigned NewBase = asBlockPointer().Base + sizeof(InlineDescriptor); |
| return Pointer(asBlockPointer().Pointee, NewBase, NewBase); |
| } |
| |
| /// Expands a pointer to the containing array, undoing narrowing. |
| [[nodiscard]] Pointer expand() const { |
| if (isElementPastEnd()) { |
| // Revert to an outer one-past-end pointer. |
| unsigned Adjust; |
| if (inPrimitiveArray()) |
| Adjust = sizeof(InitMapPtr); |
| else |
| Adjust = sizeof(InlineDescriptor); |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, |
| asBlockPointer().Base + getSize() + Adjust); |
| } |
| |
| // Do not step out of array elements. |
| if (asBlockPointer().Base != Offset) |
| return *this; |
| |
| // If at base, point to an array of base types. |
| if (asBlockPointer().Base == 0 || |
| asBlockPointer().Base == sizeof(InlineDescriptor)) |
| return Pointer(asBlockPointer().Pointee, RootPtrMark, 0); |
| |
| // Step into the containing array, if inside one. |
| unsigned Next = asBlockPointer().Base - getInlineDesc()->Offset; |
| const Descriptor *Desc = |
| Next == 0 ? getDeclDesc() : getDescriptor(Next)->Desc; |
| if (!Desc->IsArray) |
| return *this; |
| return Pointer(asBlockPointer().Pointee, Next, Offset); |
| } |
| |
| /// Checks if the pointer is null. |
| bool isZero() const { |
| if (isBlockPointer()) |
| return asBlockPointer().Pointee == nullptr; |
| assert(isIntegralPointer()); |
| return asIntPointer().Value == 0 && Offset == 0; |
| } |
| /// Checks if the pointer is live. |
| bool isLive() const { |
| if (isIntegralPointer()) |
| return true; |
| return asBlockPointer().Pointee && !asBlockPointer().Pointee->IsDead; |
| } |
| /// Checks if the item is a field in an object. |
| bool isField() const { |
| if (isIntegralPointer()) |
| return false; |
| |
| unsigned Base = asBlockPointer().Base; |
| return Base != 0 && Base != sizeof(InlineDescriptor) && |
| Base != RootPtrMark && getFieldDesc()->asDecl(); |
| } |
| |
| /// Accessor for information about the declaration site. |
| const Descriptor *getDeclDesc() const { |
| if (isIntegralPointer()) |
| return asIntPointer().Desc; |
| |
| assert(isBlockPointer()); |
| assert(asBlockPointer().Pointee); |
| return asBlockPointer().Pointee->Desc; |
| } |
| SourceLocation getDeclLoc() const { return getDeclDesc()->getLocation(); } |
| |
| /// Returns the expression or declaration the pointer has been created for. |
| DeclTy getSource() const { |
| if (isBlockPointer()) |
| return getDeclDesc()->getSource(); |
| |
| assert(isIntegralPointer()); |
| return asIntPointer().Desc ? asIntPointer().Desc->getSource() : DeclTy(); |
| } |
| |
| /// Returns a pointer to the object of which this pointer is a field. |
| [[nodiscard]] Pointer getBase() const { |
| if (asBlockPointer().Base == RootPtrMark) { |
| assert(Offset == PastEndMark && "cannot get base of a block"); |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, 0); |
| } |
| unsigned NewBase = asBlockPointer().Base - getInlineDesc()->Offset; |
| return Pointer(asBlockPointer().Pointee, NewBase, NewBase); |
| } |
| /// Returns the parent array. |
| [[nodiscard]] Pointer getArray() const { |
| if (asBlockPointer().Base == RootPtrMark) { |
| assert(Offset != 0 && Offset != PastEndMark && "not an array element"); |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, 0); |
| } |
| assert(Offset != asBlockPointer().Base && "not an array element"); |
| return Pointer(asBlockPointer().Pointee, asBlockPointer().Base, |
| asBlockPointer().Base); |
| } |
| |
| /// Accessors for information about the innermost field. |
| const Descriptor *getFieldDesc() const { |
| if (isIntegralPointer()) |
| return asIntPointer().Desc; |
| if (isBlockPointer() && |
| (asBlockPointer().Base == 0 || |
| asBlockPointer().Base == sizeof(InlineDescriptor) || |
| asBlockPointer().Base == RootPtrMark)) |
| return getDeclDesc(); |
| return getInlineDesc()->Desc; |
| } |
| |
| /// Returns the type of the innermost field. |
| QualType getType() const { |
| if (inPrimitiveArray() && Offset != asBlockPointer().Base) { |
| // Unfortunately, complex types are not array types in clang, but they are |
| // for us. |
| if (const auto *AT = getFieldDesc()->getType()->getAsArrayTypeUnsafe()) |
| return AT->getElementType(); |
| if (const auto *CT = getFieldDesc()->getType()->getAs<ComplexType>()) |
| return CT->getElementType(); |
| } |
| return getFieldDesc()->getType(); |
| } |
| |
| [[nodiscard]] Pointer getDeclPtr() const { |
| return Pointer(asBlockPointer().Pointee); |
| } |
| |
| /// Returns the element size of the innermost field. |
| size_t elemSize() const { |
| if (isIntegralPointer()) { |
| if (!asIntPointer().Desc) |
| return 1; |
| return asIntPointer().Desc->getElemSize(); |
| } |
| |
| if (asBlockPointer().Base == RootPtrMark) |
| return getDeclDesc()->getSize(); |
| return getFieldDesc()->getElemSize(); |
| } |
| /// Returns the total size of the innermost field. |
| size_t getSize() const { |
| assert(isBlockPointer()); |
| return getFieldDesc()->getSize(); |
| } |
| |
| /// Returns the offset into an array. |
| unsigned getOffset() const { |
| assert(Offset != PastEndMark && "invalid offset"); |
| if (asBlockPointer().Base == RootPtrMark) |
| return Offset; |
| |
| unsigned Adjust = 0; |
| if (Offset != asBlockPointer().Base) { |
| if (getFieldDesc()->ElemDesc) |
| Adjust = sizeof(InlineDescriptor); |
| else |
| Adjust = sizeof(InitMapPtr); |
| } |
| return Offset - asBlockPointer().Base - Adjust; |
| } |
| |
| /// Whether this array refers to an array, but not |
| /// to the first element. |
| bool isArrayRoot() const { |
| return inArray() && Offset == asBlockPointer().Base; |
| } |
| |
| /// Checks if the innermost field is an array. |
| bool inArray() const { |
| if (isBlockPointer()) |
| return getFieldDesc()->IsArray; |
| return false; |
| } |
| /// Checks if the structure is a primitive array. |
| bool inPrimitiveArray() const { |
| if (isBlockPointer()) |
| return getFieldDesc()->isPrimitiveArray(); |
| return false; |
| } |
| /// Checks if the structure is an array of unknown size. |
| bool isUnknownSizeArray() const { |
| if (!isBlockPointer()) |
| return false; |
| // If this points inside a dummy block, return true. |
| // FIXME: This might change in the future. If it does, we need |
| // to set the proper Ctor/Dtor functions for dummy Descriptors. |
| if (asBlockPointer().Base != 0 && |
| asBlockPointer().Base != sizeof(InlineDescriptor) && isDummy()) |
| return true; |
| return getFieldDesc()->isUnknownSizeArray(); |
| } |
| /// Checks if the pointer points to an array. |
| bool isArrayElement() const { |
| if (isBlockPointer()) |
| return inArray() && asBlockPointer().Base != Offset; |
| return false; |
| } |
| /// Pointer points directly to a block. |
| bool isRoot() const { |
| return (asBlockPointer().Base == 0 || |
| asBlockPointer().Base == RootPtrMark) && |
| Offset == 0; |
| } |
| /// If this pointer has an InlineDescriptor we can use to initialize. |
| bool canBeInitialized() const { |
| if (!isBlockPointer()) |
| return false; |
| |
| return asBlockPointer().Pointee && asBlockPointer().Base > 0; |
| } |
| |
| [[nodiscard]] const BlockPointer &asBlockPointer() const { |
| assert(isBlockPointer()); |
| return PointeeStorage.BS; |
| } |
| [[nodiscard]] const IntPointer &asIntPointer() const { |
| assert(isIntegralPointer()); |
| return PointeeStorage.Int; |
| } |
| bool isBlockPointer() const { return StorageKind == Storage::Block; } |
| bool isIntegralPointer() const { return StorageKind == Storage::Int; } |
| |
| /// Returns the record descriptor of a class. |
| const Record *getRecord() const { return getFieldDesc()->ElemRecord; } |
| /// Returns the element record type, if this is a non-primive array. |
| const Record *getElemRecord() const { |
| const Descriptor *ElemDesc = getFieldDesc()->ElemDesc; |
| return ElemDesc ? ElemDesc->ElemRecord : nullptr; |
| } |
| /// Returns the field information. |
| const FieldDecl *getField() const { return getFieldDesc()->asFieldDecl(); } |
| |
| /// Checks if the object is a union. |
| bool isUnion() const; |
| |
| /// Checks if the storage is extern. |
| bool isExtern() const { |
| if (isBlockPointer()) |
| return asBlockPointer().Pointee && asBlockPointer().Pointee->isExtern(); |
| return false; |
| } |
| /// Checks if the storage is static. |
| bool isStatic() const { |
| if (isIntegralPointer()) |
| return true; |
| assert(asBlockPointer().Pointee); |
| return asBlockPointer().Pointee->isStatic(); |
| } |
| /// Checks if the storage is temporary. |
| bool isTemporary() const { |
| if (isBlockPointer()) { |
| assert(asBlockPointer().Pointee); |
| return asBlockPointer().Pointee->isTemporary(); |
| } |
| return false; |
| } |
| /// Checks if the storage is a static temporary. |
| bool isStaticTemporary() const { return isStatic() && isTemporary(); } |
| |
| /// Checks if the field is mutable. |
| bool isMutable() const { |
| if (!isBlockPointer()) |
| return false; |
| return asBlockPointer().Base != 0 && |
| asBlockPointer().Base != sizeof(InlineDescriptor) && |
| getInlineDesc()->IsFieldMutable; |
| } |
| |
| bool isWeak() const { |
| if (isIntegralPointer()) |
| return false; |
| |
| assert(isBlockPointer()); |
| if (const ValueDecl *VD = getDeclDesc()->asValueDecl()) |
| return VD->isWeak(); |
| return false; |
| } |
| /// Checks if an object was initialized. |
| bool isInitialized() const; |
| /// Checks if the object is active. |
| bool isActive() const { |
| if (!isBlockPointer()) |
| return true; |
| return asBlockPointer().Base == 0 || |
| asBlockPointer().Base == sizeof(InlineDescriptor) || |
| getInlineDesc()->IsActive; |
| } |
| /// Checks if a structure is a base class. |
| bool isBaseClass() const { return isField() && getInlineDesc()->IsBase; } |
| /// Checks if the pointer points to a dummy value. |
| bool isDummy() const { |
| if (!isBlockPointer()) |
| return false; |
| |
| if (!asBlockPointer().Pointee) |
| return false; |
| |
| return getDeclDesc()->isDummy(); |
| } |
| |
| /// Checks if an object or a subfield is mutable. |
| bool isConst() const { |
| if (isIntegralPointer()) |
| return true; |
| return (asBlockPointer().Base == 0 || |
| asBlockPointer().Base == sizeof(InlineDescriptor)) |
| ? getDeclDesc()->IsConst |
| : getInlineDesc()->IsConst; |
| } |
| |
| /// Returns the declaration ID. |
| std::optional<unsigned> getDeclID() const { |
| if (isBlockPointer()) { |
| assert(asBlockPointer().Pointee); |
| return asBlockPointer().Pointee->getDeclID(); |
| } |
| return std::nullopt; |
| } |
| |
| /// Returns the byte offset from the start. |
| unsigned getByteOffset() const { |
| if (isIntegralPointer()) |
| return asIntPointer().Value + Offset; |
| return Offset; |
| } |
| |
| /// Returns the number of elements. |
| unsigned getNumElems() const { |
| if (isIntegralPointer()) |
| return ~unsigned(0); |
| return getSize() / elemSize(); |
| } |
| |
| const Block *block() const { return asBlockPointer().Pointee; } |
| |
| /// Returns the index into an array. |
| int64_t getIndex() const { |
| if (!isBlockPointer()) |
| return 0; |
| |
| if (isZero()) |
| return 0; |
| |
| if (isElementPastEnd()) |
| return 1; |
| |
| // narrow()ed element in a composite array. |
| if (asBlockPointer().Base > sizeof(InlineDescriptor) && |
| asBlockPointer().Base == Offset) |
| return 0; |
| |
| if (auto ElemSize = elemSize()) |
| return getOffset() / ElemSize; |
| return 0; |
| } |
| |
| /// Checks if the index is one past end. |
| bool isOnePastEnd() const { |
| if (isIntegralPointer()) |
| return false; |
| |
| if (!asBlockPointer().Pointee) |
| return false; |
| return isElementPastEnd() || getSize() == getOffset(); |
| } |
| |
| /// Checks if the pointer is an out-of-bounds element pointer. |
| bool isElementPastEnd() const { return Offset == PastEndMark; } |
| |
| /// Dereferences the pointer, if it's live. |
| template <typename T> T &deref() const { |
| assert(isLive() && "Invalid pointer"); |
| assert(isBlockPointer()); |
| assert(asBlockPointer().Pointee); |
| assert(Offset + sizeof(T) <= |
| asBlockPointer().Pointee->getDescriptor()->getAllocSize()); |
| |
| if (isArrayRoot()) |
| return *reinterpret_cast<T *>(asBlockPointer().Pointee->rawData() + |
| asBlockPointer().Base + sizeof(InitMapPtr)); |
| |
| return *reinterpret_cast<T *>(asBlockPointer().Pointee->rawData() + Offset); |
| } |
| |
| /// Dereferences a primitive element. |
| template <typename T> T &elem(unsigned I) const { |
| assert(I < getNumElems()); |
| assert(isBlockPointer()); |
| assert(asBlockPointer().Pointee); |
| return reinterpret_cast<T *>(asBlockPointer().Pointee->data() + |
| sizeof(InitMapPtr))[I]; |
| } |
| |
| /// Initializes a field. |
| void initialize() const; |
| /// Activats a field. |
| void activate() const; |
| /// Deactivates an entire strurcutre. |
| void deactivate() const; |
| |
| /// Compare two pointers. |
| ComparisonCategoryResult compare(const Pointer &Other) const { |
| if (!hasSameBase(*this, Other)) |
| return ComparisonCategoryResult::Unordered; |
| |
| if (Offset < Other.Offset) |
| return ComparisonCategoryResult::Less; |
| else if (Offset > Other.Offset) |
| return ComparisonCategoryResult::Greater; |
| |
| return ComparisonCategoryResult::Equal; |
| } |
| |
| /// Checks if two pointers are comparable. |
| static bool hasSameBase(const Pointer &A, const Pointer &B); |
| /// Checks if two pointers can be subtracted. |
| static bool hasSameArray(const Pointer &A, const Pointer &B); |
| |
| /// Prints the pointer. |
| void print(llvm::raw_ostream &OS) const; |
| |
| private: |
| friend class Block; |
| friend class DeadBlock; |
| friend struct InitMap; |
| |
| Pointer(Block *Pointee, unsigned Base, uint64_t Offset); |
| |
| /// Returns the embedded descriptor preceding a field. |
| InlineDescriptor *getInlineDesc() const { |
| return getDescriptor(asBlockPointer().Base); |
| } |
| |
| /// Returns a descriptor at a given offset. |
| InlineDescriptor *getDescriptor(unsigned Offset) const { |
| assert(Offset != 0 && "Not a nested pointer"); |
| assert(isBlockPointer()); |
| assert(!isZero()); |
| return reinterpret_cast<InlineDescriptor *>( |
| asBlockPointer().Pointee->rawData() + Offset) - |
| 1; |
| } |
| |
| /// Returns a reference to the InitMapPtr which stores the initialization map. |
| InitMapPtr &getInitMap() const { |
| assert(isBlockPointer()); |
| assert(!isZero()); |
| return *reinterpret_cast<InitMapPtr *>(asBlockPointer().Pointee->rawData() + |
| asBlockPointer().Base); |
| } |
| |
| /// Offset into the storage. |
| uint64_t Offset = 0; |
| |
| /// Previous link in the pointer chain. |
| Pointer *Prev = nullptr; |
| /// Next link in the pointer chain. |
| Pointer *Next = nullptr; |
| |
| union { |
| BlockPointer BS; |
| IntPointer Int; |
| } PointeeStorage; |
| Storage StorageKind = Storage::Int; |
| }; |
| |
| inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Pointer &P) { |
| P.print(OS); |
| return OS; |
| } |
| |
| } // namespace interp |
| } // namespace clang |
| |
| #endif |