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; например, нельзя выполнить функцию из компилируемого модуля.
Точнее говоря, аннотация
{-# 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⟩, где ⟨module⟩ — модуль зарегистрированного пакета, экспортирующий плагин. Плагины загружаются по порядку: сначала указанные в командной строке и флагах Cabal, а затем указанные в прагмах OPTIONS, обрабатываемых в порядке следования в файле. Аргументы плагинам можно передать с помощью параметра -fplugin-opt=⟨module⟩:⟨args⟩. Список включённых плагинов можно сбросить с помощью параметра -fclear-plugins.
-
-fplugin=⟨module⟩ -
Загрузить плагин из указанного модуля. Модуль должен входить в пакет, зарегистрированный в базе данных пакетов GHC.
-
-fplugin-opt=⟨module⟩:⟨args⟩ -
Передать аргументы модулю плагина; модуль должен быть указан с помощью
-fplugin=⟨module⟩. Порядок прагм плагинов имеет значение, а порядок прагм аргументов — нет. Один и тот же набор аргументов передаётся всем плагинам из одного модуля.-- 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"]'.
В качестве альтернативы плагины Core можно задавать с помощью Template Haskell.
addCorePlugin "Foo.Plugin"
Это добавляет плагин в качестве прохода преобразования Core в Core. В отличие от -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 передаёт GHC флаги-plugin-package-id ⟨pkg-id⟩. Параметр-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.
Вот простой пример плагина, который ничего не делает, возвращает исходный конвейер компиляции без изменений и выводит «Hello»:
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 должен вывести «Hello».
Поддерживается и запуск нескольких плагинов: для этого передайте несколько параметров -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. Подробнее о плагинах Core
CoreToDo — это, по сути, тип данных, описывающий все виды проходов оптимизации Core, выполняемых GHC. Среди них есть проходы упрощения, CSE и другие. Для плагинов предусмотрен отдельный вариант — CoreDoPluginPass :: String -> PluginPass -> CoreToDo; именно его следует всегда использовать для добавления собственного прохода в конвейер. Первый параметр — имя плагина, второй — проход, который требуется добавить.
CoreM — это монада, в которой выполняются все оптимизации Core.
Функция установки плагина (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 модуля изменяется обычным плагином Core, изменённые привязки могут попасть в развёртки интерфейсного файла модуля. Это может быть нежелательно, поскольку плагин способен вносить изменения, влияющие на встраивание или оптимизацию.
Чтобы не вносить такие изменения в интерфейсный файл, можно использовать поздние плагины. Они несколько отличаются от обычных плагинов Core:
- Они выполняются не в монаде
CoreM. Вместо этого им явно передаётсяHscEnv, а сами они выполняются вIO. - Вместо
ModGutsим передаётсяCgGuts.CgGuts— это ограниченная формаModGuts, предназначенная для генерации кода. ЗначениеCoreProgramв переданном позднему плагинуCgGutsуже будет полностью оптимизировано. - Они должны сохранять
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 см. в документации по API GHC.
7.3.4. Плагины проверки типов
Помимо плагинов Core, 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 = ...
Эта функция вызывается двумя разными способами:
- после упрощения ограничений Given, когда плагин получает возможность переписать Given;
- после того, как GHC попытался решить ограничения Wanted.
Эти два случая можно различить по ограничениям Wanted: в первом случае (и только в нём) плагину передаётся пустой список ограничений Wanted.
Затем плагин может вернуть:
- решённые ограничения, которые будут удалены из множества инертных ограничений;
- новые ограничения, которые будут добавлены в список работ;
- несовместные ограничения, которые будут выданы как ошибки.
Плагин должен возвращать ограничения того же вида: в случае (1) — только Given, а в случае (2) — только Wanted; все остальные ограничения будут проигнорированы.
Если плагин не может добиться прогресса, он должен вернуть TcPluginSolveResult [] [] []. В противном случае, если появились новые ограничения, основной решатель ограничений будет повторно вызван для их упрощения, после чего плагин будет вызван снова. Плагин отвечает за то, чтобы этот процесс в конечном итоге завершился.
Плагинам передаются все доступные ограничения (включая равенства и ограничения классов типов), но им легко отбросить не относящиеся к их предметной области ограничения, поскольку возвращать нужно только те, с которыми удалось добиться прогресса (решив их или обнаружив противоречие).
Для ограничений, решённых плагином, необходимо предоставить доказательство в виде EvTerm типа этого ограничения. Для ограничений Given это доказательство игнорируется: GHC «решает» их, просто отбрасывая. Обычно оно используется в тех случаях, когда ограничение не несёт полезной информации (например, при рефлексивных уравнениях). Для ограничений Wanted доказательство войдёт в Core-терм, сформированный после проверки типов, и может быть проверено с помощью -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
Значение UnivCo такого вида означает: «Поверьте мне: my-plugin решил это ограничение 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 — типом, в который плагин хочет переписать это применение.