packages feed

rzk-0.11.4: src/Rzk/TypeCheck/Schematic.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Check schematic dRzk declarations without treating universes as object types.
-- Family arguments are checked at their indices. Schematic rules,
-- such as function extensionality, may also be parameters of a declaration.
-- Postulates supply trusted rules; checking their statements does not prove them.
module Rzk.TypeCheck.Schematic
  ( SchematicStatement, CheckedSchematicDeclaration, checkSchematicDeclaration
  , recordSchematicDeclaration
  ) where

import Control.Monad (forM_, unless, void, when)
import Control.Monad.Except (catchError)
import Control.Monad.Reader (asks, local)
import Control.Monad.State.Strict (gets)
import Data.Bifoldable (bifoldMap)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntSet as IntSet
import Data.Maybe (isJust)
import Control.Monad.Foil (Distinct)
import qualified Control.Monad.Foil as Foil
import Control.Monad.Free.Foil (AST (..))
import Control.Monad.Free.Foil.Annotated (AnnSig (..))
import Language.Rzk.Foil.Names (TModality (..), TypeInfo (..), VarIdent)
import Language.Rzk.Foil.Syntax
import Rzk.TypeCheck.Context
import Rzk.TypeCheck.Display (namingOfContext, ppTerm)
import Rzk.TypeCheck.Error
import Rzk.TypeCheck.Eval
import Rzk.TypeCheck.Fragment.RSTT (fragmentHead)
import Rzk.TypeCheck.Monad

-- Constructors stay private: a statement alone does not certify its proof.
newtype SchematicStatement n = SchematicStatement (TermT n)
data CheckedSchematicDeclaration n = CheckedSchematicDeclaration
  (SchematicStatement n) (Maybe (TermT n))

-- | Roles of elaborated type and domain expressions in schematic declarations.
-- These are not universe levels. The fragment's formation conditions are
-- checked separately in "Rzk.TypeCheck.Fragment.RSTT".
data Sort
  = ObjectType
    -- ^ Ordinary types that may instantiate @(A : U)@, e.g. @Unit@ or @A -> A@.
  | ParameterKind
    -- ^ Kinds of schematic data: @U@, @CUBE@, @TOPE@, family kinds such as
    -- @A -> U@, and dependent sums containing non-object components.
  | SchematicRule
    -- ^ Function signatures ending in an object type and quantifying over
    -- kinds or rules, e.g. @(A : U) -> A -> A@.
  | Shape
    -- ^ Cube or tope expressions, e.g. @2@ or @TOP@; these are not object types.
  deriving (Eq)

-- | Validate an elaborated declaration and the definitions used by its proof.
-- This supplements dRzk typing with kind checks; it is not a separate RSTT kernel.
checkSchematicDeclaration
  :: Distinct n => TermT n -> Maybe (TermT n)
  -> TypeCheck n (CheckedSchematicDeclaration n)
checkSchematicDeclaration ty value = do
  declaration 256 IntSet.empty ty value
  pure (CheckedSchematicDeclaration (SchematicStatement ty) value)

-- | Apply the selected safe-mode policy and return whether validation succeeded.
recordSchematicDeclaration
  :: Distinct n => VarIdent -> TermT n -> Maybe (TermT n) -> TypeCheck n SchematicStatus
recordSchematicDeclaration name ty value = do
  enabled <- asks rsttSafeEnabled
  if not enabled then pure SchematicUnchecked else
    (checkSchematicDeclaration ty value >> pure SchematicChecked) `catchError` \err -> do
      case err of
        TypeErrorInScopedContext _ (TypeErrorSchematic problem reason) -> do
          loc <- asks ctxLocation
          warnings <- gets logWarningsRev
          -- Source checking already gives more precise locations for these.
          let alreadyReported = problem == SchematicUnsupported && any (sameDeclaration loc) warnings
          unless alreadyReported $ recordCheckWarning (RSTTScopeWarning RSTTSchematic
            (reason <> " (in " <> show name <> ")") loc)
        _ -> reportIncompleteRSTTCheck ("schematic check incomplete in " <> show name) err
      pure SchematicRejected

sameDeclaration :: Maybe LocationInfo -> CheckWarning -> Bool
sameDeclaration loc warning = withinDeclaration && case warning of
  RSTTHoleWarning{} -> True
  RSTTScopeWarning RSTTModal _ _ -> True
  RSTTScopeWarning RSTTInductive _ _ -> True
  RSTTScopeWarning RSTTInterval _ _ -> True
  RSTTScopeWarning RSTTUnsupported _ _ -> True
  _ -> False
  where
    withinDeclaration = case (loc, warningLocation warning) of
      (Just start, Just at) -> locationFilePath start == locationFilePath at &&
        locationLine at >= locationLine start
      _ -> loc == warningLocation warning

invalid :: Distinct n => String -> TermT n -> TypeCheck n a
invalid = schematicError SchematicKindMismatch

schematicError :: Distinct n => SchematicProblem -> String -> TermT n -> TypeCheck n a
schematicError problem reason t = do
  naming <- asks namingOfContext
  issueTypeError (TypeErrorSchematic problem (reason <> ": " <> ppTerm naming (untyped t)))

budget :: Distinct n => Int -> TypeCheck n ()
budget fuel = when (fuel <= 0) $
  issueTypeError (TypeErrorOther "schematic validation limit reached")

-- Check a declaration telescope separately: a definition may name a family kind
-- or a schema, whereas an argument to a type parameter must be an object type.
declaration :: Distinct n => Int -> IntSet.IntSet -> TermT n -> Maybe (TermT n) -> TypeCheck n ()
declaration fuel seen ty value = do
  budget fuel
  -- Check dependencies before reduction can obscure their failures.
  dependencies ty
  signature fuel seen ty
  mapM_ (\t -> dependencies t >> term fuel seen t) value
  where
    dependencies = mapM_ (uses fuel seen . Var) . freeVarsOfTermT

signature :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
signature fuel seen ty = do
  budget fuel
  uses fuel seen ty
  inspectRedex fuel seen ty
  fragmentHead fuel ty >>= \case
    TypeFunT _ orig md dom guard body -> do
      void (classify (fuel - 1) seen dom)
      mapM_ (\g -> inScope orig md dom g (term (fuel - 1) seen)) guard
      inScope orig md dom body (signature (fuel - 1) seen)
    t -> void (classify (fuel - 1) seen t)

-- Check each dependency before using its signature, even if safe mode was off
-- when it was declared. Successful earlier checks are cached in the context.
-- Check variables during traversal instead of rescanning every subtree.
uses :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
uses fuel seen (Var v) = do
  info <- infoOfVar id v
  cached <- gets (IntSet.member (Foil.nameId v) . logSchematicCache)
  when (varIsTopLevel info && varSchematicStatus info /= SchematicChecked && not cached) $ do
    when (varSchematicStatus info == SchematicRejected) $
      schematicError SchematicDependency "dependency failed schematic validation" (Var v)
    when (isJust (varDataRole info)) $
      schematicError SchematicUnsupported "inductive dependency" (Var v)
    budget fuel
    when (IntSet.member (Foil.nameId v) seen) $
      issueTypeError (TypeErrorOther "cyclic schematic dependency")
    -- A caller's tope assumptions cannot justify a reusable dependency check.
    local withoutCallerTopes $ declaration (fuel - 1) (IntSet.insert (Foil.nameId v) seen)
      (varType info) (varValue info)
    modifyLog $ \checkLog' -> checkLog'
      { logSchematicCache = IntSet.insert (Foil.nameId v) (logSchematicCache checkLog') }
uses _ _ _ = pure ()

withoutCallerTopes :: Context n -> Context n
withoutCallerTopes ctx = ctx
  { ctxDiscreteTopes = [], ctxTopes = [], ctxTopesNF = []
  , ctxTopesNFUnion = [[]], ctxTopesEntailBottom = Just False
  , ctxTopesSaturated = SaturationUncached
  }

-- Check erased arguments and annotations before exposing the result.
inspectRedex :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
inspectRedex fuel seen original = case original of
  LetT{} -> term fuel seen original
  TypeAscT{} -> term fuel seen original
  FirstT{} -> term fuel seen original
  SecondT{} -> term fuel seen original
  IdJT{} -> term fuel seen original
  RecOrT{} -> term fuel seen original
  _ -> applications fuel seen original

classify :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n Sort
classify fuel seen original = do
  budget fuel
  uses fuel seen original
  inspectRedex fuel seen original
  knownObjectFamily fuel seen original >>= \case
    True -> pure ObjectType
    False -> classifyHead fuel seen original

classifyHead :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n Sort
classifyHead fuel seen original =
  fragmentHead fuel original >>= \case
    UniverseT{} -> pure ParameterKind
    UniverseCubeT{} -> pure ParameterKind
    UniverseTopeT{} -> pure ParameterKind
    TypeFunT _ orig md dom guard body -> do
      d <- classify (fuel - 1) seen dom
      mapM_ (\g -> inScope orig md dom g (term (fuel - 1) seen)) guard
      r <- inScope orig md dom body (classify (fuel - 1) seen)
      case (d, r) of
        (ObjectType, ObjectType) -> pure ObjectType
        (Shape, ObjectType) -> pure ObjectType
        (_, ParameterKind) -> pure ParameterKind
        (_, Shape) -> invalid "cube or shape family" original
        _ -> pure SchematicRule
    TypeSigmaT _ orig md a b -> do
      d <- classify (fuel - 1) seen a
      r <- inScope orig md a b (classify (fuel - 1) seen)
      pure (if d == ObjectType && r == ObjectType then ObjectType else ParameterKind)
    TypeIdT _ a mt b -> do
      ty <- maybe (schematicTypeOf fuel a) pure mt
      object (fuel - 1) seen ty
      term (fuel - 1) seen a
      term (fuel - 1) seen b
      pure ObjectType
    TypeUnitT{} -> pure ObjectType
    RecBottomT{} -> pure ObjectType
    RecOrT _ branches -> do
      sorts <- mapM (\(p, v) -> localTope p (classify (fuel - 1) seen v)) branches
      case sorts of
        s : rest | all (== s) rest -> pure s
        _ -> invalid "case split between different schematic kinds" original
    TypeRestrictedT _ ty rs -> do
      s <- classify (fuel - 1) seen ty
      forM_ rs $ \(p, v) -> term (fuel - 1) seen p >> argument (fuel - 1) seen ty v
      pure s
    CubeUnitT{} -> pure Shape
    Cube2T{} -> pure Shape
    CubeProductT _ a b -> term (fuel - 1) seen a >> term (fuel - 1) seen b >> pure Shape
    t -> do
      termNeutral (fuel - 1) seen t
      schematicTypeOf fuel t >>= fragmentHead fuel >>= \case
        UniverseT{} -> pure ObjectType
        UniverseCubeT{} -> pure Shape
        UniverseTopeT{} -> pure Shape
        _ -> invalid "expected an object type, parameter kind, or schematic rule" t

-- Reuse only universally object-valued families, after checking actual arguments.
-- Other families retain reduction-based classification in the caller's context.
knownObjectFamily :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n Bool
knownObjectFamily fuel seen = spine 0
  where
    spine arity (AppT _ f _) = spine (arity + 1) f
    spine arity (Var v) = do
      info <- infoOfVar id v
      if not (varIsTopLevel info) then pure False else do
        let key = Foil.nameId v
        cached <- gets (IntMap.lookup key . logSchematicObjectFamilies)
        summary <- case cached of
          Just result -> pure result
          Nothing -> do
            cache key Nothing
            result <- (local withoutCallerTopes $
              objectFamilyArity fuel seen 0 (varType info) (Var v))
              `catchError` \_ -> pure Nothing
            cache key result
            pure result
        pure (summary == Just arity)
    spine _ _ = pure False
    cache key result = modifyLog $ \checkLog' -> checkLog'
      { logSchematicObjectFamilies = IntMap.insert key result (logSchematicObjectFamilies checkLog') }

-- Probe at fresh parameters; never generalise a concrete instance.
objectFamilyArity :: Distinct n => Int -> IntSet.IntSet -> Int -> TermT n -> TermT n -> TypeCheck n (Maybe Int)
objectFamilyArity fuel seen arity ty value = do
  budget fuel
  fragmentHead fuel ty >>= \case
    TypeFunT _ orig md dom _ body ->
      inScope orig md dom body $ \cod -> do
        bound <- asks ctxBound
        case bound of
          v : _ -> objectFamilyArity (fuel - 1) seen (arity + 1) cod
            (appT cod (Foil.sink value) (Var v))
          [] -> issueTypeError (TypeErrorOther "missing schematic index")
    UniverseT{} -> do
      s <- classify (fuel - 1) seen value
      pure (if s == ObjectType then Just arity else Nothing)
    _ -> pure Nothing

object :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
object fuel seen t = do
  s <- classify (fuel - 1) seen t
  unless (s == ObjectType) (invalid "expected an object type" t)

-- A type-family argument is checked pointwise. In particular U, A -> U, and
-- (A : U) -> A cannot instantiate a parameter declared simply as A : U.
argument :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TermT n -> TypeCheck n ()
argument fuel seen dom arg = do
  budget fuel
  functionHead fuel dom >>= \case
    UniverseT{} -> object (fuel - 1) seen arg
    TypeFunT _ orig md a guard body -> do
      s <- classify (fuel - 1) seen dom
      if s == ParameterKind then do
        -- Eta-expand only for checking; the ordinary checker has checked the
        -- argument's type. The body is inspected with the same fresh index.
        inScope orig md a body $ \cod -> do
          ctx <- asks id
          case ctxBound ctx of
            v : _ -> argument (fuel - 1) seen cod (appT cod (Foil.sink arg) (Var v))
            [] -> issueTypeError (TypeErrorOther "missing schematic index")
        mapM_ (\g -> inScope orig md a g (term (fuel - 1) seen)) guard
      else term (fuel - 1) seen arg
    _ -> term (fuel - 1) seen arg

-- Section abstraction can leave application annotations describing an earlier
-- telescope. Derive elimination types from the current signatures instead.
-- TODO: Share this reconstruction with Eval when repairing annotations there.
schematicTypeOf :: Distinct n => Int -> TermT n -> TypeCheck n (TermT n)
schematicTypeOf fuel t = do
  budget fuel
  case t of
    AppT _ f x -> schematicTypeOf (fuel - 1) f >>= functionHead fuel >>= \case
      TypeFunT _ _ _ _ _ ret -> instantiate ret x
      _ -> invalid "application without a function type" f
    FirstT _ p -> schematicTypeOf (fuel - 1) p >>= functionHead fuel >>= \case
      TypeSigmaT _ _ _ a _ -> pure a
      CubeProductT _ a _ -> pure a
      _ -> invalid "projection without a product type" p
    SecondT _ p -> schematicTypeOf (fuel - 1) p >>= functionHead fuel >>= \case
      TypeSigmaT _ _ _ a b -> instantiate b (firstT a p)
      CubeProductT _ _ b -> pure b
      _ -> invalid "projection without a product type" p
    _ -> typeOfUncomputed t

-- Boundaries constrain a function without changing its parameter kinds.
functionHead :: Distinct n => Int -> TermT n -> TypeCheck n (TermT n)
functionHead fuel t = do
  budget fuel
  fragmentHead fuel t >>= \case
    TypeRestrictedT _ base _ -> functionHead (fuel - 1) base
    head' -> pure head'

applications :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
applications fuel seen (AppT _ f x) = do
  budget fuel
  term (fuel - 1) seen f
  schematicTypeOf fuel f >>= functionHead fuel >>= \case
    TypeFunT _ _ _ dom _ _ -> argument (fuel - 1) seen dom x
    _ -> invalid "application without a schematic or object function type" f
applications _ _ _ = pure ()

term :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
term fuel seen t = do
  budget fuel
  uses fuel seen t
  case t of
    TypeFunT{} -> void (classify (fuel - 1) seen t)
    TypeSigmaT{} -> void (classify (fuel - 1) seen t)
    TypeIdT{} -> void (classify (fuel - 1) seen t)
    TypeRestrictedT{} -> void (classify (fuel - 1) seen t)
    LambdaT info orig param body -> do
      (md, dom) <- case param of
        Just (LambdaParam md dom guard) -> do
          mapM_ (\g -> inScope orig md dom g (term (fuel - 1) seen)) guard
          pure (md, dom)
        Nothing -> functionHead fuel (infoType info) >>= \case
          TypeFunT _ _ md dom _ _ -> pure (md, dom)
          _ -> invalid "lambda without a function type" t
      void (classify (fuel - 1) seen dom)
      inScope orig md dom body (term (fuel - 1) seen)
    AppT{} -> applications fuel seen t
    LetT _ orig annotation value body -> do
      mapM_ (signature (fuel - 1) seen) annotation
      term (fuel - 1) seen value
      ty <- maybe (schematicTypeOf fuel value) pure annotation
      inScopeWith orig Id ty (Just value) body (term (fuel - 1) seen)
    TypeAscT _ value ty -> signature (fuel - 1) seen ty >> term (fuel - 1) seen value
    IdJT _ a x motive d y p -> do
      object (fuel - 1) seen a
      -- J eliminates equality into object types, not into schematic kinds.
      motiveType <- schematicTypeOf fuel motive
      argument (fuel - 1) seen motiveType motive
      mapM_ (term (fuel - 1) seen) [x, motive, d, y, p]
    _ -> termNeutral fuel seen t

-- This is deliberately an allow-list. New syntax cannot inherit certification.
termNeutral :: Distinct n => Int -> IntSet.IntSet -> TermT n -> TypeCheck n ()
termNeutral fuel seen t = case t of
  Var{} -> pure ()
  UniverseT{} -> pure ()
  UniverseCubeT{} -> pure ()
  UniverseTopeT{} -> pure ()
  CubeUnitT{} -> pure ()
  CubeUnitStarT{} -> pure ()
  Cube2T{} -> pure ()
  Cube2_0T{} -> pure ()
  Cube2_1T{} -> pure ()
  TypeUnitT{} -> pure ()
  UnitT{} -> pure ()
  TopeTopT{} -> pure ()
  TopeBottomT{} -> pure ()
  CubeProductT{} -> children
  CubeSupT{} -> children
  CubeInfT{} -> children
  TopeEQT{} -> children
  TopeLEQT{} -> children
  TopeAndT{} -> children
  TopeOrT{} -> children
  PairT _ x y -> do
    ty <- schematicTypeOf fuel t
    functionHead fuel ty >>= \case
      TypeSigmaT _ _ _ a b -> do
        argument (fuel - 1) seen a x
        cod <- instantiate b x
        argument (fuel - 1) seen cod y
      CubeProductT{} -> children
      _ -> invalid "pair without a product type" t
  FirstT{} -> children
  SecondT{} -> children
  ReflT _ mx -> forM_ mx $ \(x, mty) -> do
    ty <- maybe (schematicTypeOf fuel x) pure mty
    object (fuel - 1) seen ty
    term (fuel - 1) seen x
  -- Elaboration checks the absurd tope context; recBOT has a cyclic type annotation.
  RecBottomT{} -> pure ()
  RecOrT{} -> children
  AppT{} -> applications fuel seen t
  _ -> schematicError SchematicUnsupported "unsupported schematic expression" t
  where
    children = case t of
      Node (AnnSig _ sig) -> mapM_ (term (fuel - 1) seen) (bifoldMap (const []) (:[]) sig)
      Var{} -> pure ()