-
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
Inconsistent promotion behaviour between assignments inside and outside of patterns #2857
Comments
My thoughts on In a refutable/check context, you perform a type check, which makes the checked type a type of interest. In a declaration/assignment context, the pattern does not imply any checks, no more than About unifying coercions, I still think we should do that, and I'd make the downcast-from-dynamic at the assignment point, not on the RHS value, so |
tl;dr Let's consider coercions as syntactic transformations, and cast from dynamic as rather early rd;lt We have always treated assignments such that the value and static type is based on the assigned value (RHS), not the target (LHS). This is consistent with the treatment of the setter return type and the semantics of setters. At a first glance, this seems to be consistent with @lrhn's example (where
However, this apparent consistency relies on a couple of assumptions that we might want to consider explicitly. Let's say that an assignment chain is any construct whereby the value of an assignment is used (such as We might consider the assignment chain We could then say that assignment chaining is a syntactic sugar which is resolved very early, and cast from dynamic is a syntactic sugar which is resolved "later than very early". You could say that this makes sense because assignment chaining is recognized syntactically whereas cast from dynamic depends on the static types (so assignment chaining is "more primitive", hence earlier). However, in order to take setter invocation into account, we might not even want to consider assignments as syntactic sugar (where we relied on some lower-level assignment statement above, eliminating the question about "what's the value of the assignment expression?"). It certainly isn't correct to consider If we consider the type check to be part of the storage manipulation then we can consider cast from dynamic to be even later than that which is illustrated above: Just before we store the new value in the storage location for the local variable, we test the value-to-be-stored against the declared type of the variable (and throw if there is no subtype relation). It doesn't get later than that, but there may be several intermediate degrees of lateness. We have different strengths and weaknesses:
It is my impression that other coercions are best explained as syntactic sugar:
We may add support for additional coercions such as implicit constructors in the future. Presumably, they would amount to the ability to write Int-to-double as a coercion is controversial, but we could at least consider that to be a transformation (which is allowed and maybe even required to take place at compile-time) of an integer literal I tend to think that all these things conspire to support a consistent view of coercions as syntactic transformations: Interactions like ordering are then quite flexible, and they are rather easy to understand. If we do adopt this perspective, and we consider cast from dynamic to be a local transformation, then The question raised in this issue could then be resolved as follows: Cast from dynamic is a syntactic sugar that occurs early (in particular: before assignment, if we even want to consider that as a syntactic sugar). It gives rise to promotion just like explicit |
I really want us to get away from talking about "syntactic sugar". It suggests that you can write the same thing directly, and that's not always the case. (It might be a goal to make it always possible, but we aren't there). It also usually suggests that it's a small local rewrite, which is even less the case. That out the way, let's assume we all know that
If we take this to mean that the behavior of implicit coercions can be explained as equivalent to what you'd get by inserting certain small local operations, then yes. It's an implicit operation, equivalent to a potential explicit operation that you can insert yourself. That's good for explainability.
The "desugaring" description of an assignment depends on the LHS. We must evaluate enough of it to get to an assignable expression with no further unevaluated subexpressions. The current specification does case-explosion on the possible LHSs. Using
The null-aware LHSs use this prior evaluation to guard:
That means the let v1 = e1 in let t1 = (let v2 = e2 in let v3 = e3 in let _ = v2.id=(v3) in v3) in let _ e1.id=(t1) in t1 We can optimize that ( And if doing so, the obvious place for the inserted late downcasts would be at the actual assignments, Wanting to explain downcasts as a possible source transform means late coercions is not good, like Erik says, because the source transform needs to bind switch (d) { var d2 => (n = d2 as num, i = d2 as int, d2).$3} (We have If we want So, semantics of assignment
Runtime:
I want implicit coercions (downcast, I'm open to discussing where that place is. Making it possible to get the same effect by a small local insertion of an inferred operation can be a goal. I'm sympathetic to that, but not strongly convinced. |
@lrhn wrote:
Agreed, in principle. Tiny, local transformations is the exception where syntactic sugar may well be the most understandable approach.
... is pretty complex. I think this confirms that we do not want to consider the value of an assignment as syntactic sugar. This also implies that cast from dynamic is rather straightforward to understand:
I tend to say "let's do that" because it does make things much more explainable. ;-)
I also want them to be unified, but we still need to consider the ordering. If we apply two coercions "at the same place" then they are not at the same place any more if we consider them to be local source code transformations, and that basically forces us to answer the questions about ordering. |
Then we have to define them to be mutually exclusive. If we want coercion from callable class with generic |
These seem reasonable.
I can see the logic, but intuitively, these are both pretty surprising to me. It feels sort of like action at a distance. I don't think of assigning from a variable as having any affect on that variable, only the destination one. I think of assignment as similar to parameter passing. We don't promote, and I wouldn't expect us to promote, here: takeInt(int i) {}
main() {
dynamic i = 123;
takeInt(i);
i.foo();
} Given that, I think it's fine if pattern and non-pattern assignment doesn't promote either. Smart promotion is really nice, but there comes a point where it starts to feel like the language is reading too much into the code or something. Simplicity has its own value too. |
Ok, catching up on this thread now that I'm starting to come back from my illness. It sounds like there's a general consensus around the idea that pattern matches in irrefutable contexts shouldn't cause promotions to happen. I hadn't previously considered this idea, and I like it. It's simple, intuitive, and it cleanly avoids any inconsistencies between how pattern assignments and regular assignments address promotion. I'll go ahead and make this change. Regarding coercions and dynamic downcasts, it sounds like @lrhn and @eernstg are in agreement that it's valuable to make coercions behave more consistently with each other, and that downcast, call-tear-off, and generic instantiation are all examples of coercions. I'm good with that. There's less agreement about whether int-to-double should be thought of as a coercion, and whether more coercions should be added to the language in the future; personally I'm happy to stick with the current behaviour for now (in which int-to-double is not a coercion, but an independent mechanism that does nothing but infer a missing That leaves just one open question in my mind: how do we want this code to behave? class C {
void call() {}
void m() {}
}
f(C x1, dynamic x2) {
void Function() g;
int i;
var y1 = (g) = x1;
var y2 = (i) = x2;
y1.m(); // ok or not?
y2 = 'foo'; // ok or not?
} One interpretation is that But IMHO an equally compelling interpretation is that since
In the discussion above, when considering what to do with I get that. But I don't love it. Because if we make that change at some time in the future, then unless we make other changes, it will make the inconsistency between pattern assignments and non-pattern assignments worse, because it will mean that Personally I would rather aim for a future in which we say that the value of We don't have to make a decision today about how we want to fix coercions in general, but I would love to hear opinions about the behaviour I've implemented for the code example above (no compile-time errors). |
Indeed! However, I believe we still disagree on the approach: I'd like to have a simple model (as I see it ;-) where a coercion is a source code transformation on an expression In particular, I'm not convinced that it is as easy to understand a mechanism which can't be expressed as source code as it is when you can just say "it works like this: f(C x1, dynamic x2) {
void Function() g;
int i;
var y1 = (g) = x1; // `x1` becomes `x1.call`.
var y2 = (i) = x2; // `x2` becomes `x2 as int`.
y1.m(); // Error.
y2 = 'foo'; // Error.
}
Exactly. With complex patterns it gets more tricky, but we did agree at a recent language team meeting that it is a good starting point for determining the semantics of a pattern declaration respectively pattern assignment that they work like a sequence of simple declarations/assignments (including temporary variables which are introduced in order to avoid multiple evaluations of various expressions). From that point of view the pattern declarations/assignments are reduced to regular declarations/assignments, and the above approach can be applied directly.
I don't think that's unavoidable. But I'd like to reintroduce a more basic concern about the approach to coercions in pattern assignments and pattern declarations: You could say that whenever we assign a composite entity to a pattern, or initialize a pattern with a composite entity, and we have support for "hidden coercions" (that is, coercions that are applied after the composite entity has been decomposed in one way or another), we have introduced a semantics of the pattern which is dependent on the internal details of that composite entity. In other words, this turns the pattern into an entity which is tied more intimately to the initializing expression and less of a stand-alone entity. This seems to be a bad fit for a future generalization where we allow formal parameters to be specified as patterns: class C {
void call() {}
}
X id<X>(X x) => x;
void foo(var (void Function() f1, int Function(int) f2)) {} // A pattern parameter.
void main() {
var r1 = (C(), id);
var (void Function() f1, int Function(int) f2) = r1; // OK!
foo(r1); // OK?
var r2 = (C().call, id<int>);
foo(r2); // OK, of course. Certainly very different...
} The point is that we might be able to justify the pattern declaration in main that introduces Isn't that too much magic to have in a mechanism which is just supposed to let us pass an actual argument? Also, how would dynamic invocations of I'm really tempted to suggest that we go one step backwards and remove the support for "hidden coercions". void main() {
...
var (void Function() f1, int Function(int) f2) = r1; // Compile-time error.
} I think that's a better foundation for any generalization that turns a pattern into more of a first-class entity. |
I'm thinking about the discussion we had in yesterday's language meeting (in which there seemed to be a lot of support for @eernstg's idea of just removing support for hidden coercions). I'm also looking at the calendar and realizing we have only 20 days left until the branch cut. The CFE still doesn't support hidden coercions in patterns, so a big advantage of removing this feature is that it would require zero CFE work. (There would be some analyzer work, though). That means that code like this would simply be a compile error: class C {
void call() {}
}
f((C, int) x) {
var (void Function() f, int i) = x;
} Unfortunately, because of the way patterns currently work, removing support for "hidden coercions" will also mean that an assignment like |
It does indeed look like an inconsistency, based on the idea that "it should not matter whether we're doing Conversely, assume that we do support the coercions now, and then we're somehow not happy about the semantics later on (say, because the chosen semantics makes it more difficult to introduce a unified treatment of coercions in Dart). In that situation we may actually have painted ourselves into a corner which is not optimal. |
@dart-lang/language-team, it is my impression that we do not wish to promote a variable of type Do you agree? In that case I think we should settle the matter, at least for a non-trivial period of time. |
As discussed in dart-lang/language#2857 (comment), we only want a pattern match to promote the scrutinee in refutable contexts (if-case and switch). Bug: dart-lang/language#2857, #50419 Change-Id: I187ef632e5da95b931e5cda34db06491a5228c98 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/288000 Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Paul Berry <[email protected]>
As of dart-lang/sdk@adbf363, pattern matches now only promote the scrutinee in refutable contexts (as discussed in #2857 (comment)). |
Is this done now? |
I believe so. We now consistently promote in refutable (matching) contexts but not in irrefutable (assignment or declaration) contexts; this addresses the inconsistency because classic assignments don't promote. There's a bunch of discussion in this thread about coercions, but that should all probably be discussed separately. |
In my work on flow analysis for patterns, I've made the decision that any pattern match with a required type should promote the type of the scrutinee. That means that, for instance, the following pattern match promotes
x
:And so does this:
To be consistent, I've made pattern variable declarations and pattern assignments behave the same way. It's only observable when the scrutinee is
dynamic
, because otherwise the assignment would be invalid. So, for example:and:
However, this feels a little weird, because we don't promote the RHS of ordinary assignments and variable declarations, e.g.:
and:
A related problem is that with ordinary assignment expressions, a coercion such as an implicit tearoff of
.call
affects the value of the assignment expression, e.g.:But for pattern matches it doesn't really make sense to apply coercions to the right hand side of the assignment, e.g.:
In which case how do we want this code to behave?
For consistency with non-pattern assignments, it would make sense to have an error (because the value of
(g) = c
is the coerced value); for consistency with pattern assignments, it would make sense to have no error (because the coercion happened as part of the subpattern match ofg
).Personally, my preference is to say that all pattern assignments and pattern variable declarations should follow the new rules for patterns, and we shouldn't worry about these subtle inconsistencies with old non-pattern behaviour. (In fact, in the long term, I would love to change the behaviour of old-style assignments and variable declarations to match the new pattern behaviour, but that's another story). In which case, in
(g) = c
, the coercion would happen as part of the subpattern match ofg
, and thereforex
would have typeC
, sox.m()
would be valid.But I would love to hear other people's thoughts.
CC @dart-lang/language-team
The text was updated successfully, but these errors were encountered: