Skip to content
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

[SR-8516] NSNumber.init returns instance of NSConcreteValue #51036

Open
zienag opened this issue Aug 10, 2018 · 9 comments
Open

[SR-8516] NSNumber.init returns instance of NSConcreteValue #51036

zienag opened this issue Aug 10, 2018 · 9 comments
Labels
access control Feature → modifiers: Access control and access levels bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself regression swift 4.2

Comments

@zienag
Copy link

zienag commented Aug 10, 2018

Previous ID SR-8516
Radar rdar://problem/43142446
Original Reporter @zienag
Type Bug
Environment

Xcode Version 10.0 beta 5 (10L221o)

Additional Detail from JIRA
Votes 2
Component/s Compiler
Labels Bug, 4.2Regression
Assignee None
Priority Medium

md5: ddbc0a221a0bcf1304753842fa034776

is duplicated by:

  • SR-8534 Crash with NSNumber.init + TimeInterval

Issue Description:

Encountered bug with unrecognised selector exception. After digging it down, it appeared that there was NSConcreteValue in swift array of NSNumbers.
Code to reproduce:

_ = [0, 0.2].map(NSNumber.init).map { $0.doubleValue }

It throws:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSConcreteValue doubleValue]: unrecognized selector sent to instance 0x102a5e320'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff2b8d82db __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x00007fff52a82c76 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff2b970db4 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   CoreFoundation                      0x00007fff2b84e820 ___forwarding___ + 1456
    4   CoreFoundation                      0x00007fff2b84e1e8 _CF_forwarding_prep_0 + 120
    5   test                                0x0000000100002030 $S4testSdSo8NSNumberCXEfU_ + 32
    6   test                                0x000000010000205d $SSo8NSNumberCSds5Error_pIggdzo_ABSdsAC_pIegnrzo_TR + 29
    7   test                                0x00000001000020fb $SSo8NSNumberCSds5Error_pIggdzo_ABSdsAC_pIegnrzo_TRTA + 27
    8   test                                0x0000000100079b0f $SSlsE3mapySayqd__Gqd__7ElementQzKXEKlF + 527
    9   test                                0x00000001000018e1 main + 641
    10  libdyld.dylib                       0x00007fff5369c015 start + 1
    11  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Workarounds:

// put .0 to indicate that it is a double literal.
_ = [0.0, 0.2].map(NSNumber.init).map { $0.doubleValue }
// using closure and call initializer inside
_ = [0, 0.2].map { NSNumber(value: $0) }.map { $0.doubleValue }
// explicitly marking type of a function
_ = [0, 0.2].map(NSNumber.init as (Double) -> NSNumber).map { $0.doubleValue }
// extracting array to let constant
let array = [0, 0.2]
_ = array.map(NSNumber.init).map { $0.doubleValue }
@belkadan
Copy link
Contributor

@swift-ci create

@phausler
Copy link
Member

There are two "bugs" here:

one is an ambiguous reference created by the code as written; specifically the call to

NSNumber.init

instead of the qualified

NSNumber.init(value:)

. The other side of it is the inference of the array literal type. If the array contains a Double it should all be interpreted as a Double.

_ = [0, 0.2].map(NSNumber.init(value:)).map { $0.doubleValue }

@phausler
Copy link
Member

The unrecognized selector is the map calling NSNumber() which is invalid and produces a nonsensical result. Not sure why map calls that... which seems like a really gnarly bug.

@belkadan
Copy link
Contributor

Ugh, it's specifically picking +[NSValue valueWithNonretainedObject:], which is imported as NSValue.init(nonRetainedObject:). That shouldn't be an inheritable initializer, though, so we definitely have a compiler bug here.

@phausler
Copy link
Member

Sadly NSNumber and NSValue have some complicated history in the regards to their initializers but that failure is pretty awful, @belkadan please feel free to re-assign that radar appropriately to a compiler bug.

@zienag
Copy link
Author

zienag commented Aug 13, 2018

"it's specifically picking +[NSValue valueWithNonretainedObject:]"
That's interesting. I'm just curious, why it is showed up only in last version of swift? That code behaves correctly in earlier versions. Also, if array is extracted to let constant it works too. Why it picks correct initializer in that case?

@belkadan
Copy link
Contributor

I'm not sure what changed from Swift 4.1, but that's important information, so thank you.

Separating the array to a constant means deciding on a type for the array first, without looking at how it's going to be used. In that case, the only information the compiler has is that the elements are 0 and 0.2, and therefore it decides the best type is Array<Double>. When it's all in a single expression, though, the compiler knows that it's going to be passed to an NSNumber initializer, though it doesn't know which one. We're not sure yet why it decided that the initializer that takes Any was better than the one that takes Double, but it did.

@zienag
Copy link
Author

zienag commented Aug 13, 2018

Hmm, I see. I think a figured out why it worked in previous versions of swift. Code

_ = NSNumber.init(nonretainedObject: nil).doubleValue

doesn't compile in swift 4.1 with error

 argument labels '(nonretainedObject:)' do not match any available overloads 

and compiles successfully in 4.2 (of course it fails at runtime). So it seems like in swift 4.1 compiler doesn't have any choice other than correct initializer.
Also I checked behaviour of such code:

func f(_ val: Double) { print("double") }
func f(_ val: Any) { print("any") }
[0, 0.2].forEach(f) 

And it prints any in both swift 4.1 and 4.2.

@zienag
Copy link
Author

zienag commented Feb 4, 2020

There is another interesting case with nil-coalescing operator

_ = ("1" as String?).map { Float($0) ?? 0 }.map(NSNumber.init).map { $0.doubleValue }

btw, this bug can be exceptionally hard to debug 🙂 Initial case that I reported almost two years ago was met in following circumstances: I had an animation with key times:

let fullDuration: TimeInterval = ...
let firstStepDuration: TimeInterval = ...
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.transform))
animation.keyTimes = [0, firstStepDuration / fullDuration, 1].map(NSNumber.init) // <- it is array of NSValues, not NSNumbers, but we don't know it yet
....
layer.add(animation, forKey: nil)

And of course, given forgiving nature of objc, that code doesn't crash, but it crashes when this animation is actually applying, in another runloop without any traces for above code. To debug this, I wrote down every memory address of created animations, had to inspect and debug assembly of core animation code to find out which animation had wrong values, and then I've found a match with above code.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@AnthonyLatsis AnthonyLatsis added regression swift 4.2 access control Feature → modifiers: Access control and access levels and removed 4.2 regression labels Nov 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
access control Feature → modifiers: Access control and access levels bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself regression swift 4.2
Projects
None yet
Development

No branches or pull requests

4 participants