Extracting expression to variable assignment changes its value?

I was debugging some Swift code I had just written, and found what appeared to me as unusual behaviour from the nil-coalescing operator. Simplified, my code looked like:

class A {}
let null = A()
// ...
var value: A = null
if foo {
    value = someDict[key] ?? someDict[otherKey] ?? null
} else {
    value = someDict[key] ?? null
}
otherDict[otherKey] = value

The curious part is that value appears as the null value (at least in the debugger), even when someDict[key] is non-nil. What's more, when I modified it to this:

class A {}
let null = A()
// ...
var value: A = null
if foo {
    let intermediate = someDict[key] 
    value = intermediate ?? someDict[otherKey] ?? null
} else {
    let intermediate = someDict[key] 
    value = intermediate ?? null
}
otherDict[otherKey] = value

value now correctly takes the value of someDict[key] when someDict[key] is non-nil.

Is it even possible that this is expected behaviour and not a compiler or LLDB bug? I'm stumped.

In case it makes a difference, here is the actual snippet in question (RT_Object is a reference type):

var argumentValue: RT_Object = null
if index == 0 {
    let a = actualArguments[ParameterInfo(parameter.uri)]
    argumentValue =
        a ??
        actualArguments[ParameterInfo(.direct)] ??
        null
} else {
    let a = actualArguments[ParameterInfo(parameter.uri)]
    argumentValue = a ?? null
}

builtin[variable: argument] = argumentValue

If I access the dictionary inline instead of using the let a line, then argumentValue ends up being null even when actualArguments[ParameterInfo(parameter.uri)] != nil (according to the debugger).

  1. Reproducible example would be helpful.
  2. Why is argumentValue / value a var with an initial value instead of a let without one?

Do you have a complete code example that demonstrates the problem (i.e. one that I can copy-paste and run)? I couldn't reproduce the behavior you're describing with your code snippet above, but I had to guess what some of the missing code looks like.

One potentially surprising aspect of ?? is that there are actually two overloads of it:

// This one returns `T`
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T

// This one returns `T?`
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T?

Overload resolution will pick the second overload of ?? if the first one will not work, or if the second overload will result in the expression having a better "score" even if there may be implicit optional promotions. Unfortunately, implicit optional promotions with ?? can lead to really unexpected behavior.

The difference between assigning the result to value / argumentValue and assigning it to a plain let constant with no type annotation is that the type of the variable provides context for the result type of ??, which can impact overload resolution of ?? and lead to behavior changes if you factor out part of the code into an intermediate variable with an inferred type.

Thank you for the replies.
I could provide the complete sample (it's open source code), but the project is fairly large and I ended up reworking the relevant logic anyway.
As for the var with initial value, I think the logic there was different at some point, but you're right, an uninitialized let would be better.

The difference between assigning the result to value / argumentValue and assigning it to a plain let constant with no type annotation is that the type of the variable provides context for the result type of ?? , which can impact overload resolution of ?? and lead to behavior changes if you factor out part of the code into an intermediate variable with an inferred type.

Ah, that makes sense, thank you!

Unfortunately, implicit optional promotions with ?? can lead to really unexpected behavior.

Before the problem I described, I was bitten elsewhere by an accidental double-optional that had a .some(.none) value, which caused similar ?? confusion :)

1 Like

I have found that sometimes the debugger incorrectly prints nil for p x or po x or v x, but prints the correct answer for p "\(x)". If that gives you a different result, you're probably hitting a debugger bug.

1 Like
Terms of Service

Privacy Policy

Cookie Policy