Продолжаем изучать компилятор Swift. Эта часть посвящена Swift Intermediate Language.

Если вы не видели предыдущие, рекомендую перейти по ссылке и прочитать:

Следующий шаг — преобразование типизированного AST в сырой SIL. Swift Intermediate Language(SIL) — это специально созданное для Swift промежуточное представление. Описание всех инструкций можно найти в документации.

SIL имеет SSA форму. Static Single Assignment (SSA) — представление кода, в котором каждой переменной значение присваивается только один раз. Оно создаётся из обычного кода добавлением дополнительных переменных. Например, с помощью числового суффикса, который обозначает версию переменной после каждого присваивания.

Благодаря этой форме компилятору проще оптимизировать код. Ниже приведён пример на псевдокоде. Очевидно, что первая строка является ненужной:

a = 1
a = 2
b = a

Но это только для нас. Чтобы научить компилятор это определять, пришлось бы писать нетривиальные алгоритмы. Но с помощью SSA сделать это гораздо проще. Теперь даже для простого компилятора будет очевидно, что значение переменной a1 не используется, и эту строку можно удалить:

a1 = 1
a2 = 2
b1 = a2

SIL позволяет применять к коду Swift специфичные оптимизации и проверки, которые было бы сложно или невозможно осуществить на этапе AST.

Использование генератора SIL

Для генерации SIL используется флаг -emit-silgen:

swiftc -emit-silgen main.swift

Результат выполнения команды:

sil_stage raw

import Builtin
import Swift
import SwiftShims

let x: Int

// x
sil_global hidden [let] @$S4main1xSivp : $Int

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @$S4main1xSivp                     // id: %2
  %3 = global_addr @$S4main1xSivp : $*Int         // user: %8
  %4 = metatype $@thin Int.Type                   // user: %7
  %5 = integer_literal $Builtin.Int2048, 16       // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%5, %4) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} // end sil function 'main'

// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int

SIL, как и LLVM IR, можно вывести в виде исходного кода. В нём можно обнаружить, что на этом этапе был добавлен импорт Swift модулей Builtin, Swift и SwiftShims.

Не смотря на то, что в Swift код можно писать прямо в глобальной области видимости, SILGen генерирует функцию main — точку входа в программу. Весь код был расположен внутри неё, кроме объявления константы, так как она является глобальной и должна быть доступна везде.

Большая часть строк имеет похожую структуру. Слева расположен псевдорегистр, в который сохраняется результат выполнения инструкции. Далее — сама инструкция и её параметры, а в конце — комментарий с указанием регистра, для вычисления которого будет использоваться этот регистр.

Например, в этой строке создаётся целочисленный литерал с типом Int2048 и значением 16. Этот литерал сохраняется в пятый регистр и будет использован для вычисления значения седьмого:

%5 = integer_literal $Builtin.Int2048, 16       // user: %7

Объявление функции начинается с ключевого слова sil. Далее указывается название с префиксом @, calling convention, параметры, тип возвращаемого значения и код функции. Для инициализатора Int.init(_builtinIntegerLiteral:) он, естественно, не указан, так как эта функция из другого модуля, и её нужно только объявить, но не определять. Символ доллара означает начало указания типа:

// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int

Calling convention указывает на то, как правильно вызывать функцию. Это необходимо для генерации машинного кода. Подробное описание этих принципов выходит за рамки статьи.

Название инициализаторов, как и имена структур, классов, методов, протоколов, искажаются (name mangling). Это решает сразу несколько проблем.

Во-первых, это позволяет использовать одинаковые имена в разных модулях и вложенных сущностях. Например, для первого метода fff используется имя S4main3AAAV3fffSiyF, а для второго — S4main3BBBV3fffSiyF:

struct AAA {
    func fff() -> Int {
        return 8
    }
}

struct BBB {
    func fff() -> Int {
        return 8
    }
}

S значит Swift, 4 — это число символов в названии модуля, а 3 — в названии класса. В инициализаторе литерала Si обозначает стандартный тип Swift.Int.

Во-вторых, в название добавляются имена и типы аргументов функций. Это позволяет использовать перегрузку. Например, для первого метода генерируется S4main3AAAV3fff3iiiS2i_tF, а для второго — S4main3AAAV3fff3dddSiSd_tF:

struct AAA {
    func fff(iii internalName: Int) -> Int {
        return 8
    }

    func fff(ddd internalName: Double) -> Int {
        return 8
    }
}

После названий параметров указан тип возвращаемого значения, а за ним — типы параметров. При этом их внутренние названия не указываются. К сожалению, документации по name mangling в Swift нет, а его реализация может в любой момент измениться.

За названием функции следует её определение. Оно состоит из одного или нескольких basic block. Базовый блок — последовательность инструкций с одной точкой входа, одной точкой выхода, которая не содержит инструкций ветвления или условий для раннего выхода.

У функции main есть один базовый блок, который принимает на вход все параметры, переданные в функцию, и содержит весь её код, так как в нём нет ветвлений:

bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):

Можно считать, что каждая область видимости, ограниченная фигурными скобками, является отдельным базовым блоком. Допустим, что код содержит ветвление:

// before
if 2 > 5 {
    // true
} else {
    // false
}
// after

В этом случае будет сгенерировано как минимум 4 базовых блока для:

  • кода до ветвления,
  • случая, когда выражение верно,
  • случая, когда выражение ложно,
  • кода после ветвления.

cond_br — инструкция для условного перехода. Если значение псевдорегистра %14 равно true, то выполняется переход в блок bb1. Если нет, то в bb2. br — безусловный переход, запускающий выполнение указанного базового блока:

// before
cond_br %14, bb1, bb2                           // id: %15
bb1:   
// true
  br bb3                                          // id: %21
bb2:                                              // Preds: bb0
// false
  br bb3                                          // id: %27
bb3:                                              // Preds: bb2 bb1
// after

Исходный код:

Сырое промежуточное представление, которое было получено на прошлом этапе, анализируется на корректность и трансформируется в каноничное: функции, помеченные transparent, инлайнятся (вызов функции подменяется её телом), вычисляются значения константных выражений, выполняется проверка на то, что функции, которые возвращают значения, делают это во всех ветвлениях кода и так далее.

Эти преобразования являются обязательными и выполняются, даже если оптимизация кода отключена.

Генерация каноничного SIL

Для генерации каноничного SIL используется флаг -emit-sil:

swiftc -emit-sil main.swift

Результат выполнения команды:

sil_stage canonical

import Builtin
import Swift
import SwiftShims

let x: Int

// x
sil_global hidden [let] @$S4main1xSivp : $Int

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @$S4main1xSivp                     // id: %2
  %3 = global_addr @$S4main1xSivp : $*Int         // user: %6
  %4 = integer_literal $Builtin.Int64, 16         // user: %5
  %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
  store %5 to %3 : $*Int                          // id: %6
  %7 = integer_literal $Builtin.Int32, 0          // user: %8
  %8 = struct $Int32 (%7 : $Builtin.Int32)        // user: %9
  return %8 : $Int32                              // id: %9
} // end sil function 'main'

// Int.init(_builtinIntegerLiteral:)
sil public_external [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int {
// %0                                             // user: %2
bb0(%0 : $Builtin.Int2048, %1 : $@thin Int.Type):
  %2 = builtin "s_to_s_checked_trunc_Int2048_Int64"(%0 : $Builtin.Int2048) : $(Builtin.Int64, Builtin.Int1) // user: %3
  %3 = tuple_extract %2 : $(Builtin.Int64, Builtin.Int1), 0 // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64)          // user: %5
  return %4 : $Int                                // id: %5
} // end sil function '$SSi22_builtinIntegerLiteralSiBi2048__tcfC'

В таком простом примере изменений немного. Чтобы увидеть реальную работу оптимизатора, нужно немного усложнить код. Например, добавить сложение:

let x = 16 + 8

В его сыром SIL можно найти сложение этих литералов:

%13 = function_ref @$SSi1poiyS2i_SitFZ : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %14
%14 = apply %13(%8, %12, %4) : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %15

А в каноничном его уже нет. Вместо этого используется константное значение 24:

%4 = integer_literal $Builtin.Int64, 24         // user: %5

Исходный код:

Дополнительные Swift-специфичные трансформации применяются, если включена оптимизация. Среди них специализация дженериков (оптимизация дженерик-кода под конкретный тип параметра), девиртуализация (замена динамических вызовов статическими), инлайнинг, оптимизация ARC и многое другое. Объяснение этих техник не влезает и в без того разросшуюся статью.

Исходный код:

Так как SIL – это особенность Swift, я не показывал в этот раз примеры реализации. К компилятору скобок мы вернёмся в следующей части, когда будем заниматься генерацией LLVM IR.

Источник