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
Consider allowing context type to provide a hint to "least upper bound" algorithm #1618
Comments
So, if a conditional expression has a context type, let the static type of the conditional expression be that context type if we can't find a better LUB, defined as a LUB which is a proper subtype of the context type. So, ignore all interfaces that are not subtypes of the context type when trying to find a LUB. Ship it! It never made sense to check that both branches were assignable to the context type, then throw that information away and find an unrelated LUB, and then complain about the (now disallowed) implicit down-cast. |
This idea seems to apply directly to the "Dart 1" rule, that is, the very last item in the rules for UP. |
This is closer to my proposed solution for this, which isn't really about changing LUB at all. Specifically, what I have advocated for the past (and would still like to find an opportunity to do as a breaking change) is the following:
|
Just noting that I expect this issue to become much more prominent with switch expressions. For example, the following simple definition of a map function over a lisp style linked list does not currently compile because LUB can't find an upper bound for sealed class Lisp<T> {
const Lisp();
}
class Nil extends Lisp<Never> {
const Nil();
static const nil = Nil();
}
class Cons<T> extends Lisp<T> {
final T car;
final Lisp<T> cdr;
const Cons(this.car, this.cdr);
}
Lisp<To> map<From, To>(Lisp<From> l, To Function(From) f) =>
switch (l) {
Nil() => Nil.nil,
Cons(:var car, :var cdr) => Cons(f(car), map(cdr, f))
}; We might want to consider prioritizing this for an upcoming release. |
We discussed this in yesterday's language meeting. I personally now favor the following variant of Leaf's proposal:
Other than some minor rewording (I split the fourth sub-bullet into two), the salient changes are:
Focusing on the second of these changes, note that in most situations, if the static type of an expression fails to be a subtype of the greatest closure of its context type, then an error will occur later in the analysis. The exceptions are:
So, assuming that "greatest closure" what we wind up choosing for the fourth sub-bullet, the distinction really only matters for the above cases. |
This looks good to me (obviously this generalizes to switches). I believe that in the analogous place in function literal return inference we use greatest closure. I think there are arguments for either choice, but on balance I think choosing greatest closure is probably better, and it's consistent with our treatment of function literals. |
Here's a fun example of a LUB computation that would be helped by this proposal: https://dart-review.googlesource.com/c/sdk/+/306682/comment/3abc77aa_35768de0/ |
From that review:
I wish I could say the same, but I think it's one of the deepest examples I've seen. 😉 For good measure, here is an void main(List<String> args) {
var numbers = args.isEmpty ? [1] : {1};
numbers = numbers.map((x) => x + 1);
// Error: A value of type 'Iterable<int>' can't be assigned to a variable of type 'EfficientLengthIterable<int>'
} (At least I can see my ideas of removing inaccessible declarations from the LUB computations wouldn't help in either of these examples.) I sometimes add extra layers to class hierarchies in order to make LUB give what I want. 😢 |
Here's another example VectorList<Vector> getView(Float32List buffer) {
final viewOffset = offset ~/ buffer.elementSizeInBytes;
final viewStride = stride ~/ buffer.elementSizeInBytes;
switch (size) {
case 2:
return Vector2List.view(buffer, viewOffset, viewStride);
case 3:
return Vector3List.view(buffer, viewOffset, viewStride);
case 4:
return Vector4List.view(buffer, viewOffset, viewStride);
default:
throw StateError('size of $size is not supported');
}
}
|
To make sure I understand this (and to possibly help anyone reading it), let me try to rephrase the proposed change in less formal (and possibly less precise) terms. Let me know if I have it right. Here is an example to motivate: Iterable<num> test(bool b) {
Iterable<int> ints = [1].skip(0);
List<num> nums = [1.2];
return b ? ints : nums; // <-- Error.
} This example seems entirely harmless. Both
The problem is that when calculating the type of The proposed behavior is, when type checking a
Walking through those steps with the above example:
Do I have that right? |
I think we can extend this to coercions as well. Define CUp(K; T1, ..., Tn), the "context-guided Up" of T1... Tn with context type scheme K as:
(NOTE: we might need to define what it means for a type to be assignable to a type scheme, in a way that includes coercions, and in such a way that we can find a closure of the context type which all the Tis are assignable to. Solving K for T1 ... Tn assignable to some-closure-of-K, so we get a non-schema type. Maybe that's already in there, but I don't see any "a generic function type ... is a subtype match for a non-generic function type ... if ...".) Here "assignable" includes using any of our coercions. Then we infer types for
At runtime, we evaluate
where a type S is assignable to a type T (in a certain scope, since it may depend on applicable extensions) iff any of:
and we (runtime) coerce a value v from a type S to a type T, where S is known to be assignable to T, thorugh the following steps:
|
Here is one more case where the context type can be used to inform Future<int> f(Future<int>? f, int x) async {
return Future.value(f ?? x);
} The point is that
|
One of our guidelines for UP design was to not introduce new union types (well, except nullables, so really just "no new If one of the input types were a union type, giving that back out would be fine. (Don't invent new types structural types with a structure that doesn't come from the input types. Don't invent new types in general that doesn't come from the input types, where we allow including their precise superinterfaces, but not any arbitrary supertype. For example, don't invent Let's say we do:
Then we have a context type C of We need to do any needed coercions on the individual branches, because that ensures that the context type that the branches are assignable to, is also an upper bound on the runtime types of the individual values afterwards, even if they have no shared type to coerce from. |
After a discussion with @leafpetersen today, I'm going to dust off this issue and try prototyping some solutions to it to see if any problems arise. First I'll try prototyping my suggestion from #1618 (comment) (which addresses the original bug report, but doesn't handle coercions particularly gracefully). Then, if that goes well, and there's enough time, I'll try expanding it to the approach suggested by @lrhn in #1618 (comment) (which does handle coercions). |
After discussion with Leaf, I believe the primary use case that `interface-update-3` was intended to address can be better addressed through the proposal of language issue dart-lang/language#1618 (instead of dart-lang/language#3471, which I had been working on previously). Since I had not made a lot of progress on dart-lang/language#3471, rather than set up a brand new experiment flag, it seems better to simply back out the work that I'd previously done, and repurpose the `inference-update-3` flag to work on issue 1618. Change-Id: I6ee1cb29f722f8e1f0710cbd0600cb87b8fd26a1 Bug: dart-lang/language#1618 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/350620 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Nate Bosch <nbosch@google.com> Reviewed-by: Chloe Stefantsova <cstefantsova@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
These changes reflect the proposal in dart-lang#1618 (comment).
This is now out for review: https://dart-review.googlesource.com/c/sdk/+/353440 |
In the following expression types, the static type is computed using the least upper bound ("LUB") of their subexpressions (adjusted as appropriate to account for the null-shorting behaviors of `??` and `??=`): - Conditional expressions (`a ? b : c`) - If-null expressions (`a ?? b`) - If-null assignments (`a ??= b`) - Switch expressions (`switch (s) { p0 => e0, ... }`) This can lead to problems since the LUB computation sometimes produces a greater bound than is strictly necessary (for example if there are multiple candidate bounds at the same level of the class hierarchy, the LUB algorithm will walk up the class hierarchy until it finds a level at which there is a unique result). For a discussion of the kind of problems that can arise, see dart-lang/language#1618. This change improves the situation by changing the analysis of these four expression types so that after computing a candidate static type using LUB, if that static type does not satisfy the expression's context, but the static types of all the subexpressions *do* satisfy the expression's context, then the greatest closure of the context is used as the static type instead of the LUB. This is the algorithm proposed in dart-lang/language#1618 (comment). This is theoretically a breaking change (since it can change code that demotes a local variable into code that doesn't, and then the demotion or lack of demotion can have follow-on effects in later code). So it is implemented behind the `inference-update-3` experiment flag. However, in practice it is minimally breaking; a test over all of google3 found no test failures from turning the feature on. Since one of these expression types (switch expressions) is implemented in `package:_fe_analyzer_shared`, but the other three are implemented separately in the `package:analyzer` and `package:front_end`, this change required modifications to all three packages. I've included tests for the new functionality, following the testing style of each package. I've also included a comprehensive set of language tests that fully exercises the feature regardless of how it's implemented. Since `package:front_end` has many different implementations of `??=` depending on the form of the left hand side, I've tried to be quite comprehensive in the language tests, covering each type of assignable expression that might appear to the left of `??=`. Change-Id: I13a6168b6edf6eac1e52ecdb3532985af19dbcdf Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/353440 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Chloe Stefantsova <cstefantsova@google.com> Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Erik Ernst <eernst@google.com>
This has landed (dart-lang/sdk@b279238), behind the I ran an experimental CL through google3 to see if switching on the flag would produce any test failures; it did not. I think we should consider switching on this flag in the next stable Dart release (3.4), and addressing @lrhn's proposed improvements in a future release. I'll bring up that idea in tomorrow's language team meeting. |
Ran into this issue with |
The tests for the language feature `inference-update-3` are adjusted to verify that the following hold true for an if-null expression of the form `e1 ??= e2`: - If the static type of `e2` is not a subtype of the write type of `e1`, but it is assignable via a coercion, then the coercion is performed, and the coerced type of `e2` is used to compute the static type of the whole `??=` expression. - If `e1` is a promoted local variable, then coercions are performed based solely on the declared (unpromoted) type of `e1`. These behaviors apply regardless of whether feature `inference-update-3` is enabled; accordingly, this commit updates both the `_test.dart` and `_disabled_test.dart` variants of the tests. I've manually verified that even with the work on `inference-update-3` reverted, the `_disabled_test.dart` tests continue to pass, so we can be reasonably certain that these behaviors pre-date the work on the `inference-update-3` feature. Note: the diff is large due to the fact that the front end has 6 different code paths for handling `??=`, depending on the form of the LHS, so to make sure that we have adequate test coverage, there are tests for every possible LHS form. However, the diffs for all the tests are pretty much the same except for `if_null_assignment_local_disabled_test.dart` and `if_null_assignment_local_test.dart`, which have extra test cases to cover promotion behaviors. Bug: dart-lang/language#1618 Change-Id: I711d62d9dc00fc20a2efd3967d60066d9bfaec03 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/356303 Reviewed-by: Lasse Nielsen <lrn@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
This was switched on in dart-lang/sdk@fb21055, but I forgot to close the issue at the time. Closing it now. |
Consider this example code from dart-lang/sdk#45941:
The error is happening because when we use the least upper bound algorithm to determine the type of
bar ? CNB() : AB()
, we see that bothPSW
andSW
are possible upper bounds, but we can't find any reason to prefer one over the other, so we go one step up the class hierarchy and chooseW
for the static type.But we could do better, since we have a strong hint available from the context type
PSW
. For instance, currently the least upper bound algorithm forms sets of candidate super-interfaces at each possible depth in the class hierarchy, and chooses the final type using the greatest depth for which the number of candidate super-interfaces is exactly one. We could change this so that it if the number of candidate super-interfaces is more than one, but only one of them satisfies the context type, the one that satisfies the context type is chosen.The text was updated successfully, but these errors were encountered: