Mysterious Default Value for un-assigned variable

Hello everyone :waving_hand:
I came across a real mind-twist in Swift, and I’d love to hear your thoughts.

Take a look at this code (no escaping, no concurrency involved):

func foo(date: Date, action: () -> Void) -> Date {
    action()
    return date
}

var actionDate: Date!

let resultDate = foo(date: Date.now) {
    actionDate = resultDate
    print("Assigned")
}

print(actionDate == resultDate) // πŸ‘ˆ false
print(actionDate) // πŸ‘ˆ Optional(2001-01-01 00:00:00 +0000)

Where did the reference date come from? Even if Swift wrongly uses the free init of the Date(), it should be equal to .now or at least with a fraction of a second difference, right?

I've tested with different types and observed the last print:

Int    -> passing 1234   -> prints 0
Bool   -> passing true   -> prints false
Double -> passing 12.34  -> prints 0.0
String -> passing "Hi"   -> prints nil

Why does using String return nil then? The foo function's return type is not even an Optional.

Also checked with a custom enum which does not have an initializer or rawValue, and the result was that it returns the first case as the value:

enum Bar {
    case a
    case b
}

func foo(bar: Bar, action: () -> Void) -> Bar {
    action()
    return bar
}

var actionBar: Bar!

let resultBar = foo(bar: Bar.b) {
    actionBar = resultBar
    print("Assigned")
}

print(actionBar == resultBar) // πŸ‘ˆ false
print(actionBar) // πŸ‘ˆ Optional(DemoProject.Bar.a)

I was expecting some compile time error like this:


or like this:
Demo Scope
or at least like this:
Demo Ambiguous

It seems like Swift is defaulting types to some unpredicted real values with unknown and inconsistent behaviour, which is unexpected from Swift (IMO).

So what is happening?
Is it by design or is it a bug?

I think it should be prevented by the compiler, such an inconsistency at the first place.

Thank you


Tested in:
Both DEBUG and RELEASE mode

Tested in:

That looks like unsafeBitCast(0.0, to: Date.self) β€” iirc Date stores a double number of seconds since that date. As to why the zero-initialized value is available inside that closure, that is almost certainly a bug.

I wonder if the zero-initialization only happens at certain optimization levels. Have you tested this in both debug and release builds?

That is consistent with the zero-initialization we see for other types: all-zeroes is not a valid value for String, so Optional<String> uses the same layout as String and uses all zeroes to represent nil. I’m surprised that doesn’t crash though.

Yes tested. I've added more testing info to the original post. Thanks for clarifying.

1 Like

yeah i think this is another 'quirk' of top-level code... this might be a side-effect of the fact that resultBar is being captured before it's declared. you can see this if you wrap the top-level expressions in a do {} block:

enum Bar {
    case a
    case b
}

func foo(bar: Bar, action: () -> Void) -> Bar {
    action()
    return bar
}

do {
    var actionBar: Bar!

    let resultBar = foo(bar: Bar.b) {
        actionBar = resultBar
        print("Assigned")
    }

    print(actionBar == resultBar) // πŸ‘ˆ false
    print(actionBar) // πŸ‘ˆ Optional(DemoProject.Bar.a)
}
bug.swift:14:37: error: closure captures 'resultBar' before it is declared
12 |     var actionBar: Bar!
13 | 
14 |     let resultBar = foo(bar: Bar.b) {
   |         |                           `- error: closure captures 'resultBar' before it is declared
   |         `- note: captured value declared here
15 |         actionBar = resultBar
   |                     `- note: captured here
16 |         print("Assigned")
17 |     }

there are many strange issues like this with top-level code. see here & here for some others.

1 Like

Technically fair enough.

If actionString was not accepting an optional value, it would crash like this:

But it is really surprising how a non-optional type eventually becomes optional!

ah, my apologies... i missed the 'real' example when looking at this and copied over your final 'custom enum' example. when testing in Xcode 26 locally, the following does appear to build without error even without using top-level code:

import Foundation

func foo(date: Date, action: () -> Void) -> Date {
    action()
    return date
}

func test() {
    var actionDate: Date!

    let resultDate = foo(date: Date.now) {
        actionDate = resultDate
        print("Assigned")
    }

    print(actionDate == resultDate) // πŸ‘ˆ false
    print(actionDate) // πŸ‘ˆ Optional(2001-01-01 00:00:00 +0000)
}

@main
enum App {
    static func main() {
        test()
    }
}

i'm not sure how concerning of an issue that is, but it seems unexpected at the very least – i encourage you to report a bug regarding this behavior.

1 Like

FWIW, with @main removed this situation is correctly detected as an error by godbolt.

1 Like

I can also reproduce this if none of these conditions are true. You can clone this project and change this function to:

Button("Update Name") {
    let id = toastManager.showToast() {
        OverlayTextInputToastView(toastID: id, userName: $userName)
    }
}

The module has not a main.swift file or the other condition.

Please note that this is not (only) about top level code as this post shows!

1 Like

I have no idea why the compiler bundled with Xcode 26.0 is behaving differently here (I just copied this example myself to verify it and it does compile!), but when I test this example using Godbolt on x86-64 Linux, I get the error I would expect:

<source>:11:42: error: closure captures 'resultDate' before it is declared

I can't reproduce this example with Godbolt. It does not compile because the closure attempts to capture resultDate before it is declared. After deleting that code, I get nil as the output of print(actionDate).

Interesting. I can reproduce the silent miscompile using the Foundation.Date, even with an asserts compiler.

Now, if I declare Date locally like this:

struct Date {
  var a: Double = 0.0
  static var now = Date()
}

Then, I get the correct error:

bad.swift:14:42: error: closure captures 'resultDate' before it is declared
12 |     var actionDate: Date!
13 | 
14 |     let resultDate = foo(date: Date.now) {
   |         |                                `- error: closure captures 'resultDate' before it is declared
   |         `- note: captured value declared here
15 |         actionDate = resultDate
   |                      `- note: captured here
16 |         print("Assigned")
17 |     }

And, if I make it into an address-only type:

struct Date {
  var a: Any = 0.0
  static var now = Date()
}

I get a SIL verifier error:

SIL memory lifetime failure in @$s3bad4testyyF: memory is not initialized, but should be
memory location:   %4 = alloc_stack [lexical] [var_decl] $Date, let, name "resultDate" // users: %48, %47, %19, %15
at instruction:   copy_addr %4 to [init] %14 : $*Date             // id: %15

Abort: function reportError at MemoryLifetimeVerifier.cpp:268

Looks like we have a fun set of bugs with closure captures of uninitialized vars.

5 Likes

Yes, I noticed the same with godbolt. With Xcode however I'm getting this bug.

Here is an online demo. I don’t know the engine behind it, but I doubt it’s Xcode.

You've just deleted the whole point of this issue, my friend.

You can use this if you wish :backhand_index_pointing_down:. It seems it acts similarly to what I see in Xcode.

i am a bit confused by the fact that when i pass -Xfrontend -sil-verify-all to the Xcode compiler on these examples (using the Foundation types), it still doesn't surface any issues. do you happen to know if that verification is disabled entirely in certain toolchains, or could it actually not be finding an issue with the SIL that's being produced?

What compiler version are you using? I just tried the latest 6.2 and main snapshot (10/09 and 10/02 respectively), as well as a swiftc that I built from main yesterday, and it reproduces on all three. Here is my complete test case:

struct Date {
  var a: Any = 0.0
  static var now = Date()
}

func foo(date: Date, action: () -> Void) -> Date {
    action()
    return date
}

func test() {
    var actionDate: Date!

    let resultDate = foo(date: Date.now) {
        actionDate = resultDate
        print("Assigned")
    }

    print(actionDate) // πŸ‘ˆ Optional(2001-01-01 00:00:00 +0000)
}

@main
enum App {
    static func main() {
        test()
    }
}

to clarify, the case i was testing was:

  1. Xcode 26 compiler
  2. an example using Foundation.Date

and when i compile with the SIL verify flag set it does not surface any verification errors. the locally-built toolchains and OSS ones do produce the verification errors when asked though (as you noted).


thank you @MojtabaHs for filing this issue: Compiler fails to detect illegal use of undeclared variable Β· Issue #84909 Β· swiftlang/swift Β· GitHub

2 Likes

I wasn't able to reproduce a verification crash with Foundation.Date, even with a locally-built compiler, either.

EDIT: Ok, here we go:

slava@Mac swift % cat lib.swift
public struct Date {
  var a: Double = 0

  public static var now: Date { Date() }
}

slava@Mac swift % ../build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64/bin/swiftc lib.swift -emit-module -emit-module-interface -enable-library-evolution -swift-version 6

slava@Mac swift % cat bad.swift 
import lib

func foo(date: Date, action: () -> Void) -> Date {
    action()
    return date
}

func test() {
    var actionDate: Date!

    let resultDate = foo(date: Date.now) {
        actionDate = resultDate
        print("Assigned")
    }

    print(actionDate) // πŸ‘ˆ Optional(2001-01-01 00:00:00 +0000)
}

@main
enum App {
    static func main() {
        test()
    }
}

slava@Mac swift % ../build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64/bin/swiftc bad.swift -I. -parse-as-library                                                         

// no error

Now, watch this:

slava@Mac swift % rm lib.swiftinterface                                                                                                                                     
slava@Mac swift % ../build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64/bin/swiftc lib.swift -emit-module -enable-library-evolution -swift-version 6
slava@Mac swift % ../build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64/bin/swiftc bad.swift -I. -parse-as-library                                  
error: compile command failed due to signal 6 (use -v to see invocation)

So when the library that declares Date is built from an interface file, there is no SIL error.

1 Like