hypertypes-0.2.2: src/Hyper/Class/Unify.hs
{-# LANGUAGE FlexibleContexts #-}
-- | A class for unification
module Hyper.Class.Unify
( Unify (..)
, UVarOf
, UnifyGen (..)
, BindingDict (..)
, applyBindings
, semiPruneLookup
, occursError
) where
import Control.Monad (unless)
import Control.Monad.Error.Class (MonadError (..))
import Control.Monad.Trans.Class (MonadTrans (..))
import Control.Monad.Trans.State (get, put, runStateT)
import Data.Kind (Type)
import Hyper.Class.Nodes (HNodes (..), (#>))
import Hyper.Class.Optic (HSubset (..), HSubset')
import Hyper.Class.Recursive
import Hyper.Class.Traversable (htraverse)
import Hyper.Class.ZipMatch (ZipMatch)
import Hyper.Type (HyperType, type (#))
import Hyper.Type.Pure (Pure, _Pure)
import Hyper.Unify.Constraints
import Hyper.Unify.Error (UnifyError (..))
import Hyper.Unify.QuantifiedVar (HasQuantifiedVar (..), MonadQuantify (..))
import Hyper.Unify.Term (UTerm (..), UTermBody (..), uBody)
import Hyper.Internal.Prelude
-- | Unification variable type for a unification monad
type family UVarOf (m :: Type -> Type) :: HyperType
-- | BindingDict implements unification variables for a type in a unification monad.
--
-- It is parameterized on:
--
-- * @v@: The unification variable 'HyperType'
-- * @m@: The 'Monad' to bind in
-- * @t@: The unified term's 'HyperType'
--
-- Has 2 implementations in hypertypes:
--
-- * 'Hyper.Unify.Binding.bindingDict' for pure state based unification
-- * 'Hyper.Unify.Binding.ST.stBinding' for 'Control.Monad.ST.ST' based unification
data BindingDict v m t = BindingDict
{ lookupVar :: !(v # t -> m (UTerm v # t))
, newVar :: !(UTerm v # t -> m (v # t))
, bindVar :: !(v # t -> UTerm v # t -> m ())
}
-- | @Unify m t@ enables 'Hyper.Unify.unify' to perform unification for @t@ in the 'Monad' @m@.
--
-- The 'unifyRecursive' method represents the constraint that @Unify m@ applies to all recursive child nodes.
-- It replaces context for 'Unify' to avoid @UndecidableSuperClasses@.
class
( Eq (UVarOf m # t)
, RTraversable t
, ZipMatch t
, HasTypeConstraints t
, HasQuantifiedVar t
, Monad m
, MonadQuantify (TypeConstraintsOf t) (QVar t) m
) =>
Unify m t
where
-- | The implementation for unification variables binding and lookup
binding :: BindingDict (UVarOf m) m t
-- | Handles a unification error.
--
-- If 'unifyError' is called then unification has failed.
-- A compiler implementation may present an error message based on the provided 'UnifyError' when this occurs.
unifyError :: UnifyError t # UVarOf m -> m a
default unifyError ::
(MonadError (e # Pure) m, HSubset' e (UnifyError t)) =>
UnifyError t # UVarOf m ->
m a
unifyError e =
htraverse (Proxy @(Unify m) #> applyBindings) e
>>= throwError
. (hSubset #)
\\ unifyRecursive (Proxy @m) (Proxy @t)
-- | What to do when top-levels of terms being unified do not match.
--
-- Usually this will cause a 'unifyError'.
--
-- Some AST terms could be equivalent despite not matching structurally,
-- like record field extentions with the fields ordered differently.
-- Those would override the default implementation to handle the unification of mismatching structures.
structureMismatch ::
(forall c. Unify m c => UVarOf m # c -> UVarOf m # c -> m (UVarOf m # c)) ->
t # UVarOf m ->
t # UVarOf m ->
m ()
structureMismatch _ x y = unifyError (Mismatch x y)
-- TODO: Putting documentation here causes duplication in the haddock documentation
unifyRecursive :: Proxy m -> RecMethod (Unify m) t
{-# INLINE unifyRecursive #-}
default unifyRecursive :: HNodesConstraint t (Unify m) => Proxy m -> RecMethod (Unify m) t
unifyRecursive _ _ = Dict
instance Recursive (Unify m) where
{-# INLINE recurse #-}
recurse = unifyRecursive (Proxy @m) . proxyArgument
-- | A class for unification monads with scope levels
class Unify m t => UnifyGen m t where
-- | Get the current scope constraint
scopeConstraints :: Proxy t -> m (TypeConstraintsOf t)
unifyGenRecursive :: Proxy m -> RecMethod (UnifyGen m) t
{-# INLINE unifyGenRecursive #-}
default unifyGenRecursive ::
HNodesConstraint t (UnifyGen m) => Proxy m -> RecMethod (UnifyGen m) t
unifyGenRecursive _ _ = Dict
instance Recursive (UnifyGen m) where
{-# INLINE recurse #-}
recurse = unifyGenRecursive (Proxy @m) . proxyArgument
-- | Look up a variable, and return last variable pointing to result.
-- Prunes all variables on way to point to the last variable
-- (path-compression ala union-find).
{-# INLINE semiPruneLookup #-}
semiPruneLookup ::
Unify m t =>
UVarOf m # t ->
m (UVarOf m # t, UTerm (UVarOf m) # t)
semiPruneLookup v0 =
lookupVar binding v0
>>= \case
UToVar v1 ->
do
(v, r) <- semiPruneLookup v1
bindVar binding v0 (UToVar v)
pure (v, r)
t -> pure (v0, t)
-- | Resolve a term from a unification variable.
--
-- Note that this must be done after
-- all unifications involving the term and its children are done,
-- as it replaces unification state with cached resolved terms.
{-# INLINE applyBindings #-}
applyBindings ::
forall m t.
Unify m t =>
UVarOf m # t ->
m (Pure # t)
applyBindings v0 =
do
(v1, x) <- semiPruneLookup v0
let result r = r <$ bindVar binding v1 (UResolved r)
let quantify c =
newQuantifiedVariable c
<&> (_Pure . quantifiedVar #)
>>= result
case x of
UResolving t -> occursError v1 t
UResolved t -> pure t
UUnbound c -> quantify c
USkolem c -> quantify c
UTerm b ->
do
(r, anyChild) <-
htraverse
( Proxy @(Unify m) #>
\c ->
do
get >>= lift . (`unless` bindVar binding v1 (UResolving b))
put True
applyBindings c & lift
)
(b ^. uBody)
& (`runStateT` False)
\\ unifyRecursive (Proxy @m) (Proxy @t)
_Pure # r & if anyChild then result else pure
UToVar{} -> error "lookup not expected to result in var"
UConverted{} -> error "conversion state not expected in applyBindings"
UInstantiated{} ->
-- This can happen in alphaEq,
-- where UInstantiated marks that var from one side matches var in the other.
quantify mempty
-- | Format and throw an occurs check error
occursError ::
Unify m t =>
UVarOf m # t ->
UTermBody (UVarOf m) # t ->
m a
occursError v (UTermBody c b) =
do
q <- newQuantifiedVariable c
bindVar binding v (UResolved (_Pure . quantifiedVar # q))
unifyError (Occurs (quantifiedVar # q) b)