-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 making strong mode read check less eager #31391
Comments
Yes, I think this would be a fairly easy tweak to implement. Note (from our in-person discussion): your proposed fix would only address test2. To address test1, we would need a further rule saying that the implied read check is never inserted when the surrounding context is an explicit "as" check. That should also be fairly easy to implement. |
It's a change in strategy. Currently we are saying that the expression The rule, as I read it, is to treat unsafe return values (contravariant ones) as just unsafe, like it's being I'm not sure I can find a way to abuse this (but I'll try!) List<Object> x = <int>[1];
void Function(double) f = (num x) { print(x); }
f(x[0]); /// static type :(void Function(double))(Object) This is unsafe. We make it "safe" by adding a downcast It feels like what you are doing here is similar to that, you are avoiding an (otherwise reasonable) runtime check for a statically unsafe operation, and allowing it to succeed anyway if the runtime types work out. It's different from my example in that you only use static type information, so maybe it's better. We can still get into problems like: Model<A> m = new Model<B>();
var c = m.callback; // c gets static type void Function(A), actual type is void Function(B). Here we throw because the static type become the inferred type of the variable, and is not satisfied, because |
If However, we currently type it as The proposal here would be to note that we have a sequence of several dynamic checks back-to-back in some situations, and only a proper subset of them (typically just the last one) are required for heap soundness (basically because we can accept that a compiler-generated intermediate variable has a different type than the "naive" statically known type of the expression whose value is stored in it), so we can simply omit all other dynamic checks from that back-to-back sequence. The extra issue that comes to mind here is that the overly strict caller-side check for But it seems like a rather ad-hoc improvement of the situation that we avoid these unjustified run-time errors only in the situations where we happen to have a back-to-back sequence of dynamic checks. So, as mentioned, I'd prefer to type contravariant expressions soundly in the first place, and also to generate dynamic checks that are justified by soundness requirements, and not just insert an overly strict dynamic check because we ignore a statically known kind of variance and we don't want to look up the real requirement for that dynamic check. |
It's not unreasonable but it feels like spooky magic to me. A couple of thoughts:
If there are some very compelling use cases where clearly idiomatic code is getting stuck by this, I can see it being worth doing. But otherwise, my intuition is that we should spend our cleverness budget elsewhere. |
But that's a misconcept in the first place: We should assign a sound type to each expression, rather than giving it some type which may or may not be justified by the semantics of the language, and then throwing if it happens to not have that type. Otherwise we could just as well decide arbitrarily that all expressions have type |
Another option is to make
When we added the callee-side check in DDC, we did hit runtime errors - and they're rather confusing to users. |
Another internal user just ran into this. Abstractly speaking, their code looks like: class A<T> {
void foo(T a) {...}
T x;
}
class B<T> {
void apply(A<T> a) {
a.foo(a.x);
}
}
void main {
new B<dynamic>().apply(new A<int>());
} |
Our covariant generics are unsound. The big question is when we should err because of that unsoundness. We are currently going with "expression safety" where the runtime type is a valid subtype of the static type, and the static type considers type arguments as exact types. Erik is proposing to consider a static type of Our type system doesn't handle that kind of reasoning now, but it could (although I doubt we can do that on short notice). I'm open to both options, but don't see the latter as practically possible. It complicates the type system more than we can afford at this point, without getting much more than a few omitted down-casts of known unsafe expressions. |
Sorry, definitely not something I want to take up now - I just was noting that this had come up again to remind myself for posterity. Briefly to your comments @lrhn: I don't actually see this as a really fundamental change. If you think of our covariant generics as something like I'm also actually quite interested in thinking about a broader approach around internalizing some notion of wildcard types so that we could expose to the user the fact that these types are vaguer than expected. We have other use cases for something like this, so if we could come up with a unified story around this, it could be quite nice. Future work though. |
Indeed, or I understand that it may not be possible to start using such an approach overnight, but (1) I believe that "contravariant" and "wildly variant" expressions are pretty rare, and (2) if we make this switch from using read checks to using static types that are known to hold then it basically means that some expressions which are silently unsafe today (because they may fail during a read check) will be transformed into regular downcasts. They will then be silently unsafe by default during data flow, but So even though there may be some breakage, I think it is likely to be small, and I think the required changes will be helpful for code comprehensibility. |
Sorry, I forgot to reload before responding, so I didn't see your response, Leaf.
The reason why I think it matters is that we are comparing an approach
with another approach
Whenever |
There is an implied read check on the return value of getters or methods on generic classes when the return type contains a contra-variant occurrence of an unsound generic type parameter. The check takes the form of a cast to the type implied by the static type of the target of the read. That is, if
x
isC<int>
, andC<int>.p
has some typeF
, thenx.p
is rewritten tox.p as F
. This is currently done even if the use ofx.p
is at a different type. Consider this example:In test1, the callback is immediately cast to
Object
which should always succeed, but the implied read check causes a cast failure on the second invocation before the cast toObject
can happen.In test2, the callback is immediately cast to
Callback<B>
, which would succeed for both calls, but again, the implied read check cast causes a cast failure on the second invocation.It seems reasonable to me to fix this by saying that the implied read check takes the form of a cast to
T
, whereT
is the surrounding context type if there is one, and otherwise is the type implied by the static type of the target (as is currently done).@stereotype441 Would this be just a small tweak to the implementation, or would it require more work?
@lrhn @floitschG @eernstg @munificent @anders-sandholm @vsmenon WDYT?
The text was updated successfully, but these errors were encountered: