Pre-RFC: Disallow using assignment in a function call in Rust 2018 (disallow `f(a=b)`)


#1

Summary

Disallow using assignment expression a = b directly inside a function call.

let mut a = 1;
f(a = 2); //~ ERROR: Assignment expression cannot be used as function parameter

Rationale

  • Reserve the f(a=b) syntax for named argument.

    • Chosen because we already have println!("{a}", a = 2);
  • a = b returns () which is usually useless as a function parameter. For instance, the above example can be written more clearly as

    let mut a = 1;
    a = 2;
    f(());
    
  • One could still wrap the expression in brackets like f((a = b)) or f({a = b}) if one really wants to put an assignment expression there

Detailed design

  1. In Rust 2015, emit a future-incompatible warning during parsing (an error in Rust 2018), when an assignment expression is encountered parsing a call expression.

    function(.., .., .., something.lvalue = ..
    //      ^ Call expr                   ^ Assign expr => warn!
    
  2. The warning should suggest placing that assignment expression as a statement above the function call.

  3. A macro expression which evaluates to an assignment expression should be allowed:

    macro_rules! assign { ($a:ident) => { $a = 2 } }
    function(assign!(a));  // allowed
    
  4. A macro variable which evaluates to an assignment expression should be allowed:

    macro_rules! call { ($e:expr) => function($e) }
    call!(a = 2); // allowed
    
  5. Wrapping the assignment expression in a bracket should be allowed:

    function((a = 2), { b = 3 });  // all allowed
    
  6. Using assignment expression besides function calls should still be allowed:

    let b = [a = 2];       // allowed
    let c = (a = 3,);      // allowed
    let mut d = || a = 4;  // allowed
    e[a = 5] = 6;          // allowed
    let f = Struct { g: a = 7 }; // allowed
    
    // Note that anything of the form `x(a=b)` is still disallowed, like
    let h = Some(a = 8);   // disallowed
    
  7. Compound assignment is not affected

    f(a += 1); // allowed
    
  8. Macro variables expanded via $($x:tt)* is disallowed

    macro_rules! splat { ($($x:tt)*) => { foo($($x)*) } }
    splat!(x = 3); // disallowed 
    

Drawbacks

  • Complicates parsing
  • We have not really decided to support named argument.
  • We have not really decided to support named argument with this syntax.

Alternatives (= Unresolved Questions)

  • Reserve more

    • Make assignment a statement, like Python. That is, a = b will not be usable anywhere an expression is expected.
    • Extend the treatment to compound assignment f(a += 1) too
    • Extend the treatment to any expressions which natively return () or ! e.g. f(return)
  • Reserve less

    • Don’t disallow all assignment expressions, only disallow when the LHS of the assignment is an identifier

      f(a[1] = 2, b.0 = 3, *c = 4, d::E = 5);  // all allowed?
      
  • Reserve something else

    • We may want the syntax f(a: b) for named argument, which means we should disallow type ascription too.

      Unlike assignment, I expect the result of type ascription being much more useful, and thus there is much higher chance seeing this inside a function call. OTOH type ascription is still unstable, so we could change the type ascription syntax instead.

  • Do nothing

    • This means named argument cannot consider f(a = b) as a possible syntax, though alternatives syntax e.g. f(a => b) are still available.

Note

This Pre-RFC is mainly about reserving syntax which will conflict with named argument in the old edition.

I know named argument is a hot topic, but please avoid discussing how to declare a named argument function or whether f(a=b, b=a) and f(b=a, a=b) are equivalent here, which are irrelevant to this Pre-RFC.


#2

It would be nice to warn/reserve all the potential syntaxes for named arguments: :, =>, :=, etc.


#3

It would be nice to warn/reserve all the potential syntaxes for named arguments: :, =>, :=, etc.

Well, neither of the syntaxes you proposed are valid rust code (edit: in that context) at the moment, so there is no need to reserve those! Instead the = one is currently valid.


#4

I like this idea. I have the feeling that the impact on existing code will be minimal to non-existent. (This needs to be confirmed of course)

@pietroalbini You refer to @gbutler’s comment, right?


#5

@pietroalbini You refer to @gbutler’s comment, right?

Yep, added a quote.


#6

Syntax bikeshed: foo(name: expr) looks nicer, and it is also similar to struct construction.


#7

@H2CO3 For me it’d make more sense if struct construction used the = as well. (This idea is not from me. I’ve heard this somewhere) So: let my_struct = MyStruct { a = 7 } instead of MyStruct { a: 7 }. Then : would always refer to types and = to assignment.


#8

I’d like to see Struct { a: b = c } disallowed too, for the same reason; the current struct assigment is a wart.


#9

Meh. That looks less nice exactly for the reason this does: it’s more noisy.


#10

I wrote that in my (now edited) first post as well. However, I don’t think that there is a conflict.

Disallowing it probably won’t hurt, though.


#11

More noisy? You mean you’re not used to it. It’s not noisy by any definition.


#12

I am used to it, I’ve been programming in C for 9 years, and C has the same syntax (except that it also requires a leading . before field names). I’m still glad Rust didn’t make that choice.


#13

I’ve been working with JavaScript for just as long. It uses the : as well. This doesn’t mean that it makes sense though. It’s an assignment.

In any case, you’re right with one thing: Struct construction and the syntax for named params (if we ever get them) should use the same notation. Be it : or =.


Pre-RFC: `=` for struct construction
#14

You can look at it as an assignment. But you can also look at it as a name-value pair (which it is), and in that case it makes perfect sense. Spelling both as : also has the additional advantage of requiring one less breaking change.


#15

This has been presented as one of the alternatives, though the conflict with type ascription is quite unfortunate.


I don’t see how disallowing Struct { a: b = c } is related to enabling Struct { a = c }. The latter is already invalid syntax today AFAIK.


#16

MyStruct { a: 7 } could just be deprecated very gently (no breaking change) and rustfix could update existing code very quickly to MyStruct { a = 7 }. It’d make usage of type ascription (e.g. MyStruct { a = 7: u64 } in struct construction possible. Not bad if you ask me. (Edit: Although it’s probably worthless because the struct is already typed :D)

That said, this RFC has minimal impact and it keeps the door open for both ways. Whatever the choice I’d prefer consistency between these two notations.


#17

I wasn’t even aware this was possible and can’t really see any use for it so I would support disallowing it, as well as in struct if it’s currently allowed. I doubt that anyone is using that “feature” on purpose?!


#18

@Keats a = b is an expression like any other. That’s why you can use it in situations like

match expr {
    pat => a = b,
}

It’s just that, since it has type (), it’s not a very useful expression. Banning it in certain situations would complicate Rust’s syntax though, it could break macros for instance.

This makes me wonder whether a = b should be an expression at all. Maybe match arms like that should require a block to be written:

match expr {
    pat => { a = b; },
}

Alternatively, make match the special case and have an extra syntax rule for => a = b (are there any other places where assignment-as-an-expression is useful?).

Edit: Also +1 to changing the struct expression syntax to MyStruct { x = y } in the new edition.


#19

The = for struct construction seems very interesting. I’ll create a new thread for this.


#20

Changing the type ascription syntax feels like a bad type of judgement :wink:

Personally I hope we don’t; but I have nothing against future proofing :+1:

If y’all want to change the struct initialization syntax to field = expr or at least not allow field: value and field: binding, then you need to at minimum crater run this to see the extent of the breakage. My hunch is that the breakage extent makes it wildly undoable. I think nearly every crate would be broken by this change or start emitting warnings (depending on strategy).