-
Notifications
You must be signed in to change notification settings - Fork 1.7k
NNBD tool suggests casts for map keys #49106
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
Comments
Oh interesting! I've tracked this down to a flaw in the analyzer's resolution logic. When resolving an expression of the form That's totally wrong: the type of I'll add a test case to the migration tool to repro this, and then I'll re-classify as an analyzer bug. |
Bug: #49106 Change-Id: Id207cef6b23c75eddbff24bb3e70935998a265f6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/246054 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Paul Berry <[email protected]>
Which type should be used as the context type when there are both read and write? CFE can run this: class A {
int operator [](int index) => 0;
operator []=(num index, int value) {}
}
void f(A a) {
a[ g() ]++;
}
T g<T>() => 0 as dynamic;
main() {
f( A() );
} ...and infers But not this: class A {
int operator [](num index) => 0;
operator []=(int index, int value) {}
} ...reports:
|
@stereotype441 is going to look at the example from @scheglov , cc @lrhn @munificent @eernstg |
In the language specification, So this means that there is basically nothing at all to build upon in the language specification for the context type of However, We might want to have a rule to ensure that the two index parameters don't have completely incompatible types, but we don't currently have that, so we would just get a compile-time error for any usage of |
Digging through the front end source code, for ordinary indexed reads (e.g. Whereas the analyzer, IIRC, always tries to get its context type from the type of the index parameter of As I see it, we have three options worth considering:
Note that regardless of the approach we take, it's necessary for soundness that we check for assignability between the static type of the index and the type of the index parameter of whatever operator (or operators) might get called. In both the analyzer and the CFE, the logic to check for assignability is unrelated to the logic for choosing a context type, and as far as I'm aware, there are no bugs with the assignability checking logic. So we should be able to choose among 1, 2, and 3 without worrying about soundness issues. Personally I favor option 3 because it solves the worst problems (bad migrations and CFE/analyzer mismatch) quickly and with minimal effort, and it lets us postpone the effort to bring the implementation in line with a more principled spec (which, though important, is higher effort and lower reward than just bringing the analyzer into line with the CFE). |
There was also some discussion about whether it would be good to have a lint to warn users when their definitions of |
There might be some completely different examples that someone can come up with—but conceptually, I would expect any actual design of a class that has an operator This means that we can expect the GLB of the two types to be, in practice, the type from operator This means that approach 1 is a bit inconvenient, because it uses the type from operator class A {
int operator [](Object? o) => 0; // Expected parameter type: Some supertype of `String`.
operator []=(String s, int i) {}
}
void f(X Function<X>() whatever, A a) {
a[whatever()]++; // CFE error, analyzer OK.
} This actually serves as an argument against approach 1 and 3, and in favor of keeping the analyzer unchanged (such that the current analyzer behavior will not become a breaking change). Of course, So maybe we should aim for 2 after all? |
We have a subtype restriction between getter return type and setter argument type (I never remember which, but one must be a subtype of the other). That rule was driven by always wanting to allow Using the same reasoning here, (On the other hand, that's actually the opposite of what I'd advocate for, if we had started from scratch. It makes no sense to store a value that you cannot read out again. If you can set a For the first argument, if we assume the two operators are related, it makes no sense to be able to store at a key that you cannot read at again afterwards, so maybe we should require the key of the About the desugaring: If the key argument is typed as Map<num, num> m = ...l
m[0] += (... as dynamic);
/// equivalent to
let x = ... as dynamic in m[x as Object?] = m[x as num] + 1 and if so, those downcasts are not governed by the context type. They cannot be. (And never do type inference after expanding syntactic sugar, always do it directly on the original source, precisely because, as @eernstg showed, syntactic desugaring does not preserve context. Just like CPS transformation is precisely about removing syntactic context. That seems at odds with not using being able to use the context type here, but that's because |
Good point. If we wind up going with option 2, we should definitely include a language test to check the behavior of this corner case. |
+1. I think the correspondence between getter/setters and index/index setters should be strong. I really wish the language had a formal notion of "property" or maybe "l-value" that would subsume these. It's always confusing for users when a getter and setter don't stay in sync, and likewise with index operators. It is true that Map's |
This issue has come up again, so I think we should take some action to fix it. After discussing it with @munificent and @leafpetersen last week, I think that in the short term, we should go with my suggestion 1 (switch the analyzer to match the front end behavior). In other words, for ordinary indexed reads (e.g. In the long term, we can consider changing the context type for indexed reads to use GLB for compound and if-null assignments. My rationale is: the short term fix is easy and non-breaking, and it gets the migration tool to do the right thing. The longer term fix is principled and easy to justify in the spec, but it's more effort to implement and it's technically a breaking change, so we should approach it with more caution. If there are no objections, I'll coordinate with @scheglov later in the week about fixing this in the analyzer, and I'll file a language issue so that we don't forget about the long term plan. |
FWIW, I had a fix for this since May, but never uploaded it, and for now it implements GLB. I will update it to the plan that Paul recommends. |
We talked about this in the language team meeting and we're going to go with the plan I outlined above. @scheglov, feel free to move ahead with your CL. I'll file a separate issue to discuss the possibility of using GLB for compound and if-null assignments. |
Bug: #49106 Change-Id: I522269360c6664ac4a9129cb71bf378c9dfca812 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256647 Reviewed-by: Paul Berry <[email protected]> Commit-Queue: Konstantin Shcheglov <[email protected]>
Fixed in d07005b. |
The NNBD migration tool suggests casts for keys to
Map.operator[]
.This operation has argument type
Object?
, so these casts are unnecessary, and in this case actually harmful since the code relies on lookups outside the map key type returningnull
.Example from
sdk/pkg/compiler/js/rewrite_async.dart
An additional problem is that the migration tool will insert
as Block
without ensuringBlock
is imported, and does not use the library prefix. Both problems with the inserted cast create broken migrated code./cc @stereotype441 @leafpetersen
The text was updated successfully, but these errors were encountered: