rzk-0.10.0: src/Rzk/TypeCheck/Eval.hs
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Entering a scope, evaluation, and the tope solver.
--
-- These three are one recursive knot and cannot be separated:
--
-- * entering a binder needs 'whnfT', to see whether a flat variable is a point
-- of a cube and so brings a discreteness axiom with it;
-- * 'whnfT' strips an extension type's restrictions, which asks the solver
-- whether a face's tope holds ('checkTope');
-- * 'nfT' normalises under a binder and under a tope ('localTope');
-- * and the solver normalises the topes it reasons about ('nfTope').
module Rzk.TypeCheck.Eval where
import Control.Monad (forM, forM_, unless, when)
import Control.Monad.Except (runExcept)
import Control.Monad.Reader (ask, asks, local,
runReaderT)
import Control.Monad.Trans.Writer.CPS (runWriterT)
import Data.List (intercalate, nub, nubBy,
tails)
import Data.Maybe (catMaybes)
import Control.Monad.Foil (DExt, Distinct, NameBinder)
import qualified Control.Monad.Foil as Foil
import Control.Monad.Free.Foil (AST (Node, Var),
ScopedAST (..))
import Data.Bifunctor (Bifunctor)
import Control.Monad.Free.Foil.Annotated (AnnSig (..))
import Language.Rzk.Foil.Syntax
import Language.Rzk.Foil.Names (Binder (..), TModality (..),
TypeInfo (..), VarIdent)
import Rzk.TypeCheck.Context
import Rzk.TypeCheck.Display
import Rzk.TypeCheck.Error
import Rzk.TypeCheck.Monad
-- * Variables
-- | Look up a name and project one field of its 'VarInfo'.
infoOfVar :: (VarInfo n -> a) -> Foil.Name n -> TypeCheck n a
infoOfVar f x = asks (f . lookupVarInfo x)
valueOfVar :: Foil.Name n -> TypeCheck n (Maybe (TermT n))
valueOfVar = infoOfVar varValue
typeOfVar :: Foil.Name n -> TypeCheck n (TermT n)
typeOfVar = infoOfVar varType
modalityOfVar :: Foil.Name n -> TypeCheck n TModality
modalityOfVar = infoOfVar varModality
locksOfVar :: Foil.Name n -> TypeCheck n TModality
locksOfVar = infoOfVar varModAccum
isTopLevelVar :: Foil.Name n -> TypeCheck n Bool
isTopLevelVar = infoOfVar varIsTopLevel
-- | Is a surface name defined?
checkDefinedVar :: Distinct n => VarIdent -> TypeCheck n ()
checkDefinedVar name = asks (lookupNamed name) >>= \case
Nothing -> issueTypeError (TypeErrorUndefined name)
Just _ -> return ()
typeOfUncomputed :: TermT n -> TypeCheck n (TermT n)
typeOfUncomputed = \case
Var x -> typeOfVar x
t -> case typeInfoOf t of
Just info -> pure (infoType info)
Nothing -> panicImpossible "a node with no annotation"
typeOf :: Distinct n => TermT n -> TypeCheck n (TermT n)
typeOf t = typeOfUncomputed t >>= whnfT
-- | The free variables of a typed term, including those that occur only in the
-- /types/ of the variables it mentions.
--
-- A definition can depend on a section assumption without naming it: through the
-- type of something else it uses. Closing a section has to see that dependency, and
-- it is exactly what distinguishes an implicit assumption from an explicit one.
freeVarsDeep :: TermT n -> TypeCheck n [Foil.Name n]
freeVarsDeep t = do
ctx <- ask
let typeOfName v = varType (lookupVarInfo v ctx)
-- a node's own free variables, and those of its type
partial term = case typeInfoOf term of
Nothing -> freeVarsOfTermT term
Just info -> freeVarsOfTermT term <> freeVarsOfTermT (infoType info)
go vars latest
| null new = vars
| otherwise = go (new <> vars) (foldMap (partial . typeOfName) new)
where
new = filter (`notElemName` vars) (nubNames latest)
pure (go [] (partial t))
nubNames :: [Foil.Name n] -> [Foil.Name n]
nubNames = nubBy (\a b -> Foil.nameId a == Foil.nameId b)
elemName :: Foil.Name n -> [Foil.Name n] -> Bool
elemName x = any (\y -> Foil.nameId x == Foil.nameId y)
notElemName :: Foil.Name n -> [Foil.Name n] -> Bool
notElemName x = not . elemName x
-- * Substitution, in the monad
-- | Instantiate a scoped term with an argument: the old @substituteT@.
instantiate :: Distinct n => ScopedTermT n -> TermT n -> TypeCheck n (TermT n)
instantiate scoped arg = do
scope <- asks ctxScope
pure (instantiateT scope scoped arg)
-- * Entering a binder
-- | The discreteness axiom a flat cube variable brings with it: a flat point of
-- @2@ (or of @I@) is one of the endpoints. Maintained at binder entry so that
-- entailment does not have to rescan the context on every query.
discreteAxiomOf
:: forall n l. Distinct n
=> TModality -> TermT n -> NameBinder n l -> TypeCheck n [ModalTope l]
discreteAxiomOf Flat ty binder = whnfT ty >>= \case
Cube2T{} -> pure [endpoints cube2_0T cube2_1T]
CubeIT{} -> pure [endpoints cubeI_0T cubeI_1T]
_ -> pure []
where
z = Var (Foil.nameOf binder) :: TermT l
endpoints zero one = plainTope (topeOrT (topeEQT z zero) (topeEQT z one))
discreteAxiomOf _ _ _ = pure []
-- | What a binder adds to the context.
binderInfo
:: Binder -> TModality -> TermT n -> Maybe (TermT n) -> Maybe LocationInfo
-> VarInfo n
binderInfo orig md ty mval loc = VarInfo
{ varType = ty
, varValue = mval
, varModality = md
, varModAccum = Id
, varOrig = orig
, varIsAssumption = False
, varIsTopLevel = False
, varDeclaredAssumptions = []
, varLocation = loc
}
-- | Run an action under a binder that has already been chosen.
underBinder
:: (Distinct n, DExt n l)
=> NameBinder n l -> Binder -> TModality -> TermT n -> Maybe (TermT n)
-> TypeCheck l a -> TypeCheck n a
underBinder binder orig md ty mval action = do
ctx <- ask
discrete <- discreteAxiomOf md ty binder
let info = binderInfo orig md ty mval (ctxLocation ctx)
ctx' = enterBinder binder info discrete ctx
-- A new discreteness axiom changes the saturation input; an ordinary binder
-- carries the cached value in with the rest of the context (saturation
-- commutes with renaming).
inContext ctx' $
if null discrete then action else withRefreshedTopes id action
-- | Enter a fresh binder (one the checker invents) and run an action whose result
-- says nothing about the new scope.
withBinder
:: Distinct n
=> Binder -> TModality -> TermT n
-> (forall l. (DExt n l, Distinct l) => NameBinder n l -> TypeCheck l a)
-> TypeCheck n a
withBinder orig md ty k = do
scope <- asks ctxScope
withFreshIn scope $ \binder ->
underBinder binder orig md ty Nothing (k binder)
withFreshIn
:: Distinct n
=> Foil.Scope n
-> (forall l. (DExt n l, Distinct l) => NameBinder n l -> r)
-> r
withFreshIn scope k = Foil.withFresh scope k
-- | Open a scoped term under its own binder, run the action on the body, and pack
-- the result back up as a scoped term.
underScope
:: Distinct n
=> Binder -> TModality -> TermT n -> Maybe (TermT n)
-> ScopedTermT n
-> (forall l. (DExt n l, Distinct l) => TermT l -> TypeCheck l (TermT l))
-> TypeCheck n (ScopedTermT n)
underScope orig md ty mval scoped k = do
scope <- asks ctxScope
withScopedT scope scoped $ \binder body ->
ScopedAST binder <$> underBinder binder orig md ty mval (k body)
-- | Like 'underScope', for a Π (or a λ over a shape), which binds a tope scope
-- beside the body under what the user wrote as one binder.
underScope2
:: Distinct n
=> Binder -> TModality -> TermT n
-> ScopedTermT n -> ScopedTermT n
-> (forall l. (DExt n l, Distinct l) => TermT l -> TermT l -> TypeCheck l (TermT l, TermT l))
-> TypeCheck n (ScopedTermT n, ScopedTermT n)
underScope2 orig md ty scoped1 scoped2 k = do
scope <- asks ctxScope
withScopedT2 scope scoped1 scoped2 $ \binder body1 body2 -> do
(r1, r2) <- underBinder binder orig md ty Nothing (k body1 body2)
pure (ScopedAST binder r1, ScopedAST binder r2)
-- | Open a scoped term for a computation whose result says nothing about the new
-- scope (a check, or a rendered string).
inScope
:: (Bifunctor sig, Distinct n)
=> Binder -> TModality -> TermT n -> ScopedAST NameBinder sig n
-> (forall l. (DExt n l, Distinct l) => AST NameBinder sig l -> TypeCheck l a)
-> TypeCheck n a
inScope orig md ty = inScopeWith orig md ty Nothing
-- | Like 'inScope', for a binder that stands for a known value (a @let@).
inScopeWith
:: (Bifunctor sig, Distinct n)
=> Binder -> TModality -> TermT n -> Maybe (TermT n)
-> ScopedAST NameBinder sig n
-> (forall l. (DExt n l, Distinct l) => AST NameBinder sig l -> TypeCheck l a)
-> TypeCheck n a
inScopeWith orig md ty mval scoped k = do
scope <- asks ctxScope
withScopedT scope scoped $ \binder body ->
underBinder binder orig md ty mval (k body)
-- | Open a scoped term with a binder that has just been entered.
--
-- The scoped term lives in the enclosing scope, and so may be the codomain of the
-- type a λ is being checked against, or the tope of a shape: all of them are
-- opened under the /one/ binder the λ introduces.
openScoped
:: (Bifunctor sig, DExt n l)
=> NameBinder n l -> ScopedAST NameBinder sig n -> TypeCheck l (AST NameBinder sig l)
openScoped binder scoped = do
scope <- asks ctxScope
pure (openWith scope (Foil.nameOf binder) scoped)
-- | A scope that does not use its binder: the codomain of a non-dependent function
-- type, say.
constScope :: Distinct n => TermT n -> TypeCheck n (ScopedTermT n)
constScope t = do
scope <- asks ctxScope
pure (Foil.withFresh scope $ \binder -> ScopedAST binder (Foil.sink t))
-- | Enter the binder of an /untyped/ scope — the body of a λ, or of a let, as the
-- user wrote it — and elaborate it into a typed one.
--
-- The binder comes from the term being checked, and the scopes of the /type/ it is
-- checked against are opened under that same binder with 'openScoped'.
elaborateUnder
:: Distinct n
=> Binder -> TModality -> TermT n -> Maybe (TermT n)
-> ScopedTerm n
-> (forall l. (DExt n l, Distinct l)
=> NameBinder n l -> Term l -> TypeCheck l (TermT l))
-> TypeCheck n (ScopedTermT n)
elaborateUnder orig md ty mval scoped k = do
scope <- asks ctxScope
withScopedT scope scoped $ \binder body ->
ScopedAST binder <$> underBinder binder orig md ty mval (k binder body)
-- | Enter the binder of an untyped scope and run a computation under it.
--
-- The result may not mention the new scope — but a 'ScopedAST' /hides/ its scope,
-- so the continuation can pack whatever it built with the binder it was given and
-- hand back as many scoped terms as it likes. That is how a λ returns its
-- elaborated body, its shape tope and the type it turned out to have, all at once.
checkUnderWith
:: Distinct n
=> Binder -> TModality -> TermT n -> Maybe (TermT n) -> ScopedTerm n
-> (forall l. (DExt n l, Distinct l)
=> NameBinder n l -> Term l -> TypeCheck l a)
-> TypeCheck n a
checkUnderWith orig md ty mval scoped k = do
scope <- asks ctxScope
withScopedT scope scoped $ \binder body ->
underBinder binder orig md ty mval (k binder body)
checkUnder
:: Distinct n
=> Binder -> TModality -> TermT n -> ScopedTerm n
-> (forall l. (DExt n l, Distinct l)
=> NameBinder n l -> Term l -> TypeCheck l a)
-> TypeCheck n a
checkUnder orig md ty = checkUnderWith orig md ty Nothing
-- * Modalities
enterModality :: Distinct n => TModality -> TypeCheck n b -> TypeCheck n b
enterModality Id action = action
enterModality md action = do
ctx <- asks (applyModality md)
let ctx' = ctx { ctxTopesEntailBottom = Nothing }
-- 'applyModality' invalidated the saturation cache (accessibility changed);
-- refresh it under the shifted context.
inContext ctx' (withRefreshedTopes id action)
-- * The tope context
-- | Assume a tope for the enclosed action.
localTope :: Distinct n => TermT n -> TypeCheck n a -> TypeCheck n a
localTope tope tc = do
ctx <- ask'
tope' <- nfTope tope
let modalTope' = plainTope tope'
-- A small optimisation to help unify terms faster.
let noNewInformation = case tope' of
TopeEQT _ x y | eqT x y -> True
_ -> any (eqModalTope modalTope') (ctxTopesNF ctx)
if noNewInformation
then tc
else do
entailsBottom <- (modalTope' : ctxTopesNF ctx) `entailM` topeBottomT
withRefreshedTopes (extend modalTope' entailsBottom) tc
where
ask' = asks id
extend tope' entailsBottom ctx = ctx
{ ctxTopes = plainTope tope : ctxTopes ctx
, ctxTopesNF = tope' : ctxTopesNF ctx
, ctxTopesNFUnion = map nubModalTopes
[ new <> old
| new <- simplifyLHSwithDisjunctions [tope']
, old <- ctxTopesNFUnion ctx ]
, ctxTopesEntailBottom = Just entailsBottom
}
-- | Install a deferred saturation cache for the transformed context, and run the
-- action with it.
--
-- The pipeline's effects are discharged purely into a thunk: installing costs
-- nothing, holes recorded by the speculative run are discarded, and a pipeline
-- error (a tope guard with a hole in lenient mode, say, which the per-query path
-- would never have evaluated) becomes 'Nothing', so errors surface exactly where
-- they did before.
withRefreshedTopes
:: Distinct n
=> (Context n -> Context n) -> TypeCheck n a -> TypeCheck n a
withRefreshedTopes f action = do
ctx' <- asks f
let sat = case runExcept (runWriterT (runReaderT (saturateForEntailment (ctxTopesNF ctx')) ctx')) of
Left _ -> Nothing
Right (s, _) -> Just s
local (const ctx' { ctxTopesSaturated = SaturationCached sat }) action
-- | Run a check in every alternative of a disjunctive tope context.
inAllSubContexts :: Distinct n => TypeCheck n () -> TypeCheck n () -> TypeCheck n ()
inAllSubContexts handleSingle tc = do
topeSubContexts <- asks ctxTopesNFUnion
case topeSubContexts of
[] -> panicImpossible "empty set of alternative contexts"
[_] -> handleSingle
_:_:_ ->
forM_ topeSubContexts $ \topes' ->
withRefreshedTopes (\ctx -> ctx
{ ctxTopes = topes'
, ctxTopesNF = topes'
, ctxTopesNFUnion = [topes']
}) tc
-- * Equality of topes
eqModalTope :: Distinct n => ModalTope n -> ModalTope n -> Bool
eqModalTope l r = and
[ tModAccum l == tModAccum r
, tModVar l == tModVar r
, eqT (tTope l) (tTope r)
]
nubModalTopes :: Distinct n => [ModalTope n] -> [ModalTope n]
nubModalTopes [] = []
nubModalTopes (t : ts) = t : nubModalTopes (filter (not . eqModalTope t) ts)
elemModalTope :: Distinct n => ModalTope n -> [ModalTope n] -> Bool
elemModalTope t = any (eqModalTope t)
-- * Entailment
-- | Monadic 'all' that stops at the first failing element.
allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
allM p = go
where
go [] = return True
go (x:xs) = p x >>= \case
False -> return False
True -> go xs
entailM :: Distinct n => [ModalTope n] -> TermT n -> TypeCheck n Bool
entailM modalTopes goal = do
saturated <- saturateForEntailment modalTopes
entailSaturatedM saturated goal
-- | The preprocessing 'entailM' does before searching: dedup, split off the
-- context's disjunctions, and saturate each alternative. Depends only on the
-- given topes (plus the discreteness axioms of the context), not on the goal.
saturateForEntailment
:: Distinct n => [ModalTope n] -> TypeCheck n [[ModalTope n]]
saturateForEntailment modalTopes = do
discreteAxioms <- asks ctxDiscreteTopes
let topes' = nubModalTopes (modalTopes <> discreteAxioms)
topes'' = simplifyLHSwithDisjunctions topes'
mapM (fmap (saturateTopes . saturateBottom) . saturateInv) topes''
-- | Search each saturated alternative for the goal.
entailSaturatedM
:: Distinct n => [[ModalTope n]] -> TermT n -> TypeCheck n Bool
entailSaturatedM saturated goal = asks ctxVerbosity >>= \case
Debug -> do
naming <- asks namingOfContext
let prettyTopes = map (ppTerm naming . untyped . tTope) (concat saturated)
prettyTope = ppTerm naming (untyped goal)
traceTypeCheck Debug
("entail " <> intercalate ", " prettyTopes <> " |- " <> prettyTope) $
allM (`solveRHSM` goal) saturated
_ -> allM (`solveRHSM` goal) saturated
-- | Entailment against the context's own tope context, using the cached
-- saturation when one was installed. Matching on the payload of
-- 'SaturationCached' is what forces the deferred pipeline, so the cost is paid at
-- the first query under a context, and never for one that is never queried.
entailContextM :: Distinct n => TermT n -> TypeCheck n Bool
entailContextM goal = asks ctxTopesSaturated >>= \case
SaturationCached (Just saturated) -> entailSaturatedM saturated goal
SaturationCached Nothing -> fallback
SaturationUncached -> fallback
where
fallback = asks ctxTopesNF >>= (`entailM` goal)
-- * Saturation
saturateTopes :: Distinct n => [ModalTope n] -> [ModalTope n]
saturateTopes topes = saturated <> inaccessible
where
(accessible, inaccessible) = partitionAccessible topes
saturated = saturateWith
elemModalTope
(\new old -> map plainTope (generateTopes (map tTope new) (map tTope old)))
accessible
saturateInv :: Distinct n => [ModalTope n] -> TypeCheck n [ModalTope n]
saturateInv modalTopes = do
-- FIXME: this is a workaround; ideally we should regenerate all topes on
-- EVERY modality change in any layer, but that would produce too many; for
-- now we also invert topes that were accessible before the modality shift.
let accessible = filterAccessible modalTopes
accessibleById = filter (\mt -> coe (tModVar mt) Id) modalTopes
invResults <- forM (nubModalTopes (accessible <> accessibleById)) $ \mt -> do
nf <- nfTope $ modExtractT topeT Id Op (topeInvT (tTope mt))
return $ ModalTope (tModAccum mt) Op nf
let accessibleUnderOp =
filter (\mt -> coe (tModVar mt) (comp (tModAccum mt) Op)) modalTopes
uninvResults <- forM accessibleUnderOp $ \(ModalTope acc var' phi) -> do
nf <- nfTope $ topeUninvT (modAppT topeT Op phi)
return $ ModalTope (comp acc Op) var' nf
let newTopes = nubModalTopes (invResults <> uninvResults)
fresh = filter (\t -> not (elemModalTope t modalTopes)) newTopes
return (modalTopes <> fresh)
-- | Ex falso for BOT, lifted across modalities.
--
-- A contradiction in the topes that are genuinely available at the identity
-- modality entails BOT, and BOT entails @_μ BOT@ for every modality @μ@ by the
-- absurd rule (this holds for BOT specifically; a general tope @φ@ does NOT give
-- @_μ φ@, which would need the missing unit @id ⇒ μ@). Re-asserting @_μ BOT@ at
-- each lock @μ@ where an available tope was hidden lets the contradiction survive
-- the lock: @_b BOT@ is accessible under a @_b@ lock (@coe Flat Flat@), so
-- @mod _b recBOT@ in a vacuous context is accepted.
--
-- A tope counts as available at the identity modality when its variable modality
-- coerces into @Id@: a @_b@-modal tope qualifies via the counit (@coe Flat Id@),
-- but a @_#@-modal one does not (@coe Sharp Id@ is False) — which is exactly why
-- @_# BOT@ does not leak to plain BOT.
saturateBottom :: Distinct n => [ModalTope n] -> [ModalTope n]
saturateBottom modalTopes
| null droppedAccums = modalTopes -- nothing hidden by a lock
| botDerivable = modalTopes <> fresh
| otherwise = modalTopes
where
idAccessible = filter (\mt -> coe (tModVar mt) Id) modalTopes
droppedAccums = nub [ tModAccum mt | mt <- idAccessible, not (isAccessible mt) ]
saturatedId = saturateWith elemT generateTopes (map tTope idAccessible)
botDerivable = topeBottomT `elemT` saturatedId
fresh = [ mt
| acc <- droppedAccums
, let mt = ModalTope acc acc topeBottomT
, not (elemModalTope mt modalTopes) ]
-- FIXME: cleanup
saturateWith :: (a -> [a] -> Bool) -> ([a] -> [a] -> [a]) -> [a] -> [a]
saturateWith elem' step zs = go (nub' zs) []
where
go lastNew xs
| null new = lastNew
| otherwise = lastNew <> go new xs'
where
xs' = lastNew <> xs
new = filter (not . (`elem'` xs')) (nub' $ step lastNew xs)
nub' [] = []
nub' (x:xs) = x : nub' (filter (not . (`elem'` [x])) xs)
generateTopes :: Distinct n => [TermT n] -> [TermT n] -> [TermT n]
generateTopes newTopes oldTopes
| topeBottomT `elemT` newTopes = []
| topeEQT cube2_0T cube2_1T `elemT` newTopes = [topeBottomT]
| topeEQT cubeI_0T cubeI_1T `elemT` newTopes = [topeBottomT]
| length oldTopes > 100 = [] -- FIXME
| otherwise = concat
[ -- symmetry EQ
[ topeEQT y x | TopeEQT _ty x y <- newTopes ]
-- transitivity EQ (1)
, [ topeEQT x z
| TopeEQT _ty x y : newTopes' <- tails newTopes
, TopeEQT _ty y' z <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity EQ (2)
, [ topeEQT x z
| TopeEQT _ty y z : newTopes' <- tails newTopes
, TopeEQT _ty x y' <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity LEQ (1)
, [ topeLEQT x z
| TopeLEQT _ty x y : newTopes' <- tails newTopes
, TopeLEQT _ty y' z <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity LEQ (2)
, [ topeLEQT x z
| TopeLEQT _ty y z : newTopes' <- tails newTopes
, TopeLEQT _ty x y' <- newTopes' <> oldTopes
, eqT y y' ]
-- antisymmetry LEQ
, [ topeEQT x y
| TopeLEQT _ty x y : newTopes' <- tails newTopes
, TopeLEQT _ty y' x' <- newTopes' <> oldTopes
, eqT y y'
, eqT x x' ]
-- FIXME: special case of substitution of EQ
-- transitivity EQ-LEQ (1)
, [ topeLEQT x z
| TopeEQT _ty y z : newTopes' <- tails newTopes
, TopeLEQT _ty x y' <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity EQ-LEQ (2)
, [ topeLEQT x z
| TopeEQT _ty x y : newTopes' <- tails newTopes
, TopeLEQT _ty y' z <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity EQ-LEQ (3)
, [ topeLEQT x z
| TopeLEQT _ty y z : newTopes' <- tails newTopes
, TopeEQT _ty x y' <- newTopes' <> oldTopes
, eqT y y' ]
-- transitivity EQ-LEQ (4)
, [ topeLEQT x z
| TopeLEQT _ty x y : newTopes' <- tails newTopes
, TopeEQT _ty y' z <- newTopes' <> oldTopes
, eqT y y' ]
-- FIXME: consequence of LEM for LEQ and antisymmetry for LEQ
, [ topeEQT x y | TopeLEQT _ty x y@Cube2_0T{} <- newTopes ]
, [ topeEQT x y | TopeLEQT _ty x@Cube2_1T{} y <- newTopes ]
, [ topeEQT x y | TopeLEQT _ty x y@CubeI_0T{} <- newTopes ]
, [ topeEQT x y | TopeLEQT _ty x@CubeI_1T{} y <- newTopes ]
-- subtyping 2 <: II: endpoints and order of 2 lift to II
, [ topeEQT x cubeI_0T | TopeEQT _ty x Cube2_0T{} <- newTopes ]
, [ topeEQT cubeI_0T x | TopeEQT _ty Cube2_0T{} x <- newTopes ]
, [ topeEQT x cubeI_1T | TopeEQT _ty x Cube2_1T{} <- newTopes ]
, [ topeEQT cubeI_1T x | TopeEQT _ty Cube2_1T{} x <- newTopes ]
, [ topeLEQT x cubeI_0T | TopeLEQT _ty x Cube2_0T{} <- newTopes ]
, [ topeLEQT cubeI_0T x | TopeLEQT _ty Cube2_0T{} x <- newTopes ]
, [ topeLEQT x cubeI_1T | TopeLEQT _ty x Cube2_1T{} <- newTopes ]
, [ topeLEQT cubeI_1T x | TopeLEQT _ty Cube2_1T{} x <- newTopes ]
]
generateTopesForPointsM :: Distinct n => [TermT n] -> TypeCheck n [TermT n]
generateTopesForPointsM points = do
let endpoints = [cube2_0T, cube2_1T, cubeI_0T, cubeI_1T]
pairs = nubPairs $ concat
[ [ (x, y)
| x : points' <- tails (filter (\p -> not (p `elemT` endpoints)) points)
, y <- points'
, not (eqT x y) ]
]
stars <- forM points $ \x -> do
xType <- typeOf x
return $ case xType of
CubeUnitT{} -> [topeEQT x cubeUnitStarT]
_ -> []
topes <- forM pairs $ \(x, y) -> do
xType <- typeOf x
yType <- typeOf y
return $ case (xType, yType) of
(Cube2T{}, Cube2T{}) -> [topeOrT (topeLEQT x y) (topeLEQT y x)]
_ -> []
return (concat (topes ++ stars))
where
nubPairs [] = []
nubPairs (p@(x, y) : ps) =
p : nubPairs (filter (\(x', y') -> not (eqT x x' && eqT y y')) ps)
allTopePoints :: Distinct n => TermT n -> [TermT n]
allTopePoints = nubT . foldMap subPoints . nubT . topePoints
topePoints :: TermT n -> [TermT n]
topePoints = \case
TopeTopT{} -> []
TopeBottomT{} -> []
TopeAndT _ l r -> topePoints l <> topePoints r
TopeOrT _ l r -> topePoints l <> topePoints r
TopeEQT _ x y -> [x, y]
TopeLEQT _ x y -> [x, y]
_ -> []
subPoints :: TermT n -> [TermT n]
subPoints = \case
p@(PairT _ x y) -> p : foldMap subPoints [x, y]
p@(Var _) -> [p]
p -> case typeInfoOf p of
Just TypeInfo{ infoType = CubeUnitT{} } -> [p]
Just TypeInfo{ infoType = Cube2T{} } -> [p]
_ -> []
-- * Simplifying the left-hand side
-- | Simplify the context, including disjunctions.
simplifyLHSwithDisjunctions :: Distinct n => [ModalTope n] -> [[ModalTope n]]
simplifyLHSwithDisjunctions topes = map nubModalTopes $
case topes of
[] -> [[]]
ModalTope _ _ TopeTopT{} : topes' -> simplifyLHSwithDisjunctions topes'
ModalTope mAcc mVar TopeBottomT{} : _ -> [[ModalTope mAcc mVar topeBottomT]]
ModalTope mAcc mVar (TopeAndT _ l r) : topes' ->
simplifyLHSwithDisjunctions (ModalTope mAcc mVar l : ModalTope mAcc mVar r : topes')
-- NOTE: it is inefficient to expand disjunctions immediately
ModalTope mAcc mVar (TopeOrT _ l r) : topes' ->
simplifyLHSwithDisjunctions (ModalTope mAcc mVar l : topes')
<> simplifyLHSwithDisjunctions (ModalTope mAcc mVar r : topes')
ModalTope mAcc mVar (TopeEQT _ (PairT _ x y) (PairT _ x' y')) : topes' ->
simplifyLHSwithDisjunctions
(ModalTope mAcc mVar (topeEQT x x') : ModalTope mAcc mVar (topeEQT y y') : topes')
ModalTope mAcc mVar (TypeModalT _ md inTope) : topes' ->
simplifyLHSwithDisjunctions (ModalTope mAcc (comp mVar md) inTope : topes')
t : topes' -> map (t :) (simplifyLHSwithDisjunctions topes')
-- * Solving the right-hand side
solveRHSM :: Distinct n => [ModalTope n] -> TermT n -> TypeCheck n Bool
solveRHSM modalTopes goal =
let topes = accessibleTopes modalTopes
in case goal of
_ | topeBottomT `elemT` topes -> return True
TopeTopT{} -> return True
TypeModalT _ty md inTope -> do
let shifted = applyModalityToTopes md modalTopes
resaturated = saturateTopes shifted
resaturatedInv <- saturateInv resaturated
solveRHSM resaturatedInv inTope
TopeEQT _ty (PairT _ty1 x y) (PairT _ty2 x' y') ->
solveRHSM modalTopes $ topeAndT (topeEQT x x') (topeEQT y y')
TopeEQT _ty (PairT TypeInfo{ infoType = CubeProductT _ cubeI cubeJ } x y) r ->
solveRHSM modalTopes $ topeAndT
(topeEQT x (firstT cubeI r))
(topeEQT y (secondT cubeJ r))
TopeEQT _ty l (PairT TypeInfo{ infoType = CubeProductT _ cubeI cubeJ } x y) ->
solveRHSM modalTopes $ topeAndT
(topeEQT (firstT cubeI l) x)
(topeEQT (secondT cubeJ l) y)
TopeEQT _ty l r
| or
[ eqT l r
, goal `elemT` topes
, topeEQT r l `elemT` topes
] -> return True
TopeEQT _ty l r -> do
lType <- typeOf l
rType <- typeOf r
return $ case (lType, rType) of
(CubeUnitT{}, CubeUnitT{}) -> True
_ -> False
TopeLEQT _ty l r
| eqT l r -> return True
| solveRHS topes (topeEQT l r) -> return True
| solveRHS topes (topeEQT l cube2_0T) -> return True
| solveRHS topes (topeEQT r cube2_1T) -> return True
TopeAndT _ l r -> solveRHSM modalTopes l >>= \case
False -> return False
True -> solveRHSM modalTopes r
_ | goal `elemT` topes -> return True
TopeInvT{} -> do
goal' <- nfTope goal
case goal' of
TopeInvT{} -> return False
_ -> solveRHSM modalTopes goal'
TopeUninvT{} -> do
goal' <- nfTope goal
case goal' of
TopeUninvT{} -> return False
_ -> solveRHSM modalTopes goal'
TopeOrT _ l r -> do
found <- solveRHSM modalTopes l >>= \case
True -> return True
False -> solveRHSM modalTopes r
if found
then return True
else do
lems <- generateTopesForPointsM (allTopePoints goal)
let lems' = [ lem | lem@(TopeOrT _ t1 t2) <- lems, all (`notElemT` topes) [t1, t2] ]
(accessible, hidden) = partitionAccessible modalTopes
withTope t = hidden ++ saturateTopes (plainTope t : accessible)
case lems' of
TopeOrT _ t1 t2 : _ ->
solveRHSM (withTope t1) goal >>= \case
False -> return False
True -> solveRHSM (withTope t2) goal
_ -> return False
_ -> return False
solveRHS :: Distinct n => [TermT n] -> TermT n -> Bool
solveRHS topes tope =
case tope of
_ | topeBottomT `elemT` topes -> True
TopeTopT{} -> True
TopeEQT _ty (PairT _ty1 x y) (PairT _ty2 x' y')
| solveRHS topes (topeEQT x x') && solveRHS topes (topeEQT y y') -> True
TopeEQT _ty (PairT TypeInfo{ infoType = CubeProductT _ cubeI cubeJ } x y) r
| solveRHS topes (topeEQT x (firstT cubeI r))
, solveRHS topes (topeEQT y (secondT cubeJ r)) -> True
TopeEQT _ty l (PairT TypeInfo{ infoType = CubeProductT _ cubeI cubeJ } x y)
| solveRHS topes (topeEQT (firstT cubeI l) x)
, solveRHS topes (topeEQT (secondT cubeJ l) y) -> True
TopeEQT _ty l r -> or
[ eqT l r
, tope `elemT` topes
, topeEQT r l `elemT` topes
]
TopeLEQT _ty l r
| eqT l r -> True
| solveRHS topes (topeEQT l r) -> True
| solveRHS topes (topeEQT l cube2_0T) -> True
| solveRHS topes (topeEQT r cube2_1T) -> True
TopeAndT _ l r -> solveRHS topes l && solveRHS topes r
TopeOrT _ l r -> solveRHS topes l || solveRHS topes r
_ -> tope `elemT` topes
-- | Accumulate a modality over a list of topes.
applyModalityToTopes :: TModality -> [ModalTope n] -> [ModalTope n]
applyModalityToTopes md = map (\mt -> mt { tModAccum = comp (tModAccum mt) md })
partitionAccessible :: [ModalTope n] -> ([ModalTope n], [ModalTope n])
partitionAccessible topes = (filter isAccessible topes, filter (not . isAccessible) topes)
-- * The checks the rest of the checker calls
checkTope :: Distinct n => TermT n -> TypeCheck n Bool
checkTope tope = do
topes <- asks availableTopes
performing (ActionContextEntails topes tope) $ do
tope' <- nfTope tope
entailContextM tope'
checkTopeEntails :: Distinct n => TermT n -> TypeCheck n Bool
checkTopeEntails tope = do
topes <- asks availableTopes
performing (ActionContextEntailedBy topes tope) $ do
contextTopes <- asks availableTopesNF
restrictionTope <- nfTope tope
let contextTopesRHS = foldr topeAndT topeTopT contextTopes
[plainTope restrictionTope] `entailM` contextTopesRHS
checkEntails :: Distinct n => TermT n -> TermT n -> TypeCheck n Bool
checkEntails l r = do -- FIXME: add action
l' <- nfTope l
r' <- nfTope r
[plainTope l'] `entailM` r'
contextEntails :: Distinct n => TermT n -> TypeCheck n ()
contextEntails tope = do
topes <- asks availableTopes
performing (ActionContextEntails topes tope) $ do
topeIsEntailed <- checkTope tope
topes' <- asks availableTopesNF
-- When a hole is used in a cube/tope position (as the argument of a
-- shape-restricted function, say), the tope being checked mentions the hole
-- and cannot be decided. Treat it as satisfied (defer) rather than failing.
unless (topeIsEntailed || containsHole tope) $
issueTypeError $ TypeErrorTopeNotSatisfied topes' tope
-- | Is the local tope context contradictory (does it entail ⊥)?
contextEntailsBottom :: Distinct n => TypeCheck n Bool
contextEntailsBottom = asks ctxTopesEntailBottom >>= \case
Just entails -> pure entails
Nothing -> asks ctxTopesNF >>= (`entailM` topeBottomT)
topesEquiv :: Distinct n => TermT n -> TermT n -> TypeCheck n Bool
topesEquiv expected actual = performing (ActionUnifyTerms expected actual) $ do
expected' <- nfT expected
actual' <- nfT actual
(&&)
<$> [plainTope expected'] `entailM` actual'
<*> [plainTope actual'] `entailM` expected'
-- | Check that the local tope context is included in (entails) the union of the
-- given topes. This is the COVERAGE obligation of @recOR@: every point of the
-- context must be covered by some branch guard.
--
-- Only coverage is required, not equivalence: branch guards may overhang the
-- context (when splitting with an already-defined shape, say), so we do not
-- require @OR(guards) |- context@.
contextEntailsUnion :: Distinct n => [TermT n] -> TypeCheck n ()
contextEntailsUnion topes = do
ctxTopes' <- asks availableTopes
performing (ActionContextEntailsUnion ctxTopes' topes) $ do
contextTopes <- asks ctxTopesNF
topesNF <- mapM nfTope topes
let unionRHS = foldr topeOrT topeBottomT topesNF
entailContextM unionRHS >>= \case
-- a guard mentioning an (unfilled) hole can't be decided; defer coverage
False | not (any containsHole topesNF) ->
issueTypeError $ TypeErrorTopeNotSatisfied (accessibleTopes contextTopes) unionRHS
_ -> return ()
-- | Diagnose a @recOR@ branch guard or a restriction face against the local tope
-- context. There are three cases, by how the tope relates to the context:
--
-- * DISJOINT — the tope and a consistent context have empty overlap (their
-- conjunction is ⊥). The face or branch is then vacuous everywhere, so this is
-- a hard error.
-- * OVERHANG — the tope is not entailed by the context but still overlaps it.
-- This is allowed and often intentional (splitting or restricting with an
-- already-defined shape, whose faces live on the whole cube rather than being
-- relativised to the context), so we only emit a non-fatal hint.
-- * CONTAINED — the tope entails the context: nothing to report.
checkTopeAgainstContext :: Distinct n => String -> TermT n -> TypeCheck n ()
checkTopeAgainstContext what tope = do
-- a contradictory context is handled elsewhere (recBOT)
ctxEntailsBottom <- contextEntailsBottom
unless ctxEntailsBottom $ do
contextTopes <- asks ctxTopesNF
let topes = filter (not . eqT topeTopT) (accessibleTopes contextTopes)
disjoint <- (plainTope tope : contextTopes) `entailM` topeBottomT
-- a face or guard mentioning an (unfilled) hole can't be decided; defer
if disjoint && not (containsHole tope)
then issueTypeError (TypeErrorTopeContextDisjoint tope topes)
else do
-- The hint below is opt-in (#set-option "warn-overhang"): deciding
-- whether the tope overhangs costs a solver entailment per face and
-- guard, and overhang is legitimate.
warnOverhang <- asks ctxWarnOverhang
when warnOverhang $ do
entailed <- checkTopeEntails tope -- tope |- AND(accessible context)
unless entailed $ do
naming <- asks namingOfContext
traceTypeCheck Normal
(intercalate "\n" $
[ "Warning: " <> what <> " overhangs the local tope context"
, " " <> ppTerm naming (untyped tope)
, "is not entailed by the local context (normalised)"
] <> map ((" " <>) . ppTerm naming . untyped) topes)
(return ())
-- * Restrictions and η
stripTypeRestrictions :: TermT n -> TermT n
stripTypeRestrictions (TypeRestrictedT _ty ty _restriction) = stripTypeRestrictions ty
stripTypeRestrictions t = t
-- | The term a restriction face pins down, when one of the faces holds.
tryRestriction :: Distinct n => TermT n -> TypeCheck n (Maybe (TermT n))
tryRestriction = \case
TypeRestrictedT _ _ rs -> go rs
_ -> pure Nothing
where
go [] = pure Nothing
go ((tope, term') : rs') = checkTope tope >>= \case
True -> pure (Just term')
False -> go rs'
-- | Perform at most one η-expansion at the top level, to assist unification.
etaMatch
:: Distinct n
=> Maybe (TermT n) -> TermT n -> TermT n -> TypeCheck n (TermT n, TermT n)
-- FIXME: double check the next 3 rules
etaMatch _mterm expected@TypeRestrictedT{} actual@TypeRestrictedT{} = pure (expected, actual)
etaMatch mterm expected (TypeRestrictedT _ty ty _rs) = etaMatch mterm expected ty
etaMatch (Just term) expected@TypeRestrictedT{} actual =
etaMatch (Just term) expected (typeRestrictedT actual [(topeTopT, term)])
-- Subtyping on the interval.
etaMatch _mterm CubeIT{} Cube2T{} = pure (cubeIT, cubeIT)
etaMatch _mterm expected@LambdaT{} actual@LambdaT{} = pure (expected, actual)
etaMatch _mterm expected@PairT{} actual@PairT{} = pure (expected, actual)
etaMatch _mterm expected@LambdaT{} actual = do
actual' <- etaExpand actual
pure (expected, actual')
etaMatch _mterm expected actual@LambdaT{} = do
expected' <- etaExpand expected
pure (expected', actual)
etaMatch _mterm expected@PairT{} actual = do
actual' <- etaExpand actual
pure (expected, actual')
etaMatch _mterm expected actual@PairT{} = do
expected' <- etaExpand expected
pure (expected', actual)
etaMatch _mterm expected actual = pure (expected, actual)
etaExpand :: Distinct n => TermT n -> TypeCheck n (TermT n)
etaExpand term@LambdaT{} = pure term
etaExpand term@PairT{} = pure term
etaExpand term = do
ty <- typeOf term
case stripTypeRestrictions ty of
TypeFunT _ty orig md param mtope ret -> do
scope <- asks ctxScope
pure $ withFreshIn scope $ \binder ->
let z = Var (Foil.nameOf binder)
body = appT (openWith (Foil.extendScope binder scope) (Foil.nameOf binder) ret)
(Foil.sink term) z
mtope' = fmap (\t -> ScopedAST binder (openWith (Foil.extendScope binder scope) (Foil.nameOf binder) t)) mtope
in lambdaT ty orig
(Just (LambdaParam md param mtope'))
(ScopedAST binder body)
TypeSigmaT _ty _orig _md a b -> do
let firstTerm = firstT a term
bInstantiated <- instantiate b firstTerm
pure $ pairT ty firstTerm (secondT bInstantiated term)
CubeProductT _ty a b -> pure $
pairT ty (firstT a term) (secondT b term)
_ -> pure term
-- * Layers
inCubeLayer :: Distinct n => TermT n -> TypeCheck n Bool
inCubeLayer = \case
RecBottomT{} -> pure False
UniverseT{} -> pure False
UniverseCubeT{} -> pure True
CubeProductT{} -> pure True
CubeUnitT{} -> pure True
CubeUnitStarT{} -> pure True
Cube2T{} -> pure True
Cube2_0T{} -> pure True
Cube2_1T{} -> pure True
t -> typeOf t >>= inCubeLayer
inTopeLayer :: Distinct n => TermT n -> TypeCheck n Bool
inTopeLayer = \case
RecBottomT{} -> pure False
UniverseT{} -> pure False
UniverseCubeT{} -> pure True
UniverseTopeT{} -> pure True
CubeProductT{} -> pure True
CubeUnitT{} -> pure True
CubeUnitStarT{} -> pure True
Cube2T{} -> pure True
Cube2_0T{} -> pure True
Cube2_1T{} -> pure True
TopeTopT{} -> pure True
TopeBottomT{} -> pure True
TopeAndT{} -> pure True
TopeOrT{} -> pure True
TopeEQT{} -> pure True
TopeLEQT{} -> pure True
TypeFunT _ty orig md param _mtope ret ->
inScope orig md param ret inTopeLayer
t -> typeOfUncomputed t >>= inTopeLayer
-- * Weak head normal form
-- | Memoise a term's WHNF on its top node without reducing the term itself.
--
-- The returned term has the same (unreduced) structure, so free-variable and
-- @uses@ detection see exactly what the user wrote, while a later 'whnfT' is O(1)
-- via the cached form. Used when storing a definition's elaborated type and value,
-- where an in-place reduction could otherwise discard or expose a variable
-- occurrence.
memoizeWHNF :: Distinct n => TermT n -> TypeCheck n (TermT n)
memoizeWHNF t@(Var _) = pure t
memoizeWHNF t@(Node (AnnSig info sig)) = do
w <- whnfT t
pure (Node (AnnSig info { infoWHNF = Just w } sig))
whnfT :: Distinct n => TermT n -> TypeCheck n (TermT n)
-- A memoised weak head normal form is answered before entering 'performing',
-- which would push an action and rebuild the context just to look a value up. The
-- caches are hit constantly (every 'typeOf' consults one), and the bookkeeping
-- costs more than the answer.
whnfT t | Just info <- typeInfoOf t, Just t' <- infoWHNF info = pure t'
whnfT tt = performing (ActionWHNF tt) $ case tt of
-- universe constants
UniverseT{} -> pure tt
UniverseCubeT{} -> pure tt
UniverseTopeT{} -> pure tt
-- cube layer (except vars, pairs, and applications)
CubeProductT{} -> nfTope tt
CubeUnitT{} -> pure tt
CubeUnitStarT{} -> pure tt
Cube2T{} -> pure tt
Cube2_0T{} -> pure tt
Cube2_1T{} -> pure tt
CubeIT{} -> pure tt
CubeI_0T{} -> pure tt
CubeI_1T{} -> pure tt
CubeFlipT{} -> nfTope tt
CubeUnflipT{} -> nfTope tt
-- tope layer (except vars, pairs of points, and applications)
TopeTopT{} -> pure tt
TopeBottomT{} -> pure tt
TopeAndT{} -> nfTope tt
TopeOrT{} -> nfTope tt
TopeEQT{} -> nfTope tt
TopeLEQT{} -> nfTope tt
TopeInvT{} -> nfTope tt
TopeUninvT{} -> nfTope tt
-- type layer terms that should not be evaluated further
LambdaT{} -> pure tt
PairT{} -> pure tt
ReflT{} -> pure tt
TypeFunT{} -> pure tt
TypeSigmaT{} -> pure tt
TypeIdT{} -> pure tt
TypeModalT{} -> pure tt
RecBottomT{} -> pure tt
TypeUnitT{} -> pure tt
UnitT{} -> pure tt
-- type ascriptions are ignored, since we already have a typechecked term
TypeAscT _ty term _ty' -> whnfT term
-- check if we have a cube or a tope term (if so, compute NF)
_ -> typeOf tt >>= \case
UniverseCubeT{} -> nfTope tt
UniverseTopeT{} -> nfTope tt
TypeUnitT{} -> pure unitT -- compute an expression of Unit type to unit
-- FIXME: next line is ad hoc, should be improved!
TypeRestrictedT _info TypeUnitT{} _rs -> pure unitT
-- check if we have a cube point term (if so, compute NF)
typeOf_tt -> typeOf typeOf_tt >>= \case
UniverseCubeT{} -> nfTope tt
-- now we are in the type layer
_ -> fmap termIsWHNF $ do
tryRestriction typeOf_tt >>= \case
Just tt' -> whnfT tt'
Nothing -> case tt of
-- a hole is opaque: it never reduces, it is already a normal form
HoleT{} -> pure tt
t@(Var x) ->
valueOfVar x >>= \case
Nothing -> pure t
Just term -> whnfT term
AppT{} -> do
scope <- asks ctxScope
uncurry (applySpine scope) (collectAppSpine tt)
LetT _ty _orig _mparam val body ->
instantiate body val >>= whnfT
LetModT ty orig app inn mparam val body ->
(enterModality app $ whnfT val) >>= \case
ModAppT _ md t | md == inn -> do
val' <- enterModality md $ whnfT t
instantiate body val' >>= whnfT
b' | isRA inn -> do
bty <- typeOf b' >>= \case
TypeModalT _ _ t -> pure t
_ -> panicImpossible "not modal in letmod"
instantiate body (modExtractT bty app inn b') >>= whnfT
_ -> pure (LetModT ty orig app inn mparam val body)
FirstT ty t ->
whnfT t >>= \case
PairT _ l _r -> whnfT l
t' -> pure (FirstT ty t')
SecondT ty t ->
whnfT t >>= \case
PairT _ _l r -> whnfT r
t' -> pure (SecondT ty t')
ModAppT ty md b ->
(enterModality md $ whnfT b) >>= \case
ModExtractT _ app inn t | inn == md -> enterModality (comp md app) $ whnfT t
b' -> pure $ ModAppT ty md b'
ModExtractT ty app inn b ->
(enterModality app $ whnfT b) >>= \case
ModAppT _ md t | inn == md -> enterModality inn $ whnfT t
b' -> pure (ModExtractT ty app inn b')
IdJT ty tA a tC d x p ->
whnfT p >>= \case
ReflT{} -> whnfT d
p' -> pure (IdJT ty tA a tC d x p')
RecOrT _ty rs -> do
firstMatching rs >>= \case
Just tt' -> whnfT tt'
Nothing
| [tt'] <- nubT (map snd rs) -> whnfT tt'
| otherwise -> pure tt
TypeRestrictedT ty type_ rs -> do
rs' <- traverse (\(tope, term) -> (,) <$> nfT tope <*> pure term) rs
case filter (not . eqT topeBottomT . fst) rs' of
[] -> whnfT type_ -- get rid of restrictions at BOT
rs'' -> TypeRestrictedT ty <$> whnfT type_ <*> pure rs''
-- | The branch of a @recOR@ (or the face of a restriction) whose guard holds.
firstMatching :: Distinct n => [(TermT n, TermT n)] -> TypeCheck n (Maybe (TermT n))
firstMatching [] = pure Nothing
firstMatching ((tope, t) : rest) = checkTope tope >>= \case
True -> pure (Just t)
False -> firstMatching rest
-- * Application, reducing a whole spine at once
--
-- A curried application @f x y z@ is a left-nested tower of 'AppT'. Reducing it
-- one argument at a time rebuilds the intermediate lambdas — @f x@ produces
-- @\\ y z -> …@ only for the next argument to tear it apart — and each rebuild is
-- a full 'substituteT' traversal (~70% of beta reductions on sHoTT are such
-- spines). Instead, collect the spine, then peel the head's syntactic lambda
-- chain into a /single/ substitution: @\\ a b c -> body@ applied to @x y z@ maps
-- @{a↦x, b↦y, c↦z}@ and substitutes into @body@ once.
--
-- Sound because 'whnfT' of a lambda is the identity — a lambda, its binder
-- shape-restricted or not, is already in weak head normal form — so the
-- intermediate lambdas this skips building would have been returned unchanged,
-- and substitution composes. Beta reduction ignores the binder's domain (the
-- shape restriction is a typing obligation, not enforced during reduction); the
-- tope and modality side-conditions fire only for a /neutral/ function of
-- shape-restricted function type, which is never a lambda. The moment the body
-- is not a syntactic lambda the substitution is applied and control returns to
-- 'whnfT' via 'applySpine'; a neutral head goes to 'applyWhnfFun', unchanged.
-- | The head of an application spine and its arguments, in application order,
-- each paired with the type annotation of its 'AppT' node (needed to rebuild a
-- neutral application).
collectAppSpine :: TermT n -> (TermT n, [(TypeInfo (TermT n), TermT n)])
collectAppSpine = go []
where
go acc (AppT ty f x) = go ((ty, x) : acc) f
go acc h = (h, acc)
-- | Apply a function term to a spine of arguments, reducing.
applySpine
:: Distinct n
=> Foil.Scope n -> TermT n -> [(TypeInfo (TermT n), TermT n)] -> TypeCheck n (TermT n)
applySpine _ h [] = whnfT h
applySpine scope h pairs = whnfT h >>= \h' -> case h' of
LambdaT _ _ _ (ScopedAST binder body) | (_, x) : rest <- pairs ->
peelLambdas scope (Foil.addSubst Foil.identitySubst binder x) body rest
_ -> applyNeutral scope h' pairs
-- | Peel the head's syntactic lambda chain into one substitution, then reduce.
-- @subst@ maps the binders consumed so far to their arguments; @body@ is the
-- current lambda's body, at the scope those binders extended into.
peelLambdas
:: forall i n. Distinct n
=> Foil.Scope n -> Foil.Substitution TermT i n -> TermT i
-> [(TypeInfo (TermT n), TermT n)] -> TypeCheck n (TermT n)
peelLambdas scope subst body pairs = case pairs of
[] -> whnfT (substituteT scope subst body)
(_, x) : rest -> case body of
LambdaT _ _ _ (ScopedAST binder body') ->
peelLambdas scope (Foil.addSubst subst binder x) body' rest
_ -> applySpine scope (substituteT scope subst body) pairs
-- | Apply a non-lambda (already WHNF) function to a spine, one argument at a
-- time: this is the type-directed part of application, unchanged from before.
applyNeutral
:: Distinct n
=> Foil.Scope n -> TermT n -> [(TypeInfo (TermT n), TermT n)] -> TypeCheck n (TermT n)
applyNeutral _ h [] = pure h
applyNeutral scope h ((ty, x) : rest) = do
r <- applyWhnfFun ty h x
if null rest then pure r else applySpine scope r rest
-- | Apply a non-lambda function @f'@ (already WHNF) to one argument @x@. A
-- shape-restricted function contributes a tope side-condition; a function whose
-- return type is restricted refines the application's type; everything else is a
-- neutral application. Extracted verbatim from the old single-argument @AppT@ case.
applyWhnfFun :: Distinct n => TypeInfo (TermT n) -> TermT n -> TermT n -> TypeCheck n (TermT n)
applyWhnfFun ty f' x = typeOf f' >>= \case
TypeFunT _ty _orig md _param (Just tope) (ScopedAST _ UniverseTopeT{}) -> do
x' <- enterModality md $ nfT x
sideCondition <- instantiate tope x' >>= nfT
pure (topeAndT (AppT ty f' x') sideCondition)
-- FIXME: this seems to be a hack, and will not work in all
-- situations! FIXME: for now, it seems to add ~2x slowdown
TypeFunT info _orig md _param _mtope ret@(ScopedAST _ TypeRestrictedT{})
| TypeRestrictedT{} <- infoType info -> pure (AppT ty f' x)
| otherwise -> do
x' <- enterModality md $ whnfT x
ret' <- instantiate ret x'
tryRestriction ret' >>= \case -- FIXME: too many unnecessary checks?
Nothing -> pure (AppT ty { infoType = ret' } f' x')
Just tt' -> whnfT tt'
_ -> pure (AppT ty f' x)
-- * Normal form of the tope layer
nfTope :: Distinct n => TermT n -> TypeCheck n (TermT n)
nfTope tt = performing (ActionNF tt) $ fmap termIsNF $ case tt of
HoleT{} -> pure tt
Var x ->
valueOfVar x >>= \case
Nothing -> return tt
Just term -> nfTope term
-- see if a normal form is already available
_ | Just info <- typeInfoOf tt, Just tt' <- infoNF info -> pure tt'
-- universe constants
UniverseT{} -> pure tt
UniverseCubeT{} -> pure tt
UniverseTopeT{} -> pure tt
-- cube layer constants
CubeUnitT{} -> pure tt
CubeUnitStarT{} -> pure tt
Cube2T{} -> pure tt
Cube2_0T{} -> pure tt
Cube2_1T{} -> pure tt
CubeIT{} -> pure tt
CubeI_0T{} -> pure tt
CubeI_1T{} -> pure tt
-- type layer constants
TypeUnitT{} -> pure tt
UnitT{} -> pure tt
-- cube layer with computation
CubeProductT _ty l r -> cubeProductT <$> nfTope l <*> nfTope r
CubeFlipT ty t ->
nfTope t >>= \case
CubeUnflipT _ t' -> pure t'
Cube2_0T{} -> pure (modAppT (typeModalT cubeT Op cube2T) Op cube2_1T)
Cube2_1T{} -> pure (modAppT (typeModalT cubeT Op cube2T) Op cube2_0T)
CubeI_0T{} -> pure (modAppT (typeModalT cubeT Op cubeIT) Op cubeI_1T)
CubeI_1T{} -> pure (modAppT (typeModalT cubeT Op cubeIT) Op cubeI_0T)
t' -> pure (CubeFlipT ty t')
CubeUnflipT ty t ->
nfTope t >>= \case
CubeFlipT _ t' -> pure t'
ModAppT _ Op Cube2_0T{} -> pure cube2_1T
ModAppT _ Op Cube2_1T{} -> pure cube2_0T
ModAppT _ Op CubeI_0T{} -> pure cubeI_1T
ModAppT _ Op CubeI_1T{} -> pure cubeI_0T
t' -> pure (CubeUnflipT ty t')
-- tope layer constants
TopeTopT{} -> pure tt
TopeBottomT{} -> pure tt
-- tope layer with computation
TopeAndT ty l r ->
nfTope l >>= \case
TopeBottomT{} -> pure topeBottomT
l' -> nfTope r >>= \case
TopeBottomT{} -> pure topeBottomT
r' -> pure (TopeAndT ty l' r')
TopeOrT ty l r -> do
l' <- nfTope l
r' <- nfTope r
case (l', r') of
(TopeBottomT{}, _) -> pure r'
(_, TopeBottomT{}) -> pure l'
_ -> pure (TopeOrT ty l' r')
TopeEQT ty l r -> TopeEQT ty <$> nfTope l <*> nfTope r
TopeLEQT ty l r -> TopeLEQT ty <$> nfTope l <*> nfTope r
TopeInvT ty t ->
-- Match And/Or on the *unnormalised* input: nfTope of a shape-restricted App
-- produces a TopeAnd via shape-side-condition propagation, and distributing
-- inv over that synthetic conjunction loops forever, because the recursive
-- topeInvT renormalises the same App back into a TopeAnd.
case t of
TopeTopT _ -> pure $ modAppT topeT Op topeTopT
TopeBottomT _ -> pure $ modAppT topeT Op topeBottomT
TopeLEQT _ x y -> invOf topeLEQT x y
TopeEQT _ x y -> invOf topeEQT x y
TopeAndT _ phi psi -> nfTope $
modAppT (typeModalT universeT Op topeT) Op
(topeAndT
(modExtractT topeT Id Op (topeInvT phi))
(modExtractT topeT Id Op (topeInvT psi)))
TopeOrT _ phi psi -> nfTope $
modAppT (typeModalT universeT Op topeT) Op
(topeOrT
(modExtractT topeT Id Op (topeInvT phi))
(modExtractT topeT Id Op (topeInvT psi)))
_ ->
nfTope t >>= \case
TopeTopT _ -> pure topeTopT
TopeBottomT _ -> pure topeBottomT
TopeUninvT _ phi -> pure phi
TopeLEQT _ x y -> invOf topeLEQT x y
TopeEQT _ x y -> invOf topeEQT x y
t' -> pure (TopeInvT ty t')
where
invOf mk x y = do
xTy <- typeOf x
yTy <- typeOf y
nfTope $
modAppT (typeModalT universeT Op topeT) Op
(mk (modExtractT topeT Id Op (cubeFlipT xTy y))
(modExtractT topeT Id Op (cubeFlipT yTy x)))
TopeUninvT ty t ->
case t of
ModAppT _ Op inner -> case inner of
TopeTopT _ -> pure topeTopT
TopeBottomT _ -> pure topeBottomT
TopeAndT _ phi psi ->
nfTope (topeAndT (topeUninvT phi) (topeUninvT psi))
TopeOrT _ phi psi ->
nfTope (topeOrT (topeUninvT phi) (topeUninvT psi))
_ ->
nfTope t >>= \case
TopeTopT _ -> pure topeTopT
TopeBottomT _ -> pure topeBottomT
TopeInvT _ phi -> pure phi
ModAppT _ Op inner'' -> case inner'' of
TopeLEQT _ x y -> uninvOf topeLEQT x y
TopeEQT _ x y -> uninvOf topeEQT x y
inner' ->
pure $ TopeUninvT ty
(modAppT (typeModalT universeT Op topeT) Op inner')
t' -> pure (TopeUninvT ty t')
_ ->
nfTope t >>= \case
TopeInvT _ phi -> pure phi
t'@(ModAppT _ Op _) -> nfTope (TopeUninvT ty t')
t' -> pure (TopeUninvT ty t')
where
uninvOf mk x y = do
xTy <- typeOf x
yTy <- typeOf y
nfTope $
mk (cubeUnflipT xTy (modAppT (typeModalT cubeT Op xTy) Op y))
(cubeUnflipT yTy (modAppT (typeModalT cubeT Op yTy) Op x))
-- type ascriptions are ignored, since we already have a typechecked term
TypeAscT _ty term _ty' -> nfTope term
PairT ty l r -> PairT ty <$> nfTope l <*> nfTope r
AppT ty f x ->
nfTope f >>= \case
LambdaT _ty _orig _arg body ->
instantiate body x >>= nfTope
f' -> typeOfUncomputed f' >>= \case
TypeFunT _ty _orig md _param (Just tope) (ScopedAST _ UniverseTopeT{}) -> do
x' <- enterModality md $ nfTope x
sideCondition <- instantiate tope x' >>= nfTope
pure (topeAndT (AppT ty f' x') sideCondition)
_ -> AppT ty f' <$> nfTope x
FirstT ty t ->
nfTope t >>= \case
PairT _ty x _y -> pure x
t' -> pure (FirstT ty t')
SecondT ty t ->
nfTope t >>= \case
PairT _ty _x y -> pure y
t' -> pure (SecondT ty t')
LambdaT ty orig _mparam body
| TypeFunT _ty _origF md param mtope _ret <- infoType ty -> do
-- NOTE: the domain @param@ is left unnormalised: in the tope layer it may
-- be a shape (a function type into TOPE), which nfTope cannot normalise.
body' <- underScope orig md param Nothing body nfTope
pure (LambdaT ty orig (Just (LambdaParam md param mtope)) body')
LambdaT{} -> panicImpossible "lambda with a non-function type in the tope layer"
ModAppT ty md b ->
(enterModality md $ nfTope b) >>= \case
ModExtractT _ _ inn t | inn == md -> pure t
b' -> pure $ ModAppT ty md b'
ModExtractT ty app inn b ->
(enterModality app $ nfTope b) >>= \case
ModAppT _ md t | inn == md -> pure t
b' -> pure $ ModExtractT ty app inn b'
LetModT ty orig app inn mparam val body ->
(enterModality app $ nfTope val) >>= \case
ModAppT _ md t | md == inn ->
instantiate body t >>= nfTope
b' | isRA inn -> do
bty <- typeOf b' >>= \case
TypeModalT _ _ t -> pure t
_ -> panicImpossible "not modal in letmod"
instantiate body (modExtractT bty app inn b') >>= nfTope
b' -> do
bty <- typeOf b' >>= \case
TypeModalT _ _ t -> pure t
_ -> panicImpossible "not modal in letmod"
val' <- enterModality app $ nfTope b'
body' <- underScope orig (comp app inn) bty Nothing body nfTope
pure (LetModT ty orig app inn mparam val' body')
TypeModalT ty md inner -> TypeModalT ty md <$> (enterModality md $ nfTope inner)
LetT _ty _orig _mparam val body -> instantiate body val >>= nfTope
TypeFunT{} -> panicImpossible "exposed function type in the tope layer"
TypeSigmaT{} -> panicImpossible "dependent sum type in the tope layer"
TypeIdT{} -> panicImpossible "identity type in the tope layer"
ReflT{} -> panicImpossible "refl in the tope layer"
IdJT{} -> panicImpossible "idJ eliminator in the tope layer"
TypeRestrictedT{} -> panicImpossible "extension types in the tope layer"
-- A recOR/recBOT is a term-level eliminator, never a tope. It should have been
-- rejected before reaching here (see the RecOr case of 'typecheck'); as a safety
-- net for any other path, report a type error rather than panicking.
RecOrT{} -> issueTypeError $ TypeErrorOther "a recOR cannot appear in the tope layer"
RecBottomT{} -> issueTypeError $ TypeErrorOther "a recBOT cannot appear in the tope layer"
-- * Normal form
nfT :: Distinct n => TermT n -> TypeCheck n (TermT n)
nfT tt = performing (ActionNF tt) $ case tt of
-- universe constants
UniverseT{} -> pure tt
UniverseCubeT{} -> pure tt
UniverseTopeT{} -> pure tt
-- cube layer constants
CubeUnitT{} -> pure tt
CubeUnitStarT{} -> pure tt
Cube2T{} -> pure tt
Cube2_0T{} -> pure tt
Cube2_1T{} -> pure tt
CubeIT{} -> pure tt
CubeI_0T{} -> pure tt
CubeI_1T{} -> pure tt
-- cube layer with computation
CubeProductT{} -> nfTope tt
CubeFlipT{} -> nfTope tt
CubeUnflipT{} -> nfTope tt
-- tope layer constants
TopeTopT{} -> pure tt
TopeBottomT{} -> pure tt
-- tope layer with computation
TopeAndT{} -> nfTope tt
TopeOrT{} -> nfTope tt
TopeEQT{} -> nfTope tt
TopeLEQT{} -> nfTope tt
TopeInvT{} -> nfTope tt
TopeUninvT{} -> nfTope tt
-- type layer constants
ReflT ty _x -> pure (ReflT ty Nothing)
RecBottomT{} -> pure tt
TypeUnitT{} -> pure tt
UnitT{} -> pure tt
-- type ascriptions are ignored, since we already have a typechecked term
TypeAscT _ty term _ty' -> nfT term
-- now we are in the type layer
_ ->
typeOf tt >>= tryRestriction >>= \case
Just tt' -> nfT tt'
Nothing -> case tt of
-- a hole is opaque: it never reduces, it is already a normal form
HoleT{} -> pure tt
t@(Var x) ->
valueOfVar x >>= \case
Nothing -> pure t
Just term -> nfT term
TypeFunT ty orig md param mtope ret -> do
param' <- enterModality md $ nfT param
case mtope of
Nothing -> do
ret' <- underScope orig md param' Nothing ret nfT
pure (TypeFunT ty orig md param' Nothing ret')
Just tope -> do
(tope', ret') <- underScope2 orig md param' tope ret $ \topeBody retBody -> do
topeNF <- nfT topeBody
retNF <- localTope topeNF (nfT retBody)
pure (topeNF, retNF)
pure (TypeFunT ty orig md param' (Just tope') ret')
AppT ty f x ->
whnfT f >>= \case
LambdaT _ty _orig _arg body ->
instantiate body x >>= nfT
f' -> typeOf f' >>= \case
TypeFunT _ty _orig md _param (Just tope) (ScopedAST _ UniverseTopeT{}) -> do
x' <- enterModality md $ nfT x
sideCondition <- instantiate tope x' >>= nfT
pure (topeAndT (AppT ty f' x') sideCondition)
_ -> AppT ty <$> nfT f' <*> nfT x
LetT _ty _orig _mparam val body ->
instantiate body val >>= nfT
LetModT ty orig app inn mparam val body ->
(enterModality app $ whnfT val) >>= \case
ModAppT _ md t | md == inn -> do
val' <- enterModality md $ nfT t
instantiate body val' >>= nfT
b' | isRA inn -> do
bty <- typeOf b' >>= \case
TypeModalT _ _ t -> pure t
_ -> panicImpossible "not modal in letmod"
instantiate body (modExtractT bty app inn b') >>= nfT
b' -> do
bty <- typeOf b' >>= \case
TypeModalT _ _ t -> pure t
_ -> panicImpossible "not modal in letmod"
val' <- enterModality app $ nfT b'
body' <- underScope orig (comp app inn) bty Nothing body nfT
pure (LetModT ty orig app inn mparam val' body')
LambdaT ty orig _mparam body ->
case stripTypeRestrictions (infoType ty) of
TypeFunT _ty _orig md param mtope _ret -> do
param' <- enterModality md $ nfT param
case mtope of
Nothing -> do
body' <- underScope orig md param' Nothing body nfT
pure (LambdaT ty orig (Just (LambdaParam md param' Nothing)) body')
Just tope -> do
(tope', body') <- underScope2 orig md param' tope body $ \topeBody bodyBody -> do
topeNF <- nfT topeBody
bodyNF <- localTope topeNF (nfT bodyBody)
pure (topeNF, bodyNF)
pure (LambdaT ty orig (Just (LambdaParam md param' (Just tope'))) body')
_ -> panicImpossible "lambda with a non-function type"
TypeSigmaT ty orig md a b -> do
a' <- enterModality md $ nfT a
b' <- underScope orig md a' Nothing b nfT
pure (TypeSigmaT ty orig md a' b')
PairT ty l r -> PairT ty <$> nfT l <*> nfT r
FirstT ty t ->
whnfT t >>= \case
PairT _ l _r -> nfT l
t' -> FirstT ty <$> nfT t'
SecondT ty t ->
whnfT t >>= \case
PairT _ _l r -> nfT r
t' -> SecondT ty <$> nfT t'
TypeIdT ty x _tA y -> TypeIdT ty <$> nfT x <*> pure Nothing <*> nfT y
IdJT ty tA a tC d x p ->
whnfT p >>= \case
ReflT{} -> nfT d
p' -> IdJT ty <$> nfT tA <*> nfT a <*> nfT tC <*> nfT d <*> nfT x <*> nfT p'
RecOrT _ty rs ->
firstMatching rs >>= \case
Just tt' -> nfT tt'
Nothing
| [tt'] <- nubT (map snd rs) -> nfT tt'
| otherwise -> pure tt
TypeModalT ty md b -> do
b' <- enterModality md $ nfT b
pure (TypeModalT ty md b')
ModAppT ty md b ->
(enterModality md $ whnfT b) >>= \case
ModExtractT _ app inn t | inn == md -> enterModality (comp app inn) $ nfT t
b' -> ModAppT ty md <$> (enterModality md $ nfT b')
ModExtractT ty app inn b ->
(enterModality app $ whnfT b) >>= \case
ModAppT _ md t | inn == md -> enterModality (comp app inn) $ nfT t
b' -> ModExtractT ty app inn <$> (enterModality app $ nfT b')
TypeRestrictedT ty type_ rs -> do
rs' <- forM rs $ \(tope, term) ->
nfTope tope >>= \case
TopeBottomT{} -> pure Nothing
tope' -> do
term' <- localTope tope' (nfT term)
return (Just (tope', term'))
case catMaybes rs' of
[] -> nfT type_
rs'' -> TypeRestrictedT ty <$> nfT type_ <*> pure rs''