Swift: -O incorrectly assumes access of static property of type () has no side effects
| Originator: | kevin | ||
| Number: | rdar://19464274 | Date Originated: | 1/13/2015 |
| Status: | Open | Resolved: | |
| Product: | Developer Tools | Product Version: | |
| Classification: | Serious Bug | Reproducible: | Always |
Summary:
Swift supports static properties on structs with arbitrary initializer expressions and guarantees that the value will be initialized only on first access (similar to a `lazy` instance property). However, it seems to have a bug where it completely skips calling the static property initializer if the property type is `()` and optimizations are turned on (with -O). Instead of calling the initializer, it creates a local `()` value and uses that.
This bug seems to only affect zero-sized tuples (`()`, `(())`, `((),())`, etc), and does not affect any other zero-sized values. For example, using `enum Nullary { case Value }` as the type behaves correctly.
Given that Swift is not a pure language, and so expressions can have side-effects, it does not seem to ever be valid to replace an expression that's typed as `()` with a literal `()` value unless the expression can be proven at compile-time to be effect-free.
Steps to Reproduce:
Compile and run the following code, with and without optimizations:
class Foo {
struct Static {
static let classInit: () = {
println("This should be called the first time Foo.init() is called")
return ()
}()
}
private let _init: () = Static.classInit
}
println("Calling Foo.init()")
let _ = Foo()
println("Calling Foo.init() again")
let _ = Foo()
Expected Results:
Both the optimized and unoptimized versions should print the same results:
Calling Foo.init()
This should be called the first time Foo.init() is called
Calling Foo.init() again
Actual Results:
Only the unoptimized version prints that. The optimized version prints:
Calling Foo.init()
Calling Foo.init() again
The generated SIL for the function `main.Foo.__allocating_init` in the optimized version looks like:
// main.Foo.__allocating_init (main.Foo.Type)() -> main.Foo
sil @_TFC4main3FooCfMS0_FT_S0_ : $@thin (@thick Foo.Type) -> @owned Foo {
bb0(%0 : $@thick Foo.Type):
%1 = alloc_ref $Foo // user: %2
return %1 : $Foo // id: %2
}
By contrast, if I change the type from `()` to `Bool` and have the initializer `return true`, I get the following:
// main.Foo.__allocating_init (main.Foo.Type)() -> main.Foo
sil @_TFC4main3FooCfMS0_FT_S0_ : $@thin (@thick Foo.Type) -> @owned Foo {
bb0(%0 : $@thick Foo.Type):
%1 = alloc_ref $Foo // users: %6, %8
// function_ref main.Foo.Static.classInit.mutableAddressor : Swift.Bool
%2 = function_ref @_TFVC4main3Foo6Statica9classInitSb : $@thin () -> Builtin.RawPointer // user: %3
%3 = apply %2() : $@thin () -> Builtin.RawPointer // user: %4
%4 = pointer_to_address %3 : $Builtin.RawPointer to $*Bool // user: %5
%5 = load %4 : $*Bool // user: %7
%6 = ref_element_addr %1 : $Foo, #Foo._init // user: %7
store %5 to %6 : $*Bool // id: %7
return %1 : $Foo // id: %8
}
I get the same results if I use the aforementioned zero-size `enum Nullary { case Value }` type.
Version:
Swift version 1.1 (swift-600.0.57.3)
Target: x86_64-apple-darwin14.0.0
Notes:
The pattern used in the sample code is very useful when a class or struct needs to have one-time initialization code run before the first instance of the class/struct is created. It's similar to using `+initialize` in obj-c, although it's more clearly defined as being invoked when the first instance is created, rather than being invoked when the first message is sent to the class.
This bug also manifests when avoiding init, e.g. with
struct Foo {
static let classInit: () = {
println("Foo.classInit initialization")
}()
}
println("Accessing Foo.classInit")
let a: () = Foo.classInit
println("Accessing Foo.classInit again")
let b: () = Foo.classInit
------------------------------------------------------------------------------
13-Jan-2015 04:21 PM
In the second sample code listed under Notes, using `static var` instead of `static let` works around the issue, but in the original sample code it has no effect. I am unsure why this is the case.
It also turns out that this bug can affect static lets of non-`()` types if the value of the property isn't used, e.g. `let _ = Static.prop`. A `let _ = expr` statement always exists in order to evaluate `expr` for its side-effects, yet Swift will happily skip the static let initializer when using it. Sample code:
struct Foo {
static let bar: Bool = {
println("Foo.bar")
return true
}()
static var baz: Bool {
get {
println("Foo.baz.get")
return true
}
}
}
let _ = Foo.bar
let _ = Foo.baz
let _ = Foo.bar
let _ = Foo.baz
In a non-optimized build this prints
Foo.bar
Foo.baz.get
Foo.baz.get
But in an optimized build this prints
Foo.baz.get
Foo.baz.get
It seems the correct fix is for the compiler to treat static let initializers similarly to computed properties and always call the initializer. The only exceptions that come to mind are either when the static let's initializer expression is a compile-time constant (or the compiler can otherwise prove that the initializer has no side-effects), or when the compiler can prove that the property has already been accessed in the past (e.g. accessing the static let twice within the same block of code).
Comments
Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!