-
Notifications
You must be signed in to change notification settings - Fork 28
#3057. Add pattern assignment cases to C-style for tests #3126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
A couple of tests are somewhat tricky: The description says that something is 'reachable', but I don't think the given test would actually detect whether it is reachable or unreachable.
Come to think of it, the current version of the tests could work after all because the correct analysis will give rise to no errors (more precisely: no diagnostics), and the buggy behavior that I'm hunting would give rise to a 'dead code' warning.
Anyway, please take another look and consider the issue that I described.
int i; | ||
for (;false;) { | ||
return; | ||
} | ||
i = 42; | ||
i; // Definitely assigned. | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I might have overlooked a thing here in the first iteration on this test. ;-)
The false
literal isn't actually affecting any observable behavior, I think.
First, there is no notion of "definitely reachable", so there's no difference between "will definitely reach this point every time this function is called" and "might reach this point if we're incredibly lucky".
This means that we can't test that any given piece of code is "definitely reachable", we will have to go for "is possibly reachable" no matter what the test is like in detail.
Next, it implies that there is no difference between the treatment of the code that may be executed after the for
statement when the condition is false
and when it is any boolean expression whose value might be false. This makes the reference to false
in the description somewhat misleading.
Finally, the analysis of i = 42; i;
is the same when those two statements are reachable (via something that we could call the "invisible else branch of the for
statement") and when they are not reachable (for whatever reason). In both cases the statement i;
is accepted with no errors because i = 42
is guaranteed to have been executed before we reach i;
.
I think the best way to test reachability could be to remove the code which is concerned with i
and change the return type of test1
to some non-void/non-dynamic type, and expect an error because there is no return e;
before the end of the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, the for
statement could simply be removed because it doesn't actually matter. But this would probably yield a test which is already included somewhere else in co19.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the i
related code is removed then we probably wouldn't need the new cases in test2-3-4
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is testing that after(N) = inheritTested(join(false(C), unsplit(break(S))), after(U))
. Not most obvious statement for me :) As far as I understand it the meaning is that in case of false(C)
then after(N)
doesn't depend from what's going on in S
and U
. So, here I'm tryig to test that in case of for(;false;U) S;
reachability of after(N)
doesn't depend on U
and S
. What if we'll change the test to be like:
test1() {
late int i;
if (2 > 1) {
for (;false;) {
return;
}
i = 42;
}
i; // Possibly assigned.
}
In case of for (;true;) ...
i
become definitely unassigned in this test. WDYT will it be a valid test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, after(N) = inheritTested(join(false(C), unsplit(break(S))), after(U))
is convoluted.
It specifies that the flow model M
after the for
statement is join(false(C), unsplit(break(S))
where we have taken the 'types of interest' for each variable in after(U)
and added to the types of interest for the same variable in M
(and skipped variables in after(U)
that aren't in M
).
In other words, M
is exactly join(false(C), unsplit(break(S))
as long as we ignore the types of interest. In this test U
is empty, so it doesn't contribute any types of interest, anyway. So we just need to consider the join
.
join
combines the information in two flow models. It can only be used when the reachability stacks are identical below the top (that is, they must agree precisely on reachability except for the top of the stack), and it creates a new reachability stack which is (top1 || top2) :: commonTail
, that is, it's reachable if one or the other branch is reachable.
In this case false(C) == before(N)
— which is reachable, so the join
is reachable, too. We basically just need to understand how unsplit(break(S))
can add information to before(N)
. The unsplit
part manages reachability, but we already know that the join
is reachable. So we just need to understand break(S)
. There are no break;
statements in S
, so we need to know how join
works on the empty list of operands. This is not documented, as far as I can see. (As we can see below it's unsound if that empty join
is reachable, so it must be unreachable.)
Anyway, we would clearly expect the flow analysis to consider after(N)
to be a reachable flow model, which is the reason why I'm suggesting to test this as follows:
int test1() {
for (;false;) {
return;
}
// Error: reached the end of the function body with no `return anInt;`.
}
The description could then say that we're testing that after(N)
is reachable when the condition is a literal false
.
It would also be possible to test for (;true;) ...
based on the fact that false(C)
is unreachable, so join(false(C), unsplit(break(S)))
would only be reachable if unsplit(break(S))
is reachable, which means that break(S)
must have a reachability stack of the form true :: true :: tail
.
int test1() {
for (;true;) {
return;
}
// OK to not have a `return someInt;` here.
}
About this test:
test1() { late int i; if (2 > 1) { for (;false;) { return; } i = 42; } i; // Possibly assigned. }
Yes, this would also succeed if i = 42;
is considered reachable, and fail if it is considered unreachable.
I'm thinking about this as a 'possibly assigned' test rather than a 'reachable' test, but I do recognize that it relies on reachability as well.
Also, it fixes the issue with the first version of this test where the outcome would be a success both in the case where i = 42;
is considered reachable and in the case where it is considered unreachable.
Tricky stuff! ;-)
Thank you for the detailed analysis. Updated. PTAL.
It's an interesting about type of interest. It can be tested (in a separate PR). class S {}
class T extends S {
void foo() {}
}
test1() {
S s = S();
for (;false;) {
if (s is T) {} // make `T` a type of interest
}
s = T();
s.foo(); // Not an error.
}
test2() {
S s = S();
for (;false; s is T) {
}
s = T();
s.foo(); // Error. Not an issue, result of the conservativeJoin()? Same as https://github.com/dart-lang/sdk/issues/60320 ?
} Is this an expected that there is an error in |
The This justifies the error at the end, in the line with a long comment. |
After the testing and rereading the spec I think that there is another reason of such behavior. According to the spec:
Definition of
I think it is too general definition but the key point here is "for the purposes of promotion". So s = S();
s is T;
s = T();
s.foo(); // Error. There were no promotion above And for the same reason we are getting an error in case of S s = S();
int i = 0;
for (;false; s is T ? i++ : i += 2) {}
s = T();
s.foo(); // Ok, now accepted Please correct me the above is wrong. Otherwise it'd be nice to test type of interest in all of the places where we should have a promotion.
As far as I understand to get a promotion after the type check we should have s = S();
s is T && 2 > 1; // `true(N)` branch added to `s is T`
s = T();
s.foo(); // Now Ok! So to test types of interest we should check all of the places in the flow analysys spec where Please correct me if I'm wrong. |
The flow models associated with the evaluation of the expression I do think it's fair to say that "
That's a bug! The flow model Given that It has probably never been reported because it is a pointless statement: We're computing a boolean value and discarding it, and there are no side effects. In any case, I do believe that this is a bug.
In this case the type test
No, the point is that the
Looks like the bug that came up above doesn't apply here, probably because the |
@stereotype441, do you agree that the following is a bug?: class S {}
class T extends S {
void foo() {}
}
void main() {
var s = S();
s is T;
s = T();
s.foo(); // Error, so `T` was not a type of interest, but it should be.
} |
Yes, good catch! I've filed dart-lang/sdk#60479 to track it. And nice job with the analysis above; I walked through the code in the debugger and confirmed that what's happening is exactly what you surmised. |
No description provided.