Rust: Tracking issue for RFC 803, "Type ascription"

Created on 16 Mar 2015  ยท  59Comments  ยท  Source: rust-lang/rust

Tracking issue for rust-lang/rfcs#803. Important points:

  • [x] Implement the expr:Type syntax
  • [ ] Ensure soundness with respect to ref positions:

    • [ ] let ref x = <expr> / let mut ref = <expr>

    • [ ] match <expr>: Type { ref x => ... }

    • [ ] (<expr>: Type).method_with_ref_self()

  • [ ] Permit coercions like &[1, 2, 3]: &[u8] -- https://github.com/rust-lang/rust/issues/78248
B-RFC-approved B-RFC-implemented B-unstable C-tracking-issue T-lang

Most helpful comment

that happens, how about an RFC to replace all semicolons with a less confusable character?

let plus_two = |x| {
    let mut result: i32 = xโœ“

    result += 1โœ“
    result += 1โœ“

    result
}โœ“
assert_eq!(4, plus_two(2))โœ“

makes the language even look safer, everything is checked after all :grinning:

All 59 comments

There's one thing making type ascription less convenient than it could potentially be. While it has the same operator priority as as (it is modeled after as in general), i.e. the highest from binary operators, it's still lower than member access . (dot).

Compare these two expressions

let v = v.iter().collect::<Vec<_>>().sort(); // Parens are not required
let v = (v.iter().collect(): Vec<_>).sort(); // Parens are required
// Formatted on several lines
let v = (v.iter().
           collect(): Vec<_>). // Looks bad, man
           sort();

I'm not sure if it is worth fixing or if it can be fixed without hacks, but it is worth discussing at least.

BTW: This has is a breaking change for crates that implement a similar macro syntax already, such as prettytables.

@hoodie I opened #30531, since this seems worth tracking

cc @nrc

Is it a good idea to only do ascription inside parentheses, so let a = x : T is not ascription yet, but let a = (x : T) is? This can allow more room for some further syntax additions.

For the record: a patch implementing support for method chaining with type ascription https://github.com/rust-lang/rust/pull/33380

Why is this in the book if it won't compile? (1.9.0)

https://doc.rust-lang.org/book/closures.html

@0tt what exactly?

@0tt Type ascription is specifically when adding a : T type annotation after an _expression_. Not in the parameters to a closure or a function, and not on a variable binding. Only when it follows an expression which I do not see any cases of on the page you linked.

Oops. I accidentally typo'd a semicolon as a colon:

let plus_two = |x| {
    let mut result: i32 = x;

    result += 1:
    result += 1;

    result
};
assert_eq!(4, plus_two(2));

that happens, how about an RFC to replace all semicolons with a less confusable character?

let plus_two = |x| {
    let mut result: i32 = xโœ“

    result += 1โœ“
    result += 1โœ“

    result
}โœ“
assert_eq!(4, plus_two(2))โœ“

makes the language even look safer, everything is checked after all :grinning:

Should use emoji tbh. A language for the future.

re the soundness question in the OP, this comment is probably the best summary.

Before stabilizing this feature, I would like to see how this potentially impacts optional and named arguments, namely the expr : type syntax in function call argument position.

Could named arguments use this syntax if it wasn't for type ascription?

If yes, and no other syntaxes could be devised that are as optimal as name : type, I'd strongly recommend changing the syntax for type ascription, in favor of allowing the optimal syntax for named arguments.

I believe named arguments would be much more widely used than type ascription, and the syntax optimization for ergonomics should go there instead.

Ref https://github.com/rust-lang/rfcs/issues/323

Not related to the above, but I do think that in general, the expr : type syntax can be confusing.

I do remember encountering unclear error messages that mentioned type ascription, while my intent was not to ascribe the type of an expression. But I don't remember any concrete examples.

In the RFC there is a suggestion to add lint that warns about trivial casts.
Why don't we use as to suggest the compiler the type of an expression and if the compiler knew the type, then it warns us about it.

@KalitaAlexey Because as is used for casts which does type conversion, very different from ascription that just hints the type to help inference.

Hi @leodasvacas,
Got it.

I do remember encountering unclear error messages that mentioned type ascription, while my intent was not to ascribe the type of an expression. But I don't remember any concrete examples.

My common case is something like the typo RefCell:new.

What's the state of this feature?

It seems to me that this would be very useful for futures-oriented programming. Because expressions of composed futures/streams get very complex types, it would be very useful to be able to "pin" parts of an expression to expected types, both to help type inference and to get better error messages. Current mechanisms to do this (let bindings and type-assertion functions) are cumbersome by comparison.

(ping @alexcrichton, @brson as it came up in conversation with them a couple of months ago)

@jsgf I'd prefer to address this problem more directly, though better error message heuristics or, ultimately, use of impl Trait internally to the library. But I know we need to make improvements here soon.

The state of this feature:

It is implemented, but only for exact types. To support coercion, we have to handle the soundness issues around the ref-lvalue case. I forget the details. These are not insurmountable, just a bit more complex. If anyone is interested, I can try to write up some mentoring instructions towards finishing it off.

@nikomatsakis It's not clear to me, though, whether we want to continue pushing on this feature. It might be worth some further discussion on the lang team before investing more in the implementation.

@aturon Just to be clear, what exactly do you mean by "this feature" -- type ascription in general, or type ascription triggering coercions? Are you suggesting to un-accept the RFC? I still think type ascription is useful and actually wanted to use it recently (without triggering coercions) and was sad that it is still not there...

@RalfJung type ascription in general. Many of us feel that let x: Type = suffices, and that having both : and as as expression-level operators is undesirable given the relatively small benefit. (Long term, we'd like to deprecate as in favor of more specific APIs etc, but that's a separate matter.)

Could you say a bit more about the case you ran into, and why let didn't suffice?

as is certainly not what I wanted because of potential side-effects like wrapping integers to fit smaller types.

Unfortunately, I do not remember the exact code where I wanted the :. I think it was some case where I got a value from one function and immediately passed it to another function -- a nice one-liner, I just wanted to be sure I got the return type of the first function right so things actually did what I think they should do. I certainly didn't want to store this temporary result for the rest of the current scope.

While I am a big fan of type inference, it can sometimes lead to code where the types involved are entirely unclear. Together with Rust's type-based dispatch mechanism, that can make code quite hard to follow. Type ascription as expressions helps here by making it easier to explicitly state types without having to go all the way to giving this intermediate result a name and permanent storage in the stack frame.
A let with type ascription is also very syntactically heavy IMHO.

I have wanted to use ascription in the past in actual code (that I also can't recall...) but a more unusual use-case that does spring to mind is debugging type errors - being able to reduce large chains of code with inferred types by judiciously inserting my desired types is something I do wish for from time to time. My current options are 1) stare at code until I see the problem or 2) change it all to use let, then change it back once I've figured out the issue.

I use coercions often enough when debugging code that I'd like them to stick around. They would be useful for forcing creation of DSTs if we had coercing ascription.

I would also like to see ascription in patterns, which hasn't been RFC'ed yet (there is an issue in the RFC repo).

It is probably worth reconsidering in depth our conversion story. I know the lang team has talked a bit about this. One thing from the ergonomics initiative was making integer widening implicit. It would be nice too to have casts with boundchecking with similar behviour to overflow checks (i.e., panics in debug builds, no checks in opt builds). Not sure if they have to be a new kind of cast, or whether this could be the default behviour for as after an epoch.

Personally I think the "grand plan" should be:

  • Deprecate as for being too much of a footgun
  • Finish implementing and stabilize : (ascription). With as deprecated the potential for confusion between them isn't a major issue any more.
  • Add a unary cast operator (cast foo), or maybe just a .cast() method (foo.cast()), with the checked-in-debug-unchecked-in-release semantics @nrc describes, and with the target type being inferred.
  • Instead of foo as Bar, you can now write cast foo: Bar or foo.cast(): Bar.

@nrc do you have a link a discussion about the widening integers ergonomics initiative?

@glaebhoerl so &x as *const u64 as *const u8 -> cast (cast &x: *const u64): *const u8?

I'm not a fan of that I must admit. ~I see as and ascription as being very different - as feels like a nod at the fact that Rust is a systems language and can cheaply convert some 'primitive' types (functions, pointers, numbers) into other representations, even when the translation may be lossy.~

Working with pointers in Rust is already not a great experience, it's very syntax heavy. I'd prefer not to go the same direction for integers.

I see as and ascription as being very different

I completely agree, and I'm not sure what in my comment makes you think otherwise?

I think those couple of sentences muddied my comment, please ignore them - the primary point was the extra syntax and I got sidetracked.

Yeah, syntaxwise it's a legit tradeoff. I'm pretty sure there's also cases where the cast syntax would be more convenient (e.g. when it's actually sensible to leave off the target type).

Lossy conversion without an explicitly stated target type sounds like a huge footgun to me.

TBH I don't see why as and : should be confusing. Converting things and saying that things have certain types are two very different operations. It doesn't seem to hard to explain this.

No progress on this one?

:+1: to deprecating as in favour of more targeted things, like explicitly-introduced left-to-right coercion points. Personally, I think all the non-coercion things that as does would be better as functions/methods.

hey can we remove type ascription in favor of an std::identity function?

std::identity::<Type>(stuff)

current alternatives include:

  • (|x:ty|x)(expr) for use in macros.
  • ???

if we had it in std, we wouldn't need to introduce new syntax, and we wouldn't need to deal with the soundness issues and stuff, and it could be freely used in macros.

Both a function or a closure cause a value to be moved, which affects its life time more than just a type annotation would.

#[inline(always)] and the compiler should be able to detect identity functions.

Or identity can be a magical intrinsic.

The problem is that calling a function takes argument's ownerwhip. This cannot be cancelled by being an intrinsic (that is magical but stil has to appear to be a function), right?

let s = "foo".to_string();
(s : String).len();
println!("{}", s); // OK
identity::<String>(s).len();
println!("{}", s); // ERROR use of moved value: `s`

It is meant for use without bindings.

If you wanna use it with bindings, just use let x: ty.

Oh, of course. So it may be temporary lifetime things (for x: RefCell<_>, *x.borrow() : T works, but identity::<T>(*x.borrow()) doesn't work).

Won't NLLs solve that?

How about this as a more specific plan:

  • Move towards as _becoming_ the syntax for type ascription (with coercion support)

    • Reusing as has the nice property that things like my_ref as *const _ don't need to change

  • Start linting to suggest methods instead of non-coercion as

    • _error_ messages already do this, suggesting u64::from where infallible (not as u64)

  • Over time, add more methods to cover the rest of the cases

    • For example there's now NonNull::cast; raw pointers could get the same (or similar)

    • And maybe wrapping_from between integers, or some other variant from IRLO (thread) (thread)

  • Eventually (Rust 2021?) remove support for non-coercion as

    • Which is an allowed edition change as it's in the frontend and we'd be warning about any breaks

I think @scottmcm's plan sounds good. Repeating what I said on #rust-lang: I can live with the problem of not having 100% consistency personally. Some inconsistency is to me better than not having the feature at all. Unfortunately, with the current language and our backwards compatibility promises, we can't achieve 100% consistency with typing judgements in the language across the board and I'm not advocating that we write:

let foo as type = expr;

That said, I think the idea due to @cramertj to have some sort of expr.type!(MyType) or say expr.as!(MyType) or even expr.at!(MyType) (read as: "expr is typed at MyType") is interesting. With postfix macros as in https://github.com/rust-lang/rfcs/pull/2442 you could build that.

An example:

let vec = iter.map(..).filter(..).collect().at!(Vec<u32>);

The benefit of post macros here is that it allows chaining... however, I think it is unlikely to have many type ascriptions per expression.

As a long-time user of Scala, I have only very rarely seen anyone confuse : Foo with .asInstanceOf[Foo]. Granted, as Foo is less different syntactically, but I still think it is very important for correctness to be able to distinguish between telling the compiler "I believe this type to be that, but check it for me" and "I want you to make this type be that if you can". That is, I want some way to avoid coercion, because coercion may be the wrong thing to do.

(Numeric types in Scala have a notion of "weak conformance" which means they can get coerced unexpectedly; this has turned into a a fruitful source of puzzlers. Rust has much less of an issue presently, but dropping the distinction between pure ascription and request for coercion is likely to erode the improvement. It would be especially pernicious if the very thing you would use to check your assumptions could break them instead!)

@nikomatsakis could we improve error message in this case

proto::Status:ok()

I got

error[E0658]: type ascription is experimental (see issue #23416)
  --> src/main.rs:6:26
   |
76 |             status: Some(proto::Status:ok())
   |                          ^^^^^^^^^^^^^^^^^^
   |
   = help: add #![feature(type_ascription)] to the crate attributes to enable

According this RFC type ascription should contain : and space between words. But it's spreading on typo mistake with ::

cc @estebank ^-- We discussed this at the all hands on a night out... the idea, if you remember, was that you check for this pattern when the LHS of : is valid under <LHS> and RHS is not an existing type.

@centril I remember. I made https://github.com/rust-lang/rust/pull/59150 with a different approach, more hacky but one that would have given the right error above.

@estebank Oh haha; I even reviewed that one... ๐Ÿ˜…

@Centril https://github.com/rust-lang/rust/pull/62791 and https://github.com/rust-lang/rust/pull/62816 should cover all of the (common) type ascription typos (: -> ;, foo(x: bar), foo:bar::baz(1), etc.). With that I believe that "diagnostics being terrible" is no longer a valid reason _not_ to continue with the current syntax.

What are the things that needs to be done to make this stable?

It's stuck on finding consensus in https://github.com/rust-lang/rfcs/pull/2522 plus making the necessary changes (+ writing a lot of tests for them) outlined here and there.

I think this RFC breaks loop labels. Unless I've mis-written my code... I'm trying to do this (sorry about the lack of indent in some places, cargo fmt fails because of this issue):

pub async fn probe() {
    printkln!("Init: PCI scan started");
    if let Ok(table) = acpi::init() {
        if let Some(regions) = table.pci_config_regions {
        for sg in 0..MAX_SG {
        let mut bus: u8 = 0;
            while bus < MAX_BUS.load(Ordering::SeqCst) {
                for device in 0..MAX_DEVICE {
                    func_loop: for function in 0..MAX_FUNCTION {
                            if let Some(addr) = regions.physical_address(
                                sg as u16,
                                bus as u8,
                                device as u8,
                                function as u8,
                            ) {
                                use crate::memory::{allocate_phys_range, free_range};
                                allocate_phys_range(addr, addr + 4096);
                                let mut dev = PCIDevice::default();
                                dev.segment_group = sg as u16;
                                dev.bus = bus as u8;
                                dev.slot = device as u8;
                                dev.function = function as u8;
                                dev.phys_addr = addr;
                                let vendev = read_dword(addr as usize, VENDOR_ID);
                                dev.vendor = (vendev & 0xFFFF) as u16;
                                dev.device = ((vendev & 0xFFFF0000) >> 16) as u16;
                                if dev.vendor == 0xFFFF {
                                drop(dev);
                                free_range(addr, addr + 4096);
                                continue;
                                }
                                let classrev = read_dword(addr as usize, CLASS_REV);
                                dev.class = ((classrev & 0xFF000000) >> 24) as u16;
                                dev.subclass = ((classrev & 0xFF0000) >> 16) as u16;
                                dev.prog_if = ((classrev & 0xFF00) >> 8) as u8;
                                dev.revision = (classrev & 0xFF) as u8;
                                dev.header_type = read_byte(addr as usize, HEADER_TYPE);
                                let v = dev.header_type & 0x7F;
                                if v == 1 || v == 2 {
                                    // Bridge or PCI card bus
                                    let secbus = read_byte(addr as usize, SEC_BUS);
                                    dev.secondary_bus = secbus;
                                    if MAX_BUS.load(Ordering::SeqCst) != read_byte(addras usize, SUB_BUS) {
                                    MAX_BUS.store(read_byte(addras usize, SUB_BUS), Ordering::SeqCst);
                                    }
                                }
                                printkln!("init: PCI device {:X} (vd={:X}:{:X} c={:X} sc={:X} pi={:X} bi={:X}:{:X}:{:X}:{:X})", addr, dev.vendor, dev.device, dev.class, dev.subclass, dev.prog_if, sg, bus, device, function);
                                add_device(dev);
                                if !(dev.header_type & 0x80) == 0 {
                                break func_loop;
                                }
                            } else {
                                continue;
                            }
                        }
                    }
                }
            }
            let devs = PCI_DEVICES.read();
            printkln!("init: PCI scan complete; {} devices found", devs.len());
        } else {
            printkln!("init: error: no PCI regions");
        }
    } else {
        printkln!("init: error: ACPI unsupported");
    }
}

Attempting to compile this code on rustc 1.47.0-nightly (8e21bd063 2020-08-14) yields:

error: expected identifier, found keyword `for`
   --> src\pci.rs:161:32
    |
161 |                     func_loop: for function in 0..MAX_FUNCTION {
    |                                ^^^ expected identifier, found keyword

error: expected `<`, found `function`
   --> src\pci.rs:161:36
    |
161 |                     func_loop: for function in 0..MAX_FUNCTION {
    |                              -     ^^^^^^^^ expected `<`
    |                              |
    |                              tried to parse a type due to this type ascription
    |
    = note: `#![feature(type_ascription)]` lets you annotate an expression with a type: `<expr>: <type>`
    = note: see issue #23416 <https://github.com/rust-lang/rust/issues/23416> for more information

Now in this instance the label isn't necessary, but I was trying something else and I was getting this error so Ithought I might point this out.

Oh, whoops. I use a screen reader and I never have it set to announce
all punctuation so I forgot the apostrophes. Perhaps we can add a
suggestion inthe compiler if it sees that kind of pattern?

On 8/16/20, Charmander notifications@github.com wrote:

@ethindp
https://doc.rust-lang.org/stable/rust-by-example/flow_control/loop/nested.html:
func_loop: โ†’ 'func_loop:

>

--

You are receiving this because you were mentioned.

Reply to this email directly or view it on GitHub:

https://github.com/rust-lang/rust/issues/23416#issuecomment-674570164

--
Signed,
Ethin D. Probst

It's stuck on finding consensus in rust-lang/rfcs#2522 plus making the necessary changes (+ writing a lot of tests for them) outlined here and there.

RFC 2522 has been postponed. I think that means we should consider whether to stabilise type ascription for expressions as currently implemented. Looking through this issue I found only the following outstanding concerns/objections:

There was also the soundness question summarised here https://github.com/rust-lang/rfcs/pull/803#issuecomment-78102035 . But I think having read https://github.com/rust-lang/rust/issues/78248 that in fact this soundness issue does not arise because we do not support coercion in type ascription. Supporting coercion might be nice but I don't think it should be a blocker for stablising this feature.

I opened #78248 which describes how (I think...) we can support coercions in a relatively straightforward way. I do think that coercions are an important part of this feature. However, there are some other concerns about the syntax that have been raised. Last time I checked we definitely did not have full consensus on going forward here.

Was this page helpful?
0 / 5 - 0 ratings