swiftnsdecimalnumberxcode16

`NSDecimalString` crashes Xcode 16


I have a large project that is crashing the compiler in Xcode 16. After doing a lot of digging, I have reduced the problem to the use of NSDecimalString to generate a non-localized string from a Decimal type. Here is a simplified example:

var decimal: Decimal = …
let string = NSDecimalString(&decimal, Locale(identifier: "en_US_POSIX"))

The intent in the above is to generate an locale-invariant (i.e., non-localized) string.

That worked in Xcode 15.x, but in Xcode 16, the compiler crashes (both with “Swift Language Version” of 5.10 or 6.0):

Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.

…

1.  Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)
2.  Compiling with the current language version
3.  While evaluating request ExecuteSILPipelineRequest(Run pipelines { Mandatory Diagnostic Passes + Enabling Optimization Passes } on SIL for MyApp)
4.  While running pass #287 SILModuleTransform "MandatorySILLinker".
5.  While deserializing SIL function "NSDecimalString"
6.  *** DESERIALIZATION FAILURE ***
*** If any module named here was modified in the SDK, please delete the ***
*** new swiftmodule files from the SDK and keep only swiftinterfaces.   ***
module 'Foundation', builder version '6.0(5.10)/Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)', built from swiftinterface, resilient, loaded from '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Foundation.swiftmodule/arm64e-apple-macos.swiftmodule'
SILFunction type mismatch for 'NSDecimalString': '$@convention(c) (UnsafePointer<Decimal>, Optional<AnyObject>) -> @autoreleased Optional<NSString>' != '$@convention(c) (UnsafePointer<Decimal>, Optional<AnyObject>) -> @autoreleased NSString'

It looks like there is some Foundation inconsistency related to whether NSDecimalString returns an optional or not. (The docs say it is not optional, but clearly there is something internally that assumes it was.) This problem happens in Xcode 16 (16A242), whether Swift 5.10 or 6.0 and for both iOS or macOS targets.

How can I avoid this?


Solution

  • I have filed a bug report, but my solution for now is to simply avoid NSDecimalString.

    There are a variety of ways of generating a locale-invariant strings for Decimal:

    1. You can just use string interpolation of the Decimal:

      let value: Decimal = …
      let string = "\(value)"
      

      The default string interpolation of a Decimal is non-localized.

    2. The above works, but it relies on the default localization behavior of Decimal string interpolation, so that might not be entirely prudent (in the absence of formal assurances). So, perhaps it is safer is to specify the format style directly with formatted:

      let format = Decimal.FormatStyle(locale: Locale(identifier: "en_US_POSIX"))
      let string = value.formatted(format)
      
    3. Alternatively, you can use a NumberFormatter. For example, I might add formatter support for string interpolation of Decimal:

      extension String.StringInterpolation {
          mutating func appendInterpolation(_ value: Decimal, using formatter: NumberFormatter) {
              if let formattedValue = formatter.string(from: value as NSDecimalNumber) {
                  appendLiteral(formattedValue)
              }
          }
      }
      

      And then you can do things like:

      let formatter = NumberFormatter()
      formatter.locale = Locale(identifier: "en_US_POSIX")
      formatter.numberStyle = .decimal
      
      let string = "\(decimal, using: formatter)"
      

      I jumped to this solution initially, but option 2, above, is probably cleaner.

      (As an aside, I am not generally a fan of the Decimal.formatted functions for localized strings because they apply the default localization for your locale, but do not factor in the number format override that a user may have chosen for their device: For this locale-invariant string, this “feature” is not a problem, but in general, I do not think that formatted is prudent for generating localized strings on the user’s device.)

    4. For what it is worth, one can make the above string interpolation extension a generic:

      extension String.StringInterpolation {
          mutating func appendInterpolation<T: Numeric>(_ value: T, using formatter: NumberFormatter) {
              if let formattedValue = formatter.string(for: value) {
                  appendLiteral(formattedValue)
              }
          }
      }
      

      This way, this NumberFormatter-based string interpolation works for all Numeric types.