packages feed

stock-0.1.0.0: plugin/Stock/Derive.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DerivingVia #-}
{-# OPTIONS_GHC -Wno-x-partial #-}              -- 'prodCon' head: guarded by the product contract
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -- 'classDict' always returns @EvExpr@

-- | The Stock extension SDK.
--
-- A /synthesizer/ for a class @Cls@ is a function @Class -> Datatype
-- -> Synth EvTerm@: given a structural view of the wrapped type,
-- build the class dictionary as Core — the same static, zero-cost
-- evidence the built-in synthesizers produce (no @Generic@, no
-- runtime @Rep@).
--
-- The only non-trivial primitive a synthesizer needs beyond the
-- structure is 'field': request the dictionary for a field's type and
-- continue with its evidence.  This is the \"continuation\" that lets
-- a synthesizer pause, have GHC solve a sub-constraint, and resume —
-- so @Eq@ (consumer), @Functor@ (transformer) and @Arbitrary@
-- (producer) are all just monadic programs over the same structure.
--
-- Companion packages register support for a new class by writing an
-- instance of 'DeriveStock'; the plugin discovers and runs it.  This
-- module deliberately depends only on @ghc@ + @base@, so companions
-- stay light.
module Stock.Derive
  ( -- * Structural view
    Datatype(..)
  , Constructor(..)
    -- * The synthesis monad
  , Synth
  , runSynth
  , synthTc
  , liftTc
  , field
  , fresh
  , castInto
  , classDict
  , classDictWith
  , classMethod
    -- * SOP-style sum-of-products combinators (generics-sop flavour)
  , productCon
  , matchSOP
  , injectSOP
  , fromProduct
  , toProduct
  , pureFields
  , cpureFields
  , mapFields
  , cmapFields
  , zipFields
  , czipFields
  , foldlFields
  , cfoldlFields
  , traverseFields
  , ctraverseFields
    -- * The witness interface
  , Deriver(..)
  , DeriveStock(..)
  , Deriver1(..)
  , DeriveStock1(..)
  , Deriver2(..)
  , DeriveStock2(..)
  ) where

import GHC.Plugins
import GHC.Tc.Plugin (TcPluginM, newWanted, unsafeTcPluginTcM, tcLookupId)
import GHC.Tc.Types.Constraint (Ct, mkNonCanonical, ctEvExpr)
#if MIN_VERSION_ghc(9,12,0)
import GHC.Tc.Types.CtLoc (CtLoc)
#else
import GHC.Tc.Types.Constraint (CtLoc)
#endif
import GHC.Tc.Types.Evidence (EvTerm(EvExpr))
import GHC.Core.Predicate (mkClassPred)
import GHC.Core.Class (Class, classMethods, classOpItems)
import GHC.Types.Fixity (Fixity)
import Control.Monad (forM, foldM)
import Stock.Trans (ReaderT(..), WriterT(..))
-- The kinds for the witness-class indices.  (GHC.Plugins' unqualified @Type@ is
-- the compiler's AST type; here we need the /kind/ @Data.Kind.Type@.)
import qualified Data.Kind as K

-- ---------------------------------------------------------------------------
-- Structural view of the wrapped type
-- ---------------------------------------------------------------------------

-- | What a synthesizer sees when solving @C (Stock T)@: the via-target it is
-- building the instance for (@Stock T@), the underlying analysed type (@T@),
-- the newtype-unwrap coercion between them, and @T@'s constructors.  Field
-- types are already instantiated at @T@'s actual arguments.
data Datatype = Datatype
  { dtVia    :: Type            -- ^ the via-target, e.g. @Stock T@ — what the instance is /for/
  , dtUnwrap :: Coercion        -- ^ @dtVia ~R dtType@ (newtype unwrap)
  , dtType   :: Type            -- ^ the underlying type, e.g. @T@ (or @T a@)
  , dtCons   :: [Constructor]
  }

-- | One constructor of the analysed type: its 'DataCon', field types
-- (instantiated at @T@'s arguments), fixity, and record labels if any.
data Constructor = Constructor
  { conDataCon :: DataCon
  , conFields  :: [Type]        -- ^ field types the synthesizer sees, instantiated at
                                --   @T@'s arguments — the /modifier/ types where a field
                                --   is overridden (see "Stock.Override"), else the real ones
  , conFixity  :: Fixity        -- ^ for infix constructors (default @defaultFixity@)
  , conLabels  :: Maybe [FieldLabel]  -- ^ record selectors, if a record
  , conFieldCos :: [Coercion]   -- ^ per field, @realFieldType ~R conFields!!i@; 'Refl' when
                                --   the field is not overridden.  'matchSOP'\/'injectSOP'
                                --   apply these so synthesizers see only 'conFields'.
  }

-- ---------------------------------------------------------------------------
-- The synthesis monad: a writer of emitted wanteds over a reader of the CtLoc
-- ---------------------------------------------------------------------------

-- | A Core-building computation that may request sub-instances (emitting
-- wanted constraints) and allocate fresh binders.  Structurally this is a
-- reader of the 'CtLoc' over a writer of emitted 'Ct's over 'TcPluginM', so we
-- derive the monad instances straight from that stack (the representations are
-- coercible).
newtype Synth a = Synth (CtLoc -> TcPluginM (a, [Ct]))
  deriving (Functor, Applicative, Monad)
    via ReaderT CtLoc (WriterT [Ct] TcPluginM)

-- | Run a synthesizer at a constraint location, collecting the wanteds it
-- emitted (to be returned to GHC alongside the solution).
runSynth :: CtLoc -> Synth a -> TcPluginM (a, [Ct])
runSynth loc (Synth g) = g loc

-- | Build a @Synth@ from a location-dependent, wanted-emitting action — the
-- inverse of 'runSynth'.  Lets a raw @CtLoc -> TcPluginM (EvTerm, [Ct])@
-- synthesizer be presented as a @Deriver@ (see @viaSynth@ in the plugin).
synthTc :: (CtLoc -> TcPluginM (a, [Ct])) -> Synth a
synthTc = Synth

-- | Lift a plugin action.
liftTc :: TcPluginM a -> Synth a
liftTc m = Synth \_ -> do a <- m; pure (a, [])

-- | The continuation: request the dictionary for @C ty@ and resume with its
-- evidence.  Emits the wanted so GHC solves it (possibly via this very plugin,
-- enabling recursion into the field's own instance).
field :: Class -> Type -> Synth CoreExpr
field cls ty = Synth \loc -> do
  ev <- newWanted loc (mkClassPred cls [ty])
  pure (ctEvExpr ev, [mkNonCanonical ev])

-- | A fresh local binder of the given type.
fresh :: Type -> String -> Synth Id
fresh ty s = liftTc $ do
  u <- unsafeTcPluginTcM getUniqueM
  pure (mkLocalId (mkSystemName u (mkVarOcc s)) manyDataConTy ty)

-- | A class method selected by its source name — order-independent, unlike
-- indexing @classMethods@ positionally (whose order can differ across GHC
-- versions).  Panics if the class has no such method (a plugin bug).
classMethod :: String -> Class -> Id
classMethod name cls =
  case filter ((== name) . occNameString . occName) (classMethods cls) of
    (m : _) -> m
    []      -> pprPanic "stock: classMethod: no method" (text name <+> ppr cls)

-- | Apply a class's dictionary constructor: @C:Cls \@ty m1 .. mn@.
classDict :: Class -> Type -> [CoreExpr] -> EvTerm
classDict cls ty methods =
  EvExpr (mkApps (Var (dataConWorkId (classDataCon cls))) (Type ty : methods))

-- | Build a (recursive) dictionary giving explicit superclass dictionaries and
-- implementations for the listed method indices; every other method is taken
-- from the class's own default (applied to the recursive dictionary).  Lets a
-- synthesizer fill a many-method class from a few key methods — e.g.
-- @Hashable@ from just @hashWithSalt@ (its @hash@ has a default), with its
-- @Eq@ superclass supplied as @[field eqCls ty]@.
classDictWith :: Class -> Type -> [CoreExpr] -> [(Int, CoreExpr)] -> Synth EvTerm
classDictWith cls ty supers overrides = do
  dvar    <- fresh (mkClassPred cls [ty]) "dict"
  methods <- forM (zip [0 ..] (classMethods cls)) \(i, _) ->
    case lookup i overrides of
      Just e  -> pure e
      Nothing -> case snd (classOpItems cls !! i) of
        Just (nm, _) -> do dm <- liftTc (tcLookupId nm)
                           pure (mkApps (Var dm) [Type ty, Var dvar])
        Nothing      -> error "stock: classDictWith: method has no default and no override"
  let EvExpr con = classDict cls ty (supers ++ methods)
  pure (EvExpr (Let (Rec [(dvar, con)]) (Var dvar)))

-- ---------------------------------------------------------------------------
-- SOP-style sum-of-products combinators
-- ---------------------------------------------------------------------------
--
-- A thin @generics-sop@ flavour over the structural view.  The correspondence:
--
-- @
--   generics-sop          role             Stock.Derive
--   ────────────          ────             ────────────
--   from x  (NS elim, ∃)  sum   dispatch    matchSOP   dt r x (\\i con fs -> …)
--   to . inj (NS intro)   sum   build       injectSOP  dt con fs
--   cpure_NP  (NP, Π)     field tabulate    cpureFields C k con
--   hcmap     (NP)        field map          cmapFields  C k con xs
--   cliftA2_NP (NP)       field zip          czipFields  C k con xs ys
--   hcfoldl   (NP)        field collapse     cfoldlFields C step z con xs
-- @
--
-- The split mirrors @SOP f = NS (NP f)@: 'matchSOP'\/'injectSOP' handle the
-- /sum/ (the @NS@, an existential over constructors); the @cpure@\/@cmap@\/
-- @czip@\/@cfoldl@ family handle one constructor's /product/ (its @NP@, the
-- representable @Fin n -> f@).  Because the @NP@ combinators take a
-- @Constructor@, they compose inside 'matchSOP' for any constructor of a sum,
-- not just the sole product one.  The @All c xs@ constraint is implicit: the
-- @c@-prefixed combinators call 'field' per field for the @cls@ dictionary.
-- So @eqDeriver@ (sum), @semigroupDeriver@\/@monoidDeriver@ (product) and
-- companions like @stock-hashable@\/@NFData@ read like their generic kin.

-- | The constructor of a product (the sole one).  Exported so product
-- synthesizers can feed it to the per-@Constructor@ NP combinators below.
productCon :: Datatype -> Constructor
productCon = head . dtCons

-- | The SOP eliminator (@from@ + @case@): scrutinise a value of the via-type
-- and dispatch on its constructor.  @k idx con fields@ builds the @resTy@-typed
-- branch body for constructor @con@ (index @idx@ in 'dtCons') with its bound
-- field expressions.  One alternative per constructor — so this is the
-- sum-of-products generalisation of 'fromProduct'.
matchSOP :: Datatype -> Type -> CoreExpr
         -> (Int -> Constructor -> [CoreExpr] -> Synth CoreExpr) -> Synth CoreExpr
matchSOP dt resTy v k = do
  cb   <- fresh (dtType dt) "s"
  alts <- forM (zip [0 ..] (dtCons dt)) \(i, c) -> do
    -- bind each pattern var at the /real/ field type (the coercion's LHS),
    -- then present the continuation the value coerced to the modifier type.
    xs   <- mapM (\co -> fresh (coercionLKind co) "x") (conFieldCos c)
    body <- k i c (zipWith castInto (map Var xs) (conFieldCos c))
    pure (Alt (DataAlt (conDataCon c)) xs body)
  pure (Case (Cast v (dtUnwrap dt)) cb resTy alts)

-- | @x |> co@, skipping the cast entirely when @co@ is reflexive (the
-- not-overridden case) so the generated Core stays byte-identical.
castInto :: CoreExpr -> Coercion -> CoreExpr
castInto e co = if isReflCo co then e else Cast e co

-- | The SOP introducer (@inj@ + @to@): build a value of the via-type from a
-- chosen constructor and one expression per its fields.
injectSOP :: Datatype -> Constructor -> [CoreExpr] -> CoreExpr
injectSOP dt c es =
  Cast (mkCoreConApps (conDataCon c) (map Type (tyConAppArgs (dtType dt)) ++ es'))
       (mkSymCo (dtUnwrap dt))
  where -- each result comes back at the modifier type; coerce it to the real
        -- field type before reapplying the constructor (no-op when reflexive).
        es' = zipWith (\e co -> castInto e (mkSymCo co)) es (conFieldCos c)

-- | @productTypeFrom@ + a continuation: the single-constructor case of
-- 'matchSOP'.
fromProduct :: Datatype -> Type -> CoreExpr -> ([CoreExpr] -> Synth CoreExpr)
            -> Synth CoreExpr
fromProduct dt resTy v k = matchSOP dt resTy v \_ _ fields -> k fields

-- | @productTypeTo@: the single-constructor case of 'injectSOP'.
toProduct :: Datatype -> [CoreExpr] -> CoreExpr
toProduct dt = injectSOP dt (productCon dt)

-- The NP combinators below all operate on ONE @Constructor@ (≅ one @NP@), so
-- they compose directly inside 'matchSOP' for /any/ constructor of a sum — not
-- just the sole product one.  An @NP@ is the representable functor @Fin n ->
-- f@: 'pureFields' tabulates it, 'mapFields'\/'zipFields' act positionwise, and
-- 'foldlFields' collapses it.  Each has a @c@onstrained sibling that requests
-- the field's @cls@ dictionary (the implicit @All c xs@).

-- | @pure_NP@ \/ @cpure_NP@: one result per field, produced from its type
-- (@cpure@ also from its @cls@ dictionary, e.g. @k ft d = mempty \@ft d@).
pureFields :: (Type -> Synth a) -> Constructor -> Synth [a]
pureFields k con = mapM k (conFields con)

-- | 'pureFields' that also hands each field its own @cls@ dictionary.
cpureFields :: Class -> (Type -> CoreExpr -> CoreExpr) -> Constructor -> Synth [CoreExpr]
cpureFields cls k = pureFields \ft -> do d <- field cls ft; pure (k ft d)

-- | @hmap@ \/ @hcmap@: map over the fields positionwise (the basic @NP@ action;
-- @cmap@ also hands each field's @cls@ dictionary to the step).
mapFields :: (Type -> CoreExpr -> Synth a) -> Constructor -> [CoreExpr] -> Synth [a]
mapFields k con xs = sequence (zipWith k (conFields con) xs)

-- | 'mapFields' that also hands each field its own @cls@ dictionary.
cmapFields :: Class -> (Type -> CoreExpr -> CoreExpr -> CoreExpr) -> Constructor -> [CoreExpr] -> Synth [CoreExpr]
cmapFields cls k = mapFields \ft x -> do d <- field cls ft; pure (k ft d x)

-- | @liftA2_NP@ \/ @cliftA2_NP@: combine two field-lists positionwise (@czip@
-- via each field's @cls@ dictionary, e.g. @k ft d x y = (\<>) \@ft d x y@).
zipFields :: (Type -> CoreExpr -> CoreExpr -> Synth a)
          -> Constructor -> [CoreExpr] -> [CoreExpr] -> Synth [a]
zipFields k con xs ys = sequence (zipWith3 k (conFields con) xs ys)

-- | 'zipFields' that also hands each field its own @cls@ dictionary.
czipFields :: Class -> (Type -> CoreExpr -> CoreExpr -> CoreExpr -> CoreExpr)
           -> Constructor -> [CoreExpr] -> [CoreExpr] -> Synth [CoreExpr]
czipFields cls k = zipFields \ft x y -> do d <- field cls ft; pure (k ft d x y)

-- | @hfoldl@ \/ @cfoldl_NP@: collapse the fields left-to-right through an
-- accumulator @a@ (any host value — a 'CoreExpr', a list, …).  The step of the
-- unconstrained 'foldlFields' has full @Synth@ access (request any/no/several
-- dictionaries); 'cfoldlFields' hands it each field's @cls@ dictionary.  Needed
-- by accumulating classes, e.g. @Hashable@'s @hashWithSalt@ threading the salt.
foldlFields :: (a -> Type -> CoreExpr -> Synth a) -> a -> Constructor -> [CoreExpr] -> Synth a
foldlFields step z con fields =
  foldM (\acc (ft, e) -> step acc ft e) z (zip (conFields con) fields)

-- | 'foldlFields' that also hands each field its own @cls@ dictionary.
cfoldlFields :: Class
             -> (CoreExpr -> Type -> CoreExpr -> CoreExpr -> Synth CoreExpr)  -- ^ @acc ft dict field@
             -> CoreExpr            -- ^ initial accumulator
             -> Constructor -> [CoreExpr] -> Synth CoreExpr
cfoldlFields cls step =
  foldlFields \acc ft e -> do d <- field cls ft; step acc ft d e

-- | @htraverse@ over a list of fields: produce one @a@ per field in @Synth@.
-- The most general traversal-shaped combinator — used for applicative-effectful
-- work like generating `Gen a` values (for @Arbitrary@).  'traverseFields' has
-- full @Synth@ access; 'ctraverseFields' hands each field's @cls@ dictionary
-- to the step.
traverseFields :: (Type -> CoreExpr -> Synth a) -> Constructor -> [CoreExpr] -> Synth [a]
traverseFields k con xs = sequence (zipWith k (conFields con) xs)

-- | @hctraverse@: the @c@onstrained 'traverseFields' — requests each field's
-- @cls@ dictionary and hands it to the step (alongside the field value).
-- Used by @Arbitrary@ (request `Arbitrary ft` per field, emit `Gen ft`),
-- @CoArbitrary@ (request `CoArbitrary ft`, emit function generator),
-- and @Shrink@ (request `Shrink ft`, emit shrink list).
ctraverseFields :: Class
                -> (Type -> CoreExpr -> CoreExpr -> Synth CoreExpr)  -- ^ @ft dict field@
                -> Constructor -> [CoreExpr] -> Synth [CoreExpr]
ctraverseFields cls k = traverseFields \ft e -> do d <- field cls ft; k ft d e

-- ---------------------------------------------------------------------------
-- The witness interface
-- ---------------------------------------------------------------------------

-- | A class's synthesizer, keyed by the wrapper arity it works through.
newtype Deriver = Deriver { runDeriver :: Class -> Datatype -> Synth EvTerm }

-- | Register synthesis of a class @cls@ derived @via Stock@.  The method does
-- not mention @cls@, so the plugin selects the instance by looking it up in the
-- instance environment rather than by ordinary dispatch.
class DeriveStock (cls :: K.Type -> K.Constraint) where
  deriveStock :: Deriver

-- | A @Stock1@ synthesizer for a @(Type -> Type)@ class: given the class, the
-- constraint location, the via-target @Stock1 F@ and the inner @F@, build the
-- dictionary — or 'Nothing' if a field shape is unsupported.  (Lifted classes
-- need the parameter-variance walk, so they get the raw form rather than the
-- 'Datatype'-based 'Deriver'; the @Stock1@ 'TyCon' is recoverable as
-- @tyConAppTyCon@ of the via-target.)
newtype Deriver1 = Deriver1
  { runDeriver1 :: Class -> CtLoc -> Type -> Type -> TcPluginM (Maybe (EvTerm, [Ct])) }

-- | Register synthesis of a @(Type -> Type)@ class derived @via Stock1@ (the
-- lifted counterpart of 'DeriveStock' — e.g. @NFData1@, @Hashable1@).
class DeriveStock1 (cls :: (K.Type -> K.Type) -> K.Constraint) where
  deriveStock1 :: Deriver1

-- | The @Stock2@ analogue of 'Deriver1': given the class, the location, the
-- via-target @Stock2 P@ and the inner @P@, build the dictionary.
newtype Deriver2 = Deriver2
  { runDeriver2 :: Class -> CtLoc -> Type -> Type -> TcPluginM (Maybe (EvTerm, [Ct])) }

-- | Register synthesis of a @(Type -> Type -> Type)@ class derived @via Stock2@
-- (e.g. @NFData2@).
class DeriveStock2 (cls :: (K.Type -> K.Type -> K.Type) -> K.Constraint) where
  deriveStock2 :: Deriver2