Знакомо, узнали?


Каждый раз когда вы пытались объявить опциональное замыкание @escaping в Swift компилятор ругался и писал непонятную ошибку @escaping attribute only applies to function types. Мне это не нравилось, и я решил это исправить. Теперь компилятор Swift 5.3 вместо этой ошибки напишет Closure is already escaping in optional type argument.


И сегодня мы разберемся, как сделать свой вклад в развитие языка Swift.


Зачем


Swift — огромный opensource-проект, и самый лучший способ с одной стороны разобраться в любимом инструменте, а с другой принести пользу и его улучшить — исправить один из многочисленных багов. К тому же вы получите опыт работы в огромном проекте и бейджик "contributed to Apple".


Что для этого понадобится


Для сборки Swift понадобится компьютер под управлением macOS, Linux или Windows, а также 15-70 GB свободного места в зависимости от способа сборки Swift (об этом будет чуть дальше).


Для внесения правок понадобятся знания Swift и представление о С++, однако это не обязательно, некоторые задачи требуют добавления тест-кейсов, где не нужны знания языка.


Как это сделать


Все очень просто и укладывается в 9 шагов:


  1. Ищем баг на bugs.swift.org.
  2. Скачиваем Swift.
  3. Билдим.
  4. Фиксим.
  5. Тестируем.
  6. Отправляем.
  7. Фиксим комменты по ревью.
  8. Мержим.

Рассмотрим некоторые пункты в деталях.


Ищем баг на bugs.swift.org


Если вы уже работали с JIRA — то ничего сложного в поиске бага нет, открываете вкладку Issues и выбираете наиболее интересное среди бесконечного числа багов и тасков.


Также есть фильтр для Starter Bug-ов — специальных задач для тех, кто только начинает свой путь в разработке Swift. Этот фильтр помог мне найти задачу на исправление диагностики, которую я впоследствии сделал.


Скачиваем Swift


Официальный Readme хорошо описывает процесс скачивания и сборки. Ниже пересказ, как это делать на macOS, но на Linux и Windows собрать Swift тоже можно.


Устанавливаем зависимости


brew install cmake ninja

или


brew bundle

Создаем папку под проект и клонируем


mkdir swift-source
cd swift-source
git clone https://github.com/apple/swift.git # Или лучше сразу сделайте форк и клонируйте его
./swift/utils/update-checkout --clone

Также можно добавить environment variable для быстрой навигации.


export SWIFT="~/swift-source"
export LLVM_SOURCE_ROOT="~/swift-source/llvm"

Билдим


Есть несколько способов собрать Swift — для работы с ninja или для работы с Xcode.


Сборка для Xcode медленнее, но можно будет ставить брейкпоинты и пользоваться привычным iOS разработчикам IDE.


Ninja — это легковесная C++ билд-система, которая работает быстрее чем Xcode и дает возможность делать быстрые инкрементальные билды, но нужно будет работать не в самом лучшем на свете IDE — Xcode. Разработчики Swift используют в основном ninja.


Все сборки делаются при помощи build-script — утилиты, которая умеет собирать LLDB для Swift, SPM, гонять тесты и т.д.


Первым делом надо перейти в директорию для сборки:


cd swift

Сборка при помощи ninja:


utils/build-script --release-debuginfo # соберет все в релиз моде

Чтобы собрать какие-то части для дебага можно добавить флагов, например:


utils/build-script --release-debuginfo --debug-swift # фронтенд в дебаге, остальное в релиз моде
utils/build-script --release-debuginfo --debug-swift-stdlib # std в дебаге, основное в релиз

Но если вам очень хочется пожарить яичницу на макбуке или испытать на прочность Mac Pro, можно собрать весь Swift в дебаге:


Предупреждение: это будет очень долго, лучше ставить эту сборку на ночь + это съест много места (~70 GB)


utils/build-script --debug

После сборки можно добавить ещё одну environment переменную:


export SWIFT_BUILD_DIR="~/swift-source/build/Ninja-DebugAssert/swift-macosx-x86_64"

После того как собрали весь проект, можно делать быстрые инкрементальные сборки при помощи ninja:


cd ${SWIFT_BUILD_DIR}
ninja swift

То есть делаем какие-то фиксы, а потом быстро ребилдим при помощи команды ninja swift.


Сборка при помощи Xcode


Просто добавьте --xcode флажок к командам выше.


Также после сборки можно добавить environment переменную:


export XCODE_BUILD_DIR="~/swift-source/build/Xcode-DebugAssert/swift-macosx-x86_64"

Вам соберется .xcodeproj файл, который можно просто открыть


open ${XCODE_BUILD_DIR}/Swift.xcodeproj

На старте он предложит вам создать схемы автоматически, соглашайтесь и ждите пока Xcode висит создает схемы.



После можно выбрать схему swift и спокойно нажимать Run. После этого запустится Swift, и можно будет пользоваться им через консоль Xcode.


Xcode поддерживает брейкпоинты, это может пригодиться для дебага.


То есть запускаем схему swift, ставим брейкпоинт в интересующей нас части кода и через консоль пытаемся набросать код, который приведет к вызову этого брейкпоинта.


P.S Если возникли проблемы со сборкой, отладкой, тестированием или подобным — можно написать на forums.swift.org. Там достаточно оперативно помогают разобраться.


Фиксим


Самая интересная и она же самая сложная часть для начинающих — как найти код, который нужно зафиксить.


Чтобы сэкономить время при поиске бага, неплохо для начала ознакомиться немного с теорией. Все это может выглядеть сложно, если вы раньше не работали с компиляторами и не погружались в теорию того, как работают языки программирования. По правде говоря, для того чтобы внести свой вклад в Swift, совсем не обязательно знать, как работает каждый компонент языка, достаточно владеть некоторыми хитростями, о которых вы узнаете чуть ниже.


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


Ниже проиллюстрирована архитектура компилятора и приведена выдержка из официального высокоуровневого описания архитектуры.



Взято из доклада Contributing to open source Swift by Jesse Squires


1) Парсинг (находится в lib/Parse)


Парсер — это простой преобразователь исходного кода в AST (абстрактное синтаксическое дерево). В него встроен лексер — преобразователь потока символов исходного кода программы в слова, которые можно использовать в языке. То есть сначала лексер проверяет, что в исходном коде нет неожиданных символов, а потом парсер составляет из символов AST.


Лексер выявляет ошибки типа использования неизвестных языку символов, а парсер ошибки несбалансированности открывающих/закрывающих скобок и т.п.


2) Семантический анализ (lib/Sema)


Семантический анализатор отвечает за преобразование AST с предыдущего шага в типизированное AST. В основном он выявляет ошибки типизации.


3) Импорт Clang (lib/ClangImporter)


На этом шаге импортируются Clang модули и мапятся C или Objective-C API в соответствующие Swift API.


4) Генерация SIL (lib/SILGen)


Swift Intermediate Language (SIL) — это высокоуровневый промежуточный язык, необходимый для анализа и оптимизации Swift кода. В этой части типизированное AST преобразуется в так называемый "сырой" SIL.


5) SIL преобразование (lib/SILOptimizer/Mandatory)


Тут происходят дополнительные проверки потока данных, например, использование не инициализированных переменных. Конечным результатом этого этапа является так называемый "каноничный" SIL.


6) SIL оптимизации (lib/SILOptimizer)


На этом этапе происходят высокоуровневые оптимизации, включающие в себя девиртуализацию, специализация дженериков и оптимизации ARC.


7) LLVM IR генерация (lib/IRGen)


Генерация IR (Intermediate Representation) преобразует SIL в LLVM IR, после которого LLVM может продолжить оптимизировать его и сгенерировать машинный код.


Отталкиваясь от описания выше, можно примерно понять, где должна находиться та или иная правка для языка. Но чтобы немного упростить процесс поиска можно воспользоваться некоторыми хитростями:


  1. Попросить помощи у контрибьюторов и спросить в таске, где предположительно должен быть фикс.
  2. Если задача связана с диагностикой (ошибка которую пишет компилятор), то можно попробовать найти эту диагностику по названию и оттуда уже искать.
  3. Найти похожие задачи и посмотреть, какие файлы и компоненты изменялись в их рамках.

Мне повезло и человек в комментариях к моему таску прояснил многие моменты.



Тут мы можем узнать несколько лайфхаков для решения моей задачи и задач в Swift в целом:


  • TDD подход — это отличная идея для разработки диагностик, то есть можно написать тесты на желаемое поведение, а потом сделать так, чтобы тесты начали проходить (подход к тестированию описан в следующей секции);
  • Диагностика это часть проверки типов, значит нужно искать в Sema… или TypeCheck… файлах.

Что ж, поехали.


Открываем Xcode с собранным Swift. Ищем текущую диагностику по строке, находим её в DiagnosticsSema.def под названием escaping_non_function_parameter, там же обнаруживаем, что нужна диагностика существует, но только как NOTE, поэтому нужно переделать на ERROR.


// AST/DiagnosticsSema.def

ERROR(escaping_optional_type_argument, none,
       "closure is already escaping in optional type argument", ())

Также находим тесты которые проверяют эту ошибку. Добавляем по соседству свои тесты или исправляем текущие, запускаем, видим падающие тесты.


// test/attr/attr_escaping.swift

func misuseEscaping(opt a: @escaping ((Int) -> Int)?) {} // expected-error{{closure is already escaping in optional type argument}} {{28-38=}}

func misuseEscaping(_ a: (@escaping (Int) -> Int)?) {} // expected-error{{closure is already escaping in optional type argument}} {{27-36=}}
func misuseEscaping(nest a: (((@escaping (Int) -> Int))?)) {} // expected-error{{closure is already escaping in optional type argument}} {{32-41=}}
func misuseEscaping(iuo a: (@escaping (Int) -> Int)!) {} // expected-error{{closure is already escaping in optional type argument}} {{29-38=}}

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


// *TypeCheckType.cpp*

const auto diagnoseInvalidAttr = [&](TypeAttrKind kind) {
  if (kind == TAK_escaping) {
    Type optionalObjectType = ty->getOptionalObjectType();
    if (optionalObjectType && optionalObjectType->is<AnyFunctionType>()) {
      return diagnoseInvalid(repr, attrs.getLoc(kind),
                             diag::escaping_optional_type_argument);
    }
  }
  return diagnoseInvalid(repr, attrs.getLoc(kind),
                         diag::attribute_requires_function_type,
                         TypeAttributes::getAttrName(kind));
};

Пару слов о работе TypeCheckType.cpp: здесь проверяется использование типов и по сути это набор if-проверок, в который мы добавили новую.


Важно: если вы делали правки в C++ коде, то перед коммитом нужно отформатировать код при помощи clang-format, иначе на ревью скорее всего не пропустят.


$SWIFT/clang/tools/clang-format/git-clang-format --force

Тестируем


В Swift куча разных тестов, и их обязательно нужно прогнать перед отправкой вашего кода. Для запуска тестов в основном используется утилита lit.py, но также можно вызывать их при помощи cmake. Более детально с подходами к тестированию можно ознакомиться тут.


Я пользовался только lit.py, поэтому ниже приведу cheatsheet, который поможет запустить нужный вариант тестов.


# Запуск всех тестов
${LLVM_SOURCE_ROOT}/utils/lit/lit.py ${SWIFT_BUILD_DIR}/test-macosx-x86_64/

# Запуск всех тестов без огромной портянки в консоли
${LLVM_SOURCE_ROOT}/utils/lit/lit.py -sv ${SWIFT_BUILD_DIR}/test-macosx-x86_64/

# Я сломал тест и надо посмотреть что он выводит
${LLVM_SOURCE_ROOT}/utils/lit/lit.py -a ${SWIFT_BUILD_DIR}/test-macosx-x86_64/attr/attr_escaping.swift}

# Я сломал валидационные тесты
${LLVM_SOURCE_ROOT}/utils/lit/lit.py ${SWIFT_BUILD_DIR}/validation-test-macosx-x86_64/

# Почему я запускаю все тесты? Можно только какую-то часть?
${LLVM_SOURCE_ROOT}/utils/lit/lit.py ${SWIFT_BUILD_DIR}/test-macosx-x86_64/ --filter=<REGEX>

Предупреждение: lit не умеет фильтровать по нескольким --filter флагам и берет только самый последний.


Отправляем


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


Если коммитов много, то следует сделать squash перед отправкой, чтобы отправлять все одним коммитом. Название ветки должно примерно описывать, что вы сделали. Например: fix-it-for-removing-escaping-from-optional-closure-parameter.


Фиксим комменты по ревью


Ревьюеры в Swift очень внимательные и попросят вас исправить каждый лишний отступ и каждое несоответствие их идеям о том, как это должно работать. Тут у меня тоже возникли некоторые сложности, так как С++ разработчик из меня никакой. Но добрые люди в пулл реквесте подсказали, как можно написать код, чтобы все работало и было красиво.


Мержим


После того как все исправили, остается ждать аппрува от мемберов и соответственно мержа от них же.


Профит


Заключение


В этой статье я постарался показать весь процесс вклада в огромный opensource-проект Swift и поделился некоторыми лайфхаками, которые могут упростить вход в разработку и немного осветил теоретическую часть архитектуры компилятора.


Как оказалось, помочь развитию своего любимого языка не очень сложно, особенно при поддержке мощного и дружелюбного сообщества разработчиков. Поэтому не стесняйтесь браться за Starter Task-и и помогать развиваться Swift!


Полезные ссылки