Контрибьютим в Swift
Знакомо, узнали?
Каждый раз когда вы пытались объявить опциональное замыкание @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()) {
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=
Предупреждение: 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.