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]
После вызова плагина 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.