Skip to content

Proposal: represent bit pointers at runtime #16271

Closed as not planned
Closed as not planned
@mlugg

Description

@mlugg

I've been working a lot with pointers in Sema recently, and have become fairly confident that our abstraction of "bit pointers" as it stands today is highly flawed. This is a proposal to fundamentally change them.

Background

Currently, bit-pointers work by being a component of the pointer type. The type *align(1:6:8) T contains a pointer to an 8-byte "host" value, which itself has 1-byte alignment, and which contains a value of type T at an offset of 6 bits.

This setup somewhat simplifies code generation, since we don't need to worry about accessing at arbitrary bit offsets, and bit-pointers are given the same in-memory representation as any other pointer type. However, this comes at the cost of confusion. The whole point of a runtime pointer is that we don't need to know anything about the location of its target at compile-time; that information is not encoded in the type. (Well, okay, there's alignment, but you can always use align(1) if you'd like to be able to refer to any address). Bit pointers in their current form violate this rule by effectively encoding part of the address in the type.

Proposal

I propose that we eliminate the *align(a:o:b) T form from the language, and instead introduce a new "bit pointer" type which contains a bit offset in the pointer value. I'll briefly discuss syntax later, but for now, let's just say $T is a bit-pointer to T. const, volatile, and addrspace are all valid on bit-pointers, but align is not, with bit-pointers all being considered to have no alignment requirement (align(1)).

[$]T (a bit many-pointer) can also be introduced; technically a slice-like type also could, but it's less clear how to write that with a new sigil, and besides, it seems to me unnecessary complexity for something that will essentially never be useful (if you do need it, just use a struct of [$]T and usize). Note that [$c]T wouldn't make any sense, since bit-pointers can't interop with C in the first place.

@intFromPtr and @ptrFromInt are not valid operations to convert to and from bit pointers (supercedes #2677). @alignCast is also invalid on bit pointers, but @ptrCast, @constCast, @volatileCast, and @addrSpaceCast are allowed. You cannot in any defined way cast from a bit-pointer to a standard (byte-aligned) pointer(*), but you can coerce in the other direction (this coercion is always safe; it's akin to lowering pointer alignment). Note also that this solves an existing footgun where @ptrCast on a bit-pointer can easily result in pointing to bogus memory; another example of how storing the bit offset as a part of the type leads to trouble.

(*): if desired, we could allow @ptrCast to do this, asserting that the bit offset is 0, but I doubt there's really any situation where it's practically useful.

Justification

While this change may complicate compiler implementation, I believe it significantly simplies the language, and makes it more useful. In my opinion, bit pointers as they stand today are one of the most confusing parts of Zig; they're poorly documented, buggy, rare to come by, have a confusing syntax (it's not immediately clear how they relate to pointer alignment!), and make pointers unexpectedly non-interchangable. The more "opaque" bit-pointer system proposed here is a big simplification: the language-level construct can be simply defined as that "$T is a pointer to a T which may not be byte-aligned", and any details of the representation become implementation details (assuming bit-pointers have undefined layout, which seems to me a reasonable choice).

There are no complexities from ABI interop, since this is not an ABI type. That means we also don't really lose anything from bit-pointers having undefined layout.

Also note that pointers currently have a fourth align field: the "vector index". This is intended to deal with vectors having undefined layout (which they currently don't but hey, y'know!). This proposal would definitively eliminate the need for the vector index. Here's something interesting: the vector index is permitted to be a generic value called "runtime" (this is the type resulting from e.g. &vec[runtime_index]), but there is no memory to actually store the runtime-known index, so these types generally give garbage results when used right now. If we want &vec[runtime_index] to be permitted, these pointers must store this extra information, at which point we essentially have this proposal implemented, just limited to a small subset of types!

Syntax

The $T syntax here isn't a concrete proposal (in fact I don't think I really like it much), it was purely written here so I had something to work with. I did consider the syntax *bit T, but this reserves bit as a keyword, so IMHO is a non-starter. One option would be *align(0) T, although this may be a bit confusing since we've effectively rejected the notion of align(0) in other contexts in the past (#3802), and it makes 0 a special case since it is the only align which changes pointer representation. I'm not 100% sure where to go with this; bikeshedding is encouraged.

Implementation

On the level of semantic analysis, this change, while broad, is fairly simple: allow operations working with pointers to take $T where it makes sense, and to return $T where it makes sense. In terms of runtime representation, I propose that these pointers simply store an additional "bit offset" in the range 0 to 7 alongside the normal byte address. This does have the unfortunate effect of doubling the pointer size (so on 64-bit platforms we pay the cost of 64 bits for just 3 bits of information!), but the rarity of these pointers, and particularly of storing them, makes this (in my eyes) a non-issue.

Note that this representation removes the concept of a "host size" from bit pointers. I don't believe this raises any problems; this is used today essentially tell backends how to load these pointers, but backends can freely make whatever choice they want. In fact, using the host size naively can lead to inefficient code; if loading a 4-byte value from a 64-byte packed struct, we know for a fact that we need to load at most 5 bytes of memory, which will probably fit in a register for easy shifting!

Metadata

Metadata

Assignees

No one assigned

    Labels

    proposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions