Skip to content

[DataLayout][LangRef] Split non-integral and unstable pointer properties #105735

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 12 commits into
base: users/arichardson/spr/main.datalayoutlangref-split-non-integral-and-unstable-pointer-properties
Choose a base branch
from

Conversation

arichardson
Copy link
Member

@arichardson arichardson commented Aug 22, 2024

This commit adds finer-grained versions of isNonIntegralAddressSpace() and
isNonIntegralPointerType() where the current semantics prohibit
introduction of both ptrtoint and inttoptr instructions. The current
semantics are too strict for some targets (e.g. AMDGPU/CHERI) where
ptrtoint has a stable value, but the pointer has additional metadata.
Currently, marking a pointer address space as non-integral also marks it
as having an unstable bitwise representation (e.g. when pointers can be
changed by a copying GC). This property inhibits a lot of
optimizations that are perfectly legal for other non-integral pointers
such as fat pointers or CHERI capabilities that have a well-defined
bitwise representation but can't be created with only an address.

This change splits the two properties and allows for address spaces to
be marked as unstable or non-integral (or both) independently using
the 'p' part of the DataLayout string a 'u' following the p marks the
address space as unstable and a 'n' marks it as non-integral.
Finally, we also add an 'e' flag to mark pointers with external state
(such as the CHERI capability validity) state. These pointers require
special handling of loads and stores in addition to being non-integral.

This does not change the checks in any of the passes yet - we
currently keep the existing non-integral behaviour. In the future I plan
to audit calls to DL.isNonIntegralPointerType and replace them with
the DL.shouldAvoid{IntToPtr,PtrToInt}() checks that allow for more
optimizations.

RFC: https://discourse.llvm.org/t/rfc-finer-grained-non-integral-pointer-properties/83176

Created using spr 1.3.6-beta.1
@llvmbot
Copy link
Member

llvmbot commented Aug 22, 2024

@llvm/pr-subscribers-llvm-ir

Author: Alexander Richardson (arichardson)

Changes

Theses are finer-grained versions of isNonIntegralAddressSpace() and
isNonIntegralPointerType() where the current semantics prohibit
introduction of both ptrtoint and inttoptr instructions. These semantics
are too strict for some targets (e.g. AMDGPU/CHERI) where ptrtoint has
a stable value, but the pointer cannot be recreated just from the
address since there is additional metadata.
Currently, marking a pointer address space as non-integral also marks it
as having an unstable bitwise representation (e.g. when pointers can be
changed by a copying GC). This property inhibits a lot of
optimizations that are perfectly legal for other non-integral pointers
such as fat pointers or CHERI capabilities that have a well-defined
bitwise representation but can't be created with only an address.

This change splits the two properties and allows for address spaces to
be marked as unstable or non-integral (or both) independently using
the 'p' part of the DataLayout string a 'u' following the p marks the
address space as unstable and a 'n' marks it as non-integral.

This does not change the checks in any of the passes yet - we
currently keep the existing non-integral behaviour. In the future I plan
to audit calls to DL.isNonIntegralPointerType and replace them with
the DL.shouldAvoid{IntToPtr,PtrToInt}() checks that allow for more
optimizations.


Full diff: https://github.com/llvm/llvm-project/pull/105735.diff

4 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+71-19)
  • (modified) llvm/include/llvm/IR/DataLayout.h (+70-7)
  • (modified) llvm/lib/IR/DataLayout.cpp (+27-8)
  • (modified) llvm/unittests/IR/DataLayoutTest.cpp (+46)
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 445980a18e7e93..200224c78be004 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -649,48 +649,95 @@ literal types are uniqued in recent versions of LLVM.
 
 .. _nointptrtype:
 
-Non-Integral Pointer Type
--------------------------
+Non-Integral and Unstable Pointer Types
+---------------------------------------
 
-Note: non-integral pointer types are a work in progress, and they should be
-considered experimental at this time.
+Note: non-integral/unstable pointer types are a work in progress, and they
+should be considered experimental at this time.
 
 LLVM IR optionally allows the frontend to denote pointers in certain address
-spaces as "non-integral" via the :ref:`datalayout string<langref_datalayout>`.
-Non-integral pointer types represent pointers that have an *unspecified* bitwise
-representation; that is, the integral representation may be target dependent or
-unstable (not backed by a fixed integer).
+spaces as "non-integral" or "unstable" (or both "non-integral" and "unstable")
+via the :ref:`datalayout string<langref_datalayout>`.
+
+These exact implications of these properties are target-specific, but the
+following IR semantics and restrictions to optimization passes apply:
+
+Unstable pointer representation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pointers in this address space have an *unspecified* bitwise representation
+(i.e. not backed by a fixed integer). The bitwise pattern of such pointers is
+allowed to change in a target-specific way. For example, this could be a pointer
+type used for with copying garbage collection where the garbage collector could
+update the pointer at any time in the collection sweep.
 
 ``inttoptr`` and ``ptrtoint`` instructions have the same semantics as for
 integral (i.e. normal) pointers in that they convert integers to and from
-corresponding pointer types, but there are additional implications to be
-aware of.  Because the bit-representation of a non-integral pointer may
-not be stable, two identical casts of the same operand may or may not
+corresponding pointer types, but there are additional implications to be aware
+of.
+
+For "unstable" pointer representations, the bit-representation of the pointer
+may not be stable, so two identical casts of the same operand may or may not
 return the same value.  Said differently, the conversion to or from the
-non-integral type depends on environmental state in an implementation
+"unstable" pointer type depends on environmental state in an implementation
 defined manner.
-
 If the frontend wishes to observe a *particular* value following a cast, the
 generated IR must fence with the underlying environment in an implementation
 defined manner. (In practice, this tends to require ``noinline`` routines for
 such operations.)
 
 From the perspective of the optimizer, ``inttoptr`` and ``ptrtoint`` for
-non-integral types are analogous to ones on integral types with one
+"unstable" pointer types are analogous to ones on integral types with one
 key exception: the optimizer may not, in general, insert new dynamic
 occurrences of such casts.  If a new cast is inserted, the optimizer would
 need to either ensure that a) all possible values are valid, or b)
 appropriate fencing is inserted.  Since the appropriate fencing is
 implementation defined, the optimizer can't do the latter.  The former is
 challenging as many commonly expected properties, such as
-``ptrtoint(v)-ptrtoint(v) == 0``, don't hold for non-integral types.
+``ptrtoint(v)-ptrtoint(v) == 0``, don't hold for "unstable" pointer types.
 Similar restrictions apply to intrinsics that might examine the pointer bits,
-such as :ref:`llvm.ptrmask<int_ptrmask>`. 
+such as :ref:`llvm.ptrmask<int_ptrmask>`.
 
-The alignment information provided by the frontend for a non-integral pointer
-(typically using attributes or metadata) must be valid for every possible 
+The alignment information provided by the frontend for an "unstable" pointer
+(typically using attributes or metadata) must be valid for every possible
 representation of the pointer.
 
+Non-integral pointer representation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pointers are not represented as an address, but may instead include
+additional metadata such as bounds information or a temporal identifier.
+Examples include AMDGPU buffer descriptors with a 128-bit fat pointer and a
+32-bit offset or CHERI capabilities that contain bounds, permissions and an
+out-of-band validity bit. In general, these pointers cannot be re-created
+from just an integer value.
+
+In most cases pointers with a non-integral representation behave exactly the
+same as an integral pointer, the only difference is that it is not possible to
+create a pointer just from an address.
+
+"Non-integral" pointers also impose restrictions on the optimizer, but in
+general these are less restrictive than for "unstable" pointers. The main
+difference compared to integral pointers is that ``inttoptr`` instructions
+should not be inserted by passes as they may not be able to create a valid
+pointer. This property also means that ``inttoptr(ptrtoint(x))`` cannot be
+folded to ``x`` as the ``ptrtoint`` operation may destroy the necessary metadata
+to reconstruct the pointer.
+Additionaly, since there could be out-of-band state, it is also not legal to
+convert a load/store of a non-integral pointer type to a load/store of an
+integer type with same bitwidth as that may not copy all the state.
+However, it is legal to use appropriately aligned ``llvm.memcpy`` and
+``llvm.memmove`` for copies of non-integral pointers as long as these are not
+converted into integer operations.
+
+Unlike "unstable" pointers, the bit-wise representation is stable and
+``ptrtoint(x)`` always yields a deterministic values.
+This means optimizer is still permitted to insert new ``ptrtoint`` instructions.
+However, it is important to note that ``ptrtoint`` may not yield the same value
+as storing the pointer via memory and reading it back as an integer, even if the
+bitwidth of the two types matches (since ptrtoint could involve some form of
+arithmetic or strip parts of the non-integral pointer representation).
+
 .. _globalvars:
 
 Global Variables
@@ -3056,7 +3103,7 @@ as follows:
 ``A<address space>``
     Specifies the address space of objects created by '``alloca``'.
     Defaults to the default address space of 0.
-``p[n]:<size>:<abi>[:<pref>][:<idx>]``
+``p[<flags>][n]:<size>:<abi>[:<pref>][:<idx>]``
     This specifies the *size* of a pointer and its ``<abi>`` and
     ``<pref>``\erred alignments for address space ``n``. ``<pref>`` is optional
     and defaults to ``<abi>``. The fourth parameter ``<idx>`` is the size of the
@@ -3066,6 +3113,11 @@ as follows:
     are in bits. The address space, ``n``, is optional, and if not specified,
     denotes the default address space 0. The value of ``n`` must be
     in the range [1,2^24).
+    The optional``<flags>`` are used to specify properties of pointers in this
+address space: the character ``u`` marks pointers as having an unstable
+    representation and ```n`` marks pointers as non-integral (i.e. having
+    additional metadata). See :ref:`Non-Integral Pointer Types <nointptrtype>`.
+
 ``i<size>:<abi>[:<pref>]``
     This specifies the alignment for an integer type of a given bit
     ``<size>``. The value of ``<size>`` must be in the range [1,2^24).
diff --git a/llvm/include/llvm/IR/DataLayout.h b/llvm/include/llvm/IR/DataLayout.h
index 953deb6653cc9a..9a89bad2654867 100644
--- a/llvm/include/llvm/IR/DataLayout.h
+++ b/llvm/include/llvm/IR/DataLayout.h
@@ -79,10 +79,14 @@ class DataLayout {
     Align PrefAlign;
     uint32_t IndexBitWidth;
     /// Pointers in this address space don't have a well-defined bitwise
-    /// representation (e.g. may be relocated by a copying garbage collector).
-    /// Additionally, they may also be non-integral (i.e. containing additional
-    /// metadata such as bounds information/permissions).
-    bool IsNonIntegral = false;
+    /// representation (e.g. they may be relocated by a copying garbage
+    /// collector and thus have different addresses at different times).
+    bool HasUnstableRepresentation = false;
+    /// Pointers in this address spacs are non-integral, i.e. don't have a
+    /// integer representation that simply maps to the address. An example of
+    /// this would be fat pointers with bounds information or CHERI capabilities
+    /// that include metadata as well as one out-of-band validity bit.
+    bool HasNonIntegralRepresentation = false;
     bool operator==(const PointerSpec &Other) const;
   };
 
@@ -148,7 +152,7 @@ class DataLayout {
   /// Sets or updates the specification for pointer in the given address space.
   void setPointerSpec(uint32_t AddrSpace, uint32_t BitWidth, Align ABIAlign,
                       Align PrefAlign, uint32_t IndexBitWidth,
-                      bool IsNonIntegral);
+                      bool HasUnstableRepr, bool HasNonIntegralRepr);
 
   /// Internal helper to get alignment for integer of given bitwidth.
   Align getIntegerAlignment(uint32_t BitWidth, bool abi_or_pref) const;
@@ -348,14 +352,63 @@ class DataLayout {
   SmallVector<unsigned, 8> getNonIntegralAddressSpaces() const {
     SmallVector<unsigned, 8> AddrSpaces;
     for (const PointerSpec &PS : PointerSpecs) {
-      if (PS.IsNonIntegral)
+      if (PS.HasNonIntegralRepresentation || PS.HasUnstableRepresentation)
         AddrSpaces.push_back(PS.AddrSpace);
     }
     return AddrSpaces;
   }
 
+  /// Returns whether this address space is "non-integral" and "unstable".
+  /// This means that passes should not introduce inttoptr or ptrtoint
+  /// instructions operating on pointers of this address space.
+  /// TODO: remove this function after migrating to finer-grained properties.
   bool isNonIntegralAddressSpace(unsigned AddrSpace) const {
-    return getPointerSpec(AddrSpace).IsNonIntegral;
+    const PointerSpec &PS = getPointerSpec(AddrSpace);
+    return PS.HasNonIntegralRepresentation || PS.HasUnstableRepresentation;
+  }
+
+  /// Returns whether this address space has an "unstable" pointer
+  /// representation. The bitwise pattern of such pointers is allowed to change
+  /// in a target-specific way. For example, this could be used for copying
+  /// garbage collection where the garbage collector could update the pointer
+  /// value as part of the collection sweep.
+  bool hasUnstableRepresentation(unsigned AddrSpace) const {
+    return getPointerSpec(AddrSpace).HasUnstableRepresentation;
+  }
+
+  /// Returns whether this address space has a non-integral pointer
+  /// representation, i.e. the pointer is not just an integer address but some
+  /// other bitwise representation. Examples include AMDGPU buffer descriptors
+  /// with a 128-bit fat pointer and a 32-bit offset or CHERI capabilities that
+  /// contain bounds, permissions and an out-of-band validity bit. In general,
+  /// these pointers cannot be re-created from just an integer value.
+  bool hasNonIntegralRepresentation(unsigned AddrSpace) const {
+    return getPointerSpec(AddrSpace).HasNonIntegralRepresentation;
+  }
+
+  /// Returns whether passes should avoid introducing `inttoptr` instructions
+  /// for this address space.
+  ///
+  /// This is currently the case "non-integral" pointer representations
+  /// (hasNonIntegralRepresentation()) since such pointers generally require
+  /// additional metadata beyond just an address.
+  /// New `inttoptr` instructions should also be avoided for "unstable" bitwise
+  /// representations (hasUnstableRepresentation()) unless the pass knows it is
+  /// within a critical section that retains the current representation.
+  bool shouldAvoidIntToPtr(unsigned AddrSpace) const {
+    const PointerSpec &PS = getPointerSpec(AddrSpace);
+    return PS.HasNonIntegralRepresentation || PS.HasUnstableRepresentation;
+  }
+
+  /// Returns whether passes should avoid introducing `ptrtoint` instructions
+  /// for this address space.
+  ///
+  /// This is currently the case for pointer address spaces that have an
+  /// "unstable" representation (hasUnstableRepresentation()) since the
+  /// bitwise pattern of such pointers could change unless the pass knows it is
+  /// within a critical section that retains the current representation.
+  bool shouldAvoidPtrToInt(unsigned AddrSpace) const {
+    return hasUnstableRepresentation(AddrSpace);
   }
 
   bool isNonIntegralPointerType(PointerType *PT) const {
@@ -367,6 +420,16 @@ class DataLayout {
     return PTy && isNonIntegralPointerType(PTy);
   }
 
+  bool shouldAvoidPtrToInt(Type *Ty) const {
+    auto *PTy = dyn_cast<PointerType>(Ty);
+    return PTy && shouldAvoidPtrToInt(PTy->getPointerAddressSpace());
+  }
+
+  bool shouldAvoidIntToPtr(Type *Ty) const {
+    auto *PTy = dyn_cast<PointerType>(Ty);
+    return PTy && shouldAvoidIntToPtr(PTy->getPointerAddressSpace());
+  }
+
   /// Layout pointer size, in bits
   /// FIXME: The defaults need to be removed once all of
   /// the backends/clients are updated.
diff --git a/llvm/lib/IR/DataLayout.cpp b/llvm/lib/IR/DataLayout.cpp
index a7a1fa8ef03ede..63ee730c4b5882 100644
--- a/llvm/lib/IR/DataLayout.cpp
+++ b/llvm/lib/IR/DataLayout.cpp
@@ -152,7 +152,8 @@ bool DataLayout::PointerSpec::operator==(const PointerSpec &Other) const {
   return AddrSpace == Other.AddrSpace && BitWidth == Other.BitWidth &&
          ABIAlign == Other.ABIAlign && PrefAlign == Other.PrefAlign &&
          IndexBitWidth == Other.IndexBitWidth &&
-         IsNonIntegral == Other.IsNonIntegral;
+         HasUnstableRepresentation == Other.HasUnstableRepresentation &&
+         HasNonIntegralRepresentation == Other.HasNonIntegralRepresentation;
 }
 
 namespace {
@@ -419,9 +420,24 @@ Error DataLayout::parsePointerSpec(StringRef Spec) {
 
   // Address space. Optional, defaults to 0.
   unsigned AddrSpace = 0;
-  if (!Components[0].empty())
-    if (Error Err = parseAddrSpace(Components[0], AddrSpace))
+  bool UnstableRepr = false;
+  bool NonIntegralRepr = false;
+  StringRef AddrSpaceStr = Components[0].drop_while([&](char C) {
+    if (C == 'n') {
+      NonIntegralRepr = true;
+      return true;
+    } else if (C == 'u') {
+      UnstableRepr = true;
+      return true;
+    }
+    return false;
+  });
+  if (!AddrSpaceStr.empty()) {
+    if (Error Err = parseAddrSpace(AddrSpaceStr, AddrSpace))
       return Err;
+  }
+  if (AddrSpace == 0 && (NonIntegralRepr || UnstableRepr))
+    return createStringError("address space 0 cannot be non-integral");
 
   // Size. Required, cannot be zero.
   unsigned BitWidth;
@@ -455,7 +471,7 @@ Error DataLayout::parsePointerSpec(StringRef Spec) {
         "index size cannot be larger than the pointer size");
 
   setPointerSpec(AddrSpace, BitWidth, ABIAlign, PrefAlign, IndexBitWidth,
-                 false);
+                 UnstableRepr, NonIntegralRepr);
   return Error::success();
 }
 
@@ -629,7 +645,7 @@ Error DataLayout::parseLayoutString(StringRef LayoutString) {
   for (unsigned AS : NonIntegralAddressSpaces) {
     const PointerSpec &PS = getPointerSpec(AS);
     setPointerSpec(AS, PS.BitWidth, PS.ABIAlign, PS.PrefAlign, PS.IndexBitWidth,
-                   true);
+                   true, true);
   }
 
   return Error::success();
@@ -677,17 +693,20 @@ DataLayout::getPointerSpec(uint32_t AddrSpace) const {
 
 void DataLayout::setPointerSpec(uint32_t AddrSpace, uint32_t BitWidth,
                                 Align ABIAlign, Align PrefAlign,
-                                uint32_t IndexBitWidth, bool IsNonIntegral) {
+                                uint32_t IndexBitWidth, bool HasUnstableRepr,
+                                bool HasNonIntegralRepr) {
   auto I = lower_bound(PointerSpecs, AddrSpace, LessPointerAddrSpace());
   if (I == PointerSpecs.end() || I->AddrSpace != AddrSpace) {
     PointerSpecs.insert(I, PointerSpec{AddrSpace, BitWidth, ABIAlign, PrefAlign,
-                                       IndexBitWidth, IsNonIntegral});
+                                       IndexBitWidth, HasUnstableRepr,
+                                       HasNonIntegralRepr});
   } else {
     I->BitWidth = BitWidth;
     I->ABIAlign = ABIAlign;
     I->PrefAlign = PrefAlign;
     I->IndexBitWidth = IndexBitWidth;
-    I->IsNonIntegral = IsNonIntegral;
+    I->HasUnstableRepresentation = HasUnstableRepr;
+    I->HasNonIntegralRepresentation = HasNonIntegralRepr;
   }
 }
 
diff --git a/llvm/unittests/IR/DataLayoutTest.cpp b/llvm/unittests/IR/DataLayoutTest.cpp
index 396d44af19f53f..d26eb70509f515 100644
--- a/llvm/unittests/IR/DataLayoutTest.cpp
+++ b/llvm/unittests/IR/DataLayoutTest.cpp
@@ -319,6 +319,12 @@ TEST(DataLayout, ParsePointerSpec) {
         FailedWithMessage("malformed specification, must be of the form "
                           "\"p[<n>]:<size>:<abi>[:<pref>[:<idx>]]\""));
 
+  // Only 'u' and 'n' flags are valid.
+  for (StringRef Str : {"pa:32:32", "px:32:32"})
+    EXPECT_THAT_EXPECTED(
+        DataLayout::parse(Str),
+        FailedWithMessage("address space must be a 24-bit integer"));
+
   // address space
   for (StringRef Str : {"p0x0:32:32", "px:32:32:32", "p16777216:32:32:32:32"})
     EXPECT_THAT_EXPECTED(
@@ -401,6 +407,12 @@ TEST(DataLayout, ParsePointerSpec) {
     EXPECT_THAT_EXPECTED(
         DataLayout::parse(Str),
         FailedWithMessage("index size cannot be larger than the pointer size"));
+
+  for (StringRef Str : {"pn:64:64", "pu:64:64", "pun:64:64", "pnu:64:64",
+                        "pn0:64:64", "pu0:64:64", "pun0:64:64", "pnu0:64:64"})
+    EXPECT_THAT_EXPECTED(
+        DataLayout::parse(Str),
+        FailedWithMessage("address space 0 cannot be non-integral"));
 }
 
 TEST(DataLayoutTest, ParseNativeIntegersSpec) {
@@ -553,6 +565,40 @@ TEST(DataLayout, IsNonIntegralAddressSpace) {
   EXPECT_FALSE(Custom.isNonIntegralAddressSpace(1));
   EXPECT_TRUE(Custom.isNonIntegralAddressSpace(2));
   EXPECT_TRUE(Custom.isNonIntegralAddressSpace(16777215));
+
+  // Pointers can be marked as non-integral using 'pn'
+  DataLayout NonIntegral = cantFail(DataLayout::parse("pn2:64:64:64:32"));
+  EXPECT_TRUE(NonIntegral.isNonIntegralAddressSpace(2));
+  EXPECT_TRUE(NonIntegral.hasNonIntegralRepresentation(2));
+  EXPECT_FALSE(NonIntegral.hasUnstableRepresentation(2));
+  EXPECT_TRUE(NonIntegral.shouldAvoidIntToPtr(2));
+  EXPECT_FALSE(NonIntegral.shouldAvoidPtrToInt(2));
+
+  // Pointers can be marked as unstable using 'pu'
+  DataLayout Unstable = cantFail(DataLayout::parse("pu2:64:64:64:32"));
+  EXPECT_TRUE(Unstable.isNonIntegralAddressSpace(2));
+  EXPECT_TRUE(Unstable.hasUnstableRepresentation(2));
+  EXPECT_FALSE(Unstable.hasNonIntegralRepresentation(2));
+  EXPECT_TRUE(Unstable.shouldAvoidPtrToInt(2));
+  EXPECT_TRUE(Unstable.shouldAvoidIntToPtr(2));
+
+  // Both properties can also be set using 'pnu'/'pun'
+  for (auto Layout : {"pnu2:64:64:64:32", "pun2:64:64:64:32"}) {
+    DataLayout DL = cantFail(DataLayout::parse(Layout));
+    EXPECT_TRUE(DL.isNonIntegralAddressSpace(2));
+    EXPECT_TRUE(DL.hasNonIntegralRepresentation(2));
+    EXPECT_TRUE(DL.hasUnstableRepresentation(2));
+  }
+
+  // For backwards compatibility, the ni DataLayout part overrides any p[n][u].
+  for (auto Layout : {"ni:2-pn2:64:64:64:32", "ni:2-pnu2:64:64:64:32",
+                      "ni:2-pu2:64:64:64:32", "pn2:64:64:64:32-ni:2",
+                      "pnu2:64:64:64:32-ni:2", "pu2:64:64:64:32-ni:2"}) {
+    DataLayout DL = cantFail(DataLayout::parse(Layout));
+    EXPECT_TRUE(DL.isNonIntegralAddressSpace(2));
+    EXPECT_TRUE(DL.hasNonIntegralRepresentation(2));
+    EXPECT_TRUE(DL.hasUnstableRepresentation(2));
+  }
 }
 
 TEST(DataLayoutTest, CopyAssignmentInvalidatesStructLayout) {

Created using spr 1.3.6-beta.1
Created using spr 1.3.6-beta.1
@arichardson arichardson marked this pull request as ready for review October 25, 2024 17:19
@arichardson
Copy link
Member Author

The downstream CHERI fork uses a f flag on the pointer spec ("fat pointer") to indentify CHERI capabilities. These have an additional property that partial copies are not possible since they would invalidate the result. I considered adding another flag to prevent splitting of loads/stores but this is not part of this commit. I imagine this property would also be useful for pointer schemes that use out-of-band metadata tables for pointers (e.g. Intel's no longer supported MPX or other similar schemes). Let me know if adding this would also be useful for any other downstreams.

(typically using attributes or metadata) must be valid for every possible
representation of the pointer.

Non-integral pointer representation
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is non-integral the right term for something that is more than just an integer?

Copy link
Member Author

Choose a reason for hiding this comment

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

Naming is hard - I kept this pre-existing name since it can also be interpreted as not just an integer, i.e. it can be anything else (such as integer+metadata).

Copy link
Contributor

Choose a reason for hiding this comment

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

So, just to toss out a drive-by name suggestion (though I'm fine with keeping non-integral): how about "annotated" pointers? That is, the pointer does (without unstable) have a fixed representation and point to some address, but there are bits in that representation that "annotate" the address, and so inttoptr(ptrtoint(v) + x) ??= gep i8, v, x

Created using spr 1.3.6-beta.1
Created using spr 1.3.6-beta.1
Copy link

github-actions bot commented Oct 25, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

Created using spr 1.3.6-beta.1
@arichardson
Copy link
Member Author

I've added a new test and hopefully addressed all comments.

@davidchisnall
Copy link
Contributor

I’m concerned about the semantics of unstable. This sounds like it would impact optimisation of memcmp, for example (is it still allowable to optimise away self comparisons?). I wouldn’t want that added to the LangRef without some clearer description of what optimisers can assume. That said, given that it’s a property that’s already present for NI pointers for GC, I suppose we’re stuck with it for now.

Note that CHERI LLVM’s use of ptrtoint is largely historical. We worked with the folks doing NI pointers and my plan was to move to them eventually. We didn’t initially have the address-get intrinsic and so we abused ptrtoint for that, but I believe we’ve fixed the places in the front end that do this. When we upstream, we should get rid of that entirely. This also improves optimisation because optimisers treat address-get as non-escaping whereas ptrtoint assumes the pointer may be materialised anywhere else and makes alias analysis conservative.

@arichardson
Copy link
Member Author

arichardson commented Oct 30, 2024

I’m concerned about the semantics of unstable. This sounds like it would impact optimisation of memcmp, for example (is it still allowable to optimise away self comparisons?). I wouldn’t want that added to the LangRef without some clearer description of what optimisers can assume. That said, given that it’s a property that’s already present for NI pointers for GC, I suppose we’re stuck with it for now.

I am interesting in making CHERI capabilities not have these unstable properties that are currently part of the non-integral pointers. Since it already exists I feel that that splitting it out to a new property does not add any new semantics and will allow GC users to deal with this correctly if they desire to do so. Maybe I should make the LangRef description more vague? I tried to just keep the existing description as much as possible.

Note that CHERI LLVM’s use of ptrtoint is largely historical. We worked with the folks doing NI pointers and my plan was to move to them eventually. We didn’t initially have the address-get intrinsic and so we abused ptrtoint for that, but I believe we’ve fixed the places in the front end that do this. When we upstream, we should get rid of that entirely. This also improves optimisation because optimisers treat address-get as non-escaping whereas ptrtoint assumes the pointer may be materialised anywhere else and makes alias analysis conservative.

I think the non-escaping ptrtoint should be addressed by https://discourse.llvm.org/t/rfc-improvements-to-capture-tracking/81420 and once that is implemented we will no longer need the special intrinsic.

@nikic
Copy link
Contributor

nikic commented Nov 5, 2024

I think this change would benefit from an RFC on discourse, for wider visibility.

Something that's not super clear to me is how to reconcile the statements about pointer/integer casts and in-memory type punning. I'd expect that ptrtoint returns an i128 value and if you cast back that value to a pointer, you preserve capabilities. If you truncate the i128 value to i64 and then cast it back, then you obviously don't -- but LLVM should already know that truncation is not a value preserving operation :) Or does ptrtoint actually directly return an i64 value? If it does, then I think you must have additional changes for that, because I don't think that LLVM supports that directly. And if you do, then that may be sufficient?

@arichardson
Copy link
Member Author

I think this change would benefit from an RFC on discourse, for wider visibility.

Something that's not super clear to me is how to reconcile the statements about pointer/integer casts and in-memory type punning. I'd expect that ptrtoint returns an i128 value and if you cast back that value to a pointer, you preserve capabilities. If you truncate the i128 value to i64 and then cast it back, then you obviously don't -- but LLVM should already know that truncation is not a value preserving operation :) Or does ptrtoint actually directly return an i64 value? If it does, then I think you must have additional changes for that, because I don't think that LLVM supports that directly. And if you do, then that may be sufficient?

That is a good suggestion - I will post a RFC soon.

The CHERI downstream ensures that ptrtoint for capabilities only returns the address part of the capability (i.e. i32 for 64-bit capabilities and i64 for 128-bit ones). We added additional helpers to query that width which I attempted to cleanup+upsteream in https://reviews.llvm.org/D135158 and https://reviews.llvm.org/D99660.

Storing via memory preserves capabilities (as long as the load+store are correctly sized and aligned - there is one additional out-of-band bit that is only preserved for capability-typed loads+stores), but casting via same-size integers does not. This also means that the C (u)intptr_t is lowered to ptr addrspace(200) instead of i128 in IR to preserve capability data.

@arichardson
Copy link
Member Author

Would be good to get some input from the GC pointers side regarding non-integral pointers.
I am not sure who is the best person for this now since I was told @preames is no longer interested in non-integral pointers.

@arichardson arichardson marked this pull request as draft January 14, 2025 00:23
@arichardson
Copy link
Member Author

I will update this PR soon based on the outcome of the Discourse discussions on ptrtoint semantics. This will remove most restrictions from AMDGPU non-integral pointers and shift them to the "external state" (CHERI) type pointers. Still not quite sure about the unstable part, but those are effectively just keeping the current wording.

Copy link
Contributor

@krzysz00 krzysz00 left a comment

Choose a reason for hiding this comment

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

I like the external state thing and think this is a good clarification / breakdown overall, just one comment

``ptrtoint(x)`` always yields a deterministic value.
This means transformation passes are still permitted to insert new ``ptrtoint``
instructions.
However, it is important to note that ``ptrtoint`` may not yield the same value
Copy link
Contributor

Choose a reason for hiding this comment

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

... Wait, hold on, I thought one of the firmer outcomes of the big ptrtoint semantics thread is that ptrtoint is definitionally the same as a type-punned store + load

That is

%y = ptrtoint ptr addrspace(N) %x to i[ptrsize(N)]

is exactly

%m = alloca i[ptrmemsize(N)]
store ptr addrspace(N) %x, ptr %m
%y = load i[ptrsize(N)], ptr %m

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, apologies for the delay here - I need to get around to rebasing my changes on top of the outcome of the discussion. I hope to have something next week.

Copy link
Contributor

Choose a reason for hiding this comment

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

No problem, just wanted to flag that

(Also, re that discussion - it might be good to get your thoughts on the ptrtoaddr - and in particular, ptrtoaddr as inverse of GEP - formulation)

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated, hopefully all issues resolved in the new wording.

@arichardson arichardson requested a review from krzysz00 July 21, 2025 20:16
@arichardson arichardson marked this pull request as ready for review July 21, 2025 20:16
Created using spr 1.3.6-beta.1
@arichardson
Copy link
Member Author

This should be ready for review now. I've updated it based on the conclusion of the ptrtoint semantics discussion.
@krzysz00 Would you mind taking a look at this? It no longer has any inttoptr restrictions on "normal non-integral pointers", only for ones with external state.

@arichardson arichardson requested a review from resistor July 21, 2025 20:18
Copy link
Collaborator

@resistor resistor left a comment

Choose a reason for hiding this comment

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

Small nits

Copy link
Contributor

@krzysz00 krzysz00 left a comment

Choose a reason for hiding this comment

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

Minor typo notes and one question, otherwise makes sense

be performed as loads and stores of the correct type since stores of other
types may not propagate the external data.
Therefore it is not legal to convert an existing load/store (or a ``llvm.memcpy`` /
``llvm.memmove`` intrinsic) of pointer types with external state to a load/store
Copy link
Contributor

Choose a reason for hiding this comment

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

memcpy of pointer types feels a bit weird

Is this basically saying you're now allowed to ever split a copy into smaller copies because it might contain a pointer with external state?

Copy link
Contributor

Choose a reason for hiding this comment

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

... Or at least on an architecture with at least one e pointer (which might mean we'll want a function for .mayHaveExternalPointers() on DataLayout or something

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. Though if you somehow know (whether by analysis or by frontend-provided metadata due to language semantics, like strict aliasing in C) that there are no pointers in a given range then you can still perform that optimisation. Maybe worth adding a throwaway "unless it is known no pointers with external state are present in the source"?

Also you can split into loads and stores of the pointer with external state (if there is just one such type). Just not a type that won't preserve the external state.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it

I'm just worried that this forbids memcpy() expansion

Like, it's weird to be in a situation where you can't unconditionally pessimize memcpy(it* dst, i8* src, usize len) to

for(usize i = 0; i < len; ++i) { 
  *(dst++) = *(src++);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah this is somewhat annoying. In the downstream CHERI LLVM forks, we included an attribute on memcpy/memmove that says "this copy does not contain capabilities" and then it can be expanded to integer loads/stores stores in IR/the backend.

Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

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

Under this new definition, it's not clear to me what purpose the "non-integral pointer" concept still serves that is not already implied by having an index width that does not match the pointer width. Would it not be sufficient to only have the unstable pointer and external state concepts?

@arichardson
Copy link
Member Author

Under this new definition, it's not clear to me what purpose the "non-integral pointer" concept still serves that is not already implied by having an index width that does not match the pointer width. Would it not be sufficient to only have the unstable pointer and external state concepts?

That is a really good question - in the original version of this change "normal" non-integral pointers had special ptrtoint semantics, but now the only difference is index width != pointer size, so you can do anything you could before as long as you keep all bits and not just the address.

I would be happy to change this to just unstable and external state. I'm not particularly happy with this name but I can't come up with something better.

@davidchisnall
Copy link
Contributor

Some optimisers assume that, if they know something about the address, they know something about the bit pattern in memory. That is currently captured as a non-integral property.

The other thing, which might be better captured with intrinsics, is that a ptrtoint instruction does not need to be treated as escaping because inttoptr instructions are not permitted on non-integral pointers (that doesn’t seem to be explicit, but is implied, I think).

I do wonder for CHERI if the right thing is to disallow both inttoptr and ptrtoint and upstream our set and get address intrinsics with the same provenance guarantees as the Rust strict provenance model (and our WIP C++ strict provenance rules). That would mean that NI could explicitly disallow both ptrtoint and inttoptr (and the verifiers could check that), optimisers would have more aliasing information from source languages that have sensible provenance models, and we’d have target-agnostic ways of representing the escaping vs non-escaping casts.

@nikic
Copy link
Contributor

nikic commented Jul 23, 2025

@davidchisnall See #139357.

Created using spr 1.3.6-beta.1
@nikic
Copy link
Contributor

nikic commented Jul 30, 2025

I would be happy to change this to just unstable and external state. I'm not particularly happy with this name but I can't come up with something better.

I think we should do that.

@arichardson
Copy link
Member Author

I would be happy to change this to just unstable and external state. I'm not particularly happy with this name but I can't come up with something better.

I think we should do that.

I will try to get to that later this week, but first I would like to check with @krzysz00 if there are any cases in the AMDGPU backend where the DL.isNonIntegralPointer() checks currently prevent invalid optimizations.
Right now AMDGPU pointers are "non-integral", but with the new definition they would no longer need that property.
So if there are any reasons these existing checks also need to apply for AMDGPU pointers, then we probably do need to keep the non-integral separate from repr size != address size.

Hopefully the introduction of ptrtoaddr and the clarification that ptrtoint/inttoptr behave as exposing bitcasts fixes all the AMDGPU reason for using non-integral pointers over just saying repr size != address size.

@krzysz00
Copy link
Contributor

I'm not actively aware of any cases where the non-integrality is being used to block invalid optimizations ... but I do know that, for example, SeparateConstOffsetFromGEP has a mode (which I think Nvidia uses) that turns pointer arithmetic into ptrtoint/inttoptr pairs.

I think if we wanted to drop the round-tripping restriction, we'd want to systematically audit everything that checks for a non-integral address space and determine what that check should be updated to (ex., an address width != pointer width check, a check for external/unstable, etc.)

@arichardson
Copy link
Member Author

I'm not actively aware of any cases where the non-integrality is being used to block invalid optimizations ... but I do know that, for example, SeparateConstOffsetFromGEP has a mode (which I think Nvidia uses) that turns pointer arithmetic into ptrtoint/inttoptr pairs.

I think if we wanted to drop the round-tripping restriction, we'd want to systematically audit everything that checks for a non-integral address space and determine what that check should be updated to (ex., an address width != pointer width check, a check for external/unstable, etc.)

Thanks for this feedback. I guess we could check if AMDGPU depends on the current non-integral restrictions in the optimization pipeline by removing :ni- and seeing if anything would break?

In the context of this PR, would you be okay with me dropping the whole non-integral section and for now pretending that AMDGPU pointers are "unstable" to keep the current optimization restrictions in place? We can then work on auditing all calls to DL.isNonIntegralPointer() and replacing them with the appropriate checks.

@nikic
Copy link
Contributor

nikic commented Jul 31, 2025

I'm not actively aware of any cases where the non-integrality is being used to block invalid optimizations ... but I do know that, for example, SeparateConstOffsetFromGEP has a mode (which I think Nvidia uses) that turns pointer arithmetic into ptrtoint/inttoptr pairs.

This one already guards against pointer vs index size mismatch. But after looking at this, I've put up #151477 to drop this code entirely. This is not a reasonable thing to do, any it doesn't look like it's actually used anymore.

@krzysz00
Copy link
Contributor

krzysz00 commented Aug 1, 2025

v. In the context of this PR, would you be okay with me dropping the whole non-integral section and for now pretending that AMDGPU pointers are "unstable" to keep the current optimization restrictions in place

Yeah, that seems reasonable, just stick a comment somewhere pointing out that the instability is a lie.

@krzysz00
Copy link
Contributor

krzysz00 commented Aug 1, 2025

... Actually, no, having posted that, I just had a better idea

Drop non-integral as a concept, but make DL.isNonIntegralPointer() do a index width != address width check to preserve the old behavior

@arichardson
Copy link
Member Author

arichardson commented Aug 1, 2025

Drop non-integral as a concept, but make DL.isNonIntegralPointer() do a index width != address width check to preserve the old behavior

Yes I was thinking of that option too and I think that is probably the best approach for now. Assuming we don't have any targets that have repr width != addr width and don't use the ni: property -- will audit.

@nikic
Copy link
Contributor

nikic commented Aug 4, 2025

That sounds fine as long as the plan is to remove the isNonIntegralPointer() method in the future, in favor of more precise checks.

@arichardson
Copy link
Member Author

That sounds fine as long as the plan is to remove the isNonIntegralPointer() method in the future, in favor of more precise checks.

Yes that is absolutely the goal. Will try to update with these changes shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants