I basically got this upside down. My "brutal" proposal was:
- every address in memory has an "is dereferencable pointer" bit.
- if a pointer is loaded from an address without that bit, you get a non-dereferencable pointer.
- pointer stores preserve the bit status of the original pointer.
- integer stores always leave the bit clear.
- integer to pointer casts create a dereferencable pointer, and are the only way of creating a new dereferencable pointer out of nothing.
- memcpy leaves the bit as it was before.
note 1: this disregards unaligned accesses, but they can probably be handled too.
note 2: transmute
is equivalent to a type-punned load.
Derivative properties:
- having the "dereferencable" bit set always causes less UB than having it clear. This means that a load/store pair can always be removed without making code less defined.
- loads of dereferencable pointers always sync against either an int->ptr cast or a primitive pointer operation.
- All integers with the same numeric value should be equivalent.
- memcpy can't be implemented in Rust because you can't copy the dereferencable bits manually, but rather needs to be a compiler intrinsic.
- int->ptr cast becomes a "side-effectful" unsimplifiable operation. I'm not sure how bad it is from an optimization perspective, but I don't see a good way around it (this might mean we want to shift users who merely want to smuggle an integer inside a pointer, like slice iterators, to use a type-punned load).