Proposal: UnsafePointer v2

Hello everyone! :waving_hand:

Recently I published a proposal about the issue and design flaws of the current UnsafePointer interface. This document proposes a new interface for UnsafePointer and a potential migration path.

In short, the current API of UnsafePointer has two major issues:

  1. Allowing unsafe implicit conversions of mutability and origins.
  2. Haphazard parameters: including a non-inferred mutability and a defaulted origin of (Imm/M)utableAnyOrigin.

The newly proposed interface for UnsafePointer is as follows:

struct UnsafePointerV2[
    mut: Bool,  # Inferred mutability
    type: AnyType,
    origin: Origin[mut], # Non-defaulted origin
    *,
    address_space: AddressSpace = AddressSpace.GENERIC,
]:
    ...

Along with this parameter fixing, the constructors will be updated to prevent implicit unsafe mutability casting and unsafe origin casting. (These can still be achieved, they just require explicit calls to unsafe_mut_cast and similar functions). This also brings the parameter interface in line with other pointer-like types, Span, LayoutTensor, etc…

Migration Options

The final part is the migration path/transition from the old interface to the new one.

The first option (which is outlined in the document) proposes a non-breaking solution that slowly deprecates and then eventually replaces the old type with the new v2 types.
The rough estimated timeline would look like this:

Nightly (current)

  • Introduce UnsafePointerV2 and helpful aliases.
  • Begin migrating stdlib and kernel code.

25.7 (late November 2025)

  • Mark UnsafePointer as deprecated.
  • Promote UnsafePointerV2 for general use.

26.1 (Jan 2026)

  • Remove old UnsafePointer.
  • Rename UnsafePointerV2 → UnsafePointer.
  • Deprecate the temporary UnsafePointerV2 name.

This path theoretically provides a transition period over ~2 releases that doesn’t immediately break code, but rather will deprecate the type. The issue with this is the interim UnsafePointerV2 type. As this type will be the replacement for UnsafePointer in 25.7 — requiring people to move over to it — and then in 26.1, will be deprecated, requiring people to rename UnsafePointerV2 back to UnsafePointer.

The second tentative option (not outlined in the document) is as follows:

25.7 (late November 2025)

  • Rename the current UnsafePointer to LegacyUnsafePointer.
  • Add the new and improved UnsafePointer.

26.1 (Jan 2026)

  • Deprecate LegacyUnsafePointer and eventually remove it.

This path has the downside of breaking current code that uses UnsafePointer when the rename/replacement happens, but has the upside of making a clean break in 25.7, moving all of the transition steps to a single release. This provides a way for people to simply “mass rename” all UnsafePointer instances to LegacyUnsafePointer (then devs can slowly move from LegacyUnsafePointer to the new UnsafePointer type over the next release or two) or devs can immediately update their usages to the new interface bypassing the legacy type entirely.

Asks from the community

We’re currently set on making the changes to UnsafePointer (fixing the implicit constructors, and fixing the parameters) — but it would be nice to know people’s thoughts on the migration path forward. Do people mind a breaking change or would you prefer the extended “deprecation” approach? Feel free to leave your comments/suggestions :slight_smile:

2 Likes

I’m all for removing implicit conversions from more parts of the language/libraries. Especially in the unsafe areas. It sounds like there will be a bit of an ergonomics tradeoff with these changes, but for unsafe interfaces, probably more explicit is better, even if it makes the types less convenient to use. I often get bitten by things implicitly converting when I’m not expecting it, so I’d welcome changes to make things more explicit.

As for the migration, the second option sounds like it has only one upgrade step needed on the happy path (migrate all your pointer usages at once and ignore that the Legacy thing even exists). Whereas the first option has two migrations in the best case. So I think I’m in favor of the second option.

Unsafe casts are sometimes unavoidable, and they usually force us to restate details that are “obvious” to the compiler. In practice, many such casts amount to:

q = rebind[type_of(q)](p)

rebind is too permissive and generic for this job, which is why we invented ad-hoc unsafe cast helpers. That leaves us choosing between: (a) spelling out information the compiler already knows, or (b) using rebind which looks ugly and arguably even less safe. There’s a middle ground that keeps the call site explicit while letting the compiler carry the type payload.

fn main():
  var dummy = 1

  var p = Ptr[mut=False, Int]()
  var q: Ptr[Int, origin=origin_of(dummy)]
  q = p.unsafe_cast()  # no need for explicit cast chain
                       # while still being explicit about the cast

@fieldwise_init
struct Ptr[
  mut: Bool, //, type: AnyType, origin: Origin[mut] = MutableAnyOrigin
](ImplicitlyCopyable, Movable):
  @implicit
  fn __init__(other: AutoParams, out self):
    pass

  # placeholder name
  fn unsafe_cast(self) -> AutoParams[type=type, mut=mut, origin=origin]:
    return AutoParams(self)

@fieldwise_init
struct AutoParams[
  type: AnyType, mut: Bool, origin: Origin[mut], //
](ImplicitlyCopyable, Movable):
  var val: Ptr[type, origin=origin]

I proposed this idea on Discord before, and people don’t seem to have noticed it: ref. This pattern should be helpful to other types with a thousand parameters (like LayoutTensor).

Per discussion on discord, additional fixes will be needed later once some more work on origins is done, so this isn’t going to be the final design.

However, I would like to fix whatever issue caused alignment to get dropped from UnsafePointer. Being able to use aligned loads where possible is fairly important for performance and even correctness. Was that a subtyping limitation in the compiler?

I think that making the origin explicit is fine, it will mean more verbose low-level code but that’s very reasonable given the dangers writing that level of code.

1 Like

Regarding migration, I think we can rename UnsafePointer now, and add a from ... import LegacyUnsafePointer as UnsafePointer to all stdlib files, so the diffs are more concentrated, and we are effectively promoting the new UnsafePointer from day one.

2 Likes

We could take the opportunity to introduce patterns like this:

# we don't have MutableOrigin.owned but it can just use external for now
alias UnsafeOwnedPointer[type: AnyType] = UnsafePointer[type, MutableOrigin.owned]

struct List[T: AnyType]:
  var _ptr: UnsafeOwnedPointer[T]

  fn unsafe_ptr(ref self) -> UnsafePointer[type, origin_of(self)]:
    return self._ptr.unsafe_cast_ownership[origin_of(self)]()

providing a safe-ish code-path that people are supposed to use for any type having an internal owned pointer

1 Like

If we adopt my auto conversion pattern. this could look even simpler.

fn unsafe_ptr(...) -> ...:
  return self._ptr.unsafe_cast()

I think we should build explicit conversion paths between the UnsafePointer “sub-types” so that we guide users towards how to “safely” use the type.

So in the case of unsafe_cast_ownership we constraint it to:

fn unsafe_cast_ownership[
  new_origin: Origin
](self) -> UnsafePointer[type, new_origin] where (
  Self.origin is MutableOrigin.owned, "origin should be MutableOrigin.owned"
) : ...