
Знакомо, узнали?
Каждый раз когда вы пытались объявить опциональное замыкание @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 шагов:
- Ищем баг на bugs.swift.org.
- Скачиваем Swift.
- Билдим.
- Фиксим.
- Тестируем.
- Отправляем.
- Фиксим комменты по ревью.
- Мержим.
Рассмотрим некоторые пункты в деталях.
Ищем баг на 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 может продолжить оптимизировать его и сгенерировать машинный код.
Отталкиваясь от описания выше, можно примерно понять, где должна находиться та или иная правка для языка. Но чтобы немного упростить процесс поиска можно воспользоваться некоторыми хитростями:
- Попросить помощи у контрибьюторов и спросить в таске, где предположительно должен быть фикс.
- Если задача связана с диагностикой (ошибка которую пишет компилятор), то можно попробовать найти эту диагностику по названию и оттуда уже искать.
- Найти похожие задачи и посмотреть, какие файлы и компоненты изменялись в их рамках.
Мне повезло и человек в комментариях к моему таску прояснил многие моменты.

Тут мы можем узнать несколько лайфхаков для решения моей задачи и задач в 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!
Полезные ссылки
- Цикл статей про устройство компилятора Swift на русском языке.
- Высокоуровневое описание архитектуры компилятора с swift.org.
- Раздел документации в репозитории Swift.
- Swift Weekly Brief, рассылка с новостями про разработку Swift, там же бывают новые Starter Bug.
- Пулл реквесты, где можно посмотреть, как добавляют новую диагностику: один, два, три.
- Выступление Ayaka Nonaka про то, зачем контрибьютить в Swift, и как она это сделала.
- Выступление Jesse Squires про то, как собирается свифт, из чего состоит проект и зачем туда контрибьютить.
- Статья от PSPDFKit с советами как контрибьютить в Swift.
- Туториал по LLVM.
