Flow scoping

Brian Goetz brian.goetz at oracle.com
Wed Jan 2 18:53:39 UTC 2019


> Before we get to the harder ones, can people share what made them uncomfortable, for the ones where you marked "not comfortable with the answer”?

We’re currently exploring the consequences of Doug’s query about introducing non-conditional locals into expressions (not necessarily because we want to have such a feature now, but because if we did want such a feature, we’d want the scoping machinery to handle it well, and if it doesn’t, that may suggest refinements for the scoping machinery.)  

Let’s talk about the more complicated examples in the quiz.  

Everyone seemed to do pretty well on the if/else ones (flip the condition, and it flips the scopes), the conditional-expression ones, and  the short-circuiting examples.  Let’s talk about merging.  

In this example:

```
public void test() {
    if (a instanceof String v || b instanceof String v) { 
        println(v.length());
    }
    else {
        println("foo");
    }
}
```

we have two candidate conditional variables called `v` of type `String`, and the flow rules ensure that at any point, at most one of them could be DA.  So in the `true` arm, the use of `v` is OK, and it binds to whichever of these is DA (both must have the same type, otherwise its an error.)  The rules sound complicated, but as always, our DA intuition guides us pretty well — we can’t get to the `true` arm unless exactly one of the `instanceof` expressions has matched.  

In the `&&` version of this example, people’s intuition was mostly right — that this code is illegal — but some were unsure: 

```
public void test() {
    if (a instanceof String v && b instanceof String v) { 
        println(v.length());
    }
    else {
        println("foo");
    }
}
```

The answer here is that because `v` is not DU in the second `instanceof`, and therefore might be in scope, this constitutes an illegal shadowing, and the compiler rejects it.

The hard ones (and the potentially controversial ones) were the last two — regarding use _after_ the declaring statement:

```
public void test() {
    if (!(a instanceof String v)) 
        throw new NotAStringException("a");
    if (v.length() == 0)
        throw new EmptyStringException();
        
    println(v.length());
}
```

```
public void test() {
    if (!(a instanceof String v)) 
        return;
        
    println(v.length());
}
```

In both of these cases, flow scoping allows `v` to be in scope in the last line — because `v` is DA at the point of use.  And this is because if the `insteanceof` does not match, control does not make it to the use.  In other words, we can use the full flow analysis — including reachability — to conclude the only way we could reach the use is if the binding is defined.  (If there was not a return or a throw, then the last use would not be in scope.)  

For some, this is uncomfortable at first, as not only does the scope bleed into the full `if` statement, but it bleeds out of it too.  But again, we’re driven by two goals:

 - Make scoping line up with DA-ness (so we can build on an existing mechanism rather than creating a new one)
 - Be friendly to refactoring.  

In the latter point, I’d like for

    if (E) { throw; } else { s }

to be refactorable to

    if (E) throw; s;

as it is common that users will perform precondition checking on entry to a method, throw if there’s a failure, and then “fall” into the main body of the method:

    if (!(a instanceof String s))
        throw new IllegalARgumentException(a.toString());

    // use s here

The main argument against supporting this is discomfort; it feels new and different.  (But, because we’re building on DA, it’s not, really.)


More information about the amber-spec-experts mailing list