C# Design Notes for Jan 6, 2014

Topics: C# Language Design
Coordinator
Mar 29, 2014 at 4:43 AM
Edited Mar 29, 2014 at 4:52 AM

C# Design Notes for Jan 6, 2014

Notes are archived here.

Agenda

In this meeting we reiterated on the designs of a couple of features based on issues found during implementation or through feedback from MVPs and others.
  1. Syntactic ambiguities with declaration expressions <a solution adopted>
  2. Scopes for declaration expressions <more refinement added to rules>

Syntactic ambiguities with declaration expressions

There are a couple of places where declaration expressions are grammatically ambiguous with existing expressions. The knee-jerk reaction would be to prefer the existing expressions for compatibility, but those turn out to nearly always lead to semantic errors later.

There are two kinds of full ambiguities (i.e. ones that don’t resolve with more lookahead):
a * b // multiplication expression or unitialized declaration of pointer?
a < b > c // nested comparison or uninitialized declaration of generic type?
The latter one exists also in a method argument version that seems more realistic:
a ( b < c , d > e ) // two arguments or one declaration expression?
However, in all these cases the expression, when interpreted as a declaration expression, is uninitialized. This means that in the vast majority of cases (except for structs with no accessible members) it will be considered unassigned. Which means that it will be a semantic error for it to occur as a value: it has to occur in one of the few places where an unassigned variable is allowed:
  1. On the left hand side of an assignment expression
  2. As an argument to an out parameter
  3. In parentheses in one of the previous positions
Those are places where a multiplication or comparison expression cannot currently occur, because they are always values, never variables. We can therefore essentially split the world neatly for the two interpretations of the ambiguous expressions:
  • If they occur in a place where a variable is required, they are parsed as declaration expressions
  • Everywhere else, they are parsed as they are today.
This is a purely syntactic distinction. Rules of definite assignment etc. aren’t actually used by the compiler to decide, just by us designers to justify that the rule isn’t breaking.

There is one potential future conflict we can imagine: If we start allowing ref returning methods and those include user defined overloads of operators, then you could imagine someone defining an overload of “*” that returns a ref, and would therefore give meaning to the expression (a*b) = c, even when interpreted as multiplication. The rules as proposed here would not allow that; they would try to see (a*b) as a parenthesized declaration expression of a variable b with pointer type a*.

Conclusion

We like the rule that parsing prefers declaration expressions in ambiguous cases only in places where a variable is required: when occurring as an out argument, on the left hand side of an assignment, or nested in any number of parentheses within those. This is non-breaking, and doesn’t seem too harmful to the future design space.

Scopes for declaration expressions

The initial rules for declaration expression scopes are in need of some refinement in at least two scenarios:
if (…) m(int x = 1); else m(x = 2); // cross pollution between branches?
while (…) m(int x = 1; x++); // survival across loop iterations?
We want to make sure that declarations are isolated between branches and loop iterations. This means we need to add more levels of scopes. Essentially, whenever an embedded-expression occurs as an embedded expression (as opposed to where any expression can occur), we want to introduce a new nested scope.

Additionally, for for-loops we want to nest scopes for each of the clauses in the header:
for (int i = (int a = 0);
     i < (int b = 10); // i and a in scope here
     i += (int c = 1)) // i, a and b in scope here
  (int d += i);        // i, a and b but not c in scope here
It’s as if the for loop was rewritten as
{
  int i = (int a = 0);
  while (i < (int b = 10))
  {
    { i += (int c = 1)); }
    (int d += i);
  }
}

Conclusion

We’ll adopt these extra scope levels which guard against weird spill-over.