-
Notifications
You must be signed in to change notification settings - Fork 202
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
Do we insist on tearoff equality? #1712
Comments
My two cents: I would prefer to change the implementations so that they implement the specified behavior, provided that there isn't some big implementation cost we're not seeing. If there is some big implementation cost, I would be open to changing the spec to something that's easier to implement. An example of an implementation cost that would probably convince me to change the spec would be if we found out that the dart2js representation of function objects doesn't retain enough information to be able to equality compare them the way the spec demands, and adding that information would likely produce a nontrivial regression in a real-world benchmark. But honestly I would be pretty surprised if that happened, because even in the worst case scenario where function types are completely opaque, I can imagine an expando-based approach for achieving the specified behavior (the expando maps each function object to a hidden auxiliary object whose operator== implements the necessary behavior, and Function.operator== simply defers to the auxiliary object's operator==), and that approach seems to me that it would have low enough cost. But my intuition about that might be totally wrong. Note that if we do find some hidden cost that convinces us to change the spec, I would strongly prefer that we invent a new, consistent rule, and change the spec and the implementations to implement it uniformly. I don't want to just allow the implementation behaviors to differ because the spec says certain corner cases are "undefined". |
The way I think about it, I would want tear-offs to be equal to themselves even without considering identity. So, any equality we define should allow a tearoff to be equal to itself. I also want tearing off the same function twice in the same scope to give equal function values. That's what it takes to make For non-instantiated tear-offs, that's fairly easy and well-understood, and we do it already. All non-instance tear-offs are constant and canonicalized. Instance method tear-offs are equal when the same function is torn off from the same (identical) object. For instantiated tear-offs, I'd still prefer if the same expression in the same context type gives equal values. If the type context is constant (does not contain type variables), the non-instance tear-offs are still canonicalized. For non-constant instantiated tear-offs, I'd still prefer if equality worked as long as both tear-offs happens in the same scope. So, all in all, I think I'd want equality to work everywhere. |
@lrhn wrote:
Yes, that's exactly the same kind of reasoning I've been thinking about when considering generic function/method instantiations. You could say that
That's a thorny issue. As a starting point, we could use the same rules as those that govern canonicalization. |
@stereotype441 wrote:
Agreed! The current behavior includes the following:
Other tools do slightly different things. It does look like we'd need to make some changes to implementations in order to reach a situation where we follow a consistent rule, no matter what. |
Thinking about this some more, I don't really understand the reasoning behind this. The equality in question is inherently a runtime property, so I don't understand why any of "same expression", "same scope", or constant variables are relevant. Basically, I see two consistent approaches here:
I don't understand why we would specify that instantiated instance method tear offs are sometimes equal and sometimes not, and I don't understand why we would specify that instantiated instance methods tear offs are different from other things. Basically, from my perspective, In general, I'm very hesitant to buy into extending the instance method tear off equality hack to implicit or explicit instantiations. I'm inclined to say that we leave the identity of such things implementation defined, and leave equality as identity. |
What I want with the "scope" is that if I do: void something(Foo foo) async {
x.addListener(foo.onEvent);
await whatever;
x.removeListener(foo.onEvent);
} then the two That said, it's not what we have right now. My primary wish is to have some kind of consistency across tear-offs of static, instance and local functions, so users can predict when two tear-offs will be equal. We do have the option of never making instantiated tear-offs equal to each other, no matter what. That's almost what we do now - The VM does that, dart2js makes instantiated top-level/static functions equal if they have the same type arguments. However, we have explicitly made top-level instantiated tear-offs with constant type arguments be constants and canonicalized, so the VM needs to do that too. So, our option is really to make non-constant instantiated tear-offs not equal (which, as you say, just means not overriding Or we can make instantiated tear-offs be equal if the corresponding non-instantiated tear-offs would be equal and the type arguments are the same. (For instance methods, the tear-offs close over the method declaration, What we currently do is not consistent. See https://dartpad.dev/cab475cfc86179ee10a8373d1f772e1e
Equality is a little more differentiated. The VM has equality of instance method tear-offs, both instantiated and not, if they have the same argument types, method,
That doesn't make sense to me since We can leave equality of instantiated tear-offs as identity (with the current canonicalization rules for top-level functions). |
tl;dr I'd recommend model 1, model 2 is a breaking change rd;lt @leafpetersen wrote:
I agree on using a run-time-entity-only based definition, I do not think it should matter whether they are obtained by evaluating an expression at any particular location in the code (including: in any particular scope). We have three elements:
In @leafpetersen's model 1 equality holds if we have "same code, same receiver (if present), and same generic instantiation (if present)", and, if I understand it correctly, model 2 is "same code, same receiver" (and equality never holds for tearoffs with generic instantiations). I agree that these models are simple and meaningful. However, I believe that we can't easily use model 2, because we have already had identity (hence equality) for constant expressions denoting tearoffs with generic instantiation for a while (in the analyzer, vm, dart2js), and we also had it for instance methods (vm, at least), so it's a breaking change to make them unequal at this point (I believe it does not make sense to preserve identity for two objects that aren't equal).
I believe this would be a breaking change, too. @lrhn wrote:
This is actually the main reason why I was in favor of the equality rules that we currently have in the language specification. Moreover, the natural extension to explicit generic instantiations makes sense for me:
The vm does that (
Noting that this is a breaking change for constant expressions, and for all those cases where the tests in https://dart-review.googlesource.com/c/sdk/+/202243 do succeed.
This is Leaf's model 1, as I see it. LGTM! ;-)
For the static functions, this conflicts with the fact that the vm succeeds at (I haven't included local functions in the tests, they have unspecified equality.)
Oops, I haven't tested that. ;-) |
Sorry, it may not have been clear - I'm not proposing to change canonicalization of constants. That is, we continue to view
I would prefer to make this breaking change over incurring a large implementation cost. So I think this needs to be a discussion with the implementation teams to understand the costs here.
I continue to be confused by the references to scope. Scope doesn't guarantee equality, since the closed over values are determined dynamically, so scope is neither necessary nor sufficient. I think maybe you are using scope in a different sense than I am? |
For local functions, scope is a shorthand that I've so far used to define equality of local function tear-offs from (and quite successfully, since it matches the implementation). That doesn't work for instantiated tear-offs, though, because you can't instantiate a function value. (I guess an instantiated tear-off could just be a closure containing the uninstantiated tear-off and the type arguments, and we should just define equality of those as equality of the uninstantiated tear-off plus equality of the type arguments. Then all we have to discuss is equality of uninstantiated tear-offs. Is this the point everyone's been making and I've been missing?). For the nit-picking: Equality of instance method tear-offs depends on the method itself and the values of |
I think this is a confusing way to think about it. I agree that for declarations, using "dynamic scope" as a proxy for "same allocation" (and hence same closed over values) can be made meaningful (though I think it's very confusing, especially if you don't explicitly say "dynamic"). But for instantiation, we're no longer talking about declarations, we're talking about references, and then if you want to talk about "same dynamic scope" you need to get into very finicky definitions about when two dynamic scopes are the same. This feels unnecessary to me, and confusing.
Yes, this is my underlying semantic model. The questions we are asking here can then just be thought of as:
It's not clear to me that such a method should be viewed as the "same method". In fact, I might argue it should not. |
On Thu, Jul 1, 2021 at 2:55 AM Lasse R.H. Nielsen ***@***.***> wrote:
What I want with the "scope" is that if I do:
void something(Foo foo) async {
x.addListener(foo.onEvent);
await whatever;
x.removeListener(foo.onEvent);
}
then the two foo.onEvent functions should be equal, even if they are
instantiated tear-offs.
I expect users to want and expect that (it's the same function torn off
from the same object in the same way), and it happens right next to each
other.
The "scope" is probably a red herring, just my way of say "right next to
each other".
That said, it's not what we have right now.
My primary wish is to have some kind of *consistency* across tear-offs of
static, instance and local functions, so users can predict when two
tear-offs will be equal.
*We do have the option of never making instantiated tear-offs equal to
each other, no matter what.*
That's almost what we do now - The VM does that, dart2js makes
instantiated top-level/static functions equal if they have the same type
arguments. However, we have explicitly made top-level instantiated
tear-offs with constant type arguments be *constants* and canonicalized,
so the VM needs to do that too.
So, our option is really to make *non-constant* instantiated tear-offs
not equal (which, as you say, just means not overriding Object.==, since
canonicalized ones are already equal by being identical).
*Or we can make instantiated tear-offs be equal if the corresponding
non-instantiated tear-offs would be equal and the type arguments are the
same*.
(For instance methods, the tear-offs close over the method declaration,
this and super bindings. For local functions, they close over the dynamic
scope, including any this or super bindings in instance scopes. In either
case, we *can* choose to not make them close over something they don't
actually use, and not base equality on that, effectively hoisting
declarations to the level where their dependencies begin and use that as
the dynamic scope they close over - which can be top-level if it the
function depends on nothing).
What we currently do is not consistent. See
https://dartpad.dev/cab475cfc86179ee10a8373d1f772e1e
- The VM only makes non-instantiated top-level/static methods and
non-instantiated local function in the same scope identical.
- Dart2js also makes instantiated top-level functions identical, they
cache method tear-offs (effectively canonicalizing them), but not when
going through super,.
You have to be very careful with this kind of test - dart2js will GVN
tear-offs 'getter' calls and the GVN candidates will depend on inlining. I
believe GVN this is why eqg(d.instance, d.instance) shows as identical.
…
Equality is a little more differentiated. The VM has equality of instance
method tear-offs, both instantiated and not, if they have the same argument
types, method, this and super (whether they use the super or not).
Dart2js only has equality for non-instantiated tear-offs, and only
includes super in the equality if the function actually uses it (Generic
x.method/getSuper vs Generic x.method/getSuper2 only differs in that the
"2" version uses super in the torn off method).
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#1712 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAQC3FM5K3AT32VBBO3HMULTVQ3RXANCNFSM47NULRFA>
.
|
I agree, but current implementation doesn't.
is the case where the same mixin is mixed in twice on the same class, then the two different version are torn off (one directly, one using That's why I think equality of instance method tear-offs should be based on the receiver object and the method being torn off, which includes which class it was looked up on (which, on a specific instance, is equivalent with its Unifying methods which don't use |
So, summary:
For the equality/identity of uninstantiated tear-offs:
This is very close to what we currently do. |
@lrhn wrote:
👍 That's exactly what I'd recommend, too. Footnotes: One thing isn't 100% obvious: I believe that when multiple applications of the same mixin gives rise to multiple occurrences of "the same declaration" in a sequence of superclasses, it is necessary to consider them different, because they can have different behavior. (So it's a very subtle source of bugs to have one of them in a data structure, and assume it's the other one, because of a semantically-unjustified For local functions, I think we should allow implementations to lift (so we don't promise that any two tearoffs of a local function are non-identical, hence also not that they are inequal), only that they are identical when it's the same code closing over the same objects. The crucial bit is still
I hope we can get that, but this still needs to be discussed with the tool teams. |
With "binding of I don't think we currently fail to distinguish different mixin applications of the same method where they actually do differ in behavior, but dart2js equates two different mixin application methods which are extensionally equivalent (because they don't rely on |
@lrhn wrote:
OK; in any case, we only have this ambiguity if we consider an instance method declaration in a mixin Then, with a given class as the binding of However, the converse doesn't hold: With a given implementation determined as the pair of a class and the declaration (for instance, So I'd still prefer to talk about the class and the declaration, and never worry about
I don't think we should specify any extra rules about a mixin method whose body uses superinvocations vs. one that doesn't. For instance, it could have a superinvocation on some other method (be it a method from the same mixin, or just any method in the class that we're applying the mixin to), and that could allow us to distinguish the two mixed-in versions of a method. So I'd prefer that we distinguish method implementations based on the pair |
I agree, and It's just that defining it this way will require a change in dart2js. |
The summary seems reasonable for dart2js, pending one question (below). https://dart-review.googlesource.com/c/sdk/+/202243 didn't appear to have any dart2js failures, and the failures on https://dart-review.googlesource.com/c/sdk/+/205081 will be fixed by https://dart-review.googlesource.com/c/sdk/+/205764 as soon as I finish hammering out implementation details with Stephen in the coming week. The question: where can I find the rules for what constant canonicalization considers to be the same type? |
Thanks, @fishythefish! About "same type": Take a look at this section. |
@eernstg The tests for tearoff equality between instantiated tearoffs that have been canonicalized (const or eagerly) by the CFE with tearoffs that are instantiated at runtime is the biggest sticking point for the DDC implementation. For example in an excerpt from your tests: X? genericTopLevelFunction<X>() => null;
int? Function() vIntTopLevelFunction1 = genericTopLevelFunction;
void main() {
<X>() {
X? Function() vXTopLevelFunction1 = genericTopLevelFunction;
checkEqual(vXTopLevelFunction1, vIntTopLevelFunction1);
}<int>();
} This expectation passes in sound mode. Both tearoffs are instantiated with non-nullable DDC currently doesn't have any need for the "same type" test for type objects in our internal type representation. It can be implemented but it will probably be the biggest of all the changes needed to meet the specification. It only appears to make a difference when running with weak null safety. If you are looking for places to alter the spec based on the ease of implementation that would be my vote. Do we need to specify that these two tearoffs are equal in legacy code? Would it be sufficient to specify the equality for them for null safe code only so that we don't need to use the definition of "same type" here? |
@nshahan Perhaps I'm misunderstanding something, but it looks like the "same type" test is just |
DDC handles this when we convert our internal runtime representation of the type to a
I thought about the possibility of using the same code here but didn't really like that idea since the entire conversion process fairly heavy and creates a lot of new objects. It felt like overkill for this purpose but you are right, it could be used until we implement a more efficient version. |
Thanks for the input, @nshahan! @fishythefish wrote:
Yes, cf. the following excerpt from the feature spec section I mentioned earlier:
This just confirms your description, @nshahan: The |
Implementation relying on our existing conversion to the external |
Great, @nshahan! By the way, does this mean that the need for an implementation that works on the internal representation of types is modest, because it would only be required for the execution of mixed version programs (some libraries with null safety, others without), and hence it's transitional? |
@eernstg As for the extent of the need for a more efficient implementation, that is harder to say. I believe the majority of DDC compiled code is still running in mixed mode but that improves every day and like you say will be unsupported at some point in the future. I'm going to run some experiments to try and see the impact of the code reuse marked with a TODO. |
Previously all functions objects returned by `dart.gbind()` would contain the same 3 properties: name, length, and runtimeType. For two distinct functions, if these three properties were the same our constant canonicalization would incorrectly canonicalize to the same value. Attaching the original function and the type arguments used in instantiation ensures the const canonicalization will treat different functions or instantiations as different values. These values will also be used for equality checks proposed here: dart-lang/language#1712 Fixes: #46568 Issue: #46486 Change-Id: I65111df9af80d878eb3127c5e3dfac1ffba95535 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/206423 Commit-Queue: Nicholas Shahan <nshahan@google.com> Reviewed-by: Sigmund Cherem <sigmund@google.com>
Thanks again, @nshahan!
Depending on the meaning of the word conventional, that is not 100% true. Run-time type equality applies NORM to both operands, and this means, for example, that
Sounds good! |
@eernstg I did run into a few more tests for tearoff equality that are still failing on DDC with my work in progress changes. Are these part of the proposed specification as well? language/generic_methods/explicit_instantiated_tearoff_test DDC is not handling the equality of extension methods torn off of object instances at all. We would need some significant changes to our representation for these tearoffs, possibly by opting out of the CFE lowering and handling ourselves (I think would need implementation from the CFE to do so) or by maybe recognizing the lowered nodes in some way and handling them differently than normal static method tearoffs. language/typedef/aliased_constructor_tear_off_test There are sections at the ends of these tests that expect not equal or not identical for the tearoffs instantiated at runtime. DDC is failing some (reporting they are equal or identical) and I'm curious if I should spend some time investigating now? |
The test language/specification/dartLangSpec.tex Line 6491 in 155b28a
Luckily, no implementations seem to reach that point (they fail at an expectation before that line), so it is probably OK to make this adjustment at this point. Consequently the following would not be a problem:
They are just unequal unless it's the same object, and that holds for the generic instantiations as well. Cf. https://dart-review.googlesource.com/c/sdk/+/208645.
Dinnertime, I'll return to these a bit later. ;-) |
@leafpetersen wrote:
The tests in https://dart-review.googlesource.com/c/sdk/+/202243 (landed) do have constant expressions where The same is true for https://dart-review.googlesource.com/c/sdk/+/205081 (not yet landed). The test in https://dart-review.googlesource.com/c/sdk/+/208645 contains a set of constant variables (most of them named So the answer would be that we do not (in any case at all) perform a compile-time equality test, but we do perform tests for |
@nshahan wrote (note that the file links are links to specific ranges of lines):
@lrhn, I'd expect that all the above-mentioned tests in @nshahan, for the tests in |
@lrhn, I made the changes suggested above in https://dart-review.googlesource.com/c/sdk/+/208658. |
We decided to implement the equality expected by https://dart-review.googlesource.com/c/sdk/+/205081; the implementation effort is tracked in dart-lang/sdk#46834. |
At the language team meeting Jun 23rd we decided that constant expressions yielding a function object should be canonicalized (as they already are in almost all cases), but we postponed the question about equality among function objects obtained by evaluation of expressions that are not constant.
We have specified that function objects are equal (according to
operator ==
) when they are obtained by closurization or generic function instantiation of the same function with the same type arguments. (Of course, for function literals and tearoffs of local functions, the equality only holds when the function objects have the same context.) In this case the type arguments can be arbitrary types, and we may not know their value until run time; this implies that==
must take the actual type arguments into account, if any.The implementations already do this in most cases, and this issue is concerned with the question about whether they should do so in all cases.
Based on the tests in https://dart-review.googlesource.com/c/sdk/+/202243/1, here are the cases where implementations do not consider function objects to be equal, even though they should do so according to the specification:
The rough summary of this behavior is that the implementations in some cases fail to recognize equality among function objects that are obtained by generic function instantiation, especially when the type arguments are not constant.
So do we wish to get the specified behavior implemented (that is: do we wish to make all the above tests succeed), or do we wish to change the specification to say that the above, or some subset thereof, is actually the behavior that we should have?
@munificent, @lrhn, @natebosch, @leafpetersen, @stereotype441, @jakemac53, WDYT?
The text was updated successfully, but these errors were encountered: