-
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
Don't invent a default value when no well-defined value is available #3331
Comments
I'd just not do anything. It's weird, and technically a way to distinguish whether a function is invoked with or without an argument (since they cannot pass If we could fix dart2js to not throw, then this is adequate behavior. I'm more worried about the ability to extract the declared default value of an interface method, when there is one. |
That's definitely a less breaking way ahead. But do you really think we're helping anybody by adopting such a just-keep-on-truckin' approach, effectively pretending that the invocation of Sure, it is not technically unsound because the typing of But for all invocations of noSuchMethod that are going via a noSuchMethod forwarder, we will have the property that the actual argument list is one of those that the actual member signature requires (except for exactly the situation where null is passed, even though that's a violation of the parameter type of the forwarder).
You could also say that since dart2js throws, the breakage caused by raising an error is already known to be limited. ;-)
Presumably that would be massively breaking, because lots of existing tests rely on mock objects with default values as declared for the mocked signatures.
True, but they can have default values, and I actually think it's a pretty rare case where the default values are omitted. After all, it wasn't even possible a few years ago, and I've heard comments like "Can you do that?!" many times. Anyway, it would make a lot of sense to have those default values if every implementation of said abstract declaration uses that same default value, and it might be necessary in order to get a useful behavior from mocks. |
Apparently, this has never been a problem for anyone. Either they all have default values where they need it, or they don't, and they don't mind the We have to define the signature and behavior of the Currently it probably says "same signature as the interface member", which isn't necessarily valid for a concrete method. Let's be precise about what we have today.
Method signatures can carry default values. We derive a method member signature from a method declaration by erasing some things, and possibly inserting an explicit So we are saying the in this example, We can say anything we want here, and it becomes the law.
The first two are both consistent and matches current behavior. I'm wary about causing runtime errors in Passing We could also just say that default values are not part of method signatures, since this is all they're used for. |
"OK, we have a bug? It was never reported? Very good, let's keep it". 😄 Seriously, I do recognize that it could be a breaking bug fix to do as I suggested, because the cases where a default value is passed as null even though that's a violation of the parameter type could occur in a test that appears to be working today. However, it would only be working on the VM, not with dart2js. Also note that the VM throws in the case where null is passed explicitly at the call site. In other words, this behavior allows the caller to determine whether or not an actual argument was passed: If it's null, the actual argument was definitely omitted; if it's not null, the argument was definitely provided. Even in the case where we wish to support the ability to test whether a given optional parameter was passed or not passed, we probably don't want to start by making that feature available via
Certainly!
This is true for the current language specification and also for the version (not yet landed, hint hint ;-) which has been updated to include null safety. As you mention:
The interface of C does have such a member signature because it's a compile-time error for C to have a signature conflict and not resolve it.
The null safety update does specify that the default values are omitted from member signatures.
This is true for the current language specification. It is not true for the null safety update. This change was required because it is not an error to have multiple superinterfaces containing a member signature with the same name and specifying different default values for optional formal parameters that correspond to each other. (Bad style, but not an error.) So method signatures can't carry default values any more. Both the current language specification and the language specification with null safety have no references to default values in the section about combined member signatures.
With null safety, it does not take the first signature, it uses This change was introduced specifically in order to avoid the accidental nature of a conflict resolution which relied on the textual order of superinterface declarations.
Agreed, we can do either of those two things, I just think it's a bug to use a value which (1) has no relation to the associated declarations, it's invented completely out of the blue, and (2) it would be a violation of soundness to pass null as the actual argument because the parameter type doesn't include null.
This thread is concerned with invocations of noSuchMethod forwarders. A dynamic call does not invoke a noSuchMethod forwarder with too few or too many arguments. Just like a dynamic invocation of I don't see how this could be relevant to a regular (non-dynamic) method invocation on a mock object (or whatever it is that has a non-trivial
This is already true for the null safety spec. However, I've referred to the underlying declaration all the time exactly because default values aren't part of the member signature. This treatment is required in order to allow default values to be declared inconsistently, e.g., different superinterfaces specify different values. I'd very much prefer to have a lint against that, but the language says that it must be possible to do it. So my proposal is that we determine what the default value is, when defined consistently, and we detect ambiguities and handle that case separately (multiple declarations, not the same value), and we also detect the case where there is no default value at all and handle that case separately as well.
I think this would be a huge deterioration in the quality of service delivered to users of mocks. Why would we do that? |
OK, we have several options. The current specification is under-specified - it says "use default value", but doesn't cover case where there is no default value. That makes the current behavior accidental, and we should decide which behavior we want. The current behavior (dart2js and VM both, don't know why I thought VM did something else):
It's a possible behavior, but not one I'd prefer, because it's a runtime error from a well-typed, typed invocation, which happens before the user The options I see are:
All of these will give a specified behavior. The fact that nSM forwarders today throw if called, suggests that they are not actually called like that. The current behavior is not important, so changing it should be fine. Switching to number 3 (pass Number 2 may confuse existing code which assumes that if I think one of the reasons this hasn't affected mocks yet, is that generated mocks tend to make parameters nullable. The things about any of these changes, except number 5, that can worry me, is that they'll allow a nSM implementation to distinguish passing an argument from not passing an argument. For number 3, only for parameters with no default value. It's not much, you can get a similar effect by having a private subclass which changes the parameter to nullable, and then only expose it at the superclass type, where the parameter is not nullable. People will have to do dynamic invocations with (Heck, even allowing you to widen the type of the local variable introduced by the parameter would allow that, like My vote is on option 3. |
That's a great analysis, @lrhn! As usual, I'd prefer to detect and report problems at compile time whenever possible, that is, option 5. It could be a lint, if we expect that a compile-time error would break a lot of code that currently seems to work. Option 1, raise a run-time error if the missing default value is needed, does report the situation. So we could use that as a fallback if we implement option 5 as a lint. I think option 2 and 4 will break a lot of code (tests, presumably), because existing code expects to receive the actual default values in a lot of simple cases (just one default value declared, and that's what we get): With option 2 the existing code could break at run time because the list Option 3, use null if the missing default value is needed, is the behavior that I can see with the current VM (but not with other backends). I think this is confusing because it violates a rule that we are otherwise maintaining: If If we adopt option 3 then we need to say something like "... and that forwarding method doesn't just forward what it gets: it will pass null in the |
Note that forwarding functions would make the situation described in this issue a non-problem: The default values of the forwardee are allowed to have types that could not be used for the corresponding default values in the forwarder. |
Cf. #2269 (comment).
Consider the following example (a variant of the example in the issue mentioned above):
This program has no compile-time errors (that's fine, none were expected), but the run-time behavior depends on the configuration:
dart
runs the program and prints 'null', but DartPad anddart compile js
generate code where the execution stops before theprint
expression because null is not a type correct actual argument when the parameter has typeint
.First, please make a clear distinction:
C.foo.x
has no default value. If the declared type had beenint?
then it would have had the default value null. If it had been declared with an explicit default valued
then it would have had the default valued
. In other words, an implicit null is just as much a default value as an explicitly declared default value, but it is possible for an optional parameter (of an abstract declaration) to have no default value at all.I'd recommend that we specify a dynamic error as follows:
Assume that a class
C
has an implicitly induced noSuchMethod forwarder D for a method namedm
(because it is concrete and has a non-trivialnoSuchMethod
implementation). Such noSuchMethod forwarders can be inherited, too, but we only need to specify the case where the forwarder is introduced. We could also have a noSuchMethod thrower, but they are not relevant for this issue.Assume that D is invoked, and no actual argument is provided for a given optional formal parameter
p
(which could be positional or named). A dynamic error will then occur in the case where the default value ofp
is underspecified:p
has no default value becauseC
contains an abstract declaration ofm
where no default value is specified forp
, orm
is in the interface ofC
because one or more direct superinterfaces have a member namedm
, and the most specific underlying declaration does not declare a default value forp
for any of them.p
has an ambiguous default value because multiple direct superinterfaces ofC
have a member namedm
whose most specific underlying declaration(s) have multiple non-identical default values forp
.The default value can always be made available and unambiguous by putting an abstract declaration of
m
intoC
, specifying the desired default value. This implies, for example, that it is possible to resolve missing or ambiguous default values in superinterfaces without writing an actual method implementation. This could be useful in the declaration of a mock class.This approach could be described as "fail late". We could also make it a compile-time error in every case where a noSuchmethod forwarder is being implicitly induced, and one or more optional parameters do not have a well-defined default value; that could be described as "fail early".
In both cases the change would be at least somewhat breaking: The current behavior in the VM allows
noSuchMethod
to be invoked with null as an actual argument, even in cases where that would be a soundness violation in an explicit declaration of the forwarded method, but with this change it would become a dynamic error, or even a compile-time error.With an ambiguous value, the VM currently makes a choice (perhaps using the value from the textually first superinterface that specifies a default value), which may or may not be the "right" choice. It is again a breaking change to start raising a dynamic error in the ambiguous case.
@dart-lang/language-team, WDYT? Do we need to change the current behavior? Fail early or late?
The text was updated successfully, but these errors were encountered: