-
Notifications
You must be signed in to change notification settings - Fork 209
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
'null' isn't promoted to 'Null'. #1505
Comments
This was discussed here and the resolution was that we intended to promote. @stereotype441 looks like this is a bug, or did we change our mind on this? Not sure whether we can fix this now, though it's pretty unlikely to be de facto breaking. |
Yeah, it looks like this was an oversight. I'll try to find some time this week to investigate whether it would cause any internal breakages to change this behavior; that should be a pretty good proxy for whether external users would be affected. |
Unfortunately, it seems to be quite breaking after all. So far I haven't even gotten the Dart SDK to build with this change. There's a lot of failures and I haven't checked them all out (you can look at https://dart-review.googlesource.com/c/sdk/+/192420 if you want to see details), but I've seen enough that I don't think we should consider changing this now, right after a stable release. FWIW, here are the two coding patterns I've found so far that would be broken by this change: Dead codeIf a function contains dead code for handling non-null values, the promotion can carry into it, causing type errors, e.g.: f(int? i) {
if (i != null) {
...
return;
}
...
if (i != null) {
g(i); // Used to be a warning ("dead code"), now is an error ("Null can't be assigned to int")
}
}
g(int i) { ... } Maybe we could come up with a clever way to modify flow analysis to fix this. For example, maybe at the time we detect unreachability, we un-do the promotion to Type inference when setting a null value to non-nullThis type of construction appears to be pretty common: f(List<int>? x) {
if (x == null) {
...
x = []; // Used to get type inferred as <List>[], now is an error ("List<dynamic> can't be assigned to List<int>")
}
} What's happening is that when analyzing an assignment to a variable, we use the promoted type of the LHS as the context for the RHS, so that we only demote when that context can't be satisfied. But since the promoted type of the LHS is now Again, some cleverness could probably address this. We could, for instance, say that when analyzing an assignment, we use the promoted type of the LHS only if it's not |
In the if (i != null) {
return;
}
if (i != null) {
// what is i?
} example, is there any way we can promote Not sure how useful it is. The code is dead, so all we can say is that we never have a value for |
This would be the natural outcome of using the factor algorithm as specified in the flow analysis design doc. I'm not sure we do that though. |
I prototyped a change to the type system that allows promotion to The biggest effect is on code like this (adapted from Set<Rule> get allConstrainedRules {
Set<Rule> rules = _allConstrainedRules;
if (rules != null) return rules;
rules = {}; // (1)
_traverseConstraints(rules, this);
_allConstrainedRules = rules;
return rules;
} At the line marked (1), under the current rules, where we don't promote to The problem doesn't just affect the set/map distinction; it can happen to lists too. Consider this example (from List<ComplexSelector>? _extendCompound(...) {
List<List<Extender>>? options;
for (...) {
...
if (options == null) {
options = []; // (1)
...
}
}
} In this case, at the line marked (1), there's question that I found several dozen examples of these two patterns. Another pattern, which is less common, but still problematic, occurs when promotion to abstract class TypedDataBuffer<E> extends ListBase<E> {
void insertAll(int index, Iterable<E> values, [int start = 0, int? end]) {
...
if (end != null) { // (1)
_insertKnownLength(index, values, start, end);
return;
}
...
if (end != null && writeIndex < end) { // (2)
throw RangeError.range(end, start, writeIndex, 'end');
}
...
}
...
} I've elided a lot of code here, so I should mention that there are no assignments to If we do start allowing promotion to It's my personal belief that these problems outweigh the benefit of allowing promotion to |
I'm assuming the Set<Rule>? rules = _allConstrainedRules; The problem seems to be that we assume that the context type of the assignment to a promoted variable can always be represented by the most promoted type, because if any type in the promotion chain has an instantiation of an interface type, all of them must agree. That's not true for void main() {
List<int>? o = null as dynamic;
if (o is Null) {
o = [];
// A value of type 'List<dynamic>' can't be assigned to a variable of type 'List<int>?'.
}
} And, since we allow demoting assignments, the currently promoted type is not the only relevant type for assignment. I can see that we don't want to do type inference on the sub-expression more than once. What if a context type could an entire chain of promoted types. Most of the time, it's just one type, but for assignment to promoted variables (and propagated down through that expression), every one of the types is seen as a constraint. They'll mostly agree, or be specializations, but in a few cases, where you promote a union type to one of its branches, there might be a constraint further up the chain. That goes for import "dart:async";
void main() {
FutureOr<int> foo = 42 as dynamic;
if (foo is int) {
foo = Future.error("Not a future!");
// A value of type 'Future<dynamic>' can't be assigned to a variable of type 'FutureOr<int>'.
}
} I don't know how to actually use a chain of types as context type. I guess if you need a type argument to a generic type, you check whether any of the types in the chain implements that generic type, and then you still have to choose one of those type arguments, likely the most specific. (Union types as context types are just not very useful - another reason I don't want union types in the language.) For every other use of context types, you'd probably just use the most specific promoted type. if (end != null && writeIndex < end) { // (2) again looks like something where the second |
Although the true branch of `x != null` and the false branch of `x == null` promote `x` to non-nullable, the opposing branches do not promote `x` to `Null` in the current flow analysis spec, which applies to the CFE. When switching from the dart2js static type computation to the CFE static type computation, attempting to use `x` in these branches may generate an unintended type check, which can overflow the stack if this occurs in code responsible for performing type checks. The fix is fairly straightforward - we can simply use an unchecked cast (via `JS`) instead. See dart-lang/language#1505 for discussion on why the expected promotion does not (yet) occur. Change-Id: Ia9cca4e1aa8e9c67b42c60189f0d3811afb61360 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/289061 Reviewed-by: Stephen Adams <sra@google.com>
I was surprised that this isn't allowed. Is this a bug or is this expected behavior?
The text was updated successfully, but these errors were encountered: