packages feed

rzk-0.10.0: test/Rzk/FoilCoreSpec.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The free-foil core: the surface conversions, α-equivalence, and the typing
-- context.
--
-- The context tests are the load-bearing ones. Entering a binder sinks the
-- context by coercion (see "Rzk.TypeCheck.Context"), which is sound but unsafe if
-- the argument is ever wrong, so these check the things the argument promises:
-- that an entry stays intact and keeps its meaning however deeply it is sunk.
module Rzk.FoilCoreSpec (spec) where

import qualified Control.Monad.Foil       as Foil
import           Control.Monad.Free.Foil  (AST (Var), ScopedAST (..),
                                           alphaEquiv)
import qualified Data.Text                as T
import           Test.Hspec

import           Language.Rzk.Foil.Convert (toTermClosed)
import           Language.Rzk.Foil.Print   (fromTermClosed)
import           Language.Rzk.Foil.Syntax
import           Language.Rzk.Foil.Names  (Binder (..), TModality (..),
                                           VarIdent, binderName)
import qualified Language.Rzk.Syntax       as Rzk
import           Rzk.TypeCheck.Context
import           Rzk.TypeCheck.Display (namingOfContext, ppName, ppTerm)
import           Rzk.TypeCheck.Error   (OutputDirection (..),
                                        ppTypeErrorInScopedContext)
import           Rzk.TypeCheck.Eval    (nfT, whnfT)
import           Rzk.TypeCheck.Judgements (infer, typecheck)
import           Rzk.TypeCheck.Monad   (runTypeCheck)

-- | Parse a term of the surface syntax, or fail loudly.
parse :: T.Text -> Rzk.Term
parse t =
  case Rzk.parseTerm t of
    Left err   -> error ("cannot parse " <> show t <> ": " <> T.unpack err)
    Right term -> term

-- | Parse a closed term into the core.
core :: T.Text -> Term Foil.VoidS
core = toTermClosed . parse

-- | Parse, elaborate to the core, and print back as surface syntax.
roundTrip :: T.Text -> T.Text
roundTrip = T.pack . Rzk.printTree . fromTermClosed . core

-- | Are two closed terms α-equivalent?
equivalent :: T.Text -> T.Text -> Bool
equivalent l r = alphaEquiv Foil.emptyScope (core l) (core r)

-- | A hypothesis of the given type, with no value and no modality.
hypothesis :: VarIdent -> TermT n -> VarInfo n
hypothesis name ty = VarInfo
  { varType = ty
  , varValue = Nothing
  , varModality = Id
  , varModAccum = Id
  , varOrig = BinderVar (Just name)
  , varIsAssumption = False
  , varIsTopLevel = False
  , varDeclaredAssumptions = []
  , varLocation = Nothing
  }

spec :: Spec
spec = do
  -- The terms are closed (a free variable has no name to resolve to in an empty
  -- scope), so each type-level example binds its own A and B.
  describe "surface syntax round-trips through the core" $ do
    let it_roundTrips t expected =
          it (T.unpack t) $ roundTrip t `shouldBe` expected

    it_roundTrips "\\ x -> x" "\\ x → x"
    it_roundTrips "\\ (t, s) -> t" "\\ (t, s) → t"
    it_roundTrips "\\ A -> \\ (x : A) -> x" "\\ A → \\ (x : A) → x"
    it_roundTrips "\\ A -> \\ B -> (x : A) -> B" "\\ A → \\ B → (x : A) → B"
    it_roundTrips "\\ A -> \\ B -> A -> B" "\\ A → \\ B → A → B"
    it_roundTrips "\\ A -> \\ B -> Sigma (x : A), B" "\\ A → \\ B → Σ (x : A), B"
    it_roundTrips "\\ (t, s) -> first (t, s)" "\\ (t, s) → π₁ (t, s)"
    it_roundTrips "\\ A -> \\ (x : A) -> refl_{x : A}" "\\ A → \\ (x : A) → refl_{ x : A }"
    it_roundTrips "\\ t -> \\ s -> t === s" "\\ t → \\ s → t ≡ s"

  describe "α-equivalence ignores binder names" $ do
    it "\\ x -> x is \\ y -> y" $
      equivalent "\\ x -> x" "\\ y -> y" `shouldBe` True

    -- The old representation derived Eq structurally, so it compared the binder
    -- names too and called these two unequal. This is the behaviour change.
    it "\\ x -> \\ y -> x is not \\ x -> \\ y -> y" $
      equivalent "\\ x -> \\ y -> x" "\\ x -> \\ y -> y" `shouldBe` False

    it "a pair pattern binds one variable, and its projections are right" $ do
      equivalent "\\ (t, s) -> t" "\\ (a, b) -> a" `shouldBe` True
      equivalent "\\ (t, s) -> t" "\\ (a, b) -> b" `shouldBe` False

    it "two holes with different names are different terms" $
      equivalent "?a" "?b" `shouldBe` False

  describe "the context" $ do
    it "resolves a name bound three binders up" $ do
      let ctx0 = emptyContext
      withFreshBinder ctx0 (hypothesis "A" universeT) $ \bA ctxA ->
        withFreshBinder ctxA (hypothesis "x" (Var (Foil.nameOf bA))) $ \_bx ctxx ->
          withFreshBinder ctxx (hypothesis "y" cubeT) $ \_by ctxy -> do
            -- 'A' was bound at the outermost scope and has been sunk twice.
            case lookupNamed "A" ctxy of
              Nothing -> expectationFailure "A is not in scope"
              Just nameA -> do
                let info = lookupVarInfo nameA ctxy
                binderName (varOrig info) `shouldBe` Just "A"
                alphaEquiv (ctxScope ctxy) (untyped (varType info)) (untyped universeT)
                  `shouldBe` True

    it "keeps a sunk hypothesis pointing at the name it was typed by" $ do
      -- x : A, where A is itself a bound name. After sinking x's entry into a
      -- deeper scope, its type must still be *that* name, not a stale or
      -- renamed one: this is what the coercion in 'enterBinder' promises.
      withFreshBinder emptyContext (hypothesis "A" universeT) $ \bA ctxA -> do
        let nameA = Foil.nameOf bA
        withFreshBinder ctxA (hypothesis "x" (Var nameA)) $ \_bx ctxx ->
          withFreshBinder ctxx (hypothesis "y" cubeT) $ \_by ctxy ->
            case lookupNamed "x" ctxy of
              Nothing -> expectationFailure "x is not in scope"
              Just nameX -> do
                let info = lookupVarInfo nameX ctxy
                case untyped (varType info) of
                  Var name -> Foil.nameId name `shouldBe` Foil.nameId nameA
                  _        -> expectationFailure "the type of x is no longer a variable"

    it "lets the outer binder keep its name when an inner one shadows it" $
      -- Display names are claimed oldest binding first (see 'ctxBound'), so the
      -- inner 'x' is the one refreshed. The order cannot be read off the name
      -- ids: free-foil refreshes a binder only on a clash, so after a
      -- substitution an inner binder may carry a smaller id than an outer one.
      withFreshBinder emptyContext (hypothesis "x" universeT) $ \bOuter ctxOuter ->
        withFreshBinder ctxOuter (hypothesis "x" universeT) $ \bInner ctxInner -> do
          let naming = namingOfContext ctxInner
          ppName naming (Foil.sink (Foil.nameOf bOuter)) `shouldBe` "x"
          ppName naming (Foil.nameOf bInner) `shouldNotBe` "x"

    it "records the names in scope for the shadowing check" $
      withFreshBinder emptyContext (hypothesis "A" universeT) $ \_bA ctxA ->
        shadowedBy "A" ctxA `shouldBe` ["A" :: VarIdent]

  describe "evaluation" $ do
    -- The identity on the directed interval, applied to an endpoint. A cube-layer
    -- term, so this drives the whole reduction path: whnfT dispatches on the
    -- layer, nfTope reduces the redex, and the argument is substituted into the
    -- body (invalidating the memo on the way).
    let identityOn2 :: TermT Foil.VoidS
        identityOn2 = Foil.withFresh Foil.emptyScope $ \binder ->
          let t = BinderVar (Just "t")
              ty = typeFunT t Id cube2T Nothing (ScopedAST binder cube2T)
           in lambdaT ty t (Just (LambdaParam Id cube2T Nothing))
                (ScopedAST binder (Var (Foil.nameOf binder)))

        applied = appT cube2T identityOn2 cube2_0T

    it "reduces (\\ t -> t) 0₂ to 0₂" $
      case runTypeCheck (whnfT applied) of
        Left err -> expectationFailure (ppTypeErrorInScopedContext TopDown err)
        Right result ->
          alphaEquiv Foil.emptyScope (untyped result) (untyped cube2_0T)
            `shouldBe` True

    it "does not reduce the unapplied identity" $
      case runTypeCheck (whnfT identityOn2) of
        Left err -> expectationFailure (ppTypeErrorInScopedContext TopDown err)
        Right result ->
          alphaEquiv Foil.emptyScope (untyped result) (untyped cube2_0T)
            `shouldBe` False

  describe "the checker" $ do
    -- Parse, elaborate, infer, normalise, and print back: the whole pipeline of
    -- the new core, end to end.
    let inferAndShow :: T.Text -> Either String String
        inferAndShow t =
          case runTypeCheck (infer (core t) >>= nfT) of
            Left err     -> Left (ppTypeErrorInScopedContext TopDown err)
            Right result -> Right (ppTerm (namingOfContext emptyContext) (untyped result))

        checkAndShow :: T.Text -> T.Text -> Either String String
        checkAndShow t ty =
          case runTypeCheck (infer (core ty) >>= typecheck (core t) >>= nfT) of
            Left err     -> Left (ppTypeErrorInScopedContext TopDown err)
            Right result -> Right (ppTerm (namingOfContext emptyContext) (untyped result))

    it "infers and evaluates (\\ (x : Unit) -> x) unit" $
      inferAndShow "(\\ (x : Unit) -> x) unit" `shouldBe` Right "unit"

    it "infers a dependent function type" $
      inferAndShow "(A : U) -> A -> A" `shouldBe` Right "(A : U) → A → A"

    it "checks the identity against its type" $
      -- the elaborated lambda carries the domain it was checked against
      checkAndShow "\\ A -> \\ (x : A) -> x" "(A : U) -> A -> A"
        `shouldBe` Right "\\ (A : U) → \\ (x : A) → x"

    it "rejects a term that does not have the expected type" $
      case checkAndShow "\\ A -> \\ (x : A) -> A" "(A : U) -> A -> A" of
        Left err -> err `shouldContain` "cannot unify"
        Right t  -> expectationFailure ("expected a type error, got " <> t)

    -- A cube domain with a pair pattern: the binder binds one variable whose
    -- components are projections, and the pattern is shown back.
    it "checks a function from a cube point" $
      checkAndShow "\\ A -> \\ (a : A) -> \\ ((t, s) : 2 * 2) -> a"
                   "(A : U) -> (a : A) -> (t : 2 * 2) -> A"
        `shouldBe` Right "\\ (A : U) → \\ (a : A) → \\ ((t, s) : 2 × 2) → a"