packages feed

rzk-0.11.4: src/Rzk/TypeCheck/Fragment/RSTT.hs

{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE PatternSynonyms     #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Diagnostics for declarations outside the RSTT fragment.
--
-- For RSTT, see Riehl and Shulman,
-- <https://arxiv.org/abs/1705.07442 RS17> (2017), Figure 4 and Appendix A.2 for extension types.
-- For the fragment conditions, see Kudasov, Sim and Ahrens,
-- <https://arxiv.org/abs/2607.12207 KSA26> (2026), §5.
--
-- * Free-standing restrictions in assumptions, motives, and types passed as data.
-- * Cube domains, shape conditions, and assumed boundaries depending on outer points.
-- * Schematic variables bound inside terms below the parameter prefix.
-- * Modal constructs, auxiliary intervals, and involutions.
-- * Inductive constructors and eliminators.
-- * Any syntax outside the explicit RSTT allow-list.
--
-- Free-standing restrictions along concluded codomains are allowed.
-- Assumptions and postulates remain trusted; universe levels are not checked.
module Rzk.TypeCheck.Fragment.RSTT (
  fragmentHead,
  recordFragmentUses,
  recordSyntaxUses,
) where

import           Control.Applicative      ((<|>))
import           Control.Monad            (forM_, unless, when)
import           Control.Monad.Except     (catchError)
import           Control.Monad.Reader     (asks, local)
import           Data.Bifoldable          (bifoldMap)
import           Data.List                (intercalate)
import           Data.Maybe               (fromMaybe)
import qualified Data.IntSet              as IntSet
import qualified Control.Monad.Foil       as Foil

import           Control.Monad.Foil       (Distinct)
import           Control.Monad.Free.Foil  (AST (Node, Var), ScopedAST (..))

import           Control.Monad.Free.Foil.Annotated (AnnSig (..))
import           Language.Rzk.Foil.Names  (Binder, TModality (..),
                                           TypeInfo (..), VarIdent, binderName)
import           Language.Rzk.Foil.Syntax
import           Rzk.TypeCheck.Context
import           Rzk.TypeCheck.Display
import           Rzk.TypeCheck.Eval
import           Rzk.TypeCheck.Error
import           Rzk.TypeCheck.MetaPrefix (isMetaType)
import           Rzk.TypeCheck.Monad

-- Distinguish concluded types, assumed types, and terms (including type-valued data).
data CheckedPosition
  = InTail
  | InAssumption FragmentUse
  | InTerm PrefixPosition

-- A schematic argument may have its own parameter prefix.
data PrefixPosition = InParameterPrefix | OutsideParameterPrefix
  deriving (Eq)

-- | Report restriction and schematic-binder violations in a declaration.
-- Violations are errors when @rstt-safe = "error"@.
recordFragmentUses
  :: forall n. Distinct n
  => VarIdent          -- ^ the declaration
  -> TermT n           -- ^ its type
  -> Maybe (TermT n)   -- ^ its value, for a definition
  -> Bool              -- ^ does it supply an assumption rather than a proof?
  -> TypeCheck n ()
recordFragmentUses defName ty mval isAssumption = do
  restrictions <- asks ctxWarnFreeStandingRestriction
  binders <- asks ctxWarnMetaBinder
  shapes <- asks ctxWarnShapeDependency
  when (restrictions || binders || shapes) $
    localVerbosity Silent $
      forM_ [SourceTypes, ComputedTypes] $ \view ->
        checkFragmentUses view defName ty mval isAssumption `catchError`
          reportIncompleteRSTTCheck ("RSTT fragment check incomplete in " <> show defName)

-- Keep source checks even when reduction discards an argument or annotation.
data FragmentView = SourceTypes | ComputedTypes
  deriving (Eq)

checkFragmentUses
  :: forall n. Distinct n
  => FragmentView -> VarIdent -> TermT n -> Maybe (TermT n) -> Bool -> TypeCheck n ()
checkFragmentUses view defName ty mval isAssumption = do
  go (if isAssumption then InAssumption UseBinder else InTail) ty
  mapM_ (go (InTerm InParameterPrefix)) mval
  where
    go :: forall l. Distinct l => CheckedPosition -> TermT l -> TypeCheck l ()
    go (InTerm prefixPosition) t = goTerm prefixPosition t
    go InTail t = goTail t
    go (InAssumption use) t = goAssumed use t

    expose :: forall l. Distinct l => TermT l -> TypeCheck l (TermT l)
    expose = case view of
      SourceTypes -> pure
      ComputedTypes -> fragmentHead 256

    -- Allow restrictions along the spine of concluded codomains.
    goTail :: forall l. Distinct l => TermT l -> TypeCheck l ()
    goTail original = expose original >>= \t -> case t of
      TypeRestrictedT _ ty' rs -> do
        goTail ty'
        forM_ rs $ \(tope, term) -> do
          goTerm OutsideParameterPrefix tope
          goTerm OutsideParameterPrefix term
      TypeFunT _ orig md param mtope ret -> do
        checkShapeDependencies InTail t
        goAssumed UseBinder param
        checkDomain orig md param mtope
        inScope orig md param ret goTail
      RecOrT _ rs -> forM_ rs $ \(tope, term) -> do
        goTerm OutsideParameterPrefix tope
        goTail term
      _ -> goAssumed UseConcluded t

    -- Assumed restrictions must sit directly under a shape-Π.
    goAssumed :: forall l. Distinct l => FragmentUse -> TermT l -> TypeCheck l ()
    goAssumed = goAssumedAt OutsideParameterPrefix

    goAssumedAt
      :: forall l. Distinct l
      => PrefixPosition -> FragmentUse -> TermT l -> TypeCheck l ()
    goAssumedAt prefixPosition use original = expose original >>= \t -> case t of
      TypeRestrictedT _ ty' rs -> do
        reportFreeStanding use t
        goAssumed use ty'
        forM_ rs $ \(tope, term) -> do
          goTerm OutsideParameterPrefix tope
          goTerm OutsideParameterPrefix term
      TypeFunT _ orig md param mtope ret -> do
        checkShapeDependencies (InAssumption use) t
        goAssumed UseBinder param
        checkDomain orig md param mtope
        shape <- isShapeBinder (maybe False (const True) mtope) param
        inScope orig md param ret $
          if shape then goExtCodomain use else goAssumed use
      TypeSigmaT _ orig md a b -> do
        goAssumed UseBinder a
        inScope orig md a b (goAssumed use)
      -- Identity-type arguments must be ext-style; endpoints remain terms.
      TypeIdT _ a mtA b -> do
        goTerm OutsideParameterPrefix a
        mapM_ (goAssumed UseIdentity) mtA
        goTerm OutsideParameterPrefix b
      LambdaT info orig mparam body -> do
        md <- case mparam of
          Just (LambdaParam md param mtope) -> do
            goAssumed UseBinder param
            checkDomain orig md param mtope
            when (prefixPosition == OutsideParameterPrefix) $ reportMetaBinder orig param
            pure md
          Nothing -> pure Id
        dom <- binderType info mparam
        inScope orig md dom body (goAssumedAt prefixPosition use)
      -- recordSyntaxUses reports modal syntax; descend here for restriction checks.
      TypeModalT _ _ ty' -> goAssumed use ty'
      RecOrT _ rs -> forM_ rs $ \(tope, term) -> do
        goTerm OutsideParameterPrefix tope
        goAssumed use term
      Var{} -> pure ()
      _ -> goTerm OutsideParameterPrefix t

    -- Direct shape codomains admit boundary clipping to the binder's domain.
    goExtCodomain
      :: forall l. Distinct l => FragmentUse -> TermT l -> TypeCheck l ()
    goExtCodomain use original = expose original >>= \t -> case t of
      TypeRestrictedT _ ty' rs -> do
        goAssumed use ty'
        forM_ rs $ \(face, term) -> do
          goTerm OutsideParameterPrefix face
          goTerm OutsideParameterPrefix term
      _ -> goAssumed use t

    -- Topes may contain applications with their own binders and assumed types.
    checkDomain
      :: forall l. Distinct l
      => Binder -> TModality -> TermT l -> Maybe (ScopedTermT l) -> TypeCheck l ()
    checkDomain orig md param =
      mapM_ (\tope -> inScope orig md param tope (goTerm OutsideParameterPrefix))

    -- RS17 Appendix A.2 requires shapes independent of ambient cube points.
    -- Schematic families are judgements over cube contexts, not extension types.
    checkShapeDependencies :: forall l. Distinct l => CheckedPosition -> TermT l -> TypeCheck l ()
    checkShapeDependencies position t = do
      enabled <- asks ctxWarnShapeDependency
      when enabled $ case t of
        TypeFunT _ orig md param mtope ret -> do
          schematic <- isMetaType t
          shape <- isShapeBinder (maybe False (const True) mtope) param
          when shape $ do
            scope <- asks ctxScope
            let check :: forall k. Distinct k => Foil.Name k -> TermT k -> TermT k -> TypeCheck k ()
                check point cube original = do
                  reportShapeDependency "cube domain" point cube
                  unless schematic $ expose original >>= \body -> case (position, body) of
                    (InAssumption _, TypeRestrictedT _ _ rs) ->
                      forM_ rs $ \(face, _) -> reportShapeDependency "restriction boundary" point face
                    _ -> pure ()
            case mtope of
              Nothing -> withScopedT scope ret $ \binder body ->
                underBinder binder orig md param Nothing $ check (Foil.nameOf binder) (Foil.sink param) body
              Just tope -> withScopedT2 scope tope ret $ \binder domain body ->
                underBinder binder orig md param Nothing $ do
                  unless schematic $
                    reportShapeDependency "shape domain" (Foil.nameOf binder) domain
                  check (Foil.nameOf binder) (Foil.sink param) body
        _ -> pure ()

    reportShapeDependency
      :: forall l. Distinct l => String -> Foil.Name l -> TermT l -> TypeCheck l ()
    reportShapeDependency what point tope = do
      points <- outerPoints point tope
      unless (null points) $ do
        naming <- asks namingOfContext
        loc <- asks ctxLocation
        let render = ppTerm naming . untyped
        recordCheckWarning $ RSTTScopeWarning RSTTShapeDependency
          ("context-dependent " <> what <> " " <> render tope
            <> " (outer cube points: " <> intercalate ", " (map (render . Var) points)
            <> "; in " <> show defName <> ")") loc

    -- Follow aliases and variable types without reducing away dependencies.
    outerPoints :: forall l. Distinct l => Foil.Name l -> TermT l -> TypeCheck l [Foil.Name l]
    outerPoints point t = go (IntSet.singleton (Foil.nameId point)) (freeVarsOfTermT t)
      where
        go _ [] = pure []
        go seen (v : vs)
          | Foil.nameId v `IntSet.member` seen = go seen vs
          | otherwise = do
              ty <- typeOfVar v
              value <- valueOfVar v
              cube <- typeOfUncomputed ty >>= whnfT
              let here = case (value, cube) of
                    (Nothing, UniverseCubeT{}) -> [v]
                    _ -> []
                  dependencies = freeVarsOfTermT ty <> foldMap freeVarsOfTermT value
              rest <- go (IntSet.insert (Foil.nameId v) seen) (dependencies <> vs)
              pure (here <> rest)

    -- Check term binders and route type-valued bodies through goData.
    goTerm :: forall l. Distinct l => PrefixPosition -> TermT l -> TypeCheck l ()
    goTerm InParameterPrefix t | not (isLambda t) = goDataAt InParameterPrefix t
    goTerm prefixPosition t = case t of
      Var{} -> pure ()

      -- Function types returned by lets or branches still introduce shapes.
      TypeFunT{} -> goAssumed UseData t

      -- The stored type is synthesised by elaboration; it is not assumed.
      ReflT _ mx -> forM_ mx $ \(x, _mxty) -> goTerm OutsideParameterPrefix x

      TypeAscT _ term' ty' -> do
        goTerm OutsideParameterPrefix term'
        goTail ty'

      LambdaT info orig mparam body -> do
        checkShapeDependencies InTail (infoType info)
        md <- case mparam of
          Just (LambdaParam md param mtope) -> do
            goAssumed UseBinder param
            checkDomain orig md param mtope
            when (prefixPosition == OutsideParameterPrefix) $ reportMetaBinder orig param
            pure md
          Nothing -> pure Id
        dom <- binderType info mparam
        inScope orig md dom body (goTerm prefixPosition)

      AppT{} -> do
        let (h, args) = collectSpine t []
        goTerm OutsideParameterPrefix h
        forM_ args $ \(fnode, arg) -> do
          -- Substitution can create a forbidden type in a later argument domain.
          when (view == ComputedTypes) $
            typeOfUncomputed fnode >>= funDomain >>= mapM_ (goAssumed UseBinder)
          -- Schema arguments may bind their own schematic parameters.
          plumbing <- domainIsMeta fnode
          if plumbing then goTerm InParameterPrefix arg else goData arg

      -- The motive is assumed: the eliminator binds its variables at it.
      IdJT _ tA a tC d x p -> do
        goData tA
        goTerm OutsideParameterPrefix a
        goAssumed UseMotive tC
        goTerm OutsideParameterPrefix d
        goTerm OutsideParameterPrefix x
        goTerm OutsideParameterPrefix p

      -- Preserve prefix status for parameters bound after a schematic split.
      MatchT _ _md scrut mmotive branches -> do
        goTerm OutsideParameterPrefix scrut
        mapM_ (goAssumed UseMotive) mmotive
        forM_ branches $ \(_con, branch) -> goTerm prefixPosition branch

      PairT _ l r -> do
        goData l
        goData r

      -- The let annotation records the value's concluded type.
      LetT _ orig manno value body -> do
        mapM_ goTail manno
        goData value
        let dom = fromMaybe universeT (manno <|> valueType value)
        inScopeWith orig Id dom (Just value) body (goTerm OutsideParameterPrefix)

      LetModT _ orig _nu mu manno mmotive value body -> do
        mapM_ goTail manno
        mapM_ (goAssumed UseMotive) mmotive
        goData value
        inScope orig mu (fromMaybe universeT (manno <|> valueType value)) body
          (goTerm OutsideParameterPrefix)

      Node (AnnSig _ f) ->
        mapM_ (goTerm OutsideParameterPrefix) (bifoldMap (const []) (:[]) f)

    -- Type-valued data may later become binder or motive types. A proof
    -- quantified over a universe must still be walked as a term.
    goData :: forall l. Distinct l => TermT l -> TypeCheck l ()
    goData = goDataAt OutsideParameterPrefix

    goDataAt :: forall l. Distinct l => PrefixPosition -> TermT l -> TypeCheck l ()
    goDataAt prefixPosition t = do
      isType <- landsInUniverse t
      if isType then goAssumedAt prefixPosition UseData t else goTerm OutsideParameterPrefix t

    isLambda :: forall l. TermT l -> Bool
    isLambda LambdaT{} = True
    isLambda _         = False

    valueType :: forall l. TermT l -> Maybe (TermT l)
    valueType value = infoType <$> typeInfoOf value

    binderType
      :: forall l. Distinct l
      => TypeInfo (TermT l) -> Maybe (LambdaParam (ScopedTermT l) (TermT l))
      -> TypeCheck l (TermT l)
    binderType _info (Just (LambdaParam _ param _mtope)) = pure param
    binderType info Nothing = fromMaybe universeT <$> funDomain (infoType info)

    -- A cube domain without a tope denotes the unrestricted shape.
    isShapeBinder :: forall l. Distinct l => Bool -> TermT l -> TypeCheck l Bool
    isShapeBinder True _ = pure True
    isShapeBinder False param =
      typeOfUncomputed param >>= whnfT >>= \case
        UniverseCubeT{} -> pure True
        _               -> pure False

    -- Is the Π-domain of this function node schematic?
    domainIsMeta :: forall l. Distinct l => TermT l -> TypeCheck l Bool
    domainIsMeta f = do
      tf <- typeOfUncomputed f
      whnfT (stripTypeRestrictions tf) >>= \case
        TypeFunT _ _ _ dom _ _ -> isMetaType dom
        _                      -> pure False

    -- Recognise types and type families by their final codomain.
    landsInUniverse :: forall l. Distinct l => TermT l -> TypeCheck l Bool
    landsInUniverse t =
      typeOfUncomputed t >>= go
      where
        go :: forall k. Distinct k => TermT k -> TypeCheck k Bool
        go ty = whnfT (stripTypeRestrictions ty) >>= \case
          UniverseT{}     -> pure True
          UniverseCubeT{} -> pure True
          UniverseTopeT{} -> pure True
          TypeFunT _ orig md param _mtope ret -> inScope orig md param ret go
          _               -> pure False

    funDomain :: forall l. Distinct l => TermT l -> TypeCheck l (Maybe (TermT l))
    funDomain tf =
      whnfT (stripTypeRestrictions tf) >>= \case
        TypeFunT _ _ _ dom _ _ -> pure (Just dom)
        _                      -> pure Nothing

    reportMetaBinder :: forall l. Distinct l => Binder -> TermT l -> TypeCheck l ()
    reportMetaBinder orig param = do
      enabled <- asks ctxWarnMetaBinder
      when enabled $ do
        meta <- isMetaType param
        when meta $ do
          naming <- asks namingOfContext
          loc <- asks ctxLocation
          recordCheckWarning $ MetaBinderWarning
            defName
            (fromMaybe "_" (binderName orig))
            (ppTerm naming (untyped param))
            loc

    reportFreeStanding :: forall l. Distinct l => FragmentUse -> TermT l -> TypeCheck l ()
    reportFreeStanding use t = do
      enabled <- asks ctxWarnFreeStandingRestriction
      when enabled $ do
        naming <- asks namingOfContext
        loc <- asks ctxLocation
        recordCheckWarning $
          FreeStandingRestrictionWarning defName (ppTerm naming (untyped t)) use loc

-- Expose type constructors without consulting cached normal forms or simplifying
-- their guards. Keep this reducer in step with the object-level rules in Eval.
fragmentHead :: Distinct n => Int -> TermT n -> TypeCheck n (TermT n)
fragmentHead fuel t
  | fuel <= 0 = issueTypeError (TypeErrorOther "RSTT fragment reduction limit reached")
  | otherwise = case t of
      Var v -> valueOfVar v >>= maybe (neutral t) step
      AppT info f x -> step f >>= \case
        LambdaT _ _ _ body -> instantiate body x >>= step
        f' -> neutral (AppT info f' x)
      LetT _ _ _ value body -> instantiate body value >>= step
      TypeAscT _ value _ -> step value
      FirstT info pair -> step pair >>= \case
        PairT _ a _ -> step a
        pair' -> neutral (FirstT info pair')
      SecondT info pair -> step pair >>= \case
        PairT _ _ b -> step b
        pair' -> neutral (SecondT info pair')
      IdJT info a x motive d y proof -> step proof >>= \case
        ReflT{} -> step d
        proof' -> neutral (IdJT info a x motive d y proof')
      RecOrT _ rs -> firstMatching rs >>= maybe (pure t) step
      -- In particular, stop at TypeRestrictedT before reducing its topes.
      _ -> pure t
  where
    step = fragmentHead (fuel - 1)
    neutral term = do
      ty <- typeOfUncomputed term >>= step
      tryRestriction ty >>= maybe (pure term) step

-- TODO: Share this application-spine helper with MetaPrefix in the syntax layer.
collectSpine :: TermT n -> [(TermT n, TermT n)] -> (TermT n, [(TermT n, TermT n)])
collectSpine (AppT _ f x) acc = collectSpine f ((f, x) : acc)
collectSpine h acc            = (h, acc)

-- | Check explicit syntax before typechecking erases source positions and sugars.
-- Report each outermost forbidden node at its nearest available source position.
recordSyntaxUses :: forall n. Distinct n => Term n -> TypeCheck n ()
recordSyntaxUses term = do
  enabled <- asks rsttSafeEnabled
  when enabled $ do
    roles <- mapM (\v -> (,) (Foil.nameId v) <$> infoOfVar varDataRole v)
      (freeVarsOfTerm term)
    let inductive = IntSet.fromList [v | (v, Just _) <- roles]
    loc <- asks ctxLocation
    forM_ (go inductive loc term) $ \warning ->
      local (\ctx -> ctx { ctxLocation = warningLocation warning }) $
        recordCheckWarning warning
  where
    -- Free names retain their identities under binders; bound names are fresh.
    go :: forall l. IntSet.IntSet -> Maybe LocationInfo -> Term l -> [CheckWarning]
    go inductive loc (Var v)
      | Foil.nameId v `IntSet.member` inductive =
          [RSTTScopeWarning RSTTInductive "inductive constructor or eliminator" loc]
      | otherwise = []
    go inductive loc t@(Node (AnnSig _ sig)) =
      let here = case positionOfTerm t of
            Nothing -> loc
            Just pos -> atPosition pos <$> loc
      in if allowedNode sig
        then bifoldMap (\(ScopedAST _ body) -> go inductive here body)
                       (go inductive here) sig
        else let (extension, feature) = syntaxExtension sig
             in [RSTTScopeWarning extension feature here]

-- New constructors are forbidden until explicitly admitted here.
allowedNode :: TermSig scope term -> Bool
allowedNode = \case
  UniverseF -> True
  UniverseCubeF -> True
  UniverseTopeF -> True
  CubeUnitF -> True
  CubeUnitStarF -> True
  Cube2F -> True
  Cube2_0F -> True
  Cube2_1F -> True
  CubeProductF{} -> True
  TopeTopF -> True
  TopeBottomF -> True
  TopeEQF{} -> True
  TopeLEQF{} -> True
  TopeAndF{} -> True
  TopeOrF{} -> True
  RecBottomF -> True
  RecOrF{} -> True
  TypeFunF _ Id _ _ _ -> True
  TypeSigmaF _ Id _ _ -> True
  TypeIdF{} -> True
  AppF{} -> True
  LetF{} -> True
  LambdaF _ Nothing _ -> True
  LambdaF _ (Just (LambdaParam Id _ _)) _ -> True
  PairF{} -> True
  FirstF{} -> True
  SecondF{} -> True
  ReflF{} -> True
  IdJF{} -> True
  UnitF -> True
  TypeUnitF -> True
  TypeAscF{} -> True
  TypeRestrictedF{} -> True
  -- Hole checking also handles unresolved identifiers and unfinished obligations.
  HoleF{} -> True
  _ -> False

-- Classification improves diagnostics; it never grants admission.
syntaxExtension :: TermSig scope term -> (RSTTExtension, String)
syntaxExtension = \case
  TypeFunF{} -> modal "modal function binder"
  TypeSigmaF{} -> modal "modal pair binder"
  LambdaF{} -> modal "modal lambda binder"
  TypeModalF{} -> modal "modal type"
  ModAppF{} -> modal "modal introduction"
  ModExtractF{} -> modal "modal extraction"
  LetModF{} -> modal "modal let binding"
  CubeIF -> interval "auxiliary interval II"
  CubeI_0F -> interval "auxiliary interval endpoint 0_I"
  CubeI_1F -> interval "auxiliary interval endpoint 1_I"
  CubeFlipF{} -> interval "interval flip"
  CubeUnflipF{} -> interval "interval unflip"
  TopeInvF{} -> interval "tope involution"
  TopeUninvF{} -> interval "inverse tope involution"
  MatchF{} -> (RSTTInductive, "inductive match")
  MatchArmF{} -> (RSTTInductive, "inductive match arm")
  CubeSupF{} -> (RSTTUnsupported, "cube supremum (sup)")
  CubeInfF{} -> (RSTTUnsupported, "cube infimum (inf)")
  _ -> (RSTTUnsupported, "unrecognised syntax")
  where
    modal = (,) RSTTModal
    interval = (,) RSTTInterval