packages feed

rzk-0.9.2: test/Rzk/RefIndexSpec.hs

{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Tests for the reference index behind go-to-definition, find-references,
-- and the surface-annotation tier of hover. Everything here is pure: modules
-- are parsed, never typechecked, which is exactly the situation the index is
-- for (mid-edit and ill-typed code).
module Rzk.RefIndexSpec (spec) where

import           Test.Hspec

#ifdef LSP_ENABLED
import qualified Data.Text                          as T
import           Language.Rzk.Syntax                (parseModule)
import qualified Language.Rzk.VSCode.ReferenceIndex as RI

indexOf :: [(FilePath, T.Text)] -> RI.ReferenceIndex
indexOf sources = RI.indexModules
  [ (path, m)
  | (path, src) <- sources
  , Right m <- [parseModule src]
  ]

-- All positions are 0-based, as in the LSP protocol.
lookupAt' :: RI.ReferenceIndex -> FilePath -> (Int, Int) -> Maybe RI.Binding
lookupAt' ri path (l, c) = RI.lookupAt ri (RI.Uri path) (RI.Position l c)

-- | Like 'lookupAt'', erroring out loudly when nothing is there, so a
-- misplaced test position is obvious.
bindingAt :: RI.ReferenceIndex -> FilePath -> (Int, Int) -> RI.Binding
bindingAt ri path pos = case lookupAt' ri path pos of
  Just b  -> b
  Nothing -> error ("no binding at " <> path <> ":" <> show pos)

defPos :: RI.Binding -> (FilePath, Int, Int)
defPos b =
  let RI.Location (RI.Uri p) (RI.Range (RI.Position l c) _) = RI.bindingDef b
  in (p, l, c)

libSrc :: T.Text
libSrc = T.unlines
  [ "#lang rzk-1"                                        -- 0
  , "#define id (A : U) (x : A) : A := x"                -- 1
  ]

mainSrc :: T.Text
mainSrc = T.unlines
  [ "#lang rzk-1"                                                 -- 0
  , "#variable C : U"                                             -- 1
  , "#variables D E : C -> U"                                     -- 2
  , "#define twice (A : U) (f : A -> A) (x : A) : A := f (f x)"   -- 3
  , "#define use-id (A : U) (x : A) : A := id A (twice A (id A) x)" -- 4
  , "#define shadow (id : C) : C := id"                           -- 5
  , "#define pairs"                                               -- 6
  , "  ( A : U)"                                                  -- 7
  , "  ( B : A -> U)"                                             -- 8
  , "  ( (p , q) : Sigma (x : A) , B x)"                          -- 9
  , "  ( (t , s) : 2 * 2)"                                        -- 10
  , "  ( ((a , b) , c) : (2 * 2) * 2)"                            -- 11
  , "  ( (u , v) : 2 * 2 | t <= u /\\ s <= v)"                    -- 12
  , "  ( (h , k , l) : Sigma (x : A , y : B x) , C)"              -- 13
  , "  : U"                                                       -- 14
  , "  := A"                                                      -- 15
  ]

spec :: Spec
spec = describe "reference index" $ do
  let ri = indexOf [("lib.rzk", libSrc), ("main.rzk", mainSrc)]
      annAt path pos = lookupAt' ri path pos >>= RI.bindingType

  describe "definitions and references" $ do
    it "resolves a cross-file use to its definition" $
      (defPos <$> lookupAt' ri "main.rzk" (4, 38)) `shouldBe` Just ("lib.rzk", 1, 8)

    it "lists references without duplicating the definition site" $ do
      let b = bindingAt ri "lib.rzk" (1, 8)
      length (RI.bindingRefs b) `shouldBe` 2                -- two uses in use-id
      RI.bindingDef b `notElem` RI.bindingRefs b `shouldBe` True
      length (RI.bindingSites b) `shouldBe` 3               -- def : refs

    it "resolves a local binder and its use" $ do
      let b = bindingAt ri "main.rzk" (3, 55)               -- x in f (f x)
      defPos b `shouldBe` ("main.rzk", 3, 36)
      length (RI.bindingSites b) `shouldBe` 2

    it "resolves a local that shadows a global to the local" $
      (defPos <$> lookupAt' ri "main.rzk" (5, 31))          -- id in := id
        `shouldBe` Just ("main.rzk", 5, 16)

    it "misses positions outside identifiers" $
      lookupAt' ri "main.rzk" (3, 13) `shouldBe` Nothing    -- whitespace

  describe "surface type annotations" $ do
    it "annotates parameters" $ do
      annAt "main.rzk" (3, 15) `shouldBe` Just "U"          -- (A : U)
      annAt "main.rzk" (3, 23) `shouldBe` Just "A -> A"     -- (f : A -> A)

    it "annotates assumptions (#variable, #variables)" $ do
      annAt "main.rzk" (1, 10) `shouldBe` Just "U"
      annAt "main.rzk" (2, 11) `shouldBe` Just "C -> U"
      annAt "main.rzk" (2, 13) `shouldBe` Just "C -> U"

    it "leaves top-level names to the elaborated tier" $ do
      let b = bindingAt ri "main.rzk" (3, 8)                -- twice
      RI.bindingType b `shouldBe` Nothing

    it "splits a pair against a Σ-type, instantiating the dependency" $ do
      annAt "main.rzk" (9, 5) `shouldBe` Just "A"           -- p
      annAt "main.rzk" (9, 9) `shouldBe` Just "B p"         -- q

    it "splits pairs against cube products, nested too" $ do
      annAt "main.rzk" (10, 5) `shouldBe` Just "2"          -- t
      annAt "main.rzk" (10, 9) `shouldBe` Just "2"          -- s
      annAt "main.rzk" (11, 6) `shouldBe` Just "2"          -- a
      annAt "main.rzk" (11, 10) `shouldBe` Just "2"         -- b
      annAt "main.rzk" (11, 15) `shouldBe` Just "2"         -- c

    it "gives shaped pair components their cube" $ do
      annAt "main.rzk" (12, 5) `shouldBe` Just "2"          -- u
      annAt "main.rzk" (12, 9) `shouldBe` Just "2"          -- v

    it "splits a tuple against a Σ-tuple" $ do
      annAt "main.rzk" (13, 5) `shouldBe` Just "A"          -- h
      annAt "main.rzk" (13, 9) `shouldBe` Just "B h"        -- k
      annAt "main.rzk" (13, 13) `shouldBe` Just "C"         -- l
#else
spec :: Spec
spec = describe "reference index" (pure ())
#endif