defer in SIL

Yusuke Kita
Swift type in SIL
Published in
5 min readMay 27, 2018

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

--

--