-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Meta issue for implementation of strong mode instantiate to bound for Dart 1.5 #27526
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
Relevant discusssion here: #26265 |
Status update: @leafpetersen is assigned to propose rules. Once they are in, we will possibly reassign this bug to make the changes (and/or update the spec). |
We have two proposals, which I'm going to summarize here and solicit feedback on. |
Proposal 1 (preferred). Terms: a named type class A<T extends num> {}
// A here is a raw type
A x;
// A<num> here is not a raw type
A<num> y; // Given a parameterized type The high level intuition is that to compute the default bounds for More precisely:
Examples: // Default is A<int>.
class A<T extends int> {}
// Type completed to A<int>.
A x;
// Default is B<A<int>>.
class B<T extends A> {}
// Type completed to B<A<int>>.
B x;
// Default is C<int, A<int>>.
class C<T extends int, S extends A<T>> {}
// Type completed to C<int, A<int>>.
C x;
// No error here, but no default can be computed.
class D<T extends Comparable<T>> {}
// Error: no bounds can be computed for x
D x;
// Error: D has no default
class E<T extends D>
// Error: G has no default bounds, because of a cycle
class F<T extends G> {}
// Error: F has no default bounds, because of a cycle
class G<T extends F> {} Notes: I've presented the two phases as ordered, but I believe that they are independent and could be computed in either order, so long as we treat The major downside of this approach is that computing the default bounds for a type requires a recursive computation of default bounds for any raw types mentioned in its bounds, with cycle detection. This is fairly non-local, and somewhat heavyweight. We would like feedback on any implementation concerns. |
Proposal 2 (less preferred)
class Foo<T extends List> {} // ERROR, List must be instantiated
Basically, this algorithm will require explicit arguments for F-bounded cases, but will do the right thing for non-recursive cases (where "the right thing" is using the bound as a default value). Examples: // Default is A<int>.
class A<T extends int> {}
// Type completed to A<int>.
A x;
// Error: bound is not explicitly instantiated.
class B<T extends A> {}
// Default is C<int, A<int>>.
class C<T extends int, S extends A<T>> {}
// Type completed to C<int, A<int>>.
C x;
// No error here, but no default can be computed.
class D<T extends Comparable<T>> {}
// Error: no bounds can be computed for x
D x;
// Error: D has no default
class E<T extends D>
// Error: G has no default bounds
class F<T extends G> {}
// Error: F has no default bounds
class G<T extends F> {} Notes: This proposal is less preferred because it makes more cases that seem reasonable an error. This proposal has the advantage that the calculation of the default bounds of a type is entirely local to the type declaration. |
@jmesserly @sigmundch @bwilkerson @stereotype441 @scheglov @floitschG @eernstg @lrhn @munificent Feedback on usability and implementation concerns welcome. Given that proposal 2 is just an extension of proposal 1, it might be worth implementing proposal 2 and testing it against a large corpus to see how many cases are rejected that could be potentially handled by proposal 2. |
I'd be fine with proposal 2 without any further investigation. If we want to consider proposal 1, then I would want us to first do a trial implementation inside the summary linker to make sure the effect on summaries isn't too onerous. |
I do have a concern with the non-locality of the first proposal, both from an implementation standpoint (I expect you're aware of many of the implementation issues) and from a usability standpoint. It tends to be confusing to users if a change they're making in one library causes errors to appear in other libraries. While I haven't constructed an example to prove that this could occur, it looks like a likely problem. It would definitely be interesting to know how often the problems we're trying to solve and the problems we're (potentially) introducing occur in practice. In particular, how often do the "cases that seem reasonable" mentioned in the second proposal occur? If they are very rare, then it would be worth the simplicity of the model to accept the limitations. I'm also wondering whether we really want to produce errors at the declaration site. One of the examples is:
It might be frustrating to users for this to be disallowed if all of the uses of On a minor note, I think one of the examples is wrong:
Assuming that the definition of B is
then, |
This could certainly happen, but I would expect it to be fairly rare. It can only arise if you have types which refer to each cyclically in their bounds. This isn't unheard of, but I would expect it to only happen in fairly tightly coupled code.
Given that the 1st is an extension of the 2nd, perhaps it would be worth implementing it and testing it out. I think that there will be quite a bit of code that looks like the following: class A<T extends List> {}
A x; which will be an error. If we made the modest extension to proposal 2 to allow raw types in bounds for raw types with implicit bounds, then it's plausible that this might catch most of the remaining cases.
Unfortunately, this one pretty much has to be an error. It doesn't matter whether
Fixed, thanks. |
I did a little bit of code search spelunking, and actually didn't find many uses of the obvious culprits. I think it's worth either implementing proposal 2 and testing it out, or possibly just implementing the "make a raw type in a bound an error" part and testing it out. |
status: ready for implementation. I will work with implementers to get proposal 2 implemented, run a pre-submit, and make go/no-go on the extension to proposal 1 based on data. |
Not sure anyone has specifically brought up where the generic types are recursive to the class that's defined.
In the latter case, it should resolve to
You can see how we know that At the very least this is a case to be caught at declaration time in the spec to make sure it doesn't crash in an infinite loop! But maybe it is best represented by
in this case,
These classes could include state to guard against infinite loops to report internal errors, or know when they are complete, and algorithms could detect them to do special comparisons. I think there's a proof to be made about the typing relationship of of infinite types which is something along the lines of "infinite type a is a subtype of type b if b is an infinite type where any point in the tree traced forward until it repeats perfectly matches the root of a traced forward until it repeats itself." Probably not the hardest code to write, though there may be like...one or two algorithms that are real hell to describe with infinite types as a possibility. Maybe easier to report an error when its declared :) But this is not outside the bounds of a typechecking system. Its really just a question of how complicated the algorithms that handle it become. |
Yes, Dart supports F-bounded quantification. Like you note, it comes into play in Comparable and a couple of other places (though surprisingly few for the complexity it adds to the type system). We should be handling the potentially infinite types that come from that in the analyzer already (though it has certainly been a source of bugs over the years!). :) |
I think F-Bounded (also, didn't know that's what they were called! Thanks!) types are supported and working, but F-Bounded types with undeclared parameters, and therefore instantiated to bounds, are not working as well as they could:
with
Instead of Its also worth noting that
|
Recursive types generally don't fit well into Dart's type system - there's not way to express them directly in the syntax (though they are in a certain sense there implicitly). Both of the proposals that I sketched out in comments above deal with the |
That's probably the best, at least from a difficulty-to-implement point of view. I don't yet see a problem with letting As someone who likes type theory (only well enough to not know the name for F-Bound quantification), I enjoy thinking about this problem, but there are bigger fish to fry. |
@leafpetersen #27526 (comment) suggests that this is specified, can we get the checklist updated? And is there a bug tracking the implementation in analyzer? |
There is now. :) |
Moving to 1.22 for implementation per status update in email from @leafpetersen. Informal spec is included directly in this bug itself. |
@leafpetersen @mit-mit @munificent Checking that this is critical for 1.22 w/ the spec work moved to 1.50? |
My take (which I've shared with the analyzer folks) is that the syntax changes which affect both strong mode and spec mode are critical for 1.50 (I see this as the main point of 1.50 as it stands). Strong mode only changes to bring strong mode into alignment with various language team decisions are a step down in priority: good to have in 1.50, but 1.50 still makes sense without them if they just can't happen in time. This particular issue is second on my list of strong mode only changes in terms of priority, below toplevel inference. But if this is feasible to land in that time frame and toplevel inference is not, then feel free to prioritize this. |
Changelog underway: https://codereview.chromium.org/2648203003/ |
We (the language team) have decided on a small extension to the feature, but otherwise this is set to go as implemented. |
Closing this; the follow-up issue #28580 will happen in 1.23. |
Uh oh!
There was an error while loading. Please reload this page.
Meta issue tracking the final implementation of the instantiate to bound behavior for Dart 1.5 (that is, how to treat uses of generic types/methods where no type arguments are specified).
A bit more context (but see the relevant discussion in #26265):
Dart tries to infer the type of generic arguments based on the given bounds. However, that can be complicated (and sometimes impossible). For example the following example should probably be an error:
Different approaches possible. One restriction (for example) would require to write the generic type in the bound:
Tracking bugs:
In a following release:
The text was updated successfully, but these errors were encountered: