Roslyn: let statement may break compatibility with a user-defined type named let

Created on 15 Apr 2016  路  20Comments  路  Source: dotnet/roslyn

The parser doesn't know whether or not there is a user-defined type named let, so it parses a let statement assuming that no such type exists. As a practical matter than would break existing code that uses let as the name of a type. Do we want to do anything about this?

Area-Language Design Language-C# New Language Feature - Pattern Matching Tenet-Compatibility

All 20 comments

People would be able to work around this by using @let to refer to the type. So, whilst a breaking change, it's easily fixed. The benefits that let will bring to the language far outweigh the tiny inconvenience this will cause to, at most, a few developers.

It is used in the same position as var, it should behave the same way I think in the presence of a let type in scope.

@bbarry,

Good point. The following code is valid and compiles with C# 6, so the C# 7 compiler ought to behave the same when encountering a let type:

public class Foo 
{
    static void Main()
    {
        var x = new var();
    }
}

public class var {}

However, there is a potential problem in handling the following code, which also shouldn't in a "x is readonly" type error:

public class Foo 
{
    static void Main()
    {
        let x = new let();
        x = new let();
    }
}

public class let {}

@DavidArno Re "People would be able to work around this by using @let to refer to the type"

Since they do not already do so, it would be a breaking change to require that.

@bbarry Re "It is used in the same position as var, it should behave the same way I think in the presence of a let type in scope"

The contextual keyword var does not change the way a statement is parsed. But the let statement has a different syntax than a local variable declaration. We don't use semantic information to guide parsing.

@gafter,

Since they do not already do so, it would be a breaking change to require that.

And? Surely sometimes the team has to be pragmatic over the "never introduce a breaking change"?

Should these be allowed?

  • let var x = 1;
  • let int[] x = {5};
  • let x = e1, y = e2;
  • let x = e1 when x > 0 else return;
  • let x = e1 when x > 0, y = e2 when y > 0 else return;
  • let pat = e1 when x > 0 let pat = e2 when y > 0 else return;

To make it even more complicated, add #10642.

Should these be allowed?

  • let var x = 1;

Yes, as currently specified.

  • let int[] x = {5};

No, because the right-hand-side isn't a valid expression. Patterns do not "target-type" the expression.

  • let x = e1, y = e2;

No, there is only one pattern-match in the syntax.

  • let x = e1 when x > 0 else return;

Yes.

  • let x = e1 when x > 0, y = e2 when y > 0 else return;

No, there is only one pattern-match in the syntax.

  • let pat = e1 when x > 0 let pat = e2 when y > 0 else return;

No, there is no "let expression". Or I have no idea what that let in the middle means.

To make it even more complicated, add #10642.

These are not declarators. They are patterns.

@gafter

Patterns do not "target-type" the expression.

So I suppose this is not exactly a shortcut for #115, since the semantics are quite different.

let x = e1 when x > 0 else return;

Yes.

According to the latest spec (#10644) it's not. But the following is,

let var x = e1 when x > 0 else return;

Isn't this considered as inconsistent?

If this is intended to work, perhaps let syntax should be merged with local declarations, meaning that the parser would always expect when and else clauses.

there is only one pattern-match in the syntax.

More to the point of this issue, right now let x = e1, y = e2; is legal as long as we have a type named let, right? I don't see why it should not be legal otherwise when we do have a let statement. I'm trying to say that this code _is_ and _will be_ syntactically correct. It'd be unfortunate for it to not work even if we hadlet statements in the language. It would be a good alternative to #5048 specially because let is not a placeholder for a _type_ so allowing something like let x = 1.0, y = 1; would not nullify this fact.

_pattern:_
 _let-pattern_ (instead of _var-pattern_)
 _identifier-pattern_ (has overlap with _constant-pattern_, see below)
 ...

_identifier-pattern:_
 _identifier_

_property-subpattern:_
 _identifier_ : _pattern_

_let-pattern:_ (aka _value-binding-pattern_)
let _pattern_

_local-variable-declarator:_
 _identifier_ (only when _type_ is specified)
 _identifier_ = _array-initializer_ (only when _type_ is specified)
 _pattern_ = _expression_ (_pattern_ must be an _identifier_ if not used with let)
 _pattern_ = _expression_ when _expression_ (only when used with let)

_declaration-statement:_
 _local-variable-declaration_ (no semicolon)

_local-variable-declaration:_ (_value-binding-pattern_ rules apply)
 _local-variable-type_ _local-variable-declarators_ ; (only a single declarator is allowed with var)
let _local-variable-declarators_ else _embedded-statement_ (only with fallible patterns)

_local-variable-type:_
 _type_
var
let (not a type per se, but we will check this in pattern-matching context)

Identifier patterns within a value-binding pattern bind new named variables to their matching values.

let (x, y) = e; // introduces variables `x` and `y`

Value-binding patterns cannot be nested.

let (let x, let y) = e; // illegal 

In pattern-matching context, let will be resolved as a keyword even though a type with the same name exists.

So I suppose this is not exactly a shortcut for #115, since the semantics are quite different.

I'm really confused by this. According to the spec:

let_statement
: 'let' identifier '=' expression ';'
...
is shorthand for

let var identifier = expression ;`

(i.e. a var_pattern) and is a convenient way for declaring a read-only local variable.

This appears to be exactly what #115 is asking for (WRT read-only locals; it won't cover parameters). Am I missing something?

@DavidArno Yes, but still you are _matching_ the expression against a pattern (in this case, a var pattern) not a simple assignment, so it makes sense to don't "target-type" here. However, all bound variables will be read-only, hence the sentence "a convenient way for declaring a read-only local variable."

@alrz,

Ah, that makes sense. The fact that the vars coming out of patterns are always read-only is something I'd missed. So that means that the following would be a compilation error?

var t = (1,1);
switch (t)
{
    case (var x, 1): 
        x = 2; // Cannot redefine `x`
...

@DavidArno It's a little buried in the spec, but:

The use of a pattern variables is a value, not a variable. In other words pattern variables are read-only.

I would think user-defined types named "let" would be extremely rare, given that "let" is both lowercase and a verb. It's great that you take breaking changes so seriously, but in this case I think it's worth it. I really like the proposed use of the let keyword.

I am very aware of this problem and go against the idea of let keyword from the start

And we should reuse keyword const or readonly instead of let

Because it is already keyword so it backward compatible with old code

const x = new let() will not breaking any existing code

@Thaina

If the let statement was only used to declare readonly locals then you might have a point. But if that were the case there would also be no parsing concerns as let would be treated as a contextual keyword, just like var. The parsing concerns are with using the let statement to decompose a pattern, and neither const nor readonly keywords make any amount of sense to be used in that context.

@Thaina,

To clarify @HaloFour's point, the following is handled just fine by the compiler:

class var {}
...
var x = 1;
var y = new var();

The compiler can determine that var is being used instead of int, in the first case and is referring to the type in the second. If let were merely a read-only replacement for var, then the following would be fine too:

class let {}
...
let x = 1;
let y = new let();

The issue is that let isn't proposed as a simple readonly variable declaration keyword. It's much more powerful as it allows pattern expression evaluation, for example:

let (x, 1) = F(z) else return;
// z is now a read-only value returned in a tuple from F, 
// only if the other value returned was 1.

I'm not sure if I have the exact syntax right in the above, but it gives you the idea.

So we have a choice:
1, Offer up a powerful means of assigning a value if an expression matches and take another action if not.

  1. Protect folk who ignored two language guidelines (don't use lowercase letters for the first character of class names and don't use verbs for class names) from the trivial inconvenience of adding @ to their let class references in order to up and compiling once more.

Opting for the second option strikes me as taking the "no breaking changes" rule to absurd extremes.

@gafter Couldn't the parser take additional tokens into consideration when parsing the let statement? I would think that the only time that it would matter if let were declared as a type is when there is no pattern on the left-hand side of the assignment.

class let { }

void Foo() {
    let x = 123; // CS0029 Cannot implicitly convert type 'int' to 'let'
    let var x = 123; // fine
    let int x = 123; // fine
    let (int x, int y) = (123, 456); // fine
    object o = x;
    let int x = o else return; // fine
}

@HaloFour,

That makes sense as a work-around, just so long as let var is only mandatory when there is a let class in scope. Otherwise you would be inconveniencing 99.9% of us with extra syntax just to avoid inconveniencing the 0.1% with easy-fixed, broken code.

@DavidArno Exactly, it would only be necessary in that virtually non-existent case. To most people let x = 123; would be perfectly fine. That is assuming the parser can use the subsequent tokens to make the determination as to what kind of statement it is.

I'd feel bad for anyone working on a project where both var and let are defined as types. I've seen the former used by devs who hate the idea of inferred locals.

@HaloFour,

I'd feel bad for anyone working on a project where both var and let are defined as types. I've seen the former used by devs who hate the idea of inferred locals.

It makes sense that there is an opposite extreme case to my own. I configure Resharper to report all uses of explicit types, that could be replaced with var, as an error... :grinning:

But even in that case, your let var x = 123; example could work as the x distinguishes it from other let var and var let cases.

Was this page helpful?
0 / 5 - 0 ratings