Extremely pre-rfc: postfix keywords for all

Entirely a random sketch because I just want to get this out of my head:

When we went from try! to ? it was clear that postfix was a better thing for the way people used Rust.

When we accepted .await, this truth was reinforced, while also introducing the fascinating precedent of postfix keywords.

I would like to propose, or at least plant the seed of, a massive expansion of the syntax of .await to anything and everything it would make sense for. We should minimize places where you need to introduce parens to jump between styles.

A particularly egregious case I am frequently cursed with is casts and ref/deref.

&mut *((&mut x.y as *mut T as *mut U).add(idx) as *mut V);

With the new cast methods on ptr we now have:

&mut *(&mut x.y as *mut T).cast::<U>().add(idx).cast::<V>();

which is certainly better but what if we could write (strawman):

x.y.&mut            // <-- we have the ref and mut keywords if you prefer
   .as::<*mut T>    // <-- no parens, debatable
   .cast::<U>()
   .add(idx)
   .cast::<V>()
   .*.&mut;         // <-- sadly no reserved deref keyword, and it's
                    // used in the Deref trait so not great for editions

(note ref raw might also be fine to introduce as a concept here as well)

Minor notes from the little I have thought about this:

.& is potentially ambiguous thanks to the nightmare of 0. being a valid float literal. 0.&1. / 0.&x is an interesting expression to consider. Is it taking a reference to the the float literal "0."? Might we misparse this as a fragment of a field access? As a human I can correctly determine "no" because the expression starts with a number (indicating a literal and not an ident), and the & is followed by another sub-expression, instead . to continue the chain or some sort of expression terminator like ; or ).

But I can't remember enough about parsers to know if these arguments work out formally in an LRKG(7) or whatever system we prefer our grammars to conform to.

Anyway: we should take the precedent of .await seriously, as a thing to use more!

15 Likes

Many of us consider the decision of not requiring the decimal point to be surrounded by digits to have been a major mistake, sacrificing visual parsability and forward extensibility to save typing one character. That's a great philosophy for a language that prioritizes ease of writing over ease of reading, but it's antithetical to most of Rust's goals.

10 Likes

Just use .deref() or .deref_mut().

raw pointers do not implement the deref trait, and there is no signature you can give to such an inherent method on raw pointers, as creating a reference is semantically incorrect.

2 Likes

I do like the idea of postfix .match which came up during the .await discussions. But these are .keyword, and I'm less enthusiastic about .operator.

Can you explain further? Pointers do have as_ref() and as_mut() inherent methods -- &* and &mut* would just need unchecked versions of this, no?

3 Likes

Maybe we need a syntax like Haskell's point-free style. A new operator which makes following keyword/function/operator turn into postfix form. Then we can also implement currying and facilitate it...

2 Likes

Likewise; postfix versions of prefix operators don't seem like they'd enhance readability. I'd sooner have a method version (e.g. .not() rather than .! or similar).

2 Likes

That seems like a very reasonable change to make in a future edition. You could test that in crater easily enough, and if it seems sufficiently un-invasive, you could introduce a lint to start discouraging literals like .5 or 2..

1 Like

The current prevailing stance of the Unsafe Code Guidelines effort is that creating a reference is a very strong assertion of properties the referent has (such as there existing a properly initialized and unaliased allocation for the entire region of memory that the reference's type implies should exist). Dereferencing a raw pointer (or using read/copy/write) does not make these assertions. As a result, all of these operations are compiler intrinsics that perform something inexpressible by anything whose signature involves returning a reference.

Note that box.deref().x only "works" because the . operator implicitly derefs references.

1 Like

All of those statements would have to apply to as_ref() and as_mut() too. They're wrapped in Option in case of null, but otherwise should have the exact same validity considerations. They do just use Some(&*self) and Some(&mut *self) in their implementation, after all.

That's what the unsafe signature is for, as these methods are.

There's also this note on as_ref(), "If you are sure the pointer can never be null and are looking for some kind of as_ref_unchecked that returns the &T instead of Option<&T> , know that you can dereference the pointer directly." But your desire for postfix gives a good reason to add as_ref_unchecked().

2 Likes

Except that it wasn't at all unequivocal. The lang team's choice was pushed through, but many people still didn't (and don't) like it.

.*.&mut;

Oh no, please don't do that. That looks extremely confusing. No, I don't think we should make decisions based on the single code example you cited. That example demonstrates terrible style, for several reasons.

  1. It's a very long, complicated expression with many non-trivial operations that should probably be broken up into distinct subexpressions.

  2. Not only so, but it uses raw pointers, so the cost of a mistake is even higher (read: memory safety issues/UB) if you use that pointer later.

  3. It's not demonstrative of the point you're trying to make either; it doesn't really get any better (more readable? Is that the goal?) due to the postfix keywords.

  4. This is not an argument about the code example but about the proposed feature in general. Let's not introduce even more syntax for doing the very same thing. If you have something that looks unreadable because it's one huge line of code, use existing features to decompose and refactor it. There are plenty of them. We've got named bindings, functions, and so on. This is really not warranted.


I'd still support ceasing to accept 0. and .0 and requiring digits on both sides of the decimal point; these "truncated" float literals are quite annoying to read.

9 Likes
impl<T: ?Sized> *[const|mut] T {
    // today
    pub unsafe fn as_[ref|mut]<'a>(self) -> Option<&'a T> {
        if self.is_null() {
            None
        } else {
            Some(&[mut] *self)
        }
    }

    // "deref"
    pub unsafe fn as_[ref|mut]_nonnull<'a>(self) -> &'a T {
        &*self
    }
}

// imaginary "unsafe Trait"
impl<T: ?Sized> (unsafe Deref[Mut]) for *const T {
    type Target = T;
    unsafe fn deref(&'_ [mut] self) -> &'_ [mut] T {
        &[mut] **self
    }
}

Since you're going to create the reference anyway, all of the reference invariants must apply.

1 Like

No. They are merely conveniences for promoting the raw pointer to a reference. If you don't want the semantics that come attached to creating a reference, you must not use those methods.

No, you're focusing too much on the specific example, which incidentally doesn't care about the greater flexibility of raw pointer accesses (because I took for granted that everyone knows deref doesn't work here, given there is a very important rfc about this problem). If I add a field access, like ptr.*.field.&mut, and the memory is only properly valid for that field, using something like as_ref() would potentially be Undefined Behaviour. There are many such cases, as indicated by the rfc.

This is the example you should have used, as it has additional implications.

I responded to .*.&mut which would be fine with .as_mut_unchecked().

If we get a concept of &raw, couldn't that be returned by an inherent method too?

Could that case not use .read()? Actually, I suppose it would matter in the specific case of only reading one of the fields, which I did overlook.

It's the fact you can do something with a place other than {create a reference, create a pointer, read the entire place} that I overlooked due to the specific example.

In effect, it's pointer projection that needs to be (yet "can't be") done, not ptr-to-place.

EDIT: fixed the link to point to the right place, whoops

1 Like

No, it's not a type. &raw my_expr is just syntax to perform &my_expr as *const _ without transiently creating a reference (because making a reference makes those strong validity assertions, which makes it impossible to ask the compiler to offset a raw pointer to a particular field without incidentally making those assertions about that field, which means there's no way to initialize memory by writeing to each of the fields if your type is repr(rust), because only the compiler knows the layout of such a type).

This is not the place to discuss this issue. Go to the RFC.

Please don't be dismissive. I'm not trying to debate that RFC, only to understand how it applies here.

More generally and back on topic,

I'm not really in favor of a postfix "pointer-to-place" (here, .*). Rather, we should encourage doing pointer projection with raw pointers (via some projection syntax) if the pointer needs to not be turned into a reference, and <*const _>::as_ref/friends if it can be.

I'm really ambivalent about postfix "place-to-pointer" (here, .&[raw] [mut]). If we have pointer projection, these shouldn't be necessary most of the time due to de/ref coercion, and when they are required, it's at the very start/end of the statement only, which doesn't lead to ping-ponging.

I'm in favor of continuing towards deprecating as. (Thus not really in favor to a different variant with different precedence rules.) Due to coercion and with generalized ascription, (_: &_) as *const _ can become (_: &_): *const _, though this does in its current implementation still need to be bracketed to continue the chain, IIRC.

1 Like

As observed above, it is not as simple for *. The expression *some_ref is a different expression kind, a 'place expression' (lvalue in C) and can be on the left-hand of assigments while all current postfix operators (which all produce value expressions) are not. It can also be used as the right-hand-side of a match without reading its value. It would be confusing and sketchy to find in the middle of an operator chain of a value expression in a right hand side.

However, the composite operations involving both & and * may be move interesting as post-fix in value expressions. To that effect, deref/deref_mut are already the method form of & **/&mut **. It's interesting to ask if there could also be a post-fix expression specifically for:

  • &(*ptr_like).path
  • &mut (*ptr_like).path
  • for raw RFC: &raw (*ptr_like).path

That would be "->" or otherwise "(generic) pointer projection".

3 Likes