defer in SIL
Hi all, I’m @kitasuke, iOS Engineer.
This is my second post, “defer in SIL” as series of “Swift type in SIL”. Today I’m going to share what I’ve learned about how defer
works in SIL.
If you are interested in other posts, see more details here.
defer
Let’s run over defer
first. defer
enables us to declare a block that will be executed just before transferring program control outside of the scope. Wherever it's declared inside of the scope, it'll be executed at the end.
defer
can be used to perform actions that need to happen before transferring program control such as below cases.
let db = DB()
defer { db.close() }let user = db.getUser()
...let data = Object()
defer { data.dealloc }let user = data.user
...
In this post, we’ll go over how defer
statement ensures the order of execution.
SIL
Next, let’s take a look at how defer
is represented in SIL.
Examples
There is a simple function number()
which returns Int
value. A local variable x
is declared in the function and 10 is set in a block of defer
. Expected behavior is that this function returns 0, not 10.
defer.swift
func number() -> Int {
var x: Int
defer { x = 10 }
x = 0
return x
}
canonical SIL
Let’s generate canonical SIL for defer.swift with swiftc
command below.
// number()
sil hidden @_T05defer6numberSiyF : $@convention(thin) () -> Int {
bb0:
%0 = alloc_stack $Int, var, name "x" // users: %9, %6, %3, %10
%1 = integer_literal $Builtin.Int64, 0 // user: %2
%2 = struct $Int (%1 : $Builtin.Int64) // users: %11, %4
%3 = begin_access [modify] [static] %0 : $*Int // users: %4, %5
store %2 to %3 : $*Int // id: %4
end_access %3 : $*Int // id: %5
%6 = begin_access [read] [static] %0 : $*Int // user: %7
end_access %6 : $*Int // id: %7
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
dealloc_stack %0 : $*Int // id: %10
return %2 : $Int // id: %11
} // end sil function '_T05defer6numberSiyF'// $defer #1 () in number()
sil private @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () {
// %0 // users: %4, %1
bb0(%0 : $*Int):
debug_value_addr %0 : $*Int, var, name "x", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 10 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %5
%4 = begin_access [modify] [static] %0 : $*Int // users: %5, %6
store %3 to %4 : $*Int // id: %5
end_access %4 : $*Int // id: %6
%7 = tuple () // user: %8
return %7 : $() // id: %8
} // end sil function '_T05defer6numberSiyF6$deferL_yyF'
There are two functions in output. One is number
func and the other is defer
func. As you can see, a comment in number
func describes $defer #1 () in number()
which could be a block to be executed.
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
function_ref
creates a reference to a SIL function and apply
transfers control to the function %8
, passing it the given arguments.
// $defer #1 () in number()
sil private @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () {
// %0 // users: %4, %1
bb0(%0 : $*Int):
debug_value_addr %0 : $*Int, var, name "x", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 10 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %5
%4 = begin_access [modify] [static] %0 : $*Int // users: %5, %6
store %3 to %4 : $*Int // id: %5
end_access %4 : $*Int // id: %6
%7 = tuple () // user: %8
return %7 : $() // id: %8
} // end sil function '_T05defer6numberSiyF6$deferL_yyF'
In an instruction of defer
func, struct
of integer_literal
0 is set to variable x
. I'll ignore that it returns empty tuple for now.
However, there is still one thing that we want to make sure. It’s an order of execution. According to canonical SIL, the defer
operation is applied right before return
operation. Does the number
func actually return 10, not 0 if we consider the order of declared operations?
SSA form
Before diving into more details, let me introduce SSA form. SSA stands for Static Single Assignment and it requires that each variable is assigned only once. SIL is a SSA form IR and LLVM also uses SSA form.
For example, multiple variable assigning is expressed in SSA form like below.
swift
var x = 0
x = 5
x = 10
sil
x0 = 0
x1 = 5
x2 = 10
SSA form enables to have versioned variables which brings benefit for optimizations in compiler. In fact, defer
relies on SSA form for this case.
Let’s look back at the canonical SIL.
// number()
sil hidden @_T05defer6numberSiyF : $@convention(thin) () -> Int {
bb0:
%0 = alloc_stack $Int, var, name "x" // users: %9, %6, %3, %10
%1 = integer_literal $Builtin.Int64, 0 // user: %2
%2 = struct $Int (%1 : $Builtin.Int64) // users: %11, %4
%3 = begin_access [modify] [static] %0 : $*Int // users: %4, %5
store %2 to %3 : $*Int // id: %4
end_access %3 : $*Int // id: %5
%6 = begin_access [read] [static] %0 : $*Int // user: %7
end_access %6 : $*Int // id: %7
// function_ref $defer #1 () in number()
%8 = function_ref @_T05defer6numberSiyF6$deferL_yyF : $@convention(thin) (@inout_aliasable Int) -> () // user: %9
%9 = apply %8(%0) : $@convention(thin) (@inout_aliasable Int) -> ()
dealloc_stack %0 : $*Int // id: %10
return %2 : $Int // id: %11
} // end sil function '_T05defer6numberSiyF'
The local variable x
is defined as %0
and struct
of integer_literal
0 is set to %2
. I thought that because apply
operation for defer
is defined at the very end, it takes latest value of the variable x
at that moment. However, apply
takes %0
, not %2
as an argument. Furthermore, return
uses %2
as a return value and returned value of defer
is ignored. From the above, it's guaranteed that a block of defer
is actually executed when it leaves the current scope and nothing to do for return value.
Summary
Today, we dived into defer
in SIL. We looked over that how defer
works in swift compiler. Also we found out that SIL is a SSA form IR. There is another basic idea like control flow graph and data flow analysis in swift compiler. If you are interested, I strongly recommend to lean these idea. It might be too detailed knowledge, but it's always good to know how it works as Swift developer.
References
swift/docs/SIL.rst
Swift’s High-Level IR: A Case Study of Complementing LLVM IR with Language-Specific Optimization