За последние несколько недель мы внимательно изучили GHC. Мы начали с рассмотрения шагов, которые нам потребуются для подготовки нашей локальной машины к разработке GHC. Это был особенно сложный процесс в Windows, поэтому мы сосредоточились на нем. После этого мы рассмотрели базовый способ создания цикла разработки для себя. Мы проверили это, изменив сообщение об ошибке и посмотрев, как оно отображается в компиляторе. На прошлой неделе мы внесли более сложные изменения. На этой неделе мы собираемся завершить эту серию, рассмотрев некоторые основные способы внесения вклада.

Документация

Документация - сложная вещь для любого программного проекта. В любой момент большая часть усилий уходит на то, чтобы программа работала так, как должна. Когда вы уже понимаете код, вам не нужно смотреть документацию. Так что соблазн не менять ни одного комментария. Это означает, что документация всегда может устареть. Во всяком случае, Haskell более склонен к подобным ошибкам. Мы ищем проблемы, внося изменения, компилируя и проверяя, что ломается. И документация никогда не ломается!

Опытные разработчики не забудут больше менять документацию. Тем не менее, неизбежно, что что-то проскользнет сквозь трещины. Но есть и хорошие новости для нас, новичков в кодовой базе GHC! Мы лучше всех сможем найти пробелы в документации, поскольку именно нам ее нужно читать больше всего! Так я нашел свой первый вклад.

Изучая типы лексирования, я обнаружил непонятный комментарий. Вверху compiler/basicTypes/BasicTypes.hs говорится:

-- There is considerable overlap between the logic here and the logic
-- in Lexer.x, but sadly there seems to be way to merge them.

Это не совсем правильно. Из контекста кажется довольно ясным, что автор намеревался написать кажется, нет способа их объединить. Отлично, давайте отправим для этого пул-реквест! Мы разветвим репозиторий и откроем пул реквест. Итак, мы создадим нашу вилку, клонируем репо, откроем новую ветку и откроем запрос на перенос для мастера.

Теперь есть несколько досадная проблема, связанная с тем, что сборки CI на самом деле, похоже, не проходят прямо сейчас. Но, надеюсь, в какой-то момент этот пиар вольется.

Отслеживание проблем с помощью Trac

Конечно, с GHC поставлены и гораздо более сложные вопросы. Есть реальные функции, которые мы хотим добавить в кодовую базу, и ошибки, которые мы хотим исправить! Чтобы понять, что там происходит, вам нужно заглянуть в средство отслеживания проблем. GHC использует для этого Trac, и вы можете наблюдать за всеми проблемами в этом списке. У них есть лейблы в зависимости от того, для какого выпуска они предназначены и насколько они важны.

Это может быть довольно обширный список. Я пролистал много разных билетов и не знал, чем на самом деле могу помочь. Так как же найти что-то, с чего начать? Во-первых, вы можете подписаться на список рассылки разработчиков GHC. Общение там поможет вам узнать, над чем работают люди. Во-вторых, вы можете авторизоваться на Freenode и попасть на #ghc канал. Вы можете спросить любого, что происходит и чем вы можете помочь. К счастью, в списке проблем есть отметка новички. Это проблемы, которые, как подчеркнули разработчики GHC, должны быть легкими для людей, плохо знакомых с кодовой базой. Давайте рассмотрим одну из этих проблем.

Взгляд на реальную проблему: шаблоны инфиксов

В ходе этой охоты я нашел этот билет, связанный с инфиксным значением (->). В билете утверждается, что указанный инфиксный уровень 0 для оператора стрелки на самом деле неверен. Давайте посмотрим, что они означают.

Напоминаем, что инфиксный уровень устанавливает приоритет оператора при определении порядка операций. Например, оператор умножения (*) имеет более высокий инфиксный уровень, чем оператор сложения (+). Мы можем подтвердить эту информацию с помощью быстрого сеанса ghci, используя команду :info для каждого из них.

>> :i (+)
…
infixl 6 +
>> :i (*)
…
infixl 7 *
>> 5 + 2 * 3
11 -- Would be 21 if addition were higher precedence

Теперь, когда у двух операторов один и тот же инфиксный уровень, мы обращаемся к направлению инфиксного уровня. В качестве примера мы можем сравнить вычитание со сложением. Мы обнаружим, что это тоже infixl 6. Поскольку это infixl (в отличие от infixr), мы даем приоритет левой операции. Вот пример.

>> :i (-)
…
infixl 6 -
>> 5 - 2 + 18
21 -- Not (-15)

Итак, давайте посмотрим на наш оператор стрелки, который мы используем при определении сигнатур типов:

>> :i (->)
data (->) (a :: TYPE q) (b :: TYPE r) -- Defined . `GHC.Prim`
infixr 0 `(->)`
...

Это предполагает, что для этого оператора установлен уровень инфикса 0, и что мы должны расставить приоритеты справа. Однако человек, сообщивший об ошибке, предлагает следующий код:

{-# LANGUAGE TypeOperators #-}
module Bug where
import Data.Type.Equality
type (~>) = (->)
infixr 0 ~>
f :: (a ~> b -> c) :~: (a ~> (b -> c))
f = Refl

Здесь много всего происходит с некоторыми концепциями более высокого уровня, поэтому давайте разберемся со всем этим. Во-первых, (->) - это оператор типа, что означает, что он сам по себе является типом. Таким образом, мы можем создать для него синоним типа под названием (~>). Затем мы можем присвоить этому новому оператору любой инфиксный уровень, который нам нравится. В этом случае мы выберем тот же установленный инфиксный уровень, что и для исходного оператора infixr 0.

Следующая часть создает выражение f. Его сигнатура типа использует оператор (:~:) для реляционного равенства между типами. Этот тип имеет конструктор Refl. Единственное, что вам нужно понять, это то, что каждый из наших шаблонов стрелок ((a ~> b -> c) и (a ~> (b -> c))) является типом. И этот код должен компилироваться только в том случае, если эти типы совпадают.

И на первый взгляд эти типы должны совпадать. В конце концов, оба оператора подразумевают infixr 0, а это означает, что то, как мы заключаем его в скобки в правой части (:~:), должно соответствовать его естественному порядку. Но код не компилируется!

>> ghci
>> :l Bug.hs
Bug.hs:11:5: error:
    * Couldn’t match type `a` with `a ~> b`
      `a` is a rigid type variable bound by
        f :: forall a b c. ((a ~> b) -> c) :~: (a ~> ( b -> c))
        At Bug.hs:10:1-38
      Expected type: ((a ~> b) -> c) :~: (a ~> (b -> c))
        Actual type: ((a ~> b) -> c) :~: ((a ~> b) -> c)
    * In the expression: Refl
      In an equation for `f’: f = Refl
    * Relevant bindings include
      f :: ((a ~> b) -> c) :~: (a ~> (b -> c))
        (bound at Bug.hs:11:1)
   |
11 | f = Refl
   |

В строке «Фактический тип» мы можем увидеть, как компилятор интерпретирует (a ~> b -> c). Он отдает приоритет левому, а не правому. В самом деле, если мы изменим сигнатуру типа, чтобы отразить приоритет, присвоенный (~>), наш код будет компилироваться:

f :: (a ~> b -> c) :~: ((a ~> b) -> c)
f = Refl
…
>> ghci
>> :l Bug.hs
Ok, one module loaded.

Исправление

К счастью для нас, исправление уже предложено в заявке. Компилятор представляет инфиксный уровень наших операторов с использованием типа Fixity. Мы можем видеть конкретное место, где мы определили уровень для некоторых из наших встроенных операторов:

negateFixity, funTyFixity :: Fixity
negateFixity = Fixity NoSourceText 6 InfixL -- Fixity of unary negate
funTyFixity = Fixity NoSourceText 0 InfixR -- Fixity of `->`

Мы хотим изменить фиксированность оператора функционального типа. Вместо того, чтобы он выглядел как 0, мы должны сделать так, чтобы он выглядел как -1, показывая более низкий приоритет этого оператора. Обратите внимание, что этот код относится к нашему сообщению. Фактические причины, по которым он в конечном итоге имеет более низкий приоритет, более сложны. Но давайте внесем это изменение:

funTyFixity = Fixity NoSourceText (-1) InfixR

Тестирование наших изменений

Кажется, это должно быть простое изменение для проверки. Сначала мы снова make наш код. Затем мы загрузим GHCI и запросим информацию о (->). Но когда мы пробуем, это не работает!

> make
> ghci
...
>> :i (->)
data (->) (a :: TYPE q) (b :: TYPE r) -- Defined . `GHC.Prim`
infixr 0 `(->)`
...

Проблема здесь в том, что переделка не заставляет GHCI использовать нашу новую локально созданную версию GHC. Даже при использовании ghci.exe из каталога ghc/inplace/bin он по-прежнему не учитывает это изменение. Способ решения проблемы заключается в том, что вместо использования ghci мы можем передать флаг --interactive обычному вызову ghc. Итак, нам нужно что-то вроде этого:

~/ghc/inplace/bin/ghc-stage2.exe -o prog --interactive Main.hs

Это вызовет приглашение GHCI, загружающее наш основной модуль. А теперь, когда мы продолжим и получим информацию, мы увидим, что это работает!

> ~/ghc/inplace/bin/ghc-stage2.exe -o prog --interactive Main.hs
...
>> :i (->)
data (->) (a :: TYPE q) (b :: TYPE r) -- Defined . `GHC.Prim`
infixr -1 `(->)`
...

Итак, я сделаю простой запрос на перенос, устраняющий эту ошибку. За прогрессом можно следить здесь. Я буду обновлять этот пост по мере его продвижения.

Вывод

На этом мы завершаем нашу серию статей о вкладе в GHC! Ошибок очень много, так что не бойтесь проверять все, что помечено как newcomer. Просто не забудьте взглянуть на обсуждение, которое уже было в заявке!

Чтобы узнать больше о Haskell, вы можете прочитать нашу Серия Liftoff (для начинающих) или нашу Веб-серию Haskell, если вы уже знакомы с этим языком. Вы также можете скачать наш Контрольный список для начинающих Haskell, чтобы начать работу! Или вы можете посмотреть наш Контрольный список производства, если вам нужны идеи для более сложных проектов.