Roslyn: C# Design Notes for Jan 10, 2017

Created on 27 Jan 2017  ·  15Comments  ·  Source: dotnet/roslyn

C# Language Design Notes for Jan 10, 2017

Agenda

  • Switch and local functions

Switch and local functions

The combination of expression variables, local functions and the weird scoping rule in switch statements, leads to a current hole in the compiler:

c# switch (...) { case 1: Local(); case int x: void Local() { x = 1; } }

The problem here is that the scope, and therefore lifetime, of x is limited to the case section, whereas the scope, and therefore lifetime, of the Local function that uses x is the whole switch block. Therefore, calling Local() from elsewhere uses x without it being alive.

There are other, trickier, ways to observe this effect without local functions, e.g. with refs. But this is probably the most "common" scenario (though not actually very likely to be common).

Options:

  1. Extend the scope and lifetime of expression variables to the whole switch block (then names leak and can't be reused)
  2. Have differing scopes and lifetimes, keeping the scope of expression variables the case section, while making the lifetime the whole switch block
  3. Keep the scope of local functions the section only
  4. Scope of cases that declare variables is narrower, even for local declarations inside of that case section

All options have problems:

  1. Might be a decent design, given how we extended the scope of expression variables elsewhere. But it's risky to change scope rules this late in the game
  2. Would fix the problems without changing scope rules, but it might be odd to observe that scope and lifetime differ
  3. Only solves the symptom of the problem as it relates to local functions
  4. Would introduce inconsistent scoping behavior between different case sections

Conclusion

Of all these we prefer 2 at this stage. 1 would warrant discussion if we weren't so close to shipping - it would break existing C# 7.0 code that reuses variable names across case sections, which is expected to be common. Option 2 sweeps things under the rug nicely, and keeps existing code running while addressing the compiler failure.

Area-Language Design Design Notes Language-C# New Language Feature - Local Functions New Language Feature - Out Variable Declaration New Language Feature - Pattern Matching New Language Feature - Tuples

Most helpful comment

Why not allow local function declarations in the outermost scope only?

All 15 comments

It's a little weird, but it's probably the best compromise. I'd much prefer this to option 1 and more leaking. The entire scope and lifetime proposition with switch is wonky anyway due to fall-through, no?

Why not allow local function declarations in the outermost scope only?

As you've explained the problem so far, option 3 would have my initial preference. However, you allude to a larger problem of which this example is merely a specific case (with local functions). In order to see why option 3 isn't reasonable, can you give examples of the other cases, or describe the entire problem?

@JeroenBos

That would make local functions a special case as any other identifier declared within a switch section is scoped to the entire switch statement. Although I believe that the rules of definite assignment at least prevent the same kind of scenario as with local functions. Hoisting is what makes local functions a special snowflake here since you can use it before you declare it as long as it's within scope.

In reality this entire situation is only kind of theoretical and I can't imagine that anyone would ever accidentally run into it or find practical ways of abusing it. It's not like you could read the enclosed variables since the rules of hoisting apply definite assignment to the call site. If Local were instead trying to read x calling it from the previous switch section would be a compiler error since x isn't assigned at that point.

switch (...)
{
    case 1:
        Local();
                break;
    case int x:
        void Local() => Console.WriteLine(x); // error CS0165: Use of unassigned local variable 'x'
                break;
}

@orthoxerox You can also observe this with refs, e.g.

switch (2)
{
    case 2 when 1 is int x:
        ref int y = ref x;
        goto case 1;
    case 1:
        // at this point we have “ref int y” that refers to a variable whose lifetime has ended
        M(ref y);
}

@agocke is that possible when the compiler cannot guarantee definitive assignment of y?

This causes CS0165 for example:

void M(ref int y1) {
    switch (y1)
    {
        case 2 when 1 is int x:
            ref int y = ref x;
            goto case 1;
        case 1:
            M(ref y);
            break;
    }
}

@bbarry If we allow redefinition of ref variables later you can get into the same problem. @VSadov Knows more about the rules for ref locals.

Almost feels like the scope of local functions should be changed.

@bbarry - in your example you can reach case 1 normally from the switch. Then y will not be assigned. That is why the error is given.

@VSadov right, so definite assignment and no-assignment currently protect us from seeing effects of narrower variable scope for ref locals. When could this have become a problem? When we allow reassignment of ref locals?

Almost feels like the scope of local functions should be changed.

I know the function must exist but, wouldn't making it not accessible outside its natural and expected scope solve this?

/cc @gafter @VSadov

I agree, we currently use "per-section" scoping for pattern variables in case clauses, because it's useful and makes the most sense. This is not the first time that switch scoping becomes problematic. Instead of changing scope/lifetime of variables, we could limit the scope of local functions to their enclosing section (just like pattern variables). I don't think it would be reasonable to maintain consistency with a known misdecision inherited from C++ and the one we actually did go against for pattern variables in case clauses.

In conclusion, there exist various reasons for a non-consistent scoping,

  • out/is variables leak to cover different code styles and overall usability (the most controversial so far).
  • Pattern variables in case clauses don't leak outside the corresponding section just because it's useful.
  • Local functions could also follow the same scoping to prevent abuse and to be less confusing overall.

@paulomorgado

I know the function must exist but, wouldn't making it not accessible outside its natural and expected scope solve this?

The answer is "no", because it is possible to jump (using goto) into the switch block where the function is defined, execute the function, and then jump back. This is a lifetime problem, not a scoping problem.

@gafter, I rarely use goto, so I tend to forget its peculiarities. 😀

LDM notes for Jan 10 2017 are available at https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-01-10.md
I'll close the present issue. Thanks

Was this page helpful?
0 / 5 - 0 ratings