Spec-Zone.ru › Haskell 9

7. Расширение и использование GHC как библиотеки

GHC предоставляет свои внутренние API пользователям через встроенный пакет ghc. Это позволяет вам писать программы, которые используют весь компилятор GHC, чтобы анализировать или компилировать код Haskell программно. Кроме того, GHC позволяет пользователям загружать плагины компилятора во время компиляции — модули, которые могут просматривать и изменять внутреннее промежуточное представление GHC, Core. Плагины подходят для таких задач, как экспериментальные оптимизации или анализ, и предоставляют более низкий порог входа в разработку компиляторов для многих распространённых случаев.

Кроме того, GHC предлагает механизм лёгких аннотаций, которые вы можете использовать для аннотирования исходного кода метаданными, которые вы можете позже просмотреть с помощью API компилятора или плагина компилятора.

7.1. Аннотации исходного кода

Аннотации — это небольшие директивы, которые позволяют прикрепить данные к идентификаторам в исходном коде, которые сохраняются при компиляции. Эти данные затем можно просматривать и использовать при использовании GHC как библиотеки или написании плагина компилятора.

7.1.1. Аннотирование значений

Любое выражение, имеющее экземпляры Typeable и Data, может быть прикреплено к привязке значения верхнего уровня с помощью директивы ANN. В частности, это означает, что вы можете использовать ANN для аннотирования конструкторов данных (например, Just) так же, как и обычных значений (например, take). В качестве примера, чтобы аннотировать функцию foo с аннотацией Just "Hello" вы можете сделать так:

{-# ANN foo (Just "Hello") #-}
foo = ...

Применяется ряд ограничений на использование аннотаций:

  • Переменная, которую вы аннотируете, должна быть на верхнем уровне (то есть без вложенных переменных)
  • Переменная, которую вы аннотируете, должна быть объявлена в текущем модуле
  • Выражение, которое вы аннотируете, должно иметь тип с экземплярами Typeable и Data
  • Применяются ограничения стадийности Template Haskell ограничений стадийности Template Haskell к аннотируемому выражению, поэтому, например, вы не можете выполнить функцию из компилируемого модуля.

    Точнее, аннотация {-# ANN x e #-} хорошо отстаивается тогда и только тогда, когда $(e) будет (не учитывая обычные ограничения типов синтаксиса вставки и обычное ограничение на вставку внутри вставки — $([|1|]) подходит в качестве аннотации, хотя и избыточно).

Если вы считаете, что какое-либо из этих ограничений слишком обременительно, обратитесь в команду GHC.

Однако, помимо этих ограничений, разрешается многое, включая выражения, которые не полностью вычислены! Выражения аннотации будут вычислены компилятором так же, как вставки Template Haskell.

{-# ANN f SillyAnnotation { foo = (id 10) + $([| 20 |]), bar = 'f } #-}
f = ...

7.1.2. Аннотирование типов

Вы можете аннотировать типы с помощью директивы ANN с помощью ключевого слова type. Например:

{-# ANN type Foo (Just "A `Maybe String' annotation") #-}
data Foo = ...

7.1.3. Аннотирование модулей

Вы можете аннотировать модули с помощью директивы ANN с помощью ключевого слова module. Например:

{-# ANN module (Just "A `Maybe String' annotation") #-}

7.2. Использование GHC как библиотеки

Пакет ghc предоставляет большую часть фронтального модуля GHC пользователям и, таким образом, позволяет вам писать программы, использующие его. Эта библиотека фактически такая же, как используется внутренним фронтальным драйвером компиляции GHC, и позволяет вам создавать инструменты, которые программно компилируют исходный код и проверяют его. Такая функциональность полезна для создания инструментов IDE или инструментов рефакторинга. В качестве простого примера, вот программа, которая компилирует модуль, так же, как это делает сам ghc по умолчанию при вызове:

import GHC
import GHC.Paths ( libdir )
import GHC.Driver.Session ( defaultFatalMessager, defaultFlushOut )

main =
    defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
      runGhc (Just libdir) $ do
        dflags <- getSessionDynFlags
        setSessionDynFlags dflags
        target <- guessTarget "test_main.hs" Nothing
        setTargets [target]
        load LoadAllTargets

Аргумент для runGhc немного сложен. GHC нуждается в этом для поиска своих библиотек, поэтому аргумент должен ссылаться на каталог, который выводится командой ghc --print-libdir для той же версии GHC, с которой компилируется программа. Поэтому выше мы используем пакет ghc-paths, который предоставляет это нам.

Компиляция приводит к:

$ cat test_main.hs
main = putStrLn "hi"
$ ghc -package ghc simple_ghc_api.hs
[1 of 1] Compiling Main             ( simple_ghc_api.hs, simple_ghc_api.o )
Linking simple_ghc_api ...
$ ./simple_ghc_api
$ ./test_main
hi
$

Для получения дополнительной информации об использовании API, а также дополнительных примеров и ссылок, пожалуйста, посетите эту страницу вики Haskell.org.

7.3. Плагины компилятора

GHC имеет возможность загружать плагины компилятора во время компиляции. Эта функция аналогична функции, предоставляемой GCC, и позволяет пользователям писать плагины, которые могут изменять поведение решателя ограничений, инспектировать и изменять конвейер компиляции, а также преобразовывать и инспектировать промежуточный язык GHC, Core. Плагины подходят для экспериментального анализа или оптимизации и не требуют изменений исходного кода GHC для использования.

Плагины не могут оптимизировать/инспектировать C++, а также реализовывать такие вещи, как изменения парсера/переднего конца, как в GCC, за исключением ограниченных изменений в решателе ограничений. Если вы считаете, что какие-либо из этих ограничений слишком обременительны, обратитесь в команду GHC.

Плагины не работают с -fexternal-interpreter. Если вам нужно запустить плагины с -fexternal-interpreter, сообщите разработчикам GHC в #14335.

7.3.1. Использование плагинов компилятора

Плагины могут быть добавлены в командной строке с помощью параметра -fplugin=⟨module⟩, где ⟨модуль⟩ — это модуль в зарегистрированном пакете, который экспортирует плагин. Плагины загружаются по порядку, при этом флаги командной строки и Cabal предшествуют флагам в pragмах OPTIONS, которые обрабатываются в порядке файлов. Аргументы могут быть переданы плагинам с помощью параметра -fplugin-opt=⟨module⟩:⟨args⟩. Список включенных плагинов может быть сброшен с помощью параметра -fclear-plugins.

-fplugin=⟨module⟩

Загрузить плагин в указанном модуле. Модуль должен быть членом пакета, зарегистрированного в базе данных пакетов GHC.

-fplugin-opt=⟨module⟩:⟨args⟩

Передать аргументы модулю плагина; модуль должен быть указан с помощью -fplugin=⟨module⟩. Порядок пragma-плагинов важен, но порядок arg-pragm не важен. Один и тот же набор аргументов передается всем плагинам из одного и того же модуля.

-- Two Echo plugins will both get args A and B.
{-# OPTIONS -fplugin Echo -fplugin-opt Echo:A #-}
{-# OPTIONS -fplugin Echo -fplugin-opt Echo:B #-}

-- While order of the plugins matters, arg order does not.
{-# OPTIONS -fplugin-opt Echo2:B #-}

{-# OPTIONS -fplugin Echo1 #-}
{-# OPTIONS -fplugin-opt Echo1:A #-}

{-# OPTIONS -fplugin Echo2 #-}

Если вы хотите использовать один и тот же плагин с разными аргументами, то повторно экспортируйте этот плагин из разных легких модулей.

-- Echo1 and Echo2 as lightweight modules re-exporting Echo.plugin.
module Echo1 (plugin) where import Echo (plugin)
module Echo2 (plugin) where import Echo (plugin)

-- Echo1 gets arg A while Echo2 gets arg B.
{-# OPTIONS -fplugin Echo1 -fplugin-opt Echo1:A #-}
{-# OPTIONS -fplugin Echo2 -fplugin-opt Echo2:B #-}
-fplugin-trustworthy

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

-fclear-plugins

Очистить список плагинов, ранее указанных с помощью -fplugin. Это полезно в GHCi, где просто удаление опций -fplugin из командной строки невозможно. Вместо этого можно использовать :set -fclear-plugins.

В качестве примера, для загрузки плагина, экспортированного Foo.Plugin в пакете foo-ghc-plugin, и предоставления ему параметра «baz», мы вызовем GHC так:

$ ghc -fplugin Foo.Plugin -fplugin-opt Foo.Plugin:baz Test.hs
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Loading package foo-ghc-plugin-0.1 ... linking ... done.
...
Linking Test ...
$

Плагины также могут загружаться из библиотек напрямую. Это позволяет загружать плагины в кросс-компиляторах (как обходной путь для #14335).

-fplugin-library=⟨file-path⟩;⟨unit-id⟩;⟨module⟩;⟨args⟩

Аргументы задаются в виде списка, поэтому плагин, указанный в -fplugin-library=⟨file-path⟩;⟨unit-id⟩;⟨module⟩;⟨args⟩, будет выглядеть как 'path/to/plugin;package-123;Plugin.Module;["Argument","List"]'.

В качестве альтернативы, плагины ядра могут быть указаны с помощью Template Haskell.

addCorePlugin "Foo.Plugin"

Это вставляет плагин как проход ядро-к-ядру. В отличие от -fplugin=(module), модуль плагина не может находиться в одном пакете с модулем, вызывающим Language.Haskell.TH.Syntax.addCorePlugin. Таким образом, реализация может ожидать, что плагин будет построен к моменту его необходимости.

Модули плагинов живут в отдельном пространстве имен от пространства имен импорта пользователя. По умолчанию эти два пространства имен одинаковы; однако, есть несколько параметров командной строки, которые контролируют конкретно пакеты плагинов:

-plugin-package ⟨pkg⟩

Этот параметр делает установленный пакет ⟨pkg⟩ доступным для плагинов, например, -fplugin=⟨module⟩. Пакет ⟨pkg⟩ можно указать полностью с номером версии (например, network-1.0) или номер версии можно опустить, если установлен только один вариант пакета. Если установлено несколько версий ⟨pkg⟩ и -hide-all-plugin-packages не был указан, то все остальные версии будут скрыты. -plugin-package ⟨pkg⟩ поддерживает сжатие и переименование, описанные в Сжатие и переименование модулей.

В отличие от -package ⟨pkg⟩, этот параметр НЕ вызывает привязку пакета ⟨pkg⟩ к результируемому исполняемому файлу или динамической библиотеке.

-plugin-package-id ⟨pkg-id⟩

Экспонирует пакет в пространстве имен плагинов, как -plugin-package ⟨pkg⟩, но пакет называется его установленным идентификатором пакета вместо имени. Это более надёжный способ именования пакетов и может использоваться для выбора пакетов, которые в противном случае могли быть скрыты. Cabal передает флаги -plugin-package-id ⟨pkg-id⟩ в GHC. -plugin-package-id ⟨pkg-id⟩ поддерживает сжатие и переименование, описанные в Сжатие и переименование модулей.

-hide-all-plugin-packages

По умолчанию все экспонированные пакеты в обычном пространстве имен импорта исходного кода также доступны для плагинов. Это приводит к тому, что эти пакеты по умолчанию скрываются. Если вы используете этот флаг, то любые пакеты с необходимыми вам плагинами необходимо явно экспонировать с помощью параметров -plugin-package ⟨pkg⟩.

В настоящее время единственный способ указать зависимость от плагина в Cabal — поместить его в build-depends (который использует стандартный флаг -package-id ⟨unit-id⟩); однако в будущем будет отдельное поле для указания зависимостей от плагинов.

7.3.2. Написание плагинов компилятора

Плагины — это модули, которые экспортируют по крайней мере один идентификатор, plugin, типа GHC.Plugins.Plugin. Все плагины должны import GHC.Plugins , так как он определяет интерфейс с конвейером компиляции.

Plugin фактически содержит функцию, которая устанавливает проход компиляции в конвейер компилятора. По умолчанию есть пустой плагин, который ничего не делает, GHC.Plugins.defaultPlugin, который следует переопределить с помощью синтаксиса записи, чтобы указать вашу функцию установки. Поскольку точные поля типа Plugin могут быть изменены, это лучший способ убедиться, что ваши плагины будут продолжать работать в будущем с минимальным влиянием на интерфейс.

Plugin экспортирует поле installCoreToDos, которое является функцией типа [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]. CommandLineOption фактически просто String, а CoreToDo в основном является функцией типа Core -> Core. CoreToDo даёт вашему проходу имя и выполняет его над каждым скомпилированным модулем при вызове GHC.

В качестве быстрого примера, вот простой плагин, который ничего не делает, просто возвращает исходный конвейер компиляции, без изменений, и говорит «Привет»:

module DoNothing.Plugin (plugin) where
import GHC.Plugins

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  putMsgS "Hello!"
  return todo

При условии, что вы скомпилировали этот плагин и зарегистрировали его в пакете (например, с помощью Cabal), вы можете использовать его, просто указав -fplugin=DoNothing.Plugin в командной строке, и во время компиляции вы должны увидеть, как GHC говорит «Привет».

Поддерживается и запуск нескольких плагинов, путём передачи нескольких -fplugin=... опций. GHC загрузит плагины в порядке их указания в командной строке и, при необходимости, комбинирует их эффекты в том же порядке. То есть, если у нас есть два плагина Core, Plugin1 и Plugin2, каждый определяющий функцию install , как и функция выше, тогда GHC сначала запустит Plugin1.install на стандартном [CoreToDo], возьмёт результат и передаст его Plugin2.install. -fplugin=Plugin1 -fplugin=Plugin2 обновит конвейер Core, применив Plugin1.install opts1 >=> Plugin2.install opts2 (где opts1 и opts2 — это опции, передаваемые каждому плагину с помощью -fplugin-opt=...). Это не специфично для плагинов Core, но справедливо для всех типов плагинов, которые могут быть комбинированы или упорядочены каким-либо образом: первый плагин в командной строке GHC всегда будет действовать первым.

7.3.3. Ядерные плагины более подробно

CoreToDo — это эффективный тип данных, описывающий все виды оптимизаций, которые GHC выполняет над ядром. Существуют проходы для упрощения, CSE и т. д. Есть специальный случай для плагинов, CoreDoPluginPass :: String -> PluginPass -> CoreToDo который следует всегда использовать при вставке собственного прохода в конвейер. Первый параметр — имя плагина, а второй — проход, который нужно вставить.

CoreM — это монада, внутри которой происходят все оптимизации ядра.

Функция установки плагина (install в приведенном выше примере) принимает список CoreToDo и возвращает список CoreToDo. Перед началом компиляции модулей GHC перечисляет все необходимые плагины, которые вы сказали загрузить, и выполняет все функции установки, первоначально на списке проходов, которые GHC указывает сам. После этого для каждого плагина окончательный список проходов передается оптимизатору и выполняется простым перебором списка в порядке.

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

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ _ = return []

безусловно, допустима, но также безусловно не то, чего на самом деле хочет кто-либо.

7.3.3.1. Обработка связываний

В предыдущем разделе мы увидели, что помимо имени, CoreDoPluginPass принимает проход типа PluginPass. PluginPass — это синоним для (ModGuts -> CoreM ModGuts). ModGuts — это тип, представляющий один модуль, компилируемый GHC в данный момент.

ModGuts содержит все связывания верхнего уровня модуля, которые мы можем проверить. Эти связывания имеют тип CoreBind и фактически представляют связывание имени с телом кода. Связывания верхнего уровня модуля являются частью ModGuts в поле mg_binds. Реализация прохода, обрабатывающего связывания верхнего уровня, просто требует перебора этого поля и возвращает новое ModGuts с обновлённым полем mg_binds. Поскольку это такой распространённый случай, предоставлена функция под названием bindsOnlyPass, которая поднимает функцию типа ([CoreBind] -> CoreM [CoreBind]) к типу (ModGuts -> CoreM ModGuts).

Продолжая наш пример из предыдущего раздела, мы можем написать простой плагин, который просто выводит имя всех нерекурсивных связываний в модуле, который он компилирует:

module SayNames.Plugin (plugin) where
import GHC.Plugins

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  return (CoreDoPluginPass "Say name" pass : todo)

pass :: ModGuts -> CoreM ModGuts
pass guts = do dflags <- getDynFlags
               bindsOnlyPass (mapM (printBind dflags)) guts
  where printBind :: DynFlags -> CoreBind -> CoreM CoreBind
        printBind dflags bndr@(NonRec b _) = do
          putMsgS $ "Non-recursive binding named " ++ showSDoc dflags (ppr b)
          return bndr
        printBind _ bndr = return bndr

7.3.3.2. Плагины позднего этапа

Если CoreProgram модуля изменяется в обычном плагине ядра, изменённые связывания могут попасть в развертывания файла интерфейса модуля. Это может быть нежелательно, так как плагин может внести изменения, которые повлияют на подстановку или оптимизацию.

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

  1. Они не выполняются в монаде CoreM. Вместо этого им явно передаётся HscEnv и они выполняются в IO.
  2. Им передаётся CgGuts вместо ModGuts. CgGuts — это ограниченная форма ModGuts , предназначенная для генерации кода. CoreProgram в CgGuts , переданном плагину позднего этапа, уже будет полностью оптимизирован.
  3. Они должны поддерживать CostCentreState и отслеживать любые центры затрат, которые они вводят, добавляя их в поле cg_ccs CgGuts. Это связано с тем, что автоматический сбор центров затрат происходит до этапа плагина позднего этапа. Если плагин позднего этапа не вводит никаких центров затрат, он может просто вернуть заданное состояние центра затрат.

Вот очень простой пример плагина позднего этапа, который изменяет значение связывания в модуле. Если он находит нерекурсивное связывание верхнего уровня с именем testBinding и типом Int, он изменит его значение на выражение Int 111111.

plugin :: Plugin
plugin = defaultPlugin { latePlugin = lateP }

lateP :: LatePlugin
lateP _ _ (cg_guts, cc_state) = do
    binds' <- editCoreBinding (cg_binds cg_guts)
    return (cg_guts { cg_binds = binds' }, cc_state)

editCoreBinding :: CoreProgram -> IO CoreProgram
editCoreBinding pgm = pure . go
  where
    go :: [CoreBind] -> [CoreBind]
    go (b@(NonRec v e) : bs)
      | occNameString (getOccName v) == "testBinding" && exprType e `eqType` intTy =
          NonRec v (mkUncheckedIntExpr 111111) : bs
    go (b:bs) = b : go bs
    go [] = []

Поскольку это плагин позднего этапа, изменённое значение связывания не попадет в файл интерфейса.

7.3.3.3. Использование аннотаций

Ранее мы обсуждали аннотационные директивы (Аннотации исходного кода), которые, как мы упоминали, могут быть использованы, чтобы предоставить компилятору плагины дополнительные рекомендации или информацию. Аннотации для модуля могут быть получены плагином, но вам нужно получить их через ModGuts модулей. Поскольку аннотации могут быть произвольными экземплярами Data и Typeable, вам необходимо предоставить аннотацию типа, указывающую правильный тип данных для извлечения из файла интерфейса, и необходимо убедиться, что тип аннотации, используемый вашими пользователями, совпадает с типом аннотации, используемым вашим плагином. По этой причине мы рекомендуем распространять аннотации как часть пакета, который также предоставляет компилятор плагины, если это возможно.

Чтобы получить аннотации отдельной переменной, вы можете использовать getAnnotations и указать соответствующий тип. Вот пример, который будет выводить имя любого нерекурсивного связывания верхнего уровня с аннотацией SomeAnn:

{-# LANGUAGE DeriveDataTypeable #-}
module SayAnnNames.Plugin (plugin, SomeAnn(..)) where
import GHC.Plugins
import Control.Monad (unless)
import Data.Data

data SomeAnn = SomeAnn deriving Data

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  return (CoreDoPluginPass "Say name" pass : todo)

pass :: ModGuts -> CoreM ModGuts
pass g = do
          dflags <- getDynFlags
          mapM_ (printAnn dflags g) (mg_binds g) >> return g
  where printAnn :: DynFlags -> ModGuts -> CoreBind -> CoreM CoreBind
        printAnn dflags guts bndr@(NonRec b _) = do
          anns <- annotationsOn guts b :: CoreM [SomeAnn]
          unless (null anns) $ putMsgS $ "Annotated binding found: " ++  showSDoc dflags (ppr b)
          return bndr
        printAnn _ _ bndr = return bndr

annotationsOn :: Data a => ModGuts -> CoreBndr -> CoreM [a]
annotationsOn guts bndr = do
  (_, anns) <- getAnnotations deserializeWithData guts
  return $ lookupWithDefaultUFM_Directly anns [] (varUnique bndr)

См. документацию по API GHC для получения дополнительной информации о том, как использовать внутренние API и т. д.

7.3.4. Плагины типовой проверки

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

Тип Plugin имеет поле tcPlugin типа [CommandLineOption] -> Maybe TcPlugin, где тип TcPlugin определен следующим образом:

  data TcPlugin = forall s . TcPlugin
    { tcPluginInit    :: TcPluginM s
    , tcPluginSolve   :: s -> TcPluginSolver
    , tcPluginRewrite :: s -> UniqFM TyCon TcPluginRewriter
    , tcPluginStop    :: s -> TcPluginM ()
    }

  type TcPluginSolver = EvBindsVar -> [Ct] -> [Ct] -> TcPluginM TcPluginSolveResult

  type TcPluginRewriter = RewriteEnv -> [Ct] -> [Type] -> TcPluginM TcPluginRewriteResult

data TcPluginSolveResult
  = TcPluginSolveResult
      { tcPluginInsolubleCts :: [Ct]
      , tcPluginSolvedCts    :: [(EvTerm, Ct)]
      , tcPluginNewCts       :: [Ct]
      }

  data TcPluginRewriteResult
    = TcPluginNoRewrite
    | TcPluginRewriteTo
        { tcPluginRewriteTo    :: Reduction
        , tcRewriterNewWanteds :: [Ct]
        }

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

Основная идея заключается в следующем:

  • При проверке типа модуля GHC вызывает tcPluginInit один раз перед началом решения ограничений. Это позволяет плагину искать данные в контексте, инициализировать изменяемое состояние или открыть соединение с внешним процессом (например, с внешним решателем SMT). Плагин может вернуть результат любого типа, и результат будет передан в другие поля записи TcPlugin.
  • Во время решения ограничений GHC многократно вызывает tcPluginSolve. Эта функция получает текущий набор ограничений и должна вернуть TcPluginSolveResult, указывающий, найдена ли противоречивость или достигнут прогресс. Если решатель плагина продвинулся, GHC перезапустит процесс решения ограничений, циклируя до достижения фиксированной точки.
  • При переписывании применений семейств типов GHC вызывает tcPluginRewriter. Плагин предоставляет набор семейств типов, которые его интересуют для переписывания. Для каждого из них переписыватель получает аргументы этого семейства типов, а также текущий набор ограничений Given. Затем плагин может указать переписывание для этого применения семейства типов, если это необходимо.
  • Наконец, GHC вызывает tcPluginStop после завершения решения ограничений, позволяя плагину освободить выделенные ресурсы (например, завершить процесс решателя SMT).

Код плагина выполняется в моноде TcPluginM, который предоставляет ограниченный интерфейс к функционалу API GHC, относящемуся к плагинам типовой проверки, включая IO и чтение среды. Если вам необходима функциональность, не представленная в модуле TcPluginM, вы можете использовать unsafeTcPluginTcM :: TcM a -> TcPluginM a, но рекомендуем связаться с командой GHC, чтобы предложить дополнения к интерфейсу. Обратите внимание, что TcPluginM может выполнять произвольный ввод-вывод через tcPluginIO :: IO a -> TcPluginM a, хотя с побочными эффектами (особенно в tcPluginSolve ) необходимо быть внимательным. В общем, автору плагина следует убедиться, что выполняемый им ввод-вывод безопасен.

7.3.4.1. Решение ограничений с плагинами

Ключевой компонент плагина типовой проверки — это функция типа TcPluginSolver, например:

solve :: EvBindsVar -> [Ct] -> [Ct] -> TcPluginM TcPluginSolveResult
solve binds givens wanteds = ...

Эта функция будет вызвана двумя способами:

  1. после упрощения ограничений Given, где плагин получает возможность переписать данные,
  2. после того, как GHC попытался решить ограничения Wanted.

Два способа можно отличить, проверив ограничения Wanted: в первом случае (и только в первом случае) плагину будет передан пустой список ограничений Wanted.

Затем плагин может ответить:

  • решенные ограничения, которые будут удалены из инертного набора,
  • новые ограничения, которые будут добавлены в список задач,
  • неразрешимые ограничения, которые будут сообщены об ошибках.

Плагин должен отвечать ограничениями того же типа, т. е. в (1) он должен возвращать только Givens, а для (2) — только Wanted; все остальные ограничения будут проигнорированы.

Если плагин не может добиться прогресса, он должен вернуть TcPluginSolveResult [] [] []. В противном случае, если есть новые ограничения, основной решатель ограничений будет вызван для их упрощения, а затем плагин будет вызван снова. Плагин отвечает за обеспечение того, что этот процесс в конечном итоге завершится.

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

Ограничения, решенные плагином, должны быть предоставлены с доказательством в виде EvTerm типа ограничения. Это доказательство игнорируется для ограничений Given, которые GHC «решает», просто отбрасывая их; как правило, это используется, когда они неинформативны (например, рефлексивные уравнения). Для ограничений Wanted доказательство станет частью ядра терминов, сгенерированных после проверки типа, и может быть проверено -dcore-lint.

При решении ограничения равенства Wanted (типа t1 ~N# t2 или t1 ~R# t2 для номинальных и представленных равенств соответственно) доказательство (типа EvTerm) примет форму EvExpr (Coercion co), где коэренция co имеет тип co :: t1 ~N# t2 или co :: t1 ~R# t2 соответственно.

Плагин должен сам сконструировать подходящую коэренцию co. Однако один из вариантов — создать коэренцию вида

UnivCo (PluginProv "my-plugin" gcvs) role t1 t2

Такая коэренция говорит: «Поверьте мне: мой плагин решил это Wanted, используя (только) gcvs».

Здесь

  • role должна отражать роль исходного ограничения равенства (номинального или представленного).
  • gcvs — это набор «переменных коэренции given»; это переменные коэренции, ограниченные окружающими ограничениями Given, которые плагин использовал для оправдания решения Wanted.

Для корректности очень важно включить gcvs; в противном случае GHC может преобразовать программу в форму, приводящую к сегменту. См. #23923 для подробного обсуждения.

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

7.3.4.2. Переписывание семейств типов с помощью плагинов

Плагины типовой проверки также могут непосредственно переписывать применения семейств типов, предоставив поле tcPluginRewrite записи TcPlugin.

tcPluginRewrite :: s -> UniqFM TyCon TcPluginRewriter

То есть плагин регистрирует отображение от семейства типов TyCon к связанной функции переписывания:

type TcPluginRewriter = [Ct] -> [Type] -> TcPluginM TcPluginRewriteResult

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

data TcPluginRewriteResult
  = TcPluginNoRewrite
  | TcPluginRewriteTo
      { tcPluginRewriteTo    :: Reduction
      , tcRewriterNewWanteds :: [Ct]
      }

То есть переписыватель может указать переписывание применения семейства типов — в этом случае он также может сгенерировать новые ограничения Wanted — или ничего не делать.

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

data Reduction = Reduction Coercion !Type

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

co :: F orig_arg_1 ... orig_arg_n ~ rewritten_ty

Обратите особое внимание на то, что тип LHS коэренции должен соответствовать исходному применению семейства типов, а ее тип RHS — типу, к которому плагин хочет переписать применение семейства типов.

7.3.5. Плагины для исходного кода

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

Существует несколько различных точек доступа, которые вы можете использовать для определения плагинов, которые обращаются к этим представлениям. Все эти поля получают список строк CommandLineOption, которые передаются компилятору с помощью флагов -fplugin-opt=⟨module⟩:⟨args⟩.

plugin :: Plugin
plugin = defaultPlugin {
    parsedResultAction = parsed
  , typeCheckResultAction = typechecked
  , spliceRunAction = spliceRun
  , interfaceLoadAction = interfaceLoad
  , renamedResultAction = renamed
  }

7.3.5.1. Представление после разбора

Если вам нужно определить плагин, который использует дерево синтаксического анализа исходного кода, вам нужно переопределить поле parsedResultAction. Этот пункт доступа позволяет получить информацию о лексемах и комментариях в исходном коде, а также исходное синтаксическое дерево скомпилированного модуля.

parsed :: [CommandLineOption] -> ModSummary
            -> ParsedResult -> Hsc ParsedResult

Поле ModSummary содержит полезную метаинформацию о скомпилированном модуле. Поле ParsedResult содержит HsParsedModule, которое содержит лексематическую и синтаксическую информацию, о которой мы упоминали ранее. Возвращаемый результат изменит результат разбора. Если вы не хотите изменять результат, просто верните полученное в качестве аргумента значение ParsedResult.

Если анализатор обнаруживает ошибки, которые препятствуют построению AST, плагин не будет запущен, но другие виды ошибок, а также предупреждения, будут переданы плагину через значение PsMessages поля ParsedResult. Это позволяет изменять, удалять и добавлять предупреждения или ошибки перед их отображением пользователю, хотя в большинстве случаев вы, вероятно, захотите вернуть сообщения без изменений. Этап разбора завершится ошибкой, если коллекция Messages PsError внутри возвращаемого значения ParsedResult не будет пустой после выполнения всех плагинов разбора.

7.3.5.2. Представление после проверки типов

Если вам нужно определить плагин, которому требуется семантическая информация об исходном коде, используйте поле typeCheckResultAction. Например, если ваш плагин должен определить, ссылаются ли два имени на одно и то же определение, или ему нужно проверить тип функции, он использует семантическую информацию. В этом случае вам нужно получить доступ к переименованной или проверенной по типам версии синтаксического дерева с помощью typeCheckResultAction или renamedResultAction.

typechecked :: [CommandLineOption] -> ModSummary -> TcGblEnv -> TcM TcGblEnv
renamed :: [CommandLineOption] -> TcGblEnv -> HsGroup GhcRn -> TcM (TcGblEnv, HsGroup GhcRn)

Переопределяя поле renamedResultAction, мы можем изменить каждое HsGroup после переименования. Исходный файл разделен на группы в зависимости от расположения встроек Template Haskell, поэтому содержимое этих групп может быть неинтуитивным. Для сохранения всего переименованного AST для проверки в конце проверки типов вы можете установить renamedResultAction на значение keepRenamedSource, которое предоставляется модулем Plugins. Это важно, потому что некоторые части переименованного синтаксического дерева (например, импорты) отсутствуют в проверенном по типам.

7.3.5.3. Оцененный код

Когда компилятор проверяет исходный код, вставки Template Haskell и Template Haskell Quasi-quotation будут заменены фрагментами синтаксического дерева, сгенерированными из них. Однако для инструментов, работающих с исходным кодом, обычно более интересен код генератора, чем сгенерированный код. По этой причине мы включили spliceRunAction. Это поле вызывается для каждого выражения перед его вычислением. Вход проверяется по типам, поэтому семантическая информация доступна для этих фрагментов синтаксического дерева. Если вы вернёте другое выражение, вы можете изменить сгенерированный код.

spliceRun :: [CommandLineOption] -> LHsExpr GhcTc -> TcM (LHsExpr GhcTc)

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

7.3.5.4. Файлы интерфейса

Иногда, когда вы пишете инструмент, знание исходного кода недостаточно, вам также нужно знать детали импортируемых модулей. В этом случае мы рекомендуем использовать interfaceLoadAction . Это будет вызываться каждый раз, когда загружается код уже скомпилированного модуля. Оно будет вызываться для модулей из установленных пакетов, а также для модулей, установленных с GHC. Оно НЕ будет вызываться для ваших собственных модулей.

interfaceLoad :: forall lcl . [CommandLineOption] -> ModIface
                                -> IfM lcl ModIface

В типе данных ModIface вы найдёте много полезной информации, включая экспортированные определения и экземпляры типов классов.

Тип данных ModIface также содержит средства для его расширения дополнительными данными, хранящимися в Map сериализованных полей, индексируемых по именам полей и использующих встроенный класс GHC Binary. Интерфейс для работы с этими полями:

readIfaceField :: Binary a => FieldName -> ModIface -> IO (Maybe a)
writeIfaceField :: Binary a => FieldName -> a -> ModIface -> IO ModIface
deleteIfaceField :: FieldName -> ModIface -> ModIface

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

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

package/field
ghc-n.n.n/core
package/field-n

Для чтения файла интерфейса из внешнего инструмента без связи с GHC формат описан в Файлы расширяемых интерфейсов.

7.3.5.5. Пример плагина для исходного кода

В этом примере мы проверяем все доступные детали скомпилированного исходного кода. Мы не изменяем ни одно из представлений, а выводим детали в стандартный вывод. В выводе также будет красиво отформатированное представление синтаксического дерева после разбора, переименования и проверки по типам, а также оценённые вставки и квазицитаты. Также будет отображено имя загруженных интерфейсов.

module SourcePlugin where

import Control.Monad.IO.Class
import GHC.Driver.Session (getDynFlags)
import GHC.Driver.Plugins
import GHC.Plugins
import GHC.Tc.Types
import Language.Haskell.Syntax.Extension
import GHC.Hs.Decls
import GHC.Hs.Expr
import GHC.Hs.ImpExp
import GHC.Types.Avail
import GHC.Utils.Outputable
import GHC.Hs.Doc
import GHC

plugin :: Plugin
plugin = defaultPlugin
  { parsedResultAction = parsedPlugin
  , renamedResultAction = renamedAction
  , typeCheckResultAction = typecheckPlugin
  , spliceRunAction = metaPlugin
  , interfaceLoadAction = interfaceLoadPlugin
  }

parsedPlugin :: [CommandLineOption] -> ModSummary
             -> ParsedResult -> Hsc ParsedResult
parsedPlugin _ _ parsed@(ParsedResult pm msgs)
     = do dflags <- getDynFlags
          liftIO $ putStrLn $ "parsePlugin: \n" ++ (showSDoc dflags $ ppr $ hpm_module pm)
          liftIO $ putStrLn $ "parsePlugin warnings: \n" ++ (showSDoc dflags $ ppr $ psWarnings msgs)
          liftIO $ putStrLn $ "parsePlugin errors: \n" ++ (showSDoc dflags $ ppr $ psErrors msgs)
          return parsed

renamedAction :: [CommandLineOption] -> TcGblEnv -> HsGroup GhcRn -> TcM (TcGblEnv, HsGroup GhcRn)
renamedAction _ tc gr = do
  dflags <- getDynFlags
  liftIO $ putStrLn $ "typeCheckPlugin (rn): " ++ (showSDoc dflags $ ppr gr)
  return (tc, gr)

typecheckPlugin :: [CommandLineOption] -> ModSummary -> TcGblEnv -> TcM TcGblEnv
typecheckPlugin _ _ tc
  = do dflags <- getDynFlags
       liftIO $ putStrLn $ "typeCheckPlugin (rn): \n" ++ (showSDoc dflags $ ppr $ tcg_rn_decls tc)
       liftIO $ putStrLn $ "typeCheckPlugin (tc): \n" ++ (showSDoc dflags $ ppr $ tcg_binds tc)
       return tc

metaPlugin :: [CommandLineOption] -> LHsExpr GhcTc -> TcM (LHsExpr GhcTc)
metaPlugin _ meta
  = do dflags <- getDynFlags
       liftIO $ putStrLn $ "meta: " ++ (showSDoc dflags $ ppr meta)
       return meta

interfaceLoadPlugin :: [CommandLineOption] -> ModIface -> IfM lcl ModIface
interfaceLoadPlugin _ iface
  = do dflags <- getDynFlags
       liftIO $ putStrLn $ "interface loaded: " ++ (showSDoc dflags $ ppr $ mi_module iface)
       return iface

При компиляции простого модуля, содержащего вставку Template Haskell

{-# OPTIONS_GHC -fplugin SourcePlugin #-}
{-# LANGUAGE TemplateHaskell #-}
module A where

a = ()

$(return [])

с флагами компилятора -fplugin SourcePlugin будет выведен следующий результат:

parsePlugin:
module A where
a = ()
$(return [])
parsePlugin warnings:

parsePlugin errors:

typeCheckPlugin (rn): a = ()
interface loaded: Language.Haskell.TH.Lib.Internal
meta: return []
typeCheckPlugin (rn):
typeCheckPlugin (rn):
Nothing
typeCheckPlugin (tc):
{$trModule = Module (TrNameS "main"#) (TrNameS "A"#), a = ()}

7.3.6. Плагины для подбора дыр

Плагины для подбора дыр вызываются при генерации сообщения об ошибке типа дыры и позволяют получить доступ к информации о дыре во время компиляции и настраивать допустимые предложения подбора дыры.

Используя плагины для подбора дыр, вы можете расширить поведение допустимых предложений подбора дыры, используя, например, Hoogle или другие внешние инструменты для поиска и/или синтеза допустимых предложений подбора дыры с той же информацией о дыре, что и GHC.

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

type CandPlugin = TypedHole -> [HoleFitCandidate] -> TcM [HoleFitCandidate]

type FitPlugin =  TypedHole -> [HoleFit] -> TcM [HoleFit]

data HoleFitPlugin = HoleFitPlugin
  { candPlugin :: CandPlugin
     -- ^ A plugin for modifying hole fit candidates before they're checked
  , fitPlugin :: FitPlugin
     -- ^ A plugin for modifying valid hole fits after they've been found.
  }

Где TypedHole содержит всю информацию о дыре, доступную GHC при генерации ошибки.

data TypedHole = TyH { tyHRelevantCts :: Cts
                      -- ^ Any relevant Cts to the hole
                    , tyHImplics :: [Implication]
                      -- ^ The nested implications of the hole with the
                      --   innermost implication first.
                    , tyHCt :: Maybe Ct
                      -- ^ The hole constraint itself, if available.
                    }

HoleFitPlugins определяются следующим образом

plugin :: Plugin
plugin = defaultPlugin {
    holeFitPlugin = (fmap . fmap) fromPureHFPlugin hfPlugin
  }


hfPlugin :: [CommandLineOption] -> Maybe HoleFitPlugin

Где fromPureHFPlugin :: HoleFitPlugin -> HoleFitPluginR — удобная функция, предоставляемая модулем GHC.Tc.Errors.Hole, для определения плагинов, которые не требуют внутреннего состояния.

7.3.6.1. Плагины подбора дыр с состоянием

HoleFitPlugins заключены в HoleFitPluginR, который предоставляет TcRef для плагина, чтобы отслеживать внутреннее состояние и облегчать общение между плагином кандидатов и плагином подбора.

-- | HoleFitPluginR adds a TcRef to hole fit plugins so that plugins can
-- track internal state. Note the existential quantification, ensuring that
-- the state cannot be modified from outside the plugin.
data HoleFitPluginR = forall s. HoleFitPluginR
  { hfPluginInit :: TcM (TcRef s)
    -- ^ Initializes the TcRef to be passed to the plugin
  , hfPluginRun :: TcRef s -> HoleFitPlugin
    -- ^ The function defining the plugin itself
  , hfPluginStop :: TcRef s -> TcM ()
    -- ^ Cleanup of state, guaranteed to be called even on error
  }

Плагин определяется путем предоставления значения для поля holeFitPlugin, функции, которая принимает строки CommandLineOption, передаваемые компилятору с помощью флагов -fplugin-opt=⟨module⟩:⟨args⟩, и возвращает HoleFitPluginR. Эта функция может использоваться для передачи строк CommandLineOption соответственно плагинам кандидатов и подбора.

7.3.6.2. Пример плагина подбора дыр

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

{-# LANGUAGE TypeApplications, RecordWildCards #-}
module HolePlugin where

import GHC.Plugins hiding ((<>))

import GHC.Tc.Errors.Hole

import Data.List (stripPrefix, sortOn)

import GHC.Tc.Types

import GHC.Tc.Utils.Monad

import Data.Time (UTCTime, NominalDiffTime)
import qualified Data.Time as Time

import Text.Read


data HolePluginState = HPS { timeAlloted :: Maybe NominalDiffTime
                          , elapsedTime :: NominalDiffTime
                          , timeCurStarted :: UTCTime }

bumpElapsed :: NominalDiffTime -> HolePluginState -> HolePluginState
bumpElapsed ad (HPS a e t) = HPS a (e + ad) t

setAlloted :: Maybe NominalDiffTime -> HolePluginState -> HolePluginState
setAlloted a (HPS _ e t) = HPS a e t

setCurStarted :: UTCTime -> HolePluginState -> HolePluginState
setCurStarted nt (HPS a e _) = HPS a e nt

hpStartState :: HolePluginState
hpStartState = HPS Nothing zero undefined
  where zero = fromInteger @NominalDiffTime 0

initPlugin :: [CommandLineOption] -> TcM (TcRef HolePluginState)
initPlugin [msecs] = newTcRef $ hpStartState { timeAlloted = alloted }
  where
    errMsg = "Invalid amount of milliseconds given to plugin: " <> show msecs
    alloted = case readMaybe @Integer msecs of
      Just millisecs -> Just $ fromInteger @NominalDiffTime millisecs / 1000
      _ -> error errMsg
initPlugin _ = newTcRef hpStartState

fromModule :: HoleFitCandidate -> [String]
fromModule (GreHFCand gre) =
  map (moduleNameString . importSpecModule) $ gre_imp gre
fromModule _ = []

toHoleFitCommand :: TypedHole -> String -> Maybe String
toHoleFitCommand TyH{tyHCt = Just (CHoleCan _ h)} str
    = stripPrefix ("_" <> str) $ occNameString $ holeOcc h
toHoleFitCommand _ _ = Nothing

-- | This candidate plugin filters the candidates by module,
-- using the name of the hole as module to search in
modFilterTimeoutP :: [CommandLineOption] -> TcRef HolePluginState -> CandPlugin
modFilterTimeoutP _ ref hole cands = do
  curTime <- liftIO Time.getCurrentTime
  HPS {..} <- readTcRef ref
  updTcRef ref (setCurStarted curTime)
  return $ case timeAlloted of
    -- If we're out of time we remove all the candidates. Then nothing is checked.
    Just sofar | elapsedTime > sofar -> []
    _ -> case toHoleFitCommand hole "only_" of

          Just modName -> filter (inScopeVia modName) cands
          _ -> cands
  where inScopeVia modNameStr cand@(GreHFCand _) =
          elem (toModName modNameStr) $ fromModule cand
        inScopeVia _ _ = False
        toModName = replace '_' '.'
        replace :: Eq a => a -> a -> [a] -> [a]
        replace _ _ [] = []
        replace a b (x:xs) = (if x == a then b else x):replace a b xs

modSortP :: [CommandLineOption] -> TcRef HolePluginState -> FitPlugin
modSortP _ ref hole hfs = do
  curTime <- liftIO Time.getCurrentTime
  HPS {..} <- readTcRef ref
  updTcRef ref $ bumpElapsed (Time.diffUTCTime curTime timeCurStarted)
  return $ case timeAlloted of
    -- If we're out of time, remove any candidates, so nothing is checked.
    Just sofar | elapsedTime > sofar -> [RawHoleFit $ text msg]
    _ -> case toHoleFitCommand hole "sort_by_mod" of
            -- If only_ is on, the fits will all be from the same module.
            Just ('_':'d':'e':'s':'c':_) -> reverse hfs
            Just _ -> orderByModule hfs
            _ ->  hfs
  where orderByModule :: [HoleFit] -> [HoleFit]
        orderByModule = sortOn (fmap fromModule . mbHFCand)
        mbHFCand :: HoleFit -> Maybe HoleFitCandidate
        mbHFCand HoleFit {hfCand = c} = Just c
        mbHFCand _ = Nothing
        msg = hang (text "Error: The time ran out, and the search was aborted for this hole.")
               7 $ text "Try again with a longer timeout."

plugin :: Plugin
plugin = defaultPlugin { holeFitPlugin = holeFitP, pluginRecompile = purePlugin}

holeFitP :: [CommandLineOption] -> Maybe HoleFitPluginR
holeFitP opts = Just (HoleFitPluginR initP pluginDef stopP)
  where initP = initPlugin opts
        stopP = const $ return ()
        pluginDef ref = HoleFitPlugin { candPlugin = modFilterTimeoutP opts ref
                                      , fitPlugin  = modSortP opts ref }

При компиляции модуля, содержащего следующее

{-# OPTIONS -fplugin=HolePlugin
            -fplugin-opt=HolePlugin:600
            -funclutter-valid-hole-fits #-}
module Main where

import Prelude hiding (head, last)

import Data.List (head, last)


f, g, h, i, j :: [Int] -> Int
f = _too_long
j = _
i = _sort_by_mod_desc
g = _only_Data_List
h = _only_Prelude

main :: IO ()
main = return ()

Вывод будет следующим:

Main.hs:12:5: error:
    • Found hole: _too_long :: [Int] -> Int
      Or perhaps ‘_too_long’ is mis-spelled, or not in scope
    • In the expression: _too_long
      In an equation for ‘f’: f = _too_long
    • Relevant bindings include
        f :: [Int] -> Int (bound at Main.hs:12:1)
      Valid hole fits include
        Error: The time ran out, and the search was aborted for this hole.
               Try again with a longer timeout.
  |
12 | f = _too_long
  |     ^^^^^^^^^

Main.hs:13:5: error:
    • Found hole: _ :: [Int] -> Int
    • In the expression: _
      In an equation for ‘j’: j = _
    • Relevant bindings include
        j :: [Int] -> Int (bound at Main.hs:13:1)
      Valid hole fits include
        j :: [Int] -> Int
        f :: [Int] -> Int
        g :: [Int] -> Int
        h :: [Int] -> Int
        i :: [Int] -> Int
        head :: forall a. [a] -> a
        (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits)
  |
13 | j = _
  |     ^

Main.hs:14:5: error:
    • Found hole: _sort_by_mod_desc :: [Int] -> Int
      Or perhaps ‘_sort_by_mod_desc’ is mis-spelled, or not in scope
    • In the expression: _sort_by_mod_desc
      In an equation for ‘i’: i = _sort_by_mod_desc
    • Relevant bindings include
        i :: [Int] -> Int (bound at Main.hs:14:1)
      Valid hole fits include
        sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
        product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
        minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
        maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
        length :: forall (t :: * -> *) a. Foldable t => t a -> Int
        last :: forall a. [a] -> a
        (Some hole fits suppressed; use -fmax-valid-hole-fits=N or -fno-max-valid-hole-fits)
  |
14 | i = _sort_by_mod_desc
  |     ^^^^^^^^^^^^^^^^^

Main.hs:15:5: error:
    • Found hole: _only_Data_List :: [Int] -> Int
      Or perhaps ‘_only_Data_List’ is mis-spelled, or not in scope
    • In the expression: _only_Data_List
      In an equation for ‘g’: g = _only_Data_List
    • Relevant bindings include
        g :: [Int] -> Int (bound at Main.hs:15:1)
      Valid hole fits include
        head :: forall a. [a] -> a
        last :: forall a. [a] -> a
  |
15 | g = _only_Data_List
  |     ^^^^^^^^^^^^^^^

Main.hs:16:5: error:
    • Found hole: _only_Prelude :: [Int] -> Int
      Or perhaps ‘_only_Prelude’ is mis-spelled, or not in scope
    • In the expression: _only_Prelude
      In an equation for ‘h’: h = _only_Prelude
    • Relevant bindings include
        h :: [Int] -> Int (bound at Main.hs:16:1)
      Valid hole fits include
        length :: forall (t :: * -> *) a. Foldable t => t a -> Int
        maximum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
        minimum :: forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
        product :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
        sum :: forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
  |
16 | h = _only_Prelude
  |     ^^^^^^^^^^^^^

7.3.7. Плагины по умолчанию

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

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

Плагины по умолчанию имеют единственную точку доступа в модуле GHC.Tc.Types

-- | A collection of candidate default types for sets of type variables.
data DefaultingProposal
  = DefaultingProposal
    { deProposals :: [[(TcTyVar, Type)]]
      -- ^ The type variable assignments to try.
    , deProposalCts :: [Ct]
      -- ^ The constraints against which defaults are checked.
  }

type FillDefaulting = WantedConstraints -> TcPluginM [DefaultingProposal]

-- | A plugin for controlling defaulting.
data DefaultingPlugin = forall s. DefaultingPlugin
  { dePluginInit :: TcPluginM s
    -- ^ Initialize plugin, when entering type-checker.
  , dePluginRun :: s -> FillDefaulting
    -- ^ Default some types
  , dePluginStop :: s -> TcPluginM ()
   -- ^ Clean up after the plugin, when exiting the type-checker.
  }

Плагин имеет тип WantedConstraints -> [DefaultingProposal]

  • Ему предоставляются текущие нерешённые ограничения.
  • Он возвращает список независимых «предложений по умолчанию».
  • Каждое предложение типа DefaultingProposal указывает:

    • deProposals: указывает список наборов назначений переменных типов в порядке приоритета
    • deProposalCts :: [Ct] задаёт набор ограничений (всегда подмножество входных WantedConstraints) для использования в качестве критерия принятия

После вызова плагина GHC выполняет каждое DefaultingProposal по очереди. Чтобы «выполнить» предложение, GHC последовательно пытается выполнить каждое из предложенных назначений типов в deProposals:

  • Он назначает предложенные типы переменным типов и затем пытается решить deProposalCts
  • Если эти ограничения полностью решаются присваиванием, GHC принимает присваивание и переходит к следующему DefaultingProposal
  • В противном случае GHC пытается следующее присваивание в deProposals.

Плагин может предполагать, что входные ограничения полностью «закончены» (см. страницу Wiki по законченности).

Наиболее надёжным deProposalCts является список всех желаемых ограничений, которые упоминают переменную, для которой вы устанавливаете значение по умолчанию. Если вы опустите ограничение, значение по умолчанию может быть принято, а затем потенциально привести к ошибке проверки типов, если оно несовместимо с одним из пропущенных ограничений. Это может быть полезным способом принудительно установить значение по умолчанию и сообщить пользователю об ошибках.

В наборе тестов GHC есть пример установки значений по умолчанию для типов, поднятых на верхний уровень. В каталоге testsuite/tests/plugins/ см. defaulting-plugin/ для реализации, test-defaulting-plugin.hs для примера того, когда происходит установка значений по умолчанию, и test-defaulting-plugin-fail.hs для примера того, когда значения по умолчанию не подходят и не применяются.

7.3.8. Управление повторной компиляцией

По умолчанию модули, скомпилированные с плагинами, всегда перекомпилируются, даже если исходный файл не изменён. Этот наиболее консервативный вариант выбран из-за возможности плагинов выполнять произвольные действия ввода-вывода. Для управления поведением повторной компиляции можно изменить поле pluginRecompile в Plugin.

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install,
  pluginRecompile = purePlugin
  }

Проверив пример plugin выше, мы видим, что он является чистым. Это означает, что если у двух модулей одинаковый отпечаток, то плагин всегда вернёт тот же результат. Объявление плагина как чистого означает, что плагин никогда не заставит перекомпилировать модуль.

В общем случае поле pluginRecompile имеет следующий тип:

pluginRecompile :: [CommandLineOption] -> IO PluginRecompile

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

data PluginRecompile = ForceRecompile | NoForceRecompile | MaybeRecompile Fingerprint

Плагин, который объявляет себя нечистым с помощью ForceRecompile, всегда вызовет повторную компиляцию текущего модуля. NoForceRecompile используется для «чистых» плагинов, которые не нужно перевыполнять, если модуль обычно не перекомпилируется. MaybeRecompile вычисляет Fingerprint, и если этот Fingerprint отличается от ранее вычисленного Fingerprint для плагина, то мы перекомпилируем модуль.

Следовательно, purePlugin определён как функция, которая всегда возвращает NoForceRecompile.

purePlugin :: [CommandLineOption] -> IO PluginRecompile
purePlugin _ = return NoForceRecompile

Пользователи могут использовать те же функции, что и GHC, для внутреннего вычисления отпечатков. Модуль GHC.Fingerprint предоставляет полезные функции для построения отпечатков. Например, объединение fingerprintFingerprints и fingerprintString позволяет просто и наивно создать отпечаток аргументов плагина.

pluginFlagRecompile :: [CommandLineOption] -> IO PluginRecompile
pluginFlagRecompile =
  return . MaybeRecompile . fingerprintFingerprints . map fingerprintString . sort

defaultPlugin определяет pluginRecompile как impurePlugin, что является наиболее консервативным и обратным совместимым вариантом.

impurePlugin :: [CommandLineOption] -> IO PluginRecompile
impurePlugin _ = return ForceRecompile

7.3.9. Плагины front-end

Плагин front-end позволяет добавить новые основные режимы в GHC. Вы можете предпочесть это традиционной программе, которая вызывает API GHC, так как GHC обрабатывает множество флагов разбора и административных мелочей, которые могут быть трудно управлять вручную. Чтобы загрузить плагин front-end, экспортированный Foo.FrontendPlugin, просто вызовите GHC со флагом --frontend ⟨module⟩ следующим образом:

$ ghc --frontend Foo.FrontendPlugin ...other options...

Плагины front-end, как и плагины компилятора, экспортируются зарегистрированными плагинами. Однако, в отличие от модулей компилятора, плагины front-end — это модули, которые экспортируют по крайней мере один идентификатор frontendPlugin типа GHC.Plugins.FrontendPlugin.

FrontendPlugin экспортирует поле frontend, которое является функцией [String] -> [(String, Maybe Phase)] -> Ghc (). Первый аргумент — список дополнительных флагов, переданных front-endu с помощью -ffrontend-opt; второй аргумент — список аргументов, обычно исходных файлов и имён модулей для компиляции (Phase указывает, был ли установлен флаг -x), а front-end просто выполняет некоторую операцию в монаде Ghc (которая, помимо прочего, имеет Session).

В качестве быстрого примера приведён плагин front-end, который печатает переданные ему аргументы и затем завершается.

module DoNothing.FrontendPlugin (frontendPlugin) where
import GHC.Plugins

frontendPlugin :: FrontendPlugin
frontendPlugin = defaultFrontendPlugin {
  frontend = doNothing
  }

doNothing :: [String] -> [(String, Maybe Phase)] -> Ghc ()
doNothing flags args = do
    liftIO $ print flags
    liftIO $ print args

При условии, что вы скомпилировали этот плагин и зарегистрировали его в пакете, вы можете использовать его, указав --frontend DoNothing.FrontendPlugin в командной строке для GHC.

7.3.10. Плагины DynFlags

Плагин DynFlags позволяет изменить DynFlags, которое GHC будет использовать при обработке данного (набора) файла(ов). DynFlags — это запись, содержащая всевозможные данные конфигурации и командной строки, от уровня подробности до используемой библиотеки целых чисел, включая хуки компилятора, плагины и параметры форматирования. Плагины DynFlags позволяют авторам плагинов обновлять любые из этих значений до того, как GHC начнёт реальную работу, что эффективно означает, что внесённые плагином изменения будут приняты во внимание и повлияют на поведение GHC.

Одной из мотивирующих задач была возможность регистрации хуков компилятора из плагина. Например, можно было бы изменить способ выполнения кода Template Haskell. Это достигается обновлением поля hooks типа DynFlags, записывая наш пользовательский «мета-хук» в нужное место. Пример такого простого применения показан ниже:

module DynFlagsPlugin (plugin) where

import BasicTypes
import GHC.Plugins
import GHC.Hs.Expr
import Language.Haskell.Syntax.Extension
import GHC.Hs.Lit
import Hooks
import GHC.Tc.Utils.Monad

plugin :: Plugin
plugin = driverPlugin { driverPlugin = hooksP }

hooksP :: [CommandLineOption] -> HscEnv -> IO HscEnv
hooksP opts hsc_env = do
    let hooks'   = (hsc_hooks hsc_env)
                    { runMetaHook = Just (fakeRunMeta opts) }
        hsc_env' = hsc_env { hsc_hooks = hooks' }
    return hsc_env'

-- This meta hook doesn't actually care running code in splices,
-- it just replaces any expression splice with the "0"
-- integer literal, and errors out on all other types of
-- meta requests.
fakeRunMeta :: [CommandLineOption] -> MetaHook TcM
fakeRunMeta opts (MetaE r) _ = do
  liftIO . putStrLn $ "Options = " ++ show opts
  pure $ r zero

  where zero :: LHsExpr GhcPs
        zero = L noSrcSpan $ HsLit NoExtField $
          HsInt NoExtField (mkIntegralLit (0 :: Int))

fakeRunMeta _ _ _ = error "fakeRunMeta: unimplemented"

Этот простой плагин перехватывает выполнение кода Template Haskell, заменяя любые встречающиеся выражения вставки на 0 (типа Int) и выдаёт ошибку при любом другом типе вставки.

Таким образом, если мы запустим GHC против следующего кода с помощью плагина:

{-# OPTIONS -fplugin=DynFlagsPlugin #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where

main :: IO ()
main = print $( [|1|] )

Это не фактически не вычислит [|1|], а вместо этого заменит его на литерал 0 :: Int.

Как и другие типы плагинов, вы можете написать плагины DynFlags, которые могут использовать опции, которые вы можете указать с помощью флага -fplugin-opt. В коде DynFlagsPlugin из примера указанные параметры будут доступны в аргументе opts функции hooksP.

Наконец, поскольку эти обновления DynFlags происходят после загрузки плагинов, из плагина DynFlags невозможно зарегистрировать другие плагины, просто добавив их в поле plugins типа DynFlags. Для достижения этой цели необходимо загрузить их самостоятельно и сохранить результат в поле cachedPlugins типа DynFlags.

7.4. Обращение к бэкендам

В версиях GHC до и включая 9.4, бэкенд ссылается по имени: тип Backend, из модуля GHC.Driver.Backend, является простым перечислением. В версиях GHC 9.6 и выше, Backend — это абстрактный тип. Модуль определяет предикаты и функции, связанные с бэкендом.

Это изменение представления требует изменений в клиентском коде.

7.4.1. Клиентский код, который только называет бэкенды

Предположим, ваш клиент использует Backend только для упоминания бэкендов по имени. То есть, он никогда не различает бэкенды в выражении case, определении функции или сравнении на равенство. Тогда самый простой способ мигрировать ваш код — заменить каждый конструктор значения из версии 9.4 соответствующим значением из 9.6:

Старое значение

Новое значение

NCG

ncgBackend

LLVM

llvmBackend

ViaC

viaCBackend

Interpreter

interpreterBackend

NoBackend

noBackend

7.4.2. Клиентский код, который различает бэкенды

Предположим, ваш код принимает решения на основе значения выражения типа Backend. Тогда самый простой способ мигрировать ваш код принятия решений зависит от формы кода.

  • Если ваше принятие решений основано на предикате равенства или неравенства, эквивалентный предикат может быть уже определен в модуле GHC.Driver.Backend. Например, если ваш клиент хочет убедиться, что уровни оптимизации выше -O0 разрешены, он первоначально мог сравнивать backend /= Interpreter. Но теперь есть предикат для этого: это not (backendForcesOptimization0 backend).

    Если нужный вам предикат еще не определен, вам придется использовать более общий подход, описанный ниже.

  • Если ваше принятие решений всё ещё основано на предикате, но реализация предиката проверяет форму Backend, у вас всё ещё может быть удача. Например, если вашему клиенту нужно знать, хочет ли Backend писать файлы на диск, он может запросить backendWritesFiles backend. В версии 9.4 этот предикат выполняется для бэкендов NCG, LLVM и Via-C, но не для интерпретатора или NoBackend.
  • В общем случае, для любого определения функции, выражения case или теста на равенство, которые различают бэкенды, вы можете использовать общий подход к миграции, описанный ниже.

7.4.3. Общий подход к миграции клиентского кода

Начиная с версии 9.6, каждый бэкенд может быть запрошен для получения его имени:

backendName :: Backend -> BackendName

Тип BackendName должен быть импортирован из модуля GHC.Driver.Backend.Internal. Он определяется так же, как и старый тип Backend:

data BackendName
   = NCG
   | LLVM
   | ViaC
   | Interpreter
   | NoBackend

Этот тип также является экземпляром классов Eq и Show.

Если ваш существующий код различает существующие бэкенды, используя выражение case, вам нужно применить backendName к объекту scrutinee.

case backend dflags of  -- code using the 9.4 interface
  NCG -> ...
  LLVM -> ...
  ...

может стать

case backendName $ backend dflags of  -- code using the 9.6 interface
  NCG -> ...
  LLVM -> ...
  ...

Изменяется только scrutinee, а не совпадения по шаблонам. И если ваши совпадения по шаблонам были полными раньше, они всё ещё полные.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.12.1/docs/users_guide/extending_ghc.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API