{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} -- | -- SPDX-License-Identifier: BSD-3-Clause -- -- Pretty-printing for the Swarm language. module Swarm.Language.Pretty where import Control.Lens.Combinators (pattern Empty) import Control.Unification import Control.Unification.IntVar import Data.Bool (bool) import Data.Functor.Fixedpoint (Fix, unFix) import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as M import Data.Set (Set) import Data.Set qualified as S import Data.String (fromString) import Data.Text (Text) import Data.Text qualified as T import Prettyprinter import Prettyprinter.Render.String qualified as RS import Prettyprinter.Render.Text qualified as RT import Swarm.Language.Capability import Swarm.Language.Context import Swarm.Language.Parse (getLocRange) import Swarm.Language.Syntax import Swarm.Language.Typecheck import Swarm.Language.Types import Swarm.Util (showEnum, showLowT, unsnocNE) import Witch ------------------------------------------------------------ -- PrettyPrec class + utilities -- | Type class for things that can be pretty-printed, given a -- precedence level of their context. class PrettyPrec a where prettyPrec :: Int -> a -> Doc ann -- can replace with custom ann type later if desired -- | Pretty-print a thing, with a context precedence level of zero. ppr :: (PrettyPrec a) => a -> Doc ann ppr = prettyPrec 0 -- | Render a pretty-printed document as @Text@. docToText :: Doc a -> Text docToText = RT.renderStrict . layoutPretty defaultLayoutOptions -- | Pretty-print something and render it as @Text@. prettyText :: (PrettyPrec a) => a -> Text prettyText = docToText . ppr -- | Pretty-print something and render it as (preferably) one line @Text@. prettyTextLine :: (PrettyPrec a) => a -> Text prettyTextLine = RT.renderStrict . layoutPretty (LayoutOptions Unbounded) . group . ppr -- | Render a pretty-printed document as a @String@. docToString :: Doc a -> String docToString = RS.renderString . layoutPretty defaultLayoutOptions -- | Pretty-print something and render it as a @String@. prettyString :: (PrettyPrec a) => a -> String prettyString = docToString . ppr -- | Optionally surround a document with parentheses depending on the -- @Bool@ argument and if it does not fit on line, indent the lines, -- with the parens on separate lines. pparens :: Bool -> Doc ann -> Doc ann pparens True = group . encloseWithIndent 2 lparen rparen pparens False = id encloseWithIndent :: Int -> Doc ann -> Doc ann -> Doc ann -> Doc ann encloseWithIndent i l r = nest i . enclose (l <> line') (nest (-2) $ line' <> r) -- | Surround a document with backticks. bquote :: Doc ann -> Doc ann bquote = group . enclose "`" "`" -- | Turn a 'Show' instance into a @Doc@, lowercasing it in the -- process. prettyShowLow :: Show a => a -> Doc ann prettyShowLow = pretty . showLowT -------------------------------------------------- -- Bullet lists data Prec a = Prec Int a data BulletList i = BulletList { bulletListHeader :: forall a. Doc a , bulletListItems :: [i] } instance (PrettyPrec i) => PrettyPrec (BulletList i) where prettyPrec _ (BulletList hdr items) = nest 2 . vcat $ hdr : map (("-" <+>) . ppr) items ------------------------------------------------------------ -- PrettyPrec instances for terms, types, etc. instance PrettyPrec Text where prettyPrec _ = pretty instance PrettyPrec BaseTy where prettyPrec _ BVoid = "void" prettyPrec _ BUnit = "unit" prettyPrec _ BInt = "int" prettyPrec _ BDir = "dir" prettyPrec _ BText = "text" prettyPrec _ BBool = "bool" prettyPrec _ BActor = "actor" prettyPrec _ BKey = "key" instance PrettyPrec IntVar where prettyPrec _ = pretty . mkVarName "u" -- | We can use the 'Wildcard' value to replace unification variables -- when we don't care about them, e.g. to print out the shape of a -- type like @(_ -> _) * _@ data Wildcard = Wildcard deriving (Eq, Ord, Show) instance PrettyPrec Wildcard where prettyPrec _ _ = "_" -- | Split a function type chain, so that we can pretty print -- the type parameters aligned on each line when they don't fit. class UnchainableFun t where unchainFun :: t -> [t] instance UnchainableFun Type where unchainFun (a :->: ty) = a : unchainFun ty unchainFun ty = [ty] instance UnchainableFun (UTerm TypeF ty) where unchainFun (UTerm (TyFunF ty1 ty2)) = ty1 : unchainFun ty2 unchainFun ty = [ty] instance (PrettyPrec (t (Fix t))) => PrettyPrec (Fix t) where prettyPrec p = prettyPrec p . unFix instance (PrettyPrec (t (UTerm t v)), PrettyPrec v) => PrettyPrec (UTerm t v) where prettyPrec p (UTerm t) = prettyPrec p t prettyPrec p (UVar v) = prettyPrec p v instance ((UnchainableFun t), (PrettyPrec t)) => PrettyPrec (TypeF t) where prettyPrec _ (TyBaseF b) = ppr b prettyPrec _ (TyVarF v) = pretty v prettyPrec p (TySumF ty1 ty2) = pparens (p > 1) $ prettyPrec 2 ty1 <+> "+" <+> prettyPrec 1 ty2 prettyPrec p (TyProdF ty1 ty2) = pparens (p > 2) $ prettyPrec 3 ty1 <+> "*" <+> prettyPrec 2 ty2 prettyPrec p (TyCmdF ty) = pparens (p > 9) $ "cmd" <+> prettyPrec 10 ty prettyPrec _ (TyDelayF ty) = braces $ ppr ty prettyPrec p (TyFunF ty1 ty2) = let (iniF, lastF) = unsnocNE $ ty1 NE.:| unchainFun ty2 funs = (prettyPrec 1 <$> iniF) <> [ppr lastF] inLine l r = l <+> "->" <+> r multiLine l r = l <+> "->" <> hardline <> r in pparens (p > 0) . align $ flatAlt (concatWith multiLine funs) (concatWith inLine funs) prettyPrec _ (TyRcdF m) = brackets $ hsep (punctuate "," (map prettyBinding (M.assocs m))) instance PrettyPrec Polytype where prettyPrec _ (Forall [] t) = ppr t prettyPrec _ (Forall xs t) = hsep ("∀" : map pretty xs) <> "." <+> ppr t instance PrettyPrec UPolytype where prettyPrec _ (Forall [] t) = ppr t prettyPrec _ (Forall xs t) = hsep ("∀" : map pretty xs) <> "." <+> ppr t instance (PrettyPrec t) => PrettyPrec (Ctx t) where prettyPrec _ Empty = emptyDoc prettyPrec _ (assocs -> bs) = brackets (hsep (punctuate "," (map prettyBinding bs))) prettyBinding :: (Pretty a, PrettyPrec b) => (a, b) -> Doc ann prettyBinding (x, ty) = pretty x <> ":" <+> ppr ty instance PrettyPrec Direction where prettyPrec _ = pretty . directionSyntax instance PrettyPrec Capability where prettyPrec _ c = pretty $ T.toLower (from (NE.tail $ showEnum c)) instance PrettyPrec Const where prettyPrec p c = pparens (p > fixity (constInfo c)) $ pretty . syntax . constInfo $ c instance PrettyPrec (Syntax' ty) where prettyPrec p = prettyPrec p . eraseS instance PrettyPrec Term where prettyPrec _ TUnit = "()" prettyPrec p (TConst c) = prettyPrec p c prettyPrec _ (TDir d) = ppr d prettyPrec _ (TInt n) = pretty n prettyPrec _ (TAntiInt v) = "$int:" <> pretty v prettyPrec _ (TText s) = fromString (show s) prettyPrec _ (TAntiText v) = "$str:" <> pretty v prettyPrec _ (TBool b) = bool "false" "true" b prettyPrec _ (TRobot r) = " pretty r <> ">" prettyPrec _ (TRef r) = "@" <> pretty r prettyPrec p (TRequireDevice d) = pparens (p > 10) $ "require" <+> ppr @Term (TText d) prettyPrec p (TRequire n e) = pparens (p > 10) $ "require" <+> pretty n <+> ppr @Term (TText e) prettyPrec p (TRequirements _ e) = pparens (p > 10) $ "requirements" <+> ppr e prettyPrec _ (TVar s) = pretty s prettyPrec _ (TDelay _ t) = group . encloseWithIndent 2 lbrace rbrace $ ppr t prettyPrec _ t@TPair {} = prettyTuple t prettyPrec p t@(TLam {}) = pparens (p > 9) $ prettyLambdas t -- Special handling of infix operators - ((+) 2) 3 --> 2 + 3 prettyPrec p (TApp t@(TApp (TConst c) l) r) = let ci = constInfo c pC = fixity ci in case constMeta ci of ConstMBinOp assoc -> pparens (p > pC) $ hsep [ prettyPrec (pC + fromEnum (assoc == R)) l , ppr c , prettyPrec (pC + fromEnum (assoc == L)) r ] _ -> prettyPrecApp p t r prettyPrec p (TApp t1 t2) = case t1 of TConst c -> let ci = constInfo c pC = fixity ci in case constMeta ci of ConstMUnOp P -> pparens (p > pC) $ ppr t1 <> prettyPrec (succ pC) t2 ConstMUnOp S -> pparens (p > pC) $ prettyPrec (succ pC) t2 <> ppr t1 _ -> prettyPrecApp p t1 t2 _ -> prettyPrecApp p t1 t2 prettyPrec _ (TLet _ x mty t1 t2) = sep [ prettyDefinition "let" x mty t1 <+> "in" , ppr t2 ] prettyPrec _ (TDef _ x mty t1) = sep [ prettyDefinition "def" x mty t1 , "end" ] prettyPrec p (TBind Nothing t1 t2) = pparens (p > 0) $ prettyPrec 1 t1 <> ";" <> line <> prettyPrec 0 t2 prettyPrec p (TBind (Just x) t1 t2) = pparens (p > 0) $ pretty x <+> "<-" <+> prettyPrec 1 t1 <> ";" <> line <> prettyPrec 0 t2 prettyPrec _ (TRcd m) = brackets $ hsep (punctuate "," (map prettyEquality (M.assocs m))) prettyPrec _ (TProj t x) = prettyPrec 11 t <> "." <> pretty x prettyPrec p (TAnnotate t pt) = pparens (p > 0) $ prettyPrec 1 t <+> ":" <+> ppr pt prettyEquality :: (Pretty a, PrettyPrec b) => (a, Maybe b) -> Doc ann prettyEquality (x, Nothing) = pretty x prettyEquality (x, Just t) = pretty x <+> "=" <+> ppr t prettyTuple :: Term -> Doc a prettyTuple = tupled . map ppr . unnestTuple where unnestTuple (TPair t1 t2) = t1 : unnestTuple t2 unnestTuple t = [t] prettyDefinition :: Doc ann -> Var -> Maybe Polytype -> Term -> Doc ann prettyDefinition defName x mty t1 = nest 2 . sep $ [ flatAlt (defHead <> group defType <+> eqAndLambdaLine) (defHead <> group defType' <+> defEqLambdas) , ppr defBody ] where (defBody, defLambdaList) = unchainLambdas t1 defHead = defName <+> pretty x defType = maybe "" (\ty -> ":" <+> flatAlt (line <> indent 2 (ppr ty)) (ppr ty)) mty defType' = maybe "" (\ty -> ":" <+> ppr ty) mty defEqLambdas = hsep ("=" : map prettyLambda defLambdaList) eqAndLambdaLine = if null defLambdaList then "=" else line <> defEqLambdas prettyPrecApp :: Int -> Term -> Term -> Doc a prettyPrecApp p t1 t2 = pparens (p > 10) $ prettyPrec 10 t1 <+> prettyPrec 11 t2 appliedTermPrec :: Term -> Int appliedTermPrec (TApp f _) = case f of TConst c -> fixity $ constInfo c _ -> appliedTermPrec f appliedTermPrec _ = 10 prettyLambdas :: Term -> Doc a prettyLambdas t = hsep (prettyLambda <$> lms) <> softline <> ppr rest where (rest, lms) = unchainLambdas t unchainLambdas :: Term -> (Term, [(Var, Maybe Type)]) unchainLambdas = \case TLam x mty body -> ((x, mty) :) <$> unchainLambdas body body -> (body, []) prettyLambda :: (Pretty a1, PrettyPrec a2) => (a1, Maybe a2) -> Doc ann prettyLambda (x, mty) = "\\" <> pretty x <> maybe "" ((":" <>) . ppr) mty <> "." ------------------------------------------------------------ -- Error messages -- | Format a 'ContextualTypeError' for the user and render it as -- @Text@. prettyTypeErrText :: Text -> ContextualTypeErr -> Text prettyTypeErrText code = docToText . prettyTypeErr code -- | Format a 'ContextualTypeError' for the user. prettyTypeErr :: Text -> ContextualTypeErr -> Doc ann prettyTypeErr code (CTE l tcStack te) = vcat [ teLoc <> ppr te , ppr (BulletList "" tcStack) ] where teLoc = case l of SrcLoc s e -> (showLoc . fst $ getLocRange code (s, e)) <> ": " NoLoc -> emptyDoc showLoc (r, c) = pretty r <> ":" <> pretty c instance PrettyPrec TypeErr where prettyPrec _ = \case UnifyErr ty1 ty2 -> "Can't unify" <+> ppr ty1 <+> "and" <+> ppr ty2 Mismatch Nothing (getJoin -> (ty1, ty2)) -> "Type mismatch: expected" <+> ppr ty1 <> ", but got" <+> ppr ty2 Mismatch (Just t) (getJoin -> (ty1, ty2)) -> nest 2 . vcat $ [ "Type mismatch:" , "From context, expected" <+> pprCode t <+> "to" <+> typeDescription Expected ty1 <> "," , "but it" <+> typeDescription Actual ty2 ] LambdaArgMismatch (getJoin -> (ty1, ty2)) -> "Lambda argument has type annotation" <+> pprCode ty2 <> ", but expected argument type" <+> pprCode ty1 FieldsMismatch (getJoin -> (expFs, actFs)) -> fieldMismatchMsg expFs actFs EscapedSkolem x -> "Skolem variable" <+> pretty x <+> "would escape its scope" UnboundVar x -> "Unbound variable" <+> pretty x Infinite x uty -> "Infinite type:" <+> ppr x <+> "=" <+> ppr uty DefNotTopLevel t -> "Definitions may only be at the top level:" <+> pprCode t CantInfer t -> "Couldn't infer the type of term (this shouldn't happen; please report this as a bug!):" <+> pprCode t CantInferProj t -> "Can't infer the type of a record projection:" <+> pprCode t UnknownProj x t -> "Record does not have a field with name" <+> pretty x <> ":" <+> pprCode t InvalidAtomic reason t -> "Invalid atomic block:" <+> ppr reason <> ":" <+> pprCode t Impredicative -> "Unconstrained unification type variables encountered, likely due to an impredicative type. This is a known bug; for more information see https://github.com/swarm-game/swarm/issues/351 ." where pprCode :: PrettyPrec a => a -> Doc ann pprCode = bquote . ppr -- | Given a type and its source, construct an appropriate description -- of it to go in a type mismatch error message. typeDescription :: Source -> UType -> Doc a typeDescription src ty | not (hasAnyUVars ty) = withSource src "have" "actually has" <+> "type" <+> bquote (ppr ty) | Just f <- isTopLevelConstructor ty = withSource src "be" "is actually" <+> tyNounPhrase f | otherwise = withSource src "have" "actually has" <+> "a type like" <+> bquote (ppr (fmap (const Wildcard) ty)) -- | Check whether a type contains any unification variables at all. hasAnyUVars :: UType -> Bool hasAnyUVars = ucata (const True) or -- | Check whether a type consists of a top-level type constructor -- immediately applied to unification variables. isTopLevelConstructor :: UType -> Maybe (TypeF ()) isTopLevelConstructor (UTyCmd (UVar {})) = Just $ TyCmdF () isTopLevelConstructor (UTyDelay (UVar {})) = Just $ TyDelayF () isTopLevelConstructor (UTySum (UVar {}) (UVar {})) = Just $ TySumF () () isTopLevelConstructor (UTyProd (UVar {}) (UVar {})) = Just $ TyProdF () () isTopLevelConstructor (UTyFun (UVar {}) (UVar {})) = Just $ TyFunF () () isTopLevelConstructor _ = Nothing -- | Return an English noun phrase describing things with the given -- top-level type constructor. tyNounPhrase :: TypeF () -> Doc a tyNounPhrase = \case TyBaseF b -> baseTyNounPhrase b TyVarF {} -> "a type variable" TyCmdF {} -> "a command" TyDelayF {} -> "a delayed expression" TySumF {} -> "a sum" TyProdF {} -> "a pair" TyFunF {} -> "a function" TyRcdF {} -> "a record" -- | Return an English noun phrase describing things with the given -- base type. baseTyNounPhrase :: BaseTy -> Doc a baseTyNounPhrase = \case BVoid -> "void" BUnit -> "the unit value" BInt -> "an integer" BText -> "text" BDir -> "a direction" BBool -> "a boolean" BActor -> "an actor" BKey -> "a key" -- | Generate an appropriate message when the sets of fields in two -- record types do not match, explaining which fields are extra and -- which are missing. fieldMismatchMsg :: Set Var -> Set Var -> Doc a fieldMismatchMsg expFs actFs = nest 2 . vcat $ ["Field mismatch; record literal has:"] ++ ["- Extra field(s)" <+> prettyFieldSet extraFs | not (S.null extraFs)] ++ ["- Missing field(s)" <+> prettyFieldSet missingFs | not (S.null missingFs)] where extraFs = actFs `S.difference` expFs missingFs = expFs `S.difference` actFs prettyFieldSet = hsep . punctuate "," . map (bquote . pretty) . S.toList instance PrettyPrec InvalidAtomicReason where prettyPrec _ (TooManyTicks n) = "block could take too many ticks (" <> pretty n <> ")" prettyPrec _ AtomicDupingThing = "def, let, and lambda are not allowed" prettyPrec _ (NonSimpleVarType _ ty) = "reference to variable with non-simple type" <+> ppr (prettyTextLine ty) prettyPrec _ NestedAtomic = "nested atomic block" prettyPrec _ LongConst = "commands that can take multiple ticks to execute are not allowed" instance PrettyPrec LocatedTCFrame where prettyPrec p (LocatedTCFrame _ f) = prettyPrec p f instance PrettyPrec TCFrame where prettyPrec _ (TCDef x) = "While checking the definition of" <+> pretty x prettyPrec _ TCBindL = "While checking the left-hand side of a semicolon" prettyPrec _ TCBindR = "While checking the right-hand side of a semicolon"