Uh oh!
There was an error while loading. Please reload this page.
[pigeon]fix "as Any" workaround due to nested optional - #3658
Conversation
hellohuanlin
commented
Apr 6, 2023
I probably also need to fix tests other than the |
stuartmorgan-g
commented
Apr 7, 2023
Thanks for digging into this!
We just did a bunch of this the last time we found a case where By keeping both flavors of
This seems like it would be straightforward since there's a well-defined boundary. It's there a downside to doing that?
Are we losing semantic meaning though? It's explicitly documented that I'm still struggling to understand what the semantic difference between |
stuartmorgan-g
commented
Apr 7, 2023
Alternately, maybe our purge of I don't fully understand what exactly the criteria are for implicit creation of nested optionals. |
I would be interested to learn about that case.
I don't think there's any downside, other than the optionality becomes implicit.
The criteria are (1) passing an For example, this short code snippet: So, solution 1 avoids (1) an |
Here's another example that may help better understand why the wrapping: Similarly, the The section "How did we end up with Any??" in PR description may also be helpful. |
My initial take away from this note is that Apple discourages using (Though this is just my initial thought. I can do more research on this) |
stuartmorgan-g
commented
Apr 7, 2023
Thanks, this example makes sense to me.
That's... ugh. I guess I can see how the chain of other decisions leads to this but it seems really problematic, because it's consistent in some ways, but very inconsistent in other ways. The I'd be very curious to talk to Swift developers to understand why they made
Right, I had read that, but without the foundation of the simpler cases above I was not following why that chain caused an |
stuartmorgan-g
commented
Apr 7, 2023
That seems reasonable. So if we want to go that route, we need to sweep the entire generator and undo a bunch of the changes from #3284 that were made based on my conclusion in #3284 (comment). It seems likely that all of our use of |
hellohuanlin
commented
Apr 7, 2023
My guess is that the optional wrapping is done at compile time. The compiler simply checks the static type of the argument vs the parameter, and insert the wrapping instruction there if needed. So at this moment dynamic type is unknown. They could have also added some extra logic specific about
Yeah I'm also curious if Apple regrets this decision. I was surprised to learn the different behavior between Swift and Kotlin.
True. Yeah wasn't clear there.
Yeah. I think this PR cleans up some of them already. I can also take a look at what else is missing. |
Thinking about it again, likely it all starts from the decision to use I can see why they want to use |
stuartmorgan-g
commented
Apr 7, 2023
From skimming the generated output here, it looks like collection fields are still wrong, and if Obj-C unknown types are in fact mapped to |
stuartmorgan-g
commented
Apr 18, 2023
@hellohuanlin Did you still want to move forward with this? |
hellohuanlin
commented
Apr 18, 2023
@stuartmorgan yes. I plan to work on it this week. |
This means that there will unfortunately be a breaking change - a raw dart I think it's fine, because most people will likely explicitly write
To clarify a bit - since objc |
stuartmorgan-g
commented
Apr 26, 2023
We are generally pretty relaxed about making breaking changes (as long as they affect compilation, not silently change runtime behavior) for Pigeon, since it would only show up for plugin developers when they are actively updating and re-running Pigeon generation. That's why Pigeon's major version is so high already. As long as we're confident that we're changing to a better reflection of the types we want to express, it's fine to do a breaking change for. |
I just found another interesting bug related to implicit optional (which does not cause "nested optional", but will still be fixed by this PR): This is supposed to crash if The correct codegen should instead be: This also means that the "Solution 2" in my PR description is problematic - it only fixes the nested optional issue, but it won't fix the issue above that silences the type mismatch (updating PR description to reflect that) |
c53db71 to
326d5c1CompareThere was a problem hiding this comment.
This is to fix a separate bug that silences type mismatch. more details: #3658 (comment)
There was a problem hiding this comment.
This else block (and the corresponding else in Line 753) is not used, since the caller only call this function with List/Map types:
if (type.baseName == 'List' || type.baseName == 'Map') {
return _swiftTypeForBuiltinGenericDartType(type);
}
I would either write an assert, or split it into 2 separate helpers (for list and map). But for now I'll leave it here to keep this PR focused (I can update it in separate PR)
There was a problem hiding this comment.
args.first is another nested optional (Any??) - outer nil represents "there's no first element, or list is empty", and inner nil represents "there is first element, but it is nil".
When passing it to nilOrValue, it gets coerced into Any?, hence the compiler warning in Xcode (Bonus: we should make this an error instead of warning, will do in separate PR).
There was a problem hiding this comment.
This is very strange, I would expect the old code and the new code to behave in the same way.
There was a problem hiding this comment.
The old code worked because it has "as Any" intermediate cast inside nilOrValue function. The new code here avoids implicitly optional completely, so no need the hack anymore.
b87d798 to
89dca42Compare
The problem
This PR fixes a weird casting behavior discussed here:
Without this intermediate
as Anycast, these 3 tests would crash withSIGABRT: Could not cast value of type 'Swift.Optional<Any>' (0x7ff865e84b38) to 'Swift.String' (0x7ff865e7de08).Investigation
The crash happens because
valuehere is actually of typeAny??(nested optional!). When it crashes, the debugger simply showsvalueisnil. But if we print inlldb, thevaluehere is actually an innerOptional.nonecase nested by an outerOptional.somecase.Why does
Any??crashSince outer case is
some, it fails to force cast toT?(e.g.String?) due to type mismatch.How did we end up with
Any??It's related to the signature of these 3 functions:
func toList() -> [Any?]func fromList(args: [Any])func nilOrValue<T>(_ value: Any?) -> T?Firstly
toListreturnsnil(of typeAny?) as the first element of array. Then the type gets coerced as anAnytype infromList. Then becausenilOrValuetakesAny?, thisnilvalue gets wrapped by anOptional.some. Hence the nested optional.Workarounds
Workaround 1:
as AnyThis is the current code in this PR. When casting
Optional.some(nil) as Any, it erases the outer Optional, so no problem casting toT?.Workaround 2:
Handle with nested optional directly:
A similar version of this was also attempted in this PR. It just that we did not know why that worked previously, and now we know!
Workaround 3
Casting value to nested optional (
T??), then stripe the outer optionalSolutions
These above workarounds handle nested optionals. However, a real solution should prevent nested optionals from happening in the first place, since they are so tricky.
Solution 1 (This PR)
The nested optional happens when we do cast from
Any?toAnyand then wrapped intoAny?. (Refer to "How did we end up with Any??" section).So the easiest way is just to use
func fromList(args: [Any?])to match the types offunc toListandfunc nilOrValue.Solution 2
Solution 2 is the opposite - avoid using
Any?as much as possible.Drawbacks compare to Solution 1:
a. When inter-op with ObjC,
nullable idis exported asAny?. So we can't 100% preventAny?usage. Though this can be addressed by immediately cast it toAny.b. Losing of semantic meaning of
Any?that itcanmust be optional. The hidden/implicit optional is the culprit here in the first place.c. While this solution fixes the nested optional issue, it does not address all issues related to implicit optional. For example:
https://github.com/flutter/packages/blob/c53db71f496b436e48629a8f3e4152c48e63cd66/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L563-L564
This is supposed to crash if
args[0]isnil. However, the crash is silenced becauseas! [Any]will makeargs[0]an implicit optional!The correct codegen should instead be:
Solution 3
Just remove
as Anyand update the test.The nested optional won't happen in production code, because ObjC
NSArraycontainsNSNullrather thannilwhen exporting to Swift.We can simply fix the tests by replacing
nils withNSNulls.However, if we were to re-write engine's codec to Swift, it's actually better practice to use
niland notNSNullin the array.Additional TODO
We would've caught this earlier if this were an error rather than warning in our unit test.
List which issues are fixed by this PR. You must list at least one issue.
#3545 (comment)
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
dart format.)[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style.///).If you need help, consider asking for advice on the #hackers-new channel on Discord.