packages feed

thrift-compiler-0.1.0.1: Thrift/Compiler/Typechecker/Monad.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# OPTIONS_GHC -Wno-star-is-type #-}
module Thrift.Compiler.Typechecker.Monad
  ( TC, Typechecked
  , typeError, runTypechecker, ask, asks, traverseWeird
  , Alternative(..)
  , lookupType, lookupSchema, lookupUnion, lookupEnum, lookupConst
  , lookupService, lookupEnumInt
  , TypeError(..), ErrorMsg(..), AnnotationPlacement(..)
  , orderError
  ) where

import Prelude hiding (Enum)
import Control.Applicative
import Data.Either ( rights )
import Data.Int (Int32)
import Data.Some
import Data.Text (Text)
import Control.Monad.Reader
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set

import Thrift.Compiler.Parser
import Thrift.Compiler.Types

-- Typechecking Monad ----------------------------------------------------------

-- The typechecking monad is a reader transformed either monad. This allows us
-- to keep track of state and errors
newtype TC l a = TC (ReaderT (Env l) (Either [TypeError l]) a)
  deriving (Functor,Monad, MonadReader (Env l))

instance Applicative (TC l) where
  pure a = TC (pure a)
  -- | Special Applicative instance which allows us to collect
  -- type errors from *both* parts of the computation in the
  -- event that they both fail.
  TC (ReaderT rf) <*> TC (ReaderT rx) =
    TC $ ReaderT $ \env -> rf env `collect` rx env
    where
        collect (Left e1) (Left e2) = Left $ e1 <> e2
        collect (Left e)  (Right _) = Left e
        collect (Right _) (Left e)  = Left e
        collect (Right f) (Right x) = Right $ f x

type Typechecked (l :: *) (t :: Status -> * -> * -> *) =
  TC l (t 'Resolved l Loc)

typeError :: Loc -> ErrorMsg l -> TC l a
typeError loc msg = TC $ lift $ Left [TypeError loc msg]

runTypechecker :: Env l -> TC l a -> Either [TypeError l] a
runTypechecker env (TC t) = runReaderT t env

instance Alternative (TC l) where
  -- This produces an empty list of errors, so should never be used as the
  -- first parater of '<|>'.
  empty = TC $ lift (Left [])
  t1 <|> t2 = TC $ do
    env <- ask
    lift $ case runTypechecker env t1 of
      Left err1 -> case runTypechecker env t2 of
        Left err2 -> Left (err1 <> err2)
        x2@Right{} -> x2
      x1@Right{} -> x1

-- | See T45688659 for the weird tale of how badly-typed keys are getting
-- ignored.  Try to be similar here, in weird mode, by dropping errors.
traverseWeird :: Bool -> (a -> TC l b) -> [a] -> TC l [b]
traverseWeird optsLenient f xs = do
    if not optsLenient then
      traverse f xs
    else
      TC $ ReaderT $ \env -> Right (mapEWeird (runTypechecker env . f) xs)
  where
    mapEWeird :: (a -> Either [e] b) -> [a] -> [b]
    mapEWeird ff = rights . map ff

-- Lookup Functions ------------------------------------------------------------

lookupType :: ThriftName -> Loc -> TC l (Some (Type l))
lookupType = envCtxLookup typeMap UnknownType

lookupSchema :: ThriftName -> Loc -> TC l (Some (Schema l))
lookupSchema = envLookup schemaMap UnknownType

lookupUnion :: ThriftName -> Loc -> TC l (Some (USchema l))
lookupUnion = envLookup unionMap UnknownType

lookupEnum :: ThriftName -> Loc -> TC l EnumValues
lookupEnum = envLookup enumMap UnknownType

-- | Allow enum values as default values for int fields.
-- This is a type violation.  This is added to allow parsing these old files
-- when @--lenient@ mode is active (see 'Options.optsLenient').
lookupEnumInt :: ThriftName -> Loc -> TC l Int32
lookupEnumInt name loc = do
  valueOrCollision <- envLookup enumInt UnknownEnumValue name loc
  case valueOrCollision of
    Just value -> pure value
    Nothing -> typeError loc $ MultipleEnumValues name

-- | Loc parameter of usage of ThriftName is for error messages.
-- Loc in result is where ThriftName was defined.
lookupConst :: ThriftName -> Loc -> TC l (Some (Type l), Name, Loc)
lookupConst = envCtxLookup constMap UnknownConst

-- | Loc parameter of usage of ThriftName is for error messages.
-- Loc in result is where ThriftName was defined.
lookupService :: ThriftName -> Loc -> TC l (Name, Set.Set Text, Loc)
lookupService = envLookup serviceMap UnknownService

envLookup
  :: (Env l -> Map.Map Text u)
  -> (ThriftName -> ErrorMsg l)
  -> ThriftName
  -> Loc
  -> TC l u
envLookup getMap mkError name loc = do
  env <- TC ask
  case doLookup env name of
    Nothing -> typeError loc $ mkError name
    Just u  -> pure u
  where
    doLookup env (UName n) = Map.lookup n (getMap env)
    doLookup env (QName m n) =
      Map.lookup n . getMap =<< Map.lookup m (importMap env)

envCtxLookup
  :: (Env l -> Context u)
  -> (ThriftName -> ErrorMsg l)
  -> ThriftName
  -> Loc
  -> TC l u
envCtxLookup getMap mkError name loc = do
  env <- TC ask
  case doLookup env name of
    Nothing -> typeError loc $ mkError name
    Just u  -> pure u
  where
    doLookup env (UName n) = Map.lookup n (cMap $ getMap env)
    doLookup env (QName m n) =
      Map.lookup n . cMap . getMap =<< Map.lookup m (importMap env)

-- Type Errors -----------------------------------------------------------------

data TypeError l
  -- Intra-Modular Type Errors
  = TypeError Loc (ErrorMsg l)
  -- Inter-Modular Errors
  | EmptyInput
  | CyclicModules [ThriftFile SpliceFile Loc]

data ErrorMsg l
  = CyclicTypes [Parsed Decl]
  | CyclicServices [Parsed Service]
  | UnknownType ThriftName
  | UnknownConst ThriftName
  | UnknownService ThriftName
  | UnknownEnumValue ThriftName
  | MultipleEnumValues ThriftName
  | forall t. LiteralMismatch (Type l t) (UntypedConst Loc)
  | forall u v. IdentMismatch (Type l u) (Type l v) ThriftName
  | UnknownField Text
  | MissingField Text
  | InvalidFieldId Text Int
  | InvalidField (UntypedConst Loc)
  | InvalidUnion Name Int
  | EmptyUnion Text
  | forall t. InvalidThrows (Type l t) Text
  | AnnotationMismatch (AnnotationPlacement l) (Annotation Loc)
  | DuplicateName Text
  | DuplicateEnumVal Text [Text] Int
  | forall t1 t2. TypeMismatch (Type l t1) (Type l t2)
  | forall t. NotDefinedBeforeUsed (Type l t)

data AnnotationPlacement l
  = forall t. AnnType (Type l t)
  | AnnField | AnnStruct | AnnUnion | AnnTypedef | AnnEnum | AnnPriority

orderError :: TypeError l -> TypeError l -> Ordering
-- EmptyInput always comes first
orderError EmptyInput EmptyInput = EQ
orderError EmptyInput _ = LT
orderError _ EmptyInput = GT
-- CyclicModules comes next
orderError CyclicModules{} CyclicModules{} = EQ
orderError CyclicModules{} _ = LT
orderError _ CyclicModules{} = GT
-- Type Errors are ordered by Loc
orderError (TypeError l1 _) (TypeError l2 _) = compare l1 l2