patuni/Unify.lhs
2024-06-23 18:03:32 +02:00

639 lines
26 KiB
Text

%if False
> module Unify where
> import Control.Monad.Except (catchError, throwError, when)
> import Control.Monad.Reader (ask)
> import Data.List ((\\))
> import Data.Maybe (isNothing)
> import Data.Set (Set)
> import qualified Data.Set as Set
> import Unbound.Generics.LocallyNameless (aeq, unbind, subst, substs, Fresh)
> import Kit (pp)
> import Tm (VAL(..), Elim(..), Head(..), Twin(..),
> pattern SET, pattern PI, pattern SIG, pattern PAIR,
> Nom, Type, Subs,
> ($$), (%%), ($*$),
> var, lam, lams, lams', _Pi, _Pis, (-->),
> freshNom, compSubs, occurrence, toVars, linearOn,
> isStrongRigid, isRigid, isFlexible, fvs, fmvs, isVar)
> import Check (checkProb, check, typecheck, equal, isReflexive)
> import Context (Entry(..), ProblemState(..), Problem(..), Equation(..),
> Dec(..), Param(..), Contextual, ProbId(..),
> allProb, allTwinsProb, wrapProb,
> pushL, popL, pushR, popR,
> lookupVar, localParams, lookupMeta)
> notSubsetOf :: Ord a => Set a -> Set a -> Bool
> a `notSubsetOf` b = not (a `Set.isSubsetOf` b)
> vars :: Ord a => [(a, b)] -> Set a
> vars = Set.fromList . map fst
> active :: ProbId -> Equation -> Contextual ()
> block :: ProbId -> Equation -> Contextual ()
> failed :: ProbId -> Equation -> String -> Contextual ()
> solved :: ProbId -> Equation -> Contextual ()
> simplify :: ProbId -> Problem -> [Problem] -> Contextual ()
> active n q = putProb n (Unify q) Active
> block n q = putProb n (Unify q) Blocked
> failed n q e = putProb n (Unify q) (Failed e)
> solved n q = pendingSolve n (Unify q) []
> putProb :: ProbId -> Problem -> ProblemState ->
> Contextual ()
> putProb x q s = do
> _Gam <- ask
> pushR . Right $ Prob x (wrapProb _Gam q) s
> pendingSolve :: ProbId -> Problem -> [ProbId] ->
> Contextual ()
> pendingSolve n q [] = do checkProb Solved q `catchError`
> (error . (++ "when checking problem " ++
> pp n ++ " : " ++ pp q))
> putProb n q Solved
> pendingSolve n q ds = putProb n q (Pending ds)
> simplify n q rs = help rs []
> where
> help :: [Problem] -> [ProbId] -> Contextual ()
> help [] xs = pendingSolve n q xs
> help (p:ps) xs = subgoal p (\ x -> help ps (x:xs))
>
> subgoal :: Problem -> (ProbId -> Contextual a) ->
> Contextual a
> subgoal p f = do
> x <- ProbId <$> freshNom
> _Gam <- ask
> pushL $ Prob x (wrapProb _Gam p) Active
> a <- f x
> goLeft
> return a
> goLeft :: Contextual ()
> goLeft = popL >>= pushR . Right
> hole :: [(Nom, Type)] -> Type ->
> (VAL -> Contextual a) -> Contextual a
> define :: [(Nom, Type)] -> Nom -> Type -> VAL ->
> Contextual ()
> hole _Gam _T f = do check SET (_Pis _Gam _T) `catchError`
> (error . (++ "\nwhen creating hole of type " ++
> pp (_Pis _Gam _T)))
> x <- freshNom
> pushL $ E x (_Pis _Gam _T) HOLE
> a <- f (N (Meta x) [] $*$ _Gam)
> goLeft
> return a
> defineGlobal :: Nom -> Type -> VAL -> Contextual a ->
> Contextual a
> defineGlobal x _T v m = do check _T v `catchError`
> (error . (++ "\nwhen defining " ++
> pp x ++ " : " ++ pp _T ++
> " to be " ++ pp v))
> pushL $ E x _T (DEFN v)
> pushR (Left [(x, v)])
> a <- m
> goLeft
> return a
> define _Gam x _T v =
> defineGlobal x (_Pis _Gam _T) (lams' _Gam v) (return ())
%endif
\subsection{Unification}
\label{subsec:impl:unification}
As we have seen, unification splits into four main cases:
$\eta$-expanding elements of $\Pi$ or $\Sigma$ types, rigid-rigid
equations between non-metavariable terms (Figure~\ref{fig:decompose}),
flex-rigid equations between a metavariable and a rigid term
(Figure~\ref{fig:flex-rigid}), and flex-flex equations between two
metavariables (Figure~\ref{fig:flex-flex}). When the equation is
between two functions, we use twins (see
subsection~\ref{subsec:twins}) to decompose it heterogeneously. If it
is in fact homogeneous, the twins will be simplified away later.
We will take slight liberties, here and elsewhere, with the Haskell
syntax. In particular, we will use italicised capital letters (e.g.\
|_A|) for Haskell variables representing types in the object language,
while sans-serif letters (e.g.\ |A|) will continue to stand for data
constructors.
> unify :: ProbId -> Equation -> Contextual ()
>
> unify n q@(EQN (PI _A _B) f (PI _S _T) g) = do
> x <- freshNom
> let (xL, xR) = (N (Var x TwinL) [], N (Var x TwinR) [])
> simplify n (Unify q) [ Unify (EQN SET _A SET _S),
> allTwinsProb x _A _S (Unify (EQN (_B $$ xL) (f $$ xL) (_T $$ xR) (g $$ xR)))]
> unify n q@(EQN (SIG _A _B) t (SIG _C _D) w) =
> simplify n (Unify q) [ Unify (EQN _A a _C c)
> , Unify (EQN (_B $$ a) b (_D $$ c) d)]
> where (a, b) = (t %% Hd, t %% Tl)
> (c, d) = (w %% Hd, w %% Tl)
>
> unify n q@(EQN _ (N (Meta _) _) _ (N (Meta _) _)) =
> tryPrune n q $ tryPrune n (sym q) $ flexFlex n q
>
> unify n q@(EQN _ (N (Meta _) _) _ _) =
> tryPrune n q $ flexRigid [] n q
>
> unify n q@(EQN _ _ _ (N (Meta _) _)) =
> tryPrune n (sym q) $ flexRigid [] n (sym q)
>
> unify n q = rigidRigid q >>=
> simplify n (Unify q) . map Unify
Here |sym| swaps the two sides of an equation:
> sym :: Equation -> Equation
> sym (EQN _S s _T t) = EQN _T t _S s
\subsection{Rigid-rigid decomposition}
\label{subsec:impl:rigid-rigid}
A rigid-rigid equation (between two non-metavariable terms) can either
be decomposed into simpler equations or it is impossible to solve. For
example, $[[Pi A B == Pi S T]]$ splits into $[[A == S]], [[B == T]]$,
but $[[Pi A B == Sigma S T]]$ cannot be solved.
%% The |rigidRigid|
%% function implements the steps shown in Figure~\ref{fig:decompose}.
%% excluding the $\eta$-expansion steps, which are handled by |unify|
%% above.
> rigidRigid :: Equation -> Contextual [Equation]
> rigidRigid (EQN SET SET SET SET) = return []
>
> rigidRigid (EQN SET (PI _A _B) SET (PI _S _T)) =
> return [ EQN SET _A SET _S
> , EQN (_A --> SET) _B (_S --> SET) _T]
>
> rigidRigid (EQN SET (SIG _A _B) SET (SIG _S _T)) =
> return [ EQN SET _A SET _S
> , EQN (_A --> SET) _B (_S --> SET) _T]
>
> rigidRigid (EQN _S (N (Var x w) ds) _T (N (Var y w') es))
> | x == y = do
> _X <- lookupVar x w
> _Y <- lookupVar y w'
> (EQN SET _X SET _Y :) <$>
> matchSpine _X (N (Var x w) []) ds
> _Y (N (Var y w') []) es
>
> rigidRigid _ = throwError "Rigid-rigid mismatch"
When we have the same rigid variable (or twins) at the head on both
sides, we proceed down the spine, demanding that projections are
identical and unifying terms in applications. Note that |matchSpine|
heterogeneously unfolds the types of the terms being applied to
determine the types of the arguments. For example, if
$[[x : Pi a : A . B a -> C]]$ then the constraint $[[x s t == x u v]]$
will decompose into
$[[(s : A) == (u : A) && (t : B s) == (v : B u)]]$.
> matchSpine :: Type -> VAL -> [Elim] ->
> Type -> VAL -> [Elim] ->
> Contextual [Equation]
> matchSpine (PI _A _B) u (A a:ds)
> (PI _S _T) v (A s:es) =
> (EQN _A a _S s :) <$>
> matchSpine (_B $$ a) (u $$ a) ds (_T $$ s) (v $$ s) es
> matchSpine (SIG _A _B) u (Hd:ds) (SIG _S _T) v (Hd:es) =
> matchSpine _A (u %% Hd) ds _S (v %% Hd) es
> matchSpine (SIG _A _B) u (Tl:ds) (SIG _S _T) v (Tl:es) =
> matchSpine (_B $$ a) b ds (_T $$ s) t es
> where (a, b) = (u %% Hd, u %% Tl)
> (s, t) = (v %% Hd, v %% Tl)
> matchSpine _ _ [] _ _ [] = return []
> matchSpine _ _ _ _ _ _ = throwError "spine mismatch"
\subsection{Flex-rigid equations}
\label{subsec:impl:flex-rigid}
A flex-rigid unification problem is one where one side is an applied
metavariable and the other is a non-metavariable term. We move left
in the context, accumulating a list of metavariables that the term
depends on (the bracketed list of entries $[[XI]]$ in the rules).
Once we reach the target metavariable, we attempt to find a solution
by inversion. This implements the steps in
Figure~\ref{fig:flex-rigid}, as described in
subsection~\ref{subsec:spec:flex-rigid}.
> flexRigid :: [Entry] -> ProbId -> Equation -> Contextual ()
> flexRigid _Xi n q@(EQN _ (N (Meta alpha) _) _ _) = do
> _Gam <- ask
> popL >>= \ e -> case e of
> E beta _T HOLE
> | alpha == beta && alpha `elem` fmvs _Xi -> pushL e >>
> mapM_ pushL _Xi >>
> block n q
> | alpha == beta -> mapM_ pushL _Xi >>
> tryInvert n q _T
> ( block n q >>
> pushL e)
> | beta `elem` fmvs (_Gam, _Xi, q) -> flexRigid (e : _Xi) n q
> _ -> pushR (Right e) >>
> flexRigid _Xi n q
%if False
> flexRigid _ _ q = error $ "flexRigid: " ++ show q
%endif
Given a flex-rigid or flex-flex equation whose head metavariable has
just been found in the context, the |tryInvert| control operator calls
|invert| to seek a solution to the equation. If it finds one, it
defines the metavariable and leaves the equation in the context (so
the definition will be substituted out and the equation found to be
reflexive by the constraint solver). If |invert| cannot find a
solution, it runs the continuation.
> tryInvert :: ProbId -> Equation -> Type -> Contextual () ->
> Contextual ()
> tryInvert n q@(EQN _ (N (Meta alpha) es) _ s) _T k =
> invert alpha _T es s >>= \case
> Nothing -> k
> Just v -> active n q >>
> define [] alpha _T v
%if False
> tryInvert _ _ q _ = error $ "tryInvert: " ++ show q
%endif
Given a metavariable $[[alpha]]$ of type $[[T]]$, spine
$[[</ ei // i/>]]$ and term $[[t]]$, |invert| attempts to find a value
for $[[alpha]]$ that solves the equation
$[[alpha </ ei // i /> == t]]$. It may also throw an error
if the problem is unsolvable due to an impossible (strong rigid)
occurrence.
> invert :: Nom -> Type -> [Elim] -> VAL ->
> Contextual (Maybe VAL)
> invert alpha _T es t = do
> let o = occurrence [alpha] t
> when (isStrongRigid o) $ throwError "occurrence"
> case toVars es of
> Just xs | isNothing o && linearOn t xs -> do
> b <- localParams (const []) (typecheck _T (lams xs t))
> return $ if b then Just (lams xs t) else Nothing
> _ -> return Nothing
Here |toVars :: [Elim] -> Maybe [Nom]| tries to convert a spine to a
list of variables, and |linearOn :: VAL -> [Nom] -> Bool| determines
if a list of variables is linear on the free variables of a term. Note
that we typecheck the solution |lams xs t| under no parameters, so
typechecking will fail if an out-of-scope variable is used.
\subsection{Flex-flex equations}
\label{subsec:impl:flex-flex}
A flex-flex unification problem is one where both sides are applied
metavariables. As in the flex-rigid case, we proceed leftwards
through the context, looking for one of the metavariables so we can
try to solve it with the other. This implements the steps in
Figure~\ref{fig:flex-flex}, as described in
subsection~\ref{subsec:spec:flex-flex}.
> flexFlex :: ProbId -> Equation -> Contextual ()
> flexFlex n q@(EQN _ (N (Meta alpha) ds) _ (N (Meta beta) es)) = do
> _Gam <- ask
> popL >>= \ e -> case e of
> E gamma _T HOLE
> | gamma == alpha && gamma == beta -> block n q >>
> tryIntersect alpha _T ds es
> | gamma == alpha -> tryInvert n q _T
> (flexRigid [e] n (sym q))
> | gamma == beta -> tryInvert n (sym q) _T
> (flexRigid [e] n q)
> | gamma `elem` fmvs (_Gam, q) -> pushL e >>
> block n q
> _ -> pushR (Right e) >>
> flexFlex n q
%if False
> flexFlex _ q = error $ "flexFlex: " ++ show q
%endif
%% Consider the case $[[alpha </ ei // i /> == beta </ xj // j />]]$
%% where $[[</ xj // j />]]$ is a list of variables but
%% $[[</ ei // i />]]$ is not. If we reach $[[alpha]]$ first then we
%% might get stuck even if we could potentially solve for
%% $[[beta]]$. This would be correct if order were important in the
%% metacontext, for example if we wanted to implement let-generalisation
%% \citep{gundry2010}. Here it is not, so we can simply pick up
%% $[[alpha]]$ and carry on moving left.
When we have a flex-flex equation with the same metavariable on both
sides, $[[alpha </ xi // i /> == alpha </ yi // i />]]$, where
$[[</ xi // i />]]$ and $[[</ yi // i />]]$ are both lists of
variables, we can solve the equation by restricting $[[alpha]]$ to the
arguments on which $[[</ xi // i />]]$ and $[[</ yi // i />]]$ agree
(i.e. creating a new metavariable $[[beta]]$ and using it to solve
$[[alpha]]$). The |tryIntersect| control operator tests if both spines are
lists of variables, then calls |intersect| to generate a restricted
type for the metavariable. If this succeeds, it creates a new
metavariable and solves the old one. Otherwise, it leaves the old
metavariable in the context.
> tryIntersect :: Nom -> Type -> [Elim] -> [Elim] ->
> Contextual ()
> tryIntersect alpha _T ds es = case (toVars ds, toVars es) of
> (Just xs, Just ys) -> intersect [] [] _T xs ys >>= \case
> Just (_U, f) -> hole [] _U $ \ beta ->
> define [] alpha _T (f beta)
> Nothing -> pushL (E alpha _T HOLE)
> _ -> pushL (E alpha _T HOLE)
Given the type of $[[alpha]]$ and the two spines, |intersect| produces
a type for $[[beta]]$ and a term with which to solve $[[alpha]]$ given
$[[beta]]$. It accumulates lists of the original and retained
parameters (|_Phi| and |_Psi| respectively).
> intersect :: Fresh m => [(Nom, Type)] -> [(Nom, Type)] ->
> Type -> [Nom] -> [Nom] ->
> m (Maybe (Type, VAL -> VAL))
> intersect _Phi _Psi _S [] []
> | fvs _S `Set.isSubsetOf` vars _Psi = return $ Just
> (_Pis _Psi _S, \ beta -> lams' _Phi (beta $*$ _Psi))
> | otherwise = return Nothing
> intersect _Phi _Psi (PI _A _B) (x:xs) (y:ys) = do
> z <- freshNom
> let _Psi' = _Psi ++ if x == y then [(z, _A)] else []
> intersect (_Phi ++ [(z, _A)]) _Psi' (_B $$ var z) xs ys
%if False
> intersect _ _ _ _ _ = error "intersect: ill-typed!"
%endif
Note that we have to generate fresh names in case the renamings are
not linear. Also note that the resulting type is well-formed: if the
domain of a $\Pi$ depends on a previous variable that was removed,
then the renamings will not agree, so it will be removed as well.
\subsection{Pruning}
\label{subsec:impl:pruning}
When we have a flex-rigid or flex-flex equation, we might be able to
make some progress by pruning the metavariables contained within it,
as described in Figure~\ref{fig:pruning} and
subsection~\ref{subsec:spec:pruning}. The |tryPrune| control operator calls
|prune|, and if it learns anything from pruning, leaves the current
problem where it is and instantiates the pruned metavariable. If not,
it runs the continuation.
> tryPrune :: ProbId -> Equation ->
> Contextual () -> Contextual ()
> tryPrune n q@(EQN _ (N (Meta _) ds) _ t) k = do
> _Gam <- ask
> u <- prune (Set.toList $ vars _Gam Set.\\ fvs ds) t
> case u of
> d:_ -> active n q >> instantiate d
> [] -> k
%if False
> tryPrune _ q _ = error $ "tryPrune: " ++ show q
%endif
Pruning a term requires traversing it looking for occurrences of
forbidden variables. If any occur rigidly, the corresponding
constraint is impossible. On the other hand, if we encounter a
metavariable, we observe that it cannot depend on any arguments that
contain rigid occurrences of forbidden variables. This can be
implemented by replacing it with a fresh variable of restricted type.
The |prune| function generates a list of triples |(beta, _U, f)| where
|beta| is a metavariable, |_U| is a type for a new metavariable
|gamma| and |f gamma| is a solution for |beta|. We maintain the
invariant that |_U| and |f gamma| depend only on metavariables defined
prior to |beta| in the context.
> prune :: [Nom] -> VAL ->
> Contextual [(Nom, Type, VAL -> VAL)]
> prune _ SET = return []
> prune xs (PI _S _T) = (++) <$> prune xs _S <*> prune xs _T
> prune xs (SIG _S _T) = (++) <$> prune xs _S <*> prune xs _T
> prune xs (PAIR s t) = (++) <$> prune xs s <*> prune xs t
> prune xs (L b) = prune xs . snd =<< unbind b
> prune xs (N (Var z _) es)
> | z `elem` xs = throwError "pruning error"
> | otherwise = concat <$> mapM pruneElim es
> where pruneElim (A a) = prune xs a
> pruneElim _ = return []
> prune xs (N (Meta beta) es) = do
> _T <- lookupMeta beta
> maybe [] (\ (_U, f) -> [(beta, _U, f)]) <$>
> pruneSpine [] [] xs _T es
%if False
> prune _ (C {}) = error "concat <$> mapM (prune xs) ts"
%endif
Once a metavariable has been found, |pruneSpine| unfolds its type and
inspects its arguments, generating lists of unpruned and pruned
arguments (|_Phi| and |_Psi|). If an argument contains a rigid
occurrence of a forbidden variable, or its type rigidly depends on a
previously removed argument, then it is removed. Ultimately, it
generates a simplified type for the metavariable if the codomain type
does not depend on a pruned argument.
> pruneSpine :: [(Nom, Type)] -> [(Nom, Type)] ->
> [Nom] -> Type -> [Elim] ->
> Contextual (Maybe (Type, VAL -> VAL))
> pruneSpine _Phi _Psi xs (PI _A _B) (A a:es)
> | not stuck = do
> z <- freshNom
> let _Psi' = _Psi ++ if pruned then [] else [(z, _A)]
> pruneSpine (_Phi ++ [(z, _A)]) _Psi' xs (_B $$ var z) es
> | otherwise = return Nothing
> where
> o = occurrence xs a
> o' = occurrence (Set.toList $ vars _Phi Set.\\ vars _Psi) _A
> pruned = isRigid o || isRigid o'
> stuck = isFlexible o || (isNothing o && isFlexible o')
> || (not pruned && not (isVar a))
> pruneSpine _Phi _Psi _ _T [] | fvs _T `Set.isSubsetOf` vars _Psi && _Phi /= _Psi =
> return $ Just (_Pis _Psi _T, \ v -> lams' _Phi (v $*$ _Psi))
> pruneSpine _ _ _ _ _ = return Nothing
After pruning, we can |instantiate| a pruned metavariable by moving
left through the context until we find the relevant metavariable, then
creating a new metavariable and solving the old one.
> instantiate :: (Nom, Type, VAL -> VAL) -> Contextual ()
> instantiate d@(alpha, _T, f) = popL >>= \ e -> case e of
> E beta _U HOLE | alpha == beta -> hole [] _T $ \ t ->
> define [] beta _U (f t)
> _ -> pushR (Right e) >>
> instantiate d
\subsection{Metavariable and problem simplification}
\label{subsec:impl:simplification}
Given a problem, the |solver| simplifies it according to the rules in
Figure~\ref{fig:solve}, introduces parameters and calls |unify| from
subsection~\ref{subsec:impl:unification}. In particular, it removes
$\Sigma$-types from parameters, potentially eliminating projections,
and replaces twins whose types are definitionally equal with a normal
parameter.
> solver :: ProbId -> Problem -> Contextual ()
> solver n (Unify q) = isReflexive q >>= \ b ->
> if b then solved n q
> else unify n q `catchError` failed n q
> solver n (All p b) = do
> (x, q) <- unbind b
> case p of
> _ | x `notElem` fvs q -> simplify n (All p b) [q]
> P _S -> splitSig [] x _S >>= \case
> Just (y, _A, z, _B, s, _) ->
> solver n (allProb y _A (allProb z _B (subst x s q)))
> Nothing -> localParams (++ [(x, P _S)]) $ solver n q
> Twins _S _T -> equal SET _S _T >>= \ c ->
> if c then solver n (allProb x _S (subst x (var x) q))
> else localParams (++ [(x, Twins _S _T)]) $ solver n q
>
\newpage
Given the name and type of a metavariable, |lower| attempts to
simplify it by removing $\Sigma$-types, according to the metavariable
simplification rules in Figure~\ref{fig:solve}. If it cannot be
simplified, it appends it to the (left) context.
> lower :: [(Nom, Type)] -> Nom -> Type -> Contextual ()
> lower _Phi alpha (SIG _S _T) = hole _Phi _S $ \ s ->
> hole _Phi (_T $$ s) $ \ t ->
> define _Phi alpha (SIG _S _T) (PAIR s t)
>
> lower _Phi alpha (PI _S _T) = do
> x <- freshNom
> splitSig [] x _S >>= maybe
> (lower (_Phi ++ [(x, _S)]) alpha (_T $$ var x))
> (\ (y, _A, z, _B, s, (u, v)) ->
> hole _Phi (_Pi y _A (_Pi z _B (_T $$ s))) $ \ w ->
> define _Phi alpha (PI _S _T) (lam x (w $$ u $$ v)))
>
> lower _Phi alpha _T = pushL (E alpha (_Pis _Phi _T) HOLE)
Both |solver| and |lower| above need to split $\Sigma$-types (possibly
underneath a bunch of parameters) into their components. For example,
$[[y : Pi x : X . Sigma S T]]$ splits into $[[y0 : Pi x : X . S]]$ and
$[[y1 : Pi x : X . T (y0 x)]]$. Given the name of a variable and its
type, |splitSig| attempts to split it, returning fresh variables for
the two components of the $\Sigma$-type, an inhabitant of the original
type in terms of the new variables and inhabitants of the new types by
projecting the original variable.
> splitSig :: Fresh m => [(Nom, Type)] -> Nom -> Type ->
> m (Maybe (Nom, Type, Nom, Type,
> VAL, (VAL, VAL)))
> splitSig _Phi x (SIG _S _T) = do
> y <- freshNom
> z <- freshNom
> return $ Just (y, _Pis _Phi _S, z, _Pis _Phi (_T $$ var y),
> lams' _Phi (PAIR (var y $*$ _Phi) (var z $*$ _Phi)),
> (lams' _Phi (var x $*$ _Phi %% Hd),
> lams' _Phi (var x $*$ _Phi %% Tl)))
> splitSig _Phi x (PI _A _B) = do
> a <- freshNom
> splitSig (_Phi ++ [(a, _A)]) x (_B $$ var a)
> splitSig _ _ _ = return Nothing
\subsection{Solvitur ambulando}
\label{subsec:impl:ambulando}
We organise constraint solving via an automaton that lazily propagates
a substitution rightwards through the metacontext, making progress on
active problems and maintaining the invariant that the entries to the
left include no active problems. This is not the only possible
strategy: indeed, it is crucial for guaranteeing most general
solutions that solving the constraints in any order would produce the
same result.
A problem may be in any of five possible states: |Active| and ready to
be worked on; |Blocked| and unable to make progress in its current
state; |Pending| the solution of some other problems in order to
become solved itself; |Solved| as it has become reflexive; or |Failed|
because it is unsolvable. The specification simply removes constraints
that are pending or solved, and represents failed constraints as
failure of the whole process, but in practice it is often useful to have a
more fine-grained representation.
< data ProblemState = Active | Blocked | Pending [ProbId]
< | Solved | Failed String
In the interests of simplicity, |Blocked| problems do not store any
information about when they may be resumed, and applying a
substitution that modifies them in any way makes them |Active|. A
useful optimisation would be to track the conditions under which they
should become active, typically when particular metavariables are
solved or types become definitionally equal.
The |ambulando| automaton carries a list of problems that have been
solved, for updating the state of subsequent problems, and a
substitution with definitions for metavariables.
> ambulando :: [ProbId] -> Subs -> Contextual ()
> ambulando ns theta = popR >>= \case
> -- if right context is empty, stop
> Nothing -> return ()
> -- compose suspended substitutions
> Just (Left theta') -> ambulando ns (compSubs theta theta')
> -- process entries
> Just (Right e) -> case update ns theta e of
> Prob n p Active -> pushR (Left theta) >>
> solver n p >>
> ambulando ns []
> Prob n p Solved -> pushL (Prob n p Solved) >>
> ambulando (n:ns) theta
> E alpha _T HOLE -> lower [] alpha _T >>
> ambulando ns theta
> e' -> pushL e' >>
> ambulando ns theta
Given a list of solved problems, a substitution and an entry, |update|
returns a modified entry with the substitution applied and the problem
state changed if appropriate.
> update :: [ProbId] -> Subs -> Entry -> Entry
> update _ theta (Prob n p Blocked) = Prob n p' k
> where p' = substs theta p
> k = if p `aeq` p' then Blocked else Active
> update ns theta (Prob n p (Pending ys))
> | null rs = Prob n p' Solved
> | otherwise = Prob n p' (Pending rs)
> where rs = ys \\ ns
> p' = substs theta p
> update _ _ e'@(Prob _ _ Solved) = e'
> update _ _ e'@(Prob _ _ (Failed _)) = e'
> update _ theta e' = substs theta e'