-
Notifications
You must be signed in to change notification settings - Fork 205
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
Begin specifying type inference of selector chains. #3937
base: main
Are you sure you want to change the base?
Conversation
The bulk of the intricacies in Dart type inference occur in the handling of selector chains, which encompasses the following constructs (examples in parentheses): - Static method invocations (`Foo.m()`) - Implicit instance creation (`Foo()`) - Implicit `this` invocation (`m()`) - Explicit extension invocation (`Ext(...).m(...)`) - Super invocation (`super.m(...)`) - Method invocation (`....m(...)` or `...?.m(...)`) - Explicit extension call invocation (`Ext(...)(...)`) - Super call invocation (`super(...)`) - Call invocation (`...(...)`) - Static method tearoff (`Foo.m`) - Constructor tearoff (`Foo.new`) - Explicit extension tearoff or property get (`Ext(...).p`) - Super method tearoff or property get (`super.p`) - Method tearoff or property get (`....p` or `...?.p`) - Implicit this method tearoff with type arguments (`m<...>`) - Type instantiation (`Foo<...>`) - Explicit extension index operation (`Ext(...)[...]`) - Super index operation (`super[...]`) - Index operation (`...[...]` or `...?[...]`) - Null check (`...!`) Since this is a lot of constructs, I'm going to break them up into several PRs to simplify review. This first PR establishes the general framework for how to differentiate among the above forms, and then it specifies the behavior of just four of them: static method invocations, implicit `this` invocations, non-null-aware method invocations, and call invocations. The rest of the forms are left as TODOs, and will be addressed in follow-up PRs. In order to specify these four forms, it was necessary to add some introductory material: - Bound resolution, which converts type parameters into their bounds. (This concept exists in the analyzer and CFE, but I wasn't able to find it in the spec so I added it after the "Subtype constraint generation" section.) - Argument part inference, the general process by which type inference is applied to the <argumentPart> portion of any invocation. Much of this section was adapted from the horizontal inference spec (https://github.com/dart-lang/language/blob/main/accepted/2.18/horizontal-inference/feature-specification.md). - Argument partitioning, the mechanism used by horizontal inference to choose the order in which to type infer arguments that are function expressions. This was also adapted from the horizontal inference spec. - Method invocation inference, which contains the common subroutines of type inference used for implicit `this` invocations, method invocations, and call invocations. In a later PR I intend to also use this logic to specify how user-definable operator invocations are type inferred.
@@ -969,6 +969,23 @@ with respect to `L` under constraints `C0` | |||
- If for `i` in `0...m`, `Mi` is a subtype match for `Ni` with respect to `L` | |||
under constraints `Ci`. | |||
|
|||
## Bound resolution |
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.
This is already defined on page 144 of the latest spec at spec.dart.dev, consider just referencing that rather than duplicating?
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.
As I see it, there’s an important difference. The spec notion of “T is T0 bounded” is a relation between types but not a function. For example, if A
and B
are type variables, the bound of A
is B
, and the bound of B
is C
, then A
is considered to simultaneously be A
bounded, B
bounded, and C
bounded.
The notion I’m defining here is a total function: considering the same example, the “bound resolution” of A
is uniquely C
. The way I use this notion later in the PR relies on it being a total function (a step in the “Method invocation inference” procedure is "Let U_0
be the bound resolution of T_0
").
The two notions are definitely related, and I guess we could define bound resolution in terms of "T0 bounded" as follows:
The bound resolution of T is the unique type T0 such that T0 is neither a type variable nor a union of the form X&B.
But then we would need to justify why such a type is known to exist and be unique for all T, and at that point it seems to me like defining it procedurally is both clearer and easier to reconcile with what the implementations do.
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 really don't like having this duplicated here, and I still think it is duplicated for all practical purposes. I think what you are getting at is that the spec chooses to say that T0
is T0
bounded without the qualification that T0
is neither a type variable nor an intersection. I think all we need to do to make these definitions coincide is add that qualification - I see no reason that we want to say that a type variable X
is X bounded
. I'd suggest fixing the spec on this rather than duplicating this here. cc @eernstg what do you think?
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've created a sketch of what this might look like here: #4013
resources/type-system/inference.md
Outdated
@@ -1117,6 +1134,26 @@ succintly, the syntax of Dart is extended to allow the following forms: | |||
any loss of functionality, provided they are not trying to construct a proof | |||
of soundness._ | |||
|
|||
In addition, the following forms are added to allow constructor invocations, | |||
dynamic method invocations, function object invocations, instance method |
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 couldn't parse out of here which forms are intended to cover which subsets of the semantic space. I think it would be helpful to be clear about that, even if it's not essential to your point. For example, does dynamic e; e.foo()
correspond to a DYNAMIC_INVOKE of the call method? Or a FUNCTION_INVOKE?
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.
Added some informative text to clarify. Let me know if this section makes more sense now.
|
||
- `@INSTANCE_INVOKE(m_0.id<T_1, T_2, ...>(n_1: m_1, n_2: m_2, ...))` | ||
|
||
- `@STATIC_INVOKE(f<T_1, T_2, ...>(n_1: m_1, n_2: m_2, ...))` |
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.
What is the syntactic category of f
? Is it only an identifier? Or prefixed identifier perhaps? Probably not an expression?
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.
Hmm, this is a situation where my choice of notation really fails to capture what’s in my head.
My intention is for @STATIC_INVOKE
to be the spec counterpart of the kernel’s StaticInvocation
class (https://github.com/dart-lang/sdk/blob/f1febd2cf3bf754ae641eb3fda8ee885b8bda5e5/pkg/kernel/lib/ast.dart#L6531), with f
corresponding to StaticInvocation.targetReference
, which has type Reference
. A kernel Reference
is an indirection mechanism allowing different parts of the kernel representation of a program to refer to one another unambiguously. In practice it’s serialized as a sequence of strings separated by ::
, the first of which is a URI (e.g. dart:core::int::tryParse
), but that’s an unimportant implementation detail. What’s important is that StaticInvocation.targetReference
is some way of referring to unambiguously to a top level function or static method defined in a library somewhere in the user’s program.
Putting on my spec writer’s hat, I guess if we want to be really formal about it, we could say that f
is either a pair (URI, name)
referring to a top level function named name
in the library located at URI
, or a triple (URI, name1, name2)
, where name1
is the name of a class, mixin, enum, or extension, and name2
is the name of a static method inside the thing named by name1
. But I don’t really want to be this formal because it will make my examples really unwieldy (e.g. I want to say @STATIC_INVOKE(print(s))
rather than @STATIC_INVOKE(('dart:core', 'print')(s))
).
Also, even (URI, name1, name2)
isn’t good enough to uniquely identify a static method inside an unnamed extension, since a single library could have multiple unnamed extensions containing static methods with the same name. The kernel representation deals with this by giving every unnamed extension a synthetic name, but that feels like an implementation detail of the kernel that doesn’t belong in the spec.
What I really want to say is: “gentle reader, please think of f
using whatever representation suits you for unambiguously referring to a static method or top level function that exists somewhere in the program being type inferred. Maybe that’s a combination of a URI and some strings, with some synthetic naming magic to disambiguate methods in unnamed extensions. Maybe it’s a unique integer identifier you assign to each static method or top level function in the user’s program. For my purposes in this document, I’m going to try to stick to examples where it’s clear from context what I’m referring to, like @STATIC_INVOKE(print(s))
(which clearly refers to the top level function print
in dart:core
or @STATIC_INVOKE(int.tryParse(s))
(which clearly refers to the static method tryParse
in the class int
in dart:core
).”
I’m not really sure how to express that idea in the sort of formal style that we typically use for specifications. Any suggestions?
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.
Why do you want to move to the semantic layer here? In the examples above, you just use the elaborated expression as the target. Why not something similar here? That is, you are starting with an expression (or maybe prefixed identifier?) f
, why not just continue to use that in the meta-syntax?
identifier. When present, `id` represents an identifier or operator name, and | ||
`f` represents a static method or top level function. | ||
|
||
The semantics of each of these forms is to evaluate the `m_i` in sequence, then |
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.
Technically I think "evaluating the lookup of id
in m_i
" is interposed in here. This is required for at least the DYNAMIC_INVOKE, since you don't know whether you are invoking a method or calling a function typed getter. Whether it is required for any of the others depends a bit on the answer to my previous question about what semantic forms are intended to be covered here. That is, do you intend
class A {
int Function(int) f = throw "yeehaw";
void test() {
this.f(3);
}
}
to be covered by one of the above, or do you intend to add something else to cover this later?
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.
Ok, I’ve tried to clarify this as much as I can without formally specifying the exact semantics of each form.
As to your example with this.f(3)
, I intend to eventually represent this as something like @FUNCTION_INVOKE(@INSTANCE_GET(m_0.id).call(3))
. In this section, I’ve tried to hint at this in the non-normative text I’ve added under @FUNCTION_INVOKE
.
The full details are in the “Method invocation inference” section, under the bullet “Otherwise, if U_0
has an accessible instance getter named id
, then:”, but that text is a little wishy washy because I haven’t introduced the notion of @INSTANCE_GET
yet as of this PR.
resources/type-system/inference.md
Outdated
The output of argument part inference is a sequence of elaborated expressions | ||
`{m_1, m_2, ...}` (of the same length as the sequence of input expressions), a | ||
sequence of zero or more elaborated type arguments `{U_1, U_2, ...}`, and a | ||
result type known as `R`. _(The sequence of optional names is unchanged by |
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.
"known as" reads oddly, maybe just say "result type R
"?
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.
Done.
The procedure for argument part inference is as follows: | ||
|
||
- Let `{X_1, X_2, ...}` be the list of formal type parameters in `F`, or an | ||
empty list if `F` is `∅`. |
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.
From reading the below, I think (but am not positive) that it would be very easy to just split out the case where F
is not present into a single section, instead of interspersing it throughout. If this is relatively easy to do (and I think it should be, because it basically boils down to "do inference on the arguments in the empty context") I think it will help a fair bit in making this more readable, because I'm getting lost in the nested series of "if we are in this case do X" below.
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.
Yeah, I can definitely see the appeal of that. If you don’t mind, I’d like to try to do it in a follow-up PR after landing this one, because it might involve moving a lot of text around and duplicating some things, so I want to make sure I’ve fully addressed all your other code review comments before I do that.
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.
ack.
resources/type-system/inference.md
Outdated
- Let `{X_1, X_2, ...}` be the list of formal type parameters in `F`, or an | ||
empty list if `F` is `∅`. | ||
|
||
- Let `{P_1, P_2, ...}` be the list of formal parameter types corresponding to |
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 P_i
must be type schemas, not types.
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.
Done.
invocation; the user-supplied type arguments are accepted without | ||
modification._ | ||
|
||
- Otherwise, `F` must be a function type. If the number of formal type |
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 wonder whether it would also be helpful to just split out the case where type arguments are explicitly passed into a separate section as well? There will be some redundancy, but I don't think a lot, and it would make all of this much more readable. Basically, instead of re-branching on which case you're in many times throughout the description (which is super hard to read), you'd end up with something that has the structure:
if we're in the dynamic case, do {
downwards inference on arguments using _
} else if we're in the explicit type argument case, do {
compute the contexts for downward inference K_i
downwards inference on arguments using K_i
} else if we're in the type inference case do {
stuff (with some overlap from the above)
}
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.
Good point. I’ll attempt this in a follow-up PR as well.
resources/type-system/inference.md
Outdated
|
||
- If this succeeds, then accumulate the resulting list of type constraints | ||
into `C`. Then, let `{V_1, V_2, ...}` be the constraint solution for the | ||
set of type variables `{X_1, X_2, ...}` with respect to the constaint |
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.
constaint -> constraint
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.
Fixed.
resources/type-system/inference.md
Outdated
- Initialize `{U_1, U_2, ...}` to a list of type schemas, of the same length | ||
as `{X_1, X_2, ...}`, each of which is `_`. | ||
|
||
- Using subtype constraint generation, attempt to match `R_F` as a subtype |
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.
Nit: I would avoid saying "match R_f
as a subtype of K
because it risks confusing two separate relations: subtyping and subtype matching. I would prefer to either use the symbolic notation I introduced above, or say something like "check whether R_f
is a subtype match for K
with respect to ...". The point is, the relation is "subtype match" not "subtype" (which is not defined for schemas, and is a binary relation rather than a trinary relation).
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.
Done (here and elsewhere in the PR).
resources/type-system/inference.md
Outdated
- If this succeeds, then accumulate the resulting list of type constraints | ||
into `C`. Then, let `{V_1, V_2, ...}` be the constraint solution for the | ||
set of type variables `{X_1, X_2, ...}` with respect to the constaint | ||
set `C`, with partial solution `{U_1, U_2, ...}`. Replace `{U_1, U_2, |
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.
Maybe say "Let the new values of {U_1, ...}
be {V_1...}
? Saying "replace" is a little confusing here.
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.
Done (here and elsewhere in the PR).
resources/type-system/inference.md
Outdated
- Let `S_i` be the static type of `m_i_preliminary`. | ||
|
||
- Using subtype constraint generation, attempt to match `S_i` as a subtype | ||
of `K_i` with respect to the list of type variables `{X_1, X_2, ...}`. |
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 this should be P_i
right? K_i
is has been closed WRT the X_i
by substituting in the partial solutions U_i
above?
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.
Yes, thank you. I double checked the implementation and you are absolutely right. Fixed.
Also I noticed that the “For each e_i
in stage k” bullet enclosing this bullet doesn’t specify the order in which the e_i
are considered. I’ve clarified that the order is the same as the order in which expression inference was performed.
resources/type-system/inference.md
Outdated
|
||
- Let `S_i` be the static type of `m_i_preliminary`. | ||
|
||
- Using subtype constraint generation, attempt to match `S_i` as a subtype |
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.
As above WRT terminology "subtype" vs "subtype match"
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.
Fixed, thanks.
resources/type-system/inference.md
Outdated
|
||
- If _k_ is not the last stage in the argument partitioning, then let | ||
`{V_1, V_2, ...}` be the constraint solution for the set of type | ||
variables `{X_1, X_2, ...}` with respect to the constaint set `C`, with |
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.
constaint -> constraint
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.
Fixed.
resources/type-system/inference.md
Outdated
|
||
- Otherwise (_k_ __is__ the last stage in the argument partitioning), let | ||
`{V_1, V_2, ...}` be the _grounded_ constraint solution for the set of | ||
type variables `{X_1, X_2, ...}` with respect to the constaint set `C`, |
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.
constaint -> constraint
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.
Fixed.
- For each `e_i`: | ||
|
||
- Let `K_i` be the result of substituting `{U_1, U_2, ...}` for `{X_1, X_2, | ||
...}` in `P_i`. _Note that this is now guaranteed to be a proper type, not a |
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.
At some point up above (in the case that the function type was missing) you set P_i
to _
, in which case this is not true. It's not clear to me whether you ever end up at this particular point in the algorithm if you are in the "missing function type" case, but this is an example of why I think it might be much clearer to just split out the three cases (dynamic, explicit type arguments, inferred type arguments) into three sections instead of trying to interleave them.
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.
Hmm, you are right; this doesn’t make sense. And I agree that this is a good argument for splitting the algorithm into three cases.
resources/type-system/inference.md
Outdated
_So, for example, if the invocation in question is this:_ | ||
|
||
```dart | ||
f((t, u) { ... } /* A */, () { ... } /* B */, (v) { ... } /* C */, (u) { ... } /* D */) |
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.
This example is very hard to parse as written. Consider formatting it with arguments on different lines parallel to the declaration below?
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.
Haha, this line was copied wholesale from https://github.com/dart-lang/language/blob/main/accepted/2.18/horizontal-inference/feature-specification.md, and no one complained about it there 🙂
But you have a good point. Reformatted.
|
||
- Let `U_0` be the [bound resolution](#Bound-resolution) of `T_0`. | ||
|
||
- If `U_0` is `dynamic` or `Never`, or `U_0` is `Function` and `id` is `call`, |
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.
Is there an implicit assumption here that all function calls e(...)
have been rewritten to the form e.call(...)
? If so maybe make that explicit?
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.
Yes. It’s implicit in the text that follows (in the “Selector chain inference” section), but I’ve added a clarifying comment to this section to make it explicit.
resources/type-system/inference.md
Outdated
|
||
- Invoke [argument part inference](#Argument-part-inference) on `<T_1, T_2, | ||
...>(n_1: e_1, n_2: e_2, ...)`, using a target function type of `∅` and a | ||
type schema `K`. Denote the resulting elaborated expressions by `{m_1, m_2, |
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.
Saying "a type schema K
" is a little confusing because it has two natural interpretations: "some type schema K (I'm not saying what)" and "the type schema K that I mentioned above". I would either say "the type schema K" or "K as the type schema" to make it clear that K
is not being bound here but rather is a reference to the K introduced above.
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.
Same comment for each following bullet.
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.
Good point. I went with “using ∅
as the target function type and K
as the context”.
resources/type-system/inference.md
Outdated
- Let `F` be the return type of `U_0`'s accessible instance getter named `id`. | ||
|
||
- Invoke [argument part inference](#Argument-part-inference) on `<T_1, T_2, | ||
...>(n_1: e_1, n_2: e_2, ...)`, using a target function type of `F` and a |
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 believe argument part inference assumes as a pre-condition that the target function type will either be empty, or a function type. I don't think this step satisfies that precondition necessarily, since the return type of the getter could be dynamic, Never, Function
and the code be well-typed, or it could be something else (like int
) and the code is ill typed.
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.
Good catch! Digging through the implementations, there’s definitely a problem here.
The core issue is that both the analyzer and CFE allow F
to be any arbitrary type, provided that the interface of F
contains a suitable member called call
. The analyzer requires that this member be a function; the CFE allows it to be a getter (in which case the desugaring process happens recursively). The CFE can even be made to crash with a stack overflow if this recursive desugaring process doesn’t terminate (e.g. via class C { C get call => C(); }
).
I need to spend some time reading and thinking about this issue in relation to #3482. That issue is mostly about how we handle this sort of thing in dynamic invocations, but the discussion touches on static analysis and desugaring as well.
I’ve added a note to myself to spend more time thinking about this. In the meantime, to avoid blocking this PR, I’ve added a TODO comment.
|
||
_TODO(paulberry): specify this._ | ||
|
||
### Implicit this invocation |
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 may be missing something, but this seems wrong. If I follow the algorithm here, I think it implies that the following is an error:
int Function(int) f = (x) => x;
void test() {
f(3);
}
because:
- Neither of the previous two cases apply to
f(3)
- I think
f(3)
parses so as to match this case - The selector chain here does not have access to
this
.
Am I missing something?
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.
You’re right. As a band-aid, I’ve changed the text “cannot be resolved to the name of a local variable” to “cannot be resolved using local scope resolution rules” to resolve this problem, but this is not a good solution. In particular, it doesn’t handle the possibility that the identifier can be resolved using local scope resolution rules, and it resolves to an instance method or getter in the current class.
I’ve been working ahead a bit while this PR was under review, and I’ve got some ideas for how to address this, so I’m going to defer further work on this issue to a TODO comment.
resources/type-system/inference.md
Outdated
static/toplevel method invocations to be distinguished. In these forms, `n_i: | ||
m_i` (where `i` is an integer) is used as a convenient meta-syntax to refer to | ||
an invocation argument `m_i` (an elaborated expression), possibly preceded by a | ||
name selector `n_i:` (where `n_i` is a string). In this document, we use the |
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.
String seems misleading here, maybe just say parameter name
and leave it abstract?
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.
Done.
- `@FUNCTION_INVOKE(m_0.call<T_1, T_2, ...>(n_1: m_1, n_2: m_2, ...))` | ||
|
||
_This covers invocations of expressions whose type is a function type (but not |
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 don't really understand the division between FUNCTION_INVOKE and STATIC_INVOKE. At a minimum, I think you need to say something here like "expressions which do not resolve to top level or static methods" because otherwise it is ambiguous what to do with f(3)
where f
is a top level method (because f
is an expression whose type is a function type, and it is also a top level method). I'm also confused about something like x.call()
. I guess maybe the screwy Dart grammar doesn't treat the x.call
here as an expression and therefore we can say that it is unambiguously covered by INSTANCE_INVOKE instead of being ambiguous? Is that right? So (x.call)()
is a FUNCTION_INVOKE, but x.call()
is an INSTANCE_INVOKE?
|
||
- _Explicit method invocations (e.g. `[].add(x)`), in which case `id` is the name of the method (`add` in this example)._ | ||
|
||
- _Call invocations (e.g. `f()`, where `f` is a local variable), in which case |
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.
Not just local variables though, right? Any expression, top level method etc could be in this form?
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.
Yes, that's correct.
resources/type-system/inference.md
Outdated
_Note that the spec notions of __dynamic__ boundedness and __Function__ | ||
boundedness can be defined in terms of bound resolution, as follows: a type is | ||
__dynamic__ bounded iff its bound resolution is __dynamic__, and a type is | ||
__Function__ bounded if its bound resolution if __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.
if -> is
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.
Fixed, thanks!
## Bound resolution | ||
|
||
For any type `T`, the _bound resolution_ of `T` is a type `U`, defined by the | ||
following recursive process: |
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.
(So the point of the bound resolution of T
is to find a non-type-variable type which is a (minimal) supertype of T
. If T
is not a type variable, it's T
.
If T
is a type variable or a type variable intersection, then the least non-type-variable (upper) bound is the variables bound or the intersection type, respectively. Makes sense.)
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 like this way of thinking about it, thanks! I’ve added some text to the PR to clarify this.
|
||
_Note that the spec notions of __dynamic__ boundedness and __Function__ | ||
boundedness can be defined in terms of bound resolution, as follows: a type is | ||
__dynamic__ bounded iff its bound resolution is __dynamic__, and a type is |
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.
(Not sure using bold for types is a good idea, when we also use bold for semantic functions like UP. Consider "is dynamic bounded if and only if its bound resolution is the type dynamic
" and similar for 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.
Yeah, I see what you mean. I’m usually in the habit of using a monospaced font for type names, but in this case, since I’m referring directly to text in the spec, I figured I would imitate the style of the spec. If you look at page 144 of https://spec.dart.dev/DartLangSpecDraft.pdf, it uses boldface for dynamic and Function.
The bulk of the intricacies in Dart type inference occur in the handling of selector chains, which encompasses the following constructs (examples in parentheses):
Foo.m()
)Foo()
)this
invocation (m()
)Ext(...).m(...)
)super.m(...)
)....m(...)
or...?.m(...)
)Ext(...)(...)
)super(...)
)...(...)
)Foo.m
)Foo.new
)Ext(...).p
)super.p
)....p
or...?.p
)m<...>
)Foo<...>
)Ext(...)[...]
)super[...]
)...[...]
or...?[...]
)...!
)Since this is a lot of constructs, I'm going to break them up into several PRs to simplify review. This first PR establishes the general framework for how to differentiate among the above forms, and then it specifies the behavior of just four of them: static method invocations, implicit
this
invocations, non-null-aware method invocations, and call invocations. The rest of the forms are left as TODOs, and will be addressed in follow-up PRs.In order to specify these four forms, it was necessary to add some introductory material:
Bound resolution, which converts type parameters into their bounds. (This concept exists in the analyzer and CFE, but I wasn't able to find it in the spec so I added it after the "Subtype constraint generation" section.)
Argument part inference, the general process by which type inference is applied to the portion of any invocation. Much of this section was adapted from the horizontal inference spec (https://github.com/dart-lang/language/blob/main/accepted/2.18/horizontal-inference/feature-specification.md).
Argument partitioning, the mechanism used by horizontal inference to choose the order in which to type infer arguments that are function expressions. This was also adapted from the horizontal inference spec.
Method invocation inference, which contains the common subroutines of type inference used for implicit
this
invocations, method invocations, and call invocations. In a later PR I intend to also use this logic to specify how user-definable operator invocations are type inferred.Contribution guidelines:
dart format
.Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.