Clarifiying the semantics of ptrtoint

Overall, agreed that this isn’t quite good wording for the LangRef as is - I mainly wanted to make sure that we’re happy with the plan.

Also agreed that pinning N down to the pointer width is a good idea and that the wording here isn’t great.

Also agreed that “semantics of pointer types” might be a good section.

And re the “rewritten freely” - provenanced pointers are annotated pointers, no? So the precondition of the interchange isn’t true.

And as to ptrtoaddr on true non-integral pointers … let’s go with “the same unspecified, target-specific don’t-add-a-new-one” semantics of ptrtoint on those pointers.

(Unless you meant that rewriting ptrtoint arithmetic to gpu is unsound on, say, a standard x86 pointer - I claim you can do that?)

Thanks for all the feedback - I agree that @nikic’s ptrtoaddr is the cleanest solution moving foward even though it will require in quite a lot of changes to allow LLVM to optimize it sensibly (just searching for Instruction::PtrToInt gives me 93 results).
ptrtoaddr also solves the problem of getting the address from pointers that have multiple components (e.g. segments/fat pointers/etc), where extracting the low N bits of ptrtoint might not actually be the address.

Thanks for the great summary @krzysz00. Maybe the wording for ptrtoint should be more along the lines of

ptrtoint is the equivalent of a bitcast ptr addrspace(N) to iX with the additional side-effects of “exposing provenance” such that a later inttoptr could recreate the ptr.

But since there is no clear decision on whether LLVM has exposed provenance, maybe a better wording would be to use something like “unspecified side-effects that allow later recreation of this pointer using inttoptr”.

I also don’t think we can fold inttoptr(ptrtoint(%p)) == %p for any kind of pointer (integral or non-integral). Since ptrtoint has these exposing side-effects eliding it is not possible.

I will start working on a pull request to add ptrtoaddr but as this will be a larger change it may take a while.

I’m not sure that the “allows recreation” clause is necessary. I’d be willing to give up the inttoptr(ptrtoint(x)) == x fold on annotated pointers, keeping it only for the most well-behaved ones, but I’m not as familiar with what CHERI wants out of ptrtoint.

I’ll also go ahead and pull in an old blog post that I happened to stumble across on HN, which makes a decent argument for making the inttoptr(ptrtoint(x)) => x rewrite illegal Pointers Are Complicated II, or: We need better language specs

CHERI cannot implement ptrtoint such that inttoptr(ptrtoint) gives back a usable pointer, as i128 does not have an associated tag bit. The only truly meaningful semantics for us is giving the address, everything else is compressed metadata and so does not make sense to be including in an integer. We could implement ptrtoint as giving you the i128, and inttoptr as giving you a capability with no tag, so not dereferenceable, but that’s pretty pointless, so we’d be better off just making the instruction illegal on CHERI.

Ok, well, then we’ll want to word ptrtoint on annotated pointers to still have the “no introducing ptrtoint/inttoptr pairs” wording that current non-integral pointers have, since there’re real annotated pointers out in the wild that can’t maintain that invariant.

(Though I have a naive question - if the ptrtoint result were an i129 - including the tag bit - would that be usable to round-trip all the bits of a pointer, or that the hardware flat-out not permit that sort of thing?)

Also, just to call out what seems to be an implication of the decision we’re circling around:

If %p and %q are annotated pointers (ex. an AMD fat buffer, a CHERI pointer, a far pointer from back in the segmented days) then icmp eq %p, %q yields poison if the non-address bits don’t match.

Therefore, icmp eq %p, %q refines to icmp eq (ptrtoaddr %p) (ptrtoaddr %q) by way of

  • Metadata and address match: true => true
  • Metadata matches and addresses don’t: false => false
  • Metadata mismatches, pointers agree on address: poison => true
  • Metadata mismatches, pointers point to different places: poison => false

As far as I can tell, the same sort of truth-table-ing also works for refining in ptrtoints, so what we’ve decided about pointer equality in a CHERI world is a big shrug where the backend can do whatever it wants, including systematically refining those poisons to whatever equality operation makes sense

The tag bit is not in addressable memory, and it can only be queried or cleared when in a register. It behaves in no way like i129 except for the fact that 129 bits of architectural state exist.

Ah

Makes sense. Then I think the definition of ptrtoint will be that as-if-written-to-memory bitcast, and we’ll declare that annotated pointers can’t necessarily be recreated from their integer representation.

At which point, why bother including any of the metadata in it if you can never get back to a valid pointer? Sure, you could inspect it, and you could even perform bit manipulation or arithmetic on it, but at that point it’s meaningless, your integer has an address in the low bits and compressed metadata in the upper bits, so what is the point of having that all in something you can treat as one big integer when it’s not (and there are no 128-bit integer arithmetic instructions in hardware, because capability registers are not integers)? This is how you end up with what CHERI LLVM does, which is to say that ptrtoint gives you the address field. Nothing else is meaningful. So either you end up with ptrtoint/inttoptr operating purely on the address (or some target-specific subset of the pointer that at least encompasses the address) or you end up adding a ptrtoaddr/addrtoptr that does just that and outlawing ptrtoint/inttoptr.

Except that there are targets that aren’t CHERI where you can, in fact, round-trip all this stuff.

For example, a ptr addrspace(8) or ptr addrspace(7) on AMD has a perfectly well-defined bit representation, and you absolutely can go back and forth between the relevant i128 or i160 and the pointer representation without issues. Similarly, I’d imagine an old-style far pointer on X86 - where you’re including the segment part - has a well-defined bit representation that isn’t “the address”.

So maybe the right thing to do is to not use ptrtoint on CHERI and only use ptrtoaddr.

Or, somewhat more aggressively, to define bitcast to work on pointers now and to use ptrtoint for the proposed ptrtoaddr … at the cost of needing to break everyone’s muscle memory in various contexts.

In any case, it’s important that the “get bits underlying pointer, including metadata” operation exists, and, as far as I can tell, that’s named ptrtoint. And inttoptr(ptrtoint(%p)) => %p is … not an unreasonable fold, assuming that said metadata is complete, which it is for various metadata-bearing pointers that aren’t CHERI’s.

For example, a |ptr addrspace(8)| or |ptr addrspace(7)| on AMD has a
perfectly well-defined bit representation, and you absolutely can go
back and forth between the relevant i128 or i160 and the pointer
representation without issues. Similarly, I’d imagine an old-style far
pointer on X86 - where you’re including the segment part - has a
well-defined bit representation that isn’t “the address”.

I’ll admit that I’m not that knowledgeable about programming on the old
segmented x86 architecture. But from my limited knowledge (this is of
protected mode x86, not real mode x86), the “metadata/address” bit
breakdown doesn’t make a lot of sense:

From what I can tell, the address as far as a userspace program can
generally tell is the 32-bit : combination.
The segment selector itself has what can reasonably be considered
metadata bits, at least the low 2 bits for privilege level, and arguably
bit 3 for choosing between GDT and LDT would qualify as well, given how
huge pointer arithmetic works. There are three types of pointers:

  • near pointers, where the segment selector is implied to be DS
    (arguably near function pointers and near data pointers mean there
    should be two address spaces, one where the segment selector is CS and
    one where it’s DS, but I can’t any reference to near function pointers
    in the OpenWatcom compiler’s documentation, so I don’t know if this ever
    existed)

  • far pointers, where both the segment selector and the 16-bit pointer
    are included, but objects can’t cross segment boundaries (so
    getelementptr only increments the low 16 bits, never the segment selector)

  • huge pointers, which are like far pointers, but objects can cross
    segment boundaries (so getelementptr needs to do some more complex
    arithmetic, as the metadata bits are the low bits of the selector, not
    the high bits).

First off, thanks for the history notes!

Second, I hope I didn’t give the impression that having distinct “metadata” and “address” bits was important - that’s not a property I considered relevant.

To structure things a bit, I’ll give some properties of types of pointers as I see them. In general, we’ll say that a pointer is N bits wide and has an indexing width of O - that is, GEP extends/truncates its argument to an iO.

An integral pointer - the one that’s expected by default, has the following properties:

  1. Deterministic addressing: %p == %q implies ptrtoaddr(%p) == ptrtoaddr(%q), including for cases where %q is just %p at a later point in the program.
  2. Stability under bitcast: inttoptr(ptrtoint(%p)) <=> %p (except that going through an integer complicates escape analysis)
  3. Flat addressing: N (the width of the pointer) == O (the offset/indexing width) and ptrtoaddr(%p) == ptrtoint(%p)
  4. Stability under integer arithmetic: getelementptr i8, ptr %p, iN %x <=> inttoptr(ptrtoint(%p) + %x))

An annotated pointer drops conditions 3 and 4 but not 1 and 2 That is, annotated pointers still have deterministic bit representations that you can round-trip through, but you’re not guaranteed that the implementation of GEP is the “obvious” one, and you can’t rewrite GEP to integer arithmetic.

To use the x86 examples:

  • A near function pointer or near data pointer are both integral pointers in separate address spaces that are 16-bits wide
  • A far pointer is a 32-bit pointer with 16-bit addressing. That is, you use getelementptr i8, ptr(far) %p, i16 %off and that does the right thing. However, you can’t turn that into integer math on %p 's integer representation because adding %off might overflow into the segment selector.
  • A huge pointer is a 32-bit pointer with 32-bit offset, but it’s still annotated because (unlike with modern virtual memory) you can’t just move around by adding an offset and have to do some complex arithmetic to correctly cross segment boundaries.

Now, both far and huge pointers have a meaningful implementation of ptrtoaddr ptr(far/huge) %p to i32 , but it isn’t equal to their ptrtoint p(far/huge) %p to i32. The ptrtoaddr would need to do all the bit shuffling to get the effective address as if we were in a flat memory model, but the ptrtoint is just a bitcast.

(Similarly, the AMD buffer fat pointer has both a base and offset field - for one thing, the base has to be wave-uniform and the offset doesn’t. To get the actual effective address, you’d need to add those two values together, but ptrtoint should expose them separately)

While I know the push for refining the non-integral pointer categories started with CHERI, CHERI’s pointers miss the definition of “annotated” above due to the tag bit.

I’m open to the possibility that I’ve drawn the line in the wrong place (since the category past “annotated” is “non-integral”/“GC”, where we lose all the pointer properties I gave above) but I figure that “pointer arithmetic isn’t integer arithmetic, but otherwise everything behaves” is a useful category to have that there seems to be current and historical precedent for

I think I wouldn’t call this stability because it’s not about the value being unstable in the sense that it can change over time. I’d probably call this “lossless integer round-tripping” or similar.

This doesn’t properly differentiate between the pointer size, the pointer address size and the pointer index size. CHERI has pointer size > pointer address size == pointer index size. Traditional segmentation’s far pointers have pointer size == pointer address size > pointer index size. In theory one could have both inequalities. CHERI does have a flat 32/64-bit address space, with normal page tables if you’re on an MMU-based system, and the addresses within the pointers have that full flat address, it just also constrains the region that full flat address can access. But you can have a capability that grants access to the entire address space (and that’s what the hardware gives you at boot to then subdivide).

Again not really stability. Maybe “linearity”? This also only considers linearity for ptrtoint, but we have it for ptrtoaddr, with the restriction that addrtoptr is lossy for the metadata. That is, we can give ptrtoaddr(p + x) == ptrtoaddr(p) + x.

Terminological nitpicks accepted - property 2 being “lossless integer round-tripping” makes sense.

And I do agree that ptrtoaddr(%p + %x) == ptrtoaddr(%p) + %x is probably a sensible property to consider - even if it won’t hold when address size > index size.

That is, if 0x2345:0xffff + i16 1 == 0x2345:0x0 - which isn’t unreasonable - then we have ptrtoaddr(%p + 1) == 0x23450000 but ptrtoaddr(%p) + 1 == 0x23460000.

Then again, if we just said that it has to be a nuw GEP/ptradd if address size > index size, everything works out fine, I think.

And as to the strict inequality case - I’ve got one. AMD fat buffer pointer, whose pointer size is i160, address size is i64 (where we follow the x86 sign-extension convention on the pointers, otherwise it’d be i48) and the index size is i32 .

Hi all, I’m coming to this from the GC-pointer perspective, since we (JuliaLang) were one of the original use cases that led to the implementation of non-integral pointers. My apologies for being late to the discussion. That said, I’m happy it’s being picked up again, since the semantics of non-integral addrspaces have long been problematic (both from a theoretical completeness-of-specification perspective and from a practical perspective of target-independent optimization passes missing optimizations that would be permissible, but are conservatively disallowed because the semantics are unclear).

A couple of thoughts on the above discussion (and again, please excuse my tardiness and any context I may have missed in the current discussion):

  1. In general, I like the notion of splitting the concept of splitting a pointer into “address-like” and “metadata-like” parts. I think this gives a lot of clarity around the two orthogonal pieces of information that a pointer carries.

  2. I would be careful about assuming that an integral representation of the additional metadata of pointers exists and that ptrtoint can create such. However, I would be fine to declare that it produces the same result as a memory roundtrip (store as ptr/load as integer), with the understanding that the opposite may be illegal.

  3. For the specific case of GC pointers, I was imagining that the proposed ptrtoaddr would actually be stable in that it should return the offset into the base of the allocation (i.e. what llvm.experimental.gc.get.pointer.offset does, except with a target-independent specification). This offset does not change for a pointer, even if the pointer is relocated. Otherwise I feel that ptrtoaddr has little utility for this case.

  4. I think the specification needs to allow for the possibility that the address size of the pointer is 0 (i.e. the pointer is all metadata). We have an addrspace like this for GC base pointers.

  5. Because of the previous point, I’m a little bit worried about the wording of If the metadata portion of the pointers differs, the result of comparisons is poison. I understand that the intent was to allow targets to be more strict with respect to a particular addrspace, but I am concerned that this wording would permit a target-independent pass to fold a zero-address-size pointer to literal poison on the else branch of a comparison, which we would not want.

  6. I think we are still missing a specification of what exactly addrspacecast is allowed to do. However, since that has historically been somewhat wrought in controversy, it is probably ok to punt on that and make progress on the pieces we can make progess on - however, I want to avoid making any spec changes that would expand the scope of addrspacecast’s semantics without thinking about them carefully.

Let me also tag @gbaraldi in this discussion who was just looking at missing optimization cases due to underspecified non-integrall addressspace semantics.

1 Like

Hi @Keno, thanks for adding another set of perspectives and requirements to the discussion!

The one thing I’m going to poke at is “address-like bits”, in the sense that not all pointers are clearly seperable into fields like that. For instance, the address computation on a ptr addrspace (7) %p is %p[80:32] + zext(p[31:0]), if we’re being somewhat strict about it.

I’m going to agree with the proposal that you can have address size = 0, since, like you said, GC roots don’t meaningfully have an address part. I think that such a “no address” pointer could also model (my understanding of) SPIR-V style bindings, where you have this abstract thing called, say, “resource #0” or “buffer #5”, which you can take offsets from, but you’re never allowed to see the address it points to.

And to kick off another round of proposed text

Pointers, version N + 1, draft 1

A pointer is a thing that refers to some object in memory. Values can be loaded from and stored to the memory referred to by a pointer.

The abstract location to which a pointer refers is stable. While, in the case of GC pointers, the location of the value pointed to in system memory can change, one may always assume that, in the absence of intervening modifications through an aliasing pointer (including pointers on other threads of execution), the value stored to a pointer will be the value returned by a subsequent load operation. (Since some pointers refer to memory that is used to communicate with hardware or other progarms, the volatile modifier can be used to require stores and loads to actually be performed.)

Pointers point into objects, which I’m pretty sure are well-defined in the LangRef already so I’m not going to repeat it. You can use

Pointers have address spaces, which are used to define either distinct kinds of memory or distinct means of accessing the same underlying memory. (… pick our favorite examples: TLS on X86 is a different means of accessing the same underlying RAM because it goes to an allocated thread-local bit, while AMDGPU has addrspace(1) and addrspace(3) for global and shared memory, and those are very distinct objects). Whether or not pointers can be losslessly converted between address spaces using the addrspacecast instruction, whether pointers in two address spaces may alias, and whether a cast is even permitted are all target-dependent properties.

Attributes of address spaces

An address space N has five (note, up from four currently) widths associated with it that are stored in the data layout

  • The pointer width P , which is the number of bits used to round-trip a pointer in address space P to memory or store it in registers, if applicable
  • The ABI width M, which is the number of bits used to hold the pointer in memory. Unlike the pointer width, this must be a power of two. By default, this equals the pointer width
  • The ABI alignment, which defaults to match the ABI width and is used for aligning structures, the stack, etc. (See DataLayout, wording to be refined)
  • The index width I, which is the number of bits that can be used to take offsets from a pointer. This is, by default, equal to the pointer width. This (should,must,is?) be less than or equal to the pointer width. It may be 0, representing memory locations that cannot be meaningfully traversed. (andgpu p8, indirect bindings, GC roots that don’t carry an offset if that’s a thing). The index width must be less than or equal to the pointer width.
  • (new!) The address width A, which is the number of bits used to represent the address within the pointer’s backing memory to which this pointer refers. If this address cannot or should not be computed, or is non-deterministic, the address width may be 0. Note that “backing memory” is often, but not always, a distinct hardware object - for example, a GC pointer that tracks an offset into an object may be “backed” by the memory allocated for that object, so its address component may be an offset. (Similarity, AMDGPU has address space 5 for stack allocations, which are 32-bit pointers that are really pointing into some hardware-managed chunk of stack, which means that, while, from the global view, they’re aimed at a 64-bit address, their address width is 32 bits). It [[TODO, is/isn’t ]] permitted for the address width of a pointer to be greater than its pointer width.

Pointer-handling instructions: getelementptr

(and ptradd if we go there)

getelementptr adds offsets to pointer, existing language fine, nothing’s changing here, the index width is still the index width.

Pointer-handling instructions: ptrtoint

%x = ptrtoint ptr addrspace(N) to iP transforms a pointer %p into its underlying representation as if by bitcast.

That is, it is equivalent to

%m = alloca iM, align(abi width / 8)
store ptr addrspace(N) %p, ptr %m
%x = load iP, ptr %m

ptrtoint may impact escape or alias analysis in [todo nail down what we lose by letting these bitr run raound].

(If the integer type on the ptrtoint isn’t actually the pointer width, zero-extend or truncate the iP as appropriate)

Pointer handling instructions: inttoptr

inttoptr is the inverse of ptrtoint - it takes an iP and produces a ptr addrspace(N) from it.

This is also an as-if-by-memory-round trip bitcast analogous to ptrtoint.

(Same zero-extension/truncation note)

Unseriazable / “non-integral” address spaces

Address spaces have a property separate from their associated widths - serializability. An address space is serializable if the semantics of a pointer can survive the type-punning trip through memory implied by ptrtoint and inttoptr. That is, a serializable address space is one where inttoptr(ptrtoint(%p)) == %p for all %p.

(new!) Pointer-handling instructions: ptrtoaddr

%x = ptrtoaddr ptr addrspace(N) %p to iA returns the underlying address for a pointer as an A-bit integer.

If the address width for a pointer is 0, then ptrtoaddr on that pointer is poison.

The ptrtoaddr instruction is linear with respect to pointer offseting. That is

%p.addr = pntroaddr ptr addrspace(N) %p to iA
%q = getelementptr i8, ptr addrspace(N) %p, iI %x
%q.addr = ptrtoaddr ptr addrspace(N) %q to iA
%px.addr = add_wrapping_low_bits iA %p.addr,  iI %x.addr
assert(icmp eq %px.addr, %q.addr)

holds for all pointers %p and all indices %x.

If the pointer width and address width of a pointer are equal, then the behavior of ptrtoint must be the same as the behavior of ptrtoaddr. That is, if your pointer can refer to A bits of memory using A bits of internal storage, that storage must contain said bits. If the pointer width and address width are not equal, there are no guarantees about the relationship between ptrtoint and ptrtoaddr.

(The usual sext/trunc note)

Pointer equality

The equality relationship between pointers in an address space is, ultimately, a target-specific decision. However, it must obey the following invariants:

First, if there exists a pointer %p such that %q = getelementptr i8, ptr addrspace(N) %p, iI %x and %r = getelementptr i8, ptr addrspace(N) %p, iI %y then icmp eq ptr addrspace(N) %q, %r is true if and only if icmp eq iI %x, %y is true, and icmp ne ptr addrspace(N) %q, %r is true if and only if icmp ne iI %x, %y is true.

The getelementptrs in the above definition are not required to produce pointers that are in-bounds or associated with the common ancestor %p.

Note that, if the pointer width, address width, and index are equal, this reduces to comparison between addresses by setting %p = ptr addrspace(N) null. Then, since, ptrtoaddr %q = ptrtoaddr null + %x and ptrtoaddr %r = ptrtoaddr null + %y, by linearity of ptrtoaddr, icmp eq ptr addrspace(N) %q, %r <=> icmp eq iP (ptrtoaddr %q), (ptrtoaddr %r) (As a caveat, if the index width is less than the address width, not all pointers are guaranteed to be reachable from the null value by offseting.)

At a minimum, this invariant requires that pointers within an object that are derived from the same source can be compared for equality based on their offsets by letting the common ancestor %p be the beginning of the object.

The second invariant is that if two pointers can be proven to refer to distinct objects, they must compare unequal. That is, if the set of locations associated with %q and the set of locations associated with %r are disjoint, %q and %r must not compare equal.

The third invariant is that comparison of a pointer to null is always a check that that pointer is equal to the null pointer for that address space in a ptrtoint sense.

For the CHERI folks, I think the above definition lets you have C-style “pointer equality is address equality” if you want it without too much pain. As far as middle-end passes are concerned, you can reason about equality of pointers that point into the same thing and share a capability, and if you know that two pointers don’t alias (two globals, or a global and an alloca, say), you can target-independently conclude that they’re not equal. From there, you’ve got wiggle room to define that two pointers into the same object are equal even if their capabilities don’t match, but nothing above required that.

And then it’s possible to take the opposite extension of == into an annotated pointer, and I can say that two buffer fat pointers are unequal if they’ve got different base addresses, whether or not there’s aliasing between those, because those two resources don’t have a common ancestor I can construct them from with GEP.

Integral address spaces

For brevity / historical reasons, a serializable address space where the pointer width, address width, and index width are all equal is called “integral”.

Special notes about address space 0

Address space 0 is required to be integral, to have a bit-pattern-0 pointer tha’t can’t be dereferenced unless annotated with null_pointer_is_valid, and to have (ptrtoint ptr null) == 0 (I think that last one’s still true).

Tangents

Side RFC: Can we get an addrspacecast-like that takes arguments?

There’re various operations on pointers - p8 @llvm.amdgcn.make.buffer.rsrc(p{0,1} base, i16 stride, i32 numRecords, i32 flags) comes to mind that are basically addrspacecast but need extra arguments. I think the current handling for these is to list them out in a function that contains all the intrinsics that return pointers which alias their argument. Is this fine, or do we want to something a bit more systematic here?

Side RFC: Multiple indices

There exist pointers (pointers into textures in graphics are the example at the top of my head) that meangfully have more than one index field. For example, some buffer resources on (at least AMD) GPUs are indexed by both an index and an offset, which form an indexing hierarchy of sorts. That is, bringing [RFC] Replacing getelementptr with ptradd - #16 by nhaehnle back to folks’ attention since we’re having another round of this sort of thing

  • (new!) The /address width/ A, which is the number of bits used
    to represent the address within the pointer’s backing memory to
    which this pointer refers. If this address cannot or should not be
    computed, or is non-deterministic, the address width may be 0.
    Note that “backing memory” is often, but not always, a distinct
    hardware object - for example, a GC pointer that tracks an offset
    into an object may be “backed” by the memory allocated for that
    object, so its address component may be an offset. (Similarity,
    AMDGPU has address space 5 for stack allocations, which are 32-bit
    pointers that are really pointing into some hardware-managed chunk
    of stack, which means that, while, from the global view, they’re
    aimed at a 64-bit address, their address width is 32 bits). It
    [[TODO, is/isn’t ]] permitted for the address width of a pointer
    to be greater than its pointer width.

Thinking back on the near/far/huge pointer model, I think there are two
subtly different notions here. The “effective address width”, which is
the actual effective address size, and the “offset width”, which is the
size of the integer you can trivially adjust with the pointer (in the
protected-mode x86, for all types pointers, this would be 16 bits). From
your parenthetical, I think you really want the offset width here, not
the effective address width.

(If the integer type on the |ptrtoint| isn’t actually the pointer
width, sign-extend or truncate the iP as appropriate)

Per InstCombine transforms, it’s a zext, not a sext.

If the pointer width and address width of a pointer are equal, then
the behavior of |ptrtoint| must be the same as the behavior of
|ptrtoaddr|. That is, if your pointer can refer to A bits of memory
using A bits of internal storage, that storage must contain said bits.
If the pointer width and address width are not equal, there are no
guarantees about the relationship between |ptrtoint| and |ptrtoaddr|.

Looking at this, I worry there is too much conflation here. If we take
“address width” here to mean what I call “offset width”, then this seems
to be trivially implementable. But for larger-than-offset-width (but
still no larger than “effective address width”) cases, there are cases
where you want ptrtoaddr to be meaningful, but you can’t guarantee any
easy relationship with ptrtoint. And the “pointer width and address
width are not equal” isn’t sufficient to segregate these cases, unless
you’re going to tell people to specify an “address width” that is not a
legal integer size (i.e., call a huge pointer an address width of, uh,
29), which I suspect would play havoc with attempts to use the property
for canonicalization.

Huh, yeah, you’re right, will edit. Got confused with GEP.

The “offset width” in your points is the existing “index width” - it’s the native size of a GEP offset.

I did actually mean the width of the effective address as “address width”. That’s the axis that pointers haven’t been distinguished along before.

So ptrtoaddr is “get the effective address for this pointer”. Having an address width of 0 means that you’re a kind of pointer that doesn’t meaningfully have an effective address - GC pointers, for example. My parenthetical was meant to note that there’s nothing stopping you from defining the effective address size as larger than the size of the pointer - unless we decide to ban that.

So yeah, I’d argue that a huge pointer does, in fact, have an address width of 29, a pointer width of 32, and … probably an index width of 32, since that’s what you’d GEP by. This means that you have %q = getelementptr i8, ptr(huge) %p, i32 %o and %x = ptrtoint ptr(huge) %p to i32 but %y = ptrtoaddr ptr(huge) %p to i29 - and yeah, that’s not a power of 2, but it’s a very sensible way to encode “four of the bits of this pointer aren’t doing addressing”.

Now, re that parenthetical about letting the effective address width be bigger than the pointer width … I suspect stack allocations on AMD cards may not be the best example since you really want to treat them as 32-bit pointers into some region of memory that’s isolated from everything else. So imposing [pointer width] >= [effective address width] is probably fine? Or at least I can’t think of a reason to not do that.

Now, in general, there’s no relationship between ptrtoint and ptrtoaddr - the function that goes from bits in registers/RAM/… to an effective address can be any pure function the hardware people dreamt up this week. However, if your pointer can effective address N bits of memory using N bits of data, then we make the entirely reasonable assumption that ptrtoint == ptrtoaddr.

The GEP-related property I gave for ptrtoaddr is, roughly, “moving by N bytes moves the effective address of a pointer by N bytes” up to various truncations/wrapping around the edge. That seems like a fairly sensible property to impose.