packages feed

rzk-0.10.0: src/Rzk/TypeCheck/Context.hs

-- The scope-extension evidence on the three sinkers that are coercions
-- ('sinkContextUnchecked', 'sinkVars', 'sinkNamed') is their soundness contract,
-- not an argument they can consume, so GHC calls it redundant. It stays.
{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-redundant-constraints #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The typing context on free-foil.
--
-- The successor of @Rzk.TypeCheck@'s @Context var@. Two things change, and they
-- are the point of the migration.
--
-- [A variable is a name, and so is a top-level entry.] A free-foil term refers to
-- a variable by 'Foil.Name' (an @Int@), and by nothing else: there is no way to
-- put a 'VarIdent' inside a term. So a top-level definition is a name too, bound
-- in the outermost scope with no binder above it, and the hypotheses — global and
-- local alike — are one 'Foil.NameMap', looked up in constant time. The old
-- context was an association list keyed by a @var@ whose equality walked an
-- @S@-chain; @lookupVarInfo@ alone was 11.5% of the checker's time. The surface
-- name of an entry is resolved through 'ctxNamed'.
--
-- This also gives sections their natural shape. A @#assume@d assumption is an
-- ordinary binder, so the definitions of a section are checked in its scope, and
-- closing the section abstracts the assumption by pairing its binder with the
-- body — no rewrite of the elaborated terms (see @Rzk.TypeCheck.Decl@).
--
-- [Entering a binder rebuilds nothing.] 'enterBinder' extends the scope and
-- sinks the rest of the context by coercion, so it costs O(1) rather than a walk
-- of the context. The old @enterScopeContext@ mapped @S \<$\>@ over the whole
-- context, rebuilding every elaborated term it held; the heap profile showed
-- those forced copies retaining most of the live heap, and @GlobalScopeInfo@ /
-- @globalEmbed@ (PR #277) exist only to keep that shift off the ~1500 top-level
-- entries. All of that machinery is gone.
module Rzk.TypeCheck.Context where

import           Control.Monad.Foil          (DExt, Distinct, NameBinder,
                                              NameMap, Scope)
import qualified Control.Monad.Foil          as Foil
-- NOTE: free-foil 0.2.0 gives 'NameMap' no 'Functor' instance (it has one on the
-- unreleased main), so 'mapNameMap' goes through the underlying 'IntMap'.
import           Control.Monad.Foil.Internal (NameMap (..))
import qualified Data.IntMap                 as IntMap
import           Data.Map                    (Map)
import qualified Data.Map                    as Map
import           Unsafe.Coerce               (unsafeCoerce)

import           Language.Rzk.Foil.Syntax
import           Language.Rzk.Foil.Names    (Binder (..), TModality (..),
                                              VarIdent, binderName)
import qualified Language.Rzk.Syntax         as Rzk

-- * The pieces of a context

data Covariance
  = Covariant     -- ^ Positive position.
  | Contravariant -- ^ Negative position.
  | Invariant     -- ^ Unknown position.

data RenderBackend
  = RenderSVG
  | RenderLaTeX

data Verbosity
  = Debug
  | Normal
  | Silent
  deriving (Eq, Ord)

data LocationInfo = LocationInfo
  { locationFilePath :: Maybe FilePath
  , locationLine     :: Maybe Int
  } deriving (Eq, Show)

-- | What is known about a hypothesis, local or top-level.
data VarInfo n = VarInfo
  { varType                :: TermT n
  , varValue               :: Maybe (TermT n)
  , varModality            :: TModality
  , varModAccum            :: TModality
  , varOrig                :: Binder
    -- ^ the names the binder introduced, for display only
  , varIsAssumption        :: Bool -- FIXME: perhaps, introduce something like decl kind?
  , varIsTopLevel          :: Bool
  , varDeclaredAssumptions :: [Foil.Name n]
  , varLocation            :: Maybe LocationInfo
  }

-- | A tope, together with the modalities under which it is available.
data ModalTope n = ModalTope
  { tModAccum :: TModality
  , tModVar   :: TModality
  , tTope     :: TermT n
  }

-- | The judgement being performed, for tracing and for the error report.
data Action n
  = ActionTypeCheck (Term n) (TermT n)
  | ActionUnify (TermT n) (TermT n) (TermT n)
  | ActionUnifyTerms (TermT n) (TermT n)
  | ActionInfer (Term n)
  | ActionContextEntailedBy [TermT n] (TermT n)
  | ActionContextEntails [TermT n] (TermT n)
  | ActionContextEntailsUnion [TermT n] [TermT n]
  | ActionWHNF (TermT n)
  | ActionNF (TermT n)
  | ActionCheckCoherence (TermT n, TermT n) (TermT n, TermT n)
  | ActionCloseSection (Maybe Rzk.SectionName)
  | ActionCheckLetValue (Maybe VarIdent)

-- | The state of the tope-saturation cache in a 'Context'
-- (see 'ctxTopesSaturated').
data CachedSaturation n
  = SaturationUncached
    -- ^ No cache was installed for this tope context (entailment falls back to
    -- the per-query pipeline).
  | SaturationCached (Maybe [[ModalTope n]])
    -- ^ A deferred pipeline run: forced by the first query under this context.
    -- 'Nothing' records that the pipeline errored; queries then fall back, so
    -- the error surfaces exactly where it would have.

-- * The context

data Context n = Context
  { ctxScope               :: Scope n
    -- ^ Every name in scope: the top-level entries, the section assumptions and
    -- the binders entered on the way here. Substitution and freshness need it.
  , ctxVars                :: NameMap n (VarInfo n)
    -- ^ What each name in scope stands for. Total on 'ctxScope', which is what
    -- 'Foil.lookupName' assumes.
  , ctxNamed               :: Map VarIdent (Foil.Name n)
    -- ^ The surface name of each top-level entry (and of each named binder), for
    -- resolving an identifier the parser produced.
  , ctxBound               :: [Foil.Name n]
    -- ^ Every name in scope, most recently bound first.
    --
    -- The order has to be recorded, not recovered: free-foil refreshes a binder
    -- only when its name clashes, so after a substitution an inner binder may
    -- well carry a /smaller/ id than an outer one (@\\ x2 -> \\ x1 -> …@), and the
    -- ascending order of 'ctxVars' is not the order things were bound in. Display
    -- depends on this: names claim their display name oldest-first, so that an
    -- inner binder is the one refreshed away from an outer name, and not the
    -- other way round.
  , ctxSections            :: [SectionInfo n]
    -- ^ The open sections, innermost first. Each records the entries declared in
    -- it, so closing it can turn them into declarations.
  , ctxDiscreteTopes       :: [ModalTope n]
    -- ^ Discreteness axioms for the flat cube variables in scope (a flat point of
    -- @2@ or @I@ is an endpoint). Maintained at binder entry, so entailment does
    -- not rescan the context on every query.
  , ctxTopes               :: [ModalTope n]
  , ctxTopesNF             :: [ModalTope n]
  , ctxTopesNFUnion        :: [[ModalTope n]]
  , ctxTopesEntailBottom   :: Maybe Bool
  , ctxTopesSaturated      :: CachedSaturation n
    -- ^ The saturated alternatives for this tope context, cached at the points
    -- where the tope context changes.
  , ctxShadow              :: Map VarIdent [VarIdent]
    -- ^ The identifiers in scope, keyed by their spelling (a 'VarIdent' compares by
    -- spelling and carries the position of its defining occurrence, so the values
    -- are what a shadowing report names).
    --
    -- The check runs at every binder entry, and scanning every name in scope each
    -- time was O(context) per binder — with every top-level definition of a project
    -- in scope, that is most of them.
  , ctxActionStack         :: [Action n]
  , ctxActionStackDepth    :: Int
    -- ^ The length of 'ctxActionStack', maintained alongside it: measuring the
    -- list on every judgement made each action cost O(depth), and each path
    -- O(depth²).
  , ctxCurrentCommand      :: Maybe Rzk.Command
  , ctxLocation            :: Maybe LocationInfo
  , ctxVerbosity           :: Verbosity
  , ctxCovariance          :: Covariance
  , ctxRenderBackend       :: Maybe RenderBackend
  , ctxRenderHideTerm      :: Bool
    -- ^ When rendering a diagram, hide the proof term: drop the @\<title\>@ (which
    -- carries the full term) from every cell and blank the visible label of
    -- proof-coloured (interior) cells, keeping the given boundary labels.
  , ctxHolesAreErrors      :: Bool
    -- ^ When 'True' (the default), an unfilled hole is reported as a
    -- @TypeErrorUnsolvedHole@; finished work (and CI) must have no holes. The
    -- lenient mode ('allowHoles') instead records each hole's goal and context.
  , ctxDeferHoleMismatches :: Bool
    -- ^ How holes behave during unification, giving three modes overall. With
    -- 'ctxHolesAreErrors' a hole is rejected outright (strict). Otherwise a hole
    -- always unifies as a leaf; this flag then chooses what happens when the
    -- /surrounding/ structure disagrees: 'True' (the default) defers — any term
    -- containing a hole is accepted, for an in-progress sketch — while 'False'
    -- keeps such a mismatch an error ('structuralHoleUnify').
  , ctxHintLemmas          :: [VarIdent]
    -- ^ Named top-level definitions a hole's candidate list may draw on, beyond
    -- the local hypotheses (see 'withHintLemmas').
  , ctxWarnOverhang        :: Bool
    -- ^ When 'True', a restriction face or @recOR@ guard that overhangs the
    -- local tope context (is not entailed by it, while still overlapping it)
    -- is reported with a non-fatal hint. Off by default: deciding the
    -- overhang costs a solver entailment per face and guard, and the overhang
    -- is legitimate (see @happy-restrict-face-not-contained@). Enabled with
    -- @#set-option "warn-overhang" "yes"@. The /disjointness/ error next to
    -- it is unaffected: a vacuous face is always rejected.
  }

-- | An open section: the entries declared in it, newest first.
data SectionInfo n = SectionInfo
  { sectionName    :: Maybe Rzk.SectionName
  , sectionEntries :: [Foil.Name n]
  }

emptyContext :: Context Foil.VoidS
emptyContext = Context
  { ctxScope = Foil.emptyScope
  , ctxVars = Foil.emptyNameMap
  , ctxNamed = Map.empty
  , ctxBound = []
  , ctxSections = [SectionInfo Nothing []]
  , ctxDiscreteTopes = []
  , ctxTopes = emptyTopeContext
  , ctxTopesNF = emptyTopeContext
  , ctxTopesNFUnion = [emptyTopeContext]
  , ctxTopesEntailBottom = Just False
  , ctxTopesSaturated = SaturationUncached
  , ctxShadow = Map.empty
  , ctxActionStack = []
  , ctxActionStackDepth = 0
  , ctxCurrentCommand = Nothing
  , ctxLocation = Nothing
  , ctxVerbosity = Normal
  , ctxCovariance = Covariant
  , ctxRenderBackend = Nothing
  , ctxRenderHideTerm = False
  , ctxHolesAreErrors = True
  , ctxDeferHoleMismatches = True
  , ctxHintLemmas = []
  , ctxWarnOverhang = False
  }

-- | The tope context of an empty context: @⊤@ holds under every modality.
emptyTopeContext :: [ModalTope n]
emptyTopeContext =
  [ ModalTope Id Id    topeTopT
  , ModalTope Id Flat  topeTopT
  , ModalTope Id Op    topeTopT
  , ModalTope Id Sharp topeTopT
  ]

-- * Entering a binder
--
-- $sinking
--
-- Everything in a context that mentions the scope is a term, or a container of
-- terms, and so is sinkable: a term whose free names lie in @n@ has its free
-- names in any @l@ that extends @n@. free-foil sinks such a value by coercion
-- (@sink = unsafeCoerce@, sound because @DExt n l@ makes the renaming the
-- identity on raw names; see §3.5 of the Foil paper), and the same argument
-- applies to a whole record of them: renaming every field along the identity is
-- the identity on the representation.
--
-- Two fields are /not/ sinkable, and 'enterBinder' restores both in the same
-- breath, which is why the coercion below is not exported:
--
--   * 'ctxScope' is the set of names /in/ @n@. It has to grow, not coerce —
--     tellingly, free-foil gives 'Foil.Scope' no 'Foil.Sinkable' instance.
--   * 'ctxVars' must stay total on the names in scope ('Foil.lookupName' assumes
--     it), so the new binder's entry has to be added.

-- | Sink a context along a scope extension. Unsound on its own: leaves 'ctxScope'
-- and 'ctxVars' describing the /old/ scope, and the caller must fix both. See the
-- note above; 'enterBinder' is the only caller.
sinkContextUnchecked :: DExt n l => Context n -> Context l
sinkContextUnchecked = unsafeCoerce

-- | Enter the scope of a binder: extend the scope with it, record what it is
-- called and what it stands for, and carry the rest of the context in.
enterBinder
  :: DExt n l
  => NameBinder n l
  -> VarInfo n       -- ^ its type, value and modality
  -> [ModalTope l]   -- ^ discreteness axioms the binder brings (a flat cube point,
                     --   which are about the variable itself, hence at its scope)
  -> Context n
  -> Context l
enterBinder binder info discrete ctx = (sinkContextUnchecked ctx)
  { ctxScope = Foil.extendScope binder (ctxScope ctx)
  , ctxVars = Foil.addNameBinder binder (Foil.sink info) (sinkVars (ctxVars ctx))
  , ctxBound = Foil.nameOf binder : sinkNames (ctxBound ctx)
  , ctxNamed = case binderName (varOrig info) of
      Nothing   -> sinkNamed (ctxNamed ctx)
      Just name -> Map.insert name (Foil.nameOf binder) (sinkNamed (ctxNamed ctx))
  , ctxDiscreteTopes = discrete <> sinkTopes (ctxDiscreteTopes ctx)
  , ctxShadow = addBinderNames (varOrig info) (ctxShadow ctx)
  }

-- | Enter a /fresh/ binder: one the checker invents rather than one a term
-- carries (to look under a Π when comparing two of them, say).
withFreshBinder
  :: Distinct n
  => Context n
  -> VarInfo n
  -> (forall l. DExt n l => NameBinder n l -> Context l -> r)
  -> r
withFreshBinder ctx info k =
  Foil.withFresh (ctxScope ctx) $ \binder ->
    k binder (enterBinder binder info [] ctx)

-- | Record the name a binder introduces (if any), for the shadowing check.
addBinderNames :: Binder -> Map VarIdent [VarIdent] -> Map VarIdent [VarIdent]
addBinderNames orig names =
  case binderName orig of
    Nothing   -> names
    Just name -> Map.insertWith (<>) name [name] names

-- | The identifiers in scope spelled like this one.
shadowedBy :: VarIdent -> Context n -> [VarIdent]
shadowedBy name ctx = Map.findWithDefault [] name (ctxShadow ctx)

-- * Sinking the parts

instance Foil.Sinkable VarInfo where
  sinkabilityProof rename info = info
    { varType = Foil.sinkabilityProof rename (varType info)
    , varValue = Foil.sinkabilityProof rename <$> varValue info
    , varDeclaredAssumptions = rename <$> varDeclaredAssumptions info
    }

instance Foil.Sinkable ModalTope where
  sinkabilityProof rename tope =
    tope { tTope = Foil.sinkabilityProof rename (tTope tope) }

sinkVars :: DExt n l => NameMap n (VarInfo n) -> NameMap n (VarInfo l)
sinkVars = Foil.sinkContainer

sinkTopes :: DExt n l => [ModalTope n] -> [ModalTope l]
sinkTopes = Foil.sinkContainer

sinkNamed :: DExt n l => Map VarIdent (Foil.Name n) -> Map VarIdent (Foil.Name l)
sinkNamed = Foil.sinkContainer

sinkNames :: DExt n l => [Foil.Name n] -> [Foil.Name l]
sinkNames = Foil.sinkContainer

-- * Lookup

-- | What a name stands for. Total: every name in scope has an entry.
lookupVarInfo :: Foil.Name n -> Context n -> VarInfo n
lookupVarInfo name ctx = Foil.lookupName name (ctxVars ctx)

-- | What every name in scope stands for, in no particular order.
varInfos :: Context n -> [VarInfo n]
varInfos ctx = IntMap.elems m
  where
    NameMap m = ctxVars ctx

-- | Every hypothesis in scope, oldest binding first (see 'ctxBound': the ids
-- themselves do not tell us this).
varsInScope :: Context n -> [(Foil.Name n, VarInfo n)]
varsInScope ctx =
  [ (name, lookupVarInfo name ctx) | name <- reverse (ctxBound ctx) ]

-- | The name a surface identifier resolves to, if any.
lookupNamed :: VarIdent -> Context n -> Maybe (Foil.Name n)
lookupNamed name ctx = Map.lookup name (ctxNamed ctx)

-- | The display binder of a name (the whole pattern, for projection folding).
binderOfName :: Foil.Name n -> Context n -> Binder
binderOfName name = varOrig . lookupVarInfo name

-- * Modalities

class ModeTheory m where
    iden :: m
    comp :: m -> m -> m
    coe :: m -> m -> Bool
    isRA :: m -> Bool

instance ModeTheory TModality where
  iden = Id

  comp Flat Flat   = Flat
  comp Flat Sharp  = Flat
  comp Flat Op     = Flat
  comp Op Flat     = Flat
  comp Sharp Sharp = Sharp
  comp Sharp Flat  = Sharp
  comp Sharp Op    = Sharp
  comp Op Sharp    = Sharp
  comp Op Op       = Id
  comp Id m        = m
  comp m Id        = m

  coe Flat Id    = True
  coe Flat Op    = True
  coe Id Sharp   = True
  coe Flat Sharp = True
  coe Op Sharp   = True
  coe a b        = a == b

  isRA Sharp = True
  isRA Op    = True
  isRA Id    = True
  isRA _     = False

-- | Accumulate a modality (a lock) over the hypotheses and the topes.
--
-- Note that top-level entries are /not/ touched: a top-level variable is exempt
-- from the accessibility check (see @infer@), so accumulating a modality over
-- them was work with no observable effect. The old context did it to all ~1500
-- of them on every modal binder.
applyModality :: TModality -> Context n -> Context n
applyModality md ctx = ctx
  { ctxVars = mapNameMap addToVar (ctxVars ctx)
  , ctxTopes = map addToTope (ctxTopes ctx)
  , ctxTopesNF = map addToTope (ctxTopesNF ctx)
  , ctxTopesNFUnion = map (map addToTope) (ctxTopesNFUnion ctx)
  , ctxTopesSaturated = SaturationUncached  -- accessibility changed
  }
  where
    addToVar info
      | varIsTopLevel info = info
      | otherwise = info { varModAccum = comp (varModAccum info) md }
    addToTope tope = tope { tModAccum = comp (tModAccum tope) md }

-- | Map the values of a name map, keeping its keys.
mapNameMap :: (a -> b) -> NameMap n a -> NameMap n b
mapNameMap f (NameMap m) = NameMap (IntMap.map f m)

-- | Replace what a name stands for.
--
-- Closing a section rewrites the definitions made in it, so that each takes the
-- section's assumptions as explicit parameters; this is how the rewritten entries
-- go back into the context.
insertVarInfo :: Foil.Name n -> VarInfo n -> Context n -> Context n
insertVarInfo name info ctx = ctx { ctxVars = replace (ctxVars ctx) }
  where
    replace (NameMap m) = NameMap (IntMap.insert (Foil.nameId name) info m)

-- * Topes

isAccessible :: ModalTope n -> Bool
isAccessible mt = coe (tModVar mt) (tModAccum mt)

filterAccessible :: [ModalTope n] -> [ModalTope n]
filterAccessible = filter isAccessible

accessibleTopes :: [ModalTope n] -> [TermT n]
accessibleTopes = map tTope . filterAccessible

plainTope :: TermT n -> ModalTope n
plainTope = ModalTope Id Id

availableTopes :: Context n -> [TermT n]
availableTopes = accessibleTopes . ctxTopes

availableTopesNF :: Context n -> [TermT n]
availableTopesNF = accessibleTopes . ctxTopesNF

-- * Hole modes

-- | Switch to lenient hole mode: record each hole's goal and context instead of
-- reporting it as an error.
allowHoles :: Context n -> Context n
allowHoles ctx = ctx { ctxHolesAreErrors = False }

-- | Allow a hole's candidate list to draw on the given named top-level
-- definitions, in addition to the local hypotheses.
withHintLemmas :: [VarIdent] -> Context n -> Context n
withHintLemmas lemmas ctx = ctx { ctxHintLemmas = lemmas }

-- | Within the given action, a hole unifies only as a leaf of an otherwise
-- matching structure: a structural mismatch around a hole stays an error rather
-- than being deferred (see 'ctxDeferHoleMismatches').
structuralHoleUnify :: Context n -> Context n
structuralHoleUnify ctx = ctx { ctxDeferHoleMismatches = False }