language icon indicating copy to clipboard operation
language copied to clipboard

Spec is unclear about scoping of variables declared in `for` loops

Open stereotype441 opened this issue 6 years ago • 3 comments

Consider the following code:

void f(int x) {
  for (int y = x, x = 0; x < 10; x++) {
    print(x);
  }
}
main() {
  f(0);
}

The front end rejects this code with the error:

file:///usr/local/google/home/paulberry/tmp/test.dart:2:19: Error: Can't declare 'x' because it was already used in this scope.
  for (int y = x, x = 0; x < 10; x++) {
                  ^
file:///usr/local/google/home/paulberry/tmp/test.dart:2:16: Context: Previous use of 'x'.
  for (int y = x, x = 0; x < 10; x++) {

whereas the analyzer accepts the code without complaint.

It appears that the front end's rule is: the scope of all declarations appearing in the initializer part of the for-loop is the entire for-loop, therefore in the code above, the x in y = x refers to the x declared by x = 0; thus this is a use-before-declaration (and hence an error). Whereas the analyzer's rule appears to be: the scope of all declarations appearing in the initializer part of the for-loop is from the declaration onward to the end of the for-loop; therefore the scope of the declaration x = 0 does not cover the previous declaration y = x.

I looked at the spec to see which behavior is correct, and could not find clear guidance. The section "statements - for" says nothing about the scope of the declarations in the loop.

Note that the spec isn't clear about the scoping of variables declared in for-in loops either, but fortunately the front end and the analyzer agree: the scope of a variable declared in a for-in loop is just the body of the loop; it does not cover the iterable. So for example this code is accepted by both the front end and the analyzer:

void f(List<int> x) {
  for (int x in x) {
    print(x);
  }
}
void main() {
  f([1, 2, 3]);
}

stereotype441 avatar Mar 18 '19 20:03 stereotype441

cc @lrhn @eernstg @munificent

leafpetersen avatar Mar 18 '19 20:03 leafpetersen

The initilizer declaration should probably follow the same rules as the local variable declaration in a fresh scope. That would make int y = x, x = 0; invalid because it refers to x before its initialization. The same thing happens with int x = 0; { int y = x, x = 0; } in normal code.

So:

  • The scope of variables declared in the initializer part is the entire for statement (even if we actually introduce new variables for each loop iteration). It needs to cover the check and increment parts, and it has the form of a multi-declaration, so it should act the same.
  • Multiple variables declared in the same initializer are all in scope, but may not be referenced before their declaration.

So, as I see it, the front end is correct. This is the same behavior as C#, where Java seems to not allow shadowing any local variable, even inside a nested block.

That for-in loops are different is reasonable, the iterable expression is evaluated completely before the first iteration variable is introduced. It's more similar to a forEach call than a C-style for-loop.

lrhn avatar Mar 19 '19 10:03 lrhn

These sections ('For', 'For-in') have not been updated recently, and they are on a list of sections that I plan to rewrite extensively. For example, it is completely unspecified how to execute a C-style for statement whose iteration variable has a type annotation. ;-)

Clearly, the C-style for statement needs to have its scoping specified, and I agree with @lrhn about the kind of rules that we'd want there. Similarly for the asynchronous for-in statement. But the scoping of a (synchronous) for-in statement is specified because it is desugared.

eernstg avatar Mar 19 '19 12:03 eernstg