{- 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 OverloadedStrings #-} -- | Generate "Glean.Schema" module Glean.Schema.Gen.Haskell ( genSchemaHS ) where import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.Maybe import Data.Text (Text) import qualified Data.Text as Text import GHC.Stack (HasCallStack) import System.FilePath import TextShow import Glean.Schema.Gen.Utils import Glean.Angle.Types import Glean.Schema.Types genSchemaHS :: Version -> [ResolvedPredicateDef] -> [ResolvedTypeDef] -> Maybe Oncall -> [(FilePath,Text)] genSchemaHS _version preddefs typedefs oncall = ("hs" "TARGETS", genTargets declsPerNamespace oncall) : [ ("thrift" Text.unpack (underscored namespaces) <> "_include" <.> "hs", Text.intercalate (newline <> newline) (header namespaces deps : doGen preds types)) | (namespaces, (deps, preds, types)) <- schemas ] ++ [ ("hs" "Glean" "Schema" Text.unpack (Text.concat (map cap1 namespaces)) <.> "hs", genAllPredicates namespaces preds) | (namespaces, (_deps, preds, _types)) <- schemas ] where schemas = HashMap.toList declsPerNamespace declsPerNamespace = addNamespaceDependencies $ sortDeclsByNamespace preddefs typedefs namePolicy = mkNamePolicy preddefs typedefs doGen :: [ResolvedPredicateDef] -> [ResolvedTypeDef] -> [Text] doGen preds types = concat gen ++ reverse extra where (gen :: [[Text]], extra :: [Text]) = runM [] namePolicy typedefs $ do ps <- mapM genPredicate preds ts <- mapM (\TypeDef{..} -> genType typeDefRef typeDefType) types return (ps ++ ts) genTargets :: HashMap NameSpaces ([NameSpaces], [ResolvedPredicateDef], [ResolvedTypeDef]) -> Maybe Oncall -> Text genTargets info oncall = Text.unlines $ [ "# \x40generated" , "# to regenerate: ./glean/schema/sync" , "load(\"@fbcode_macros//build_defs:haskell_library.bzl\", " <> "\"haskell_library\")" , buckOncallAnnotation oncall , "" ] ++ concatMap genTarget (HashMap.keys info) where genTarget ns = let namespace = underscored ns in -- mini Haskell library for the module containing allPredicates [ "haskell_library(" , " name = \"" <> namespace <> "\"," , " srcs = [\"Glean/Schema/" <> Text.concat (map cap1 ns) <> ".hs\"]," , " deps = [\"//glean/if:glean-hs2\"]" , ")" , "" ] genAllPredicates :: NameSpaces -> [ResolvedPredicateDef] -> Text genAllPredicates namespace preds = Text.unlines $ [ "-- @" <> "generated" , "module Glean.Schema." <> Text.concat (map cap1 namespace) <> " (allPredicates) where" , "" , "import Glean.Types" , "" , "allPredicates :: [PredicateRef]" , "allPredicates =" ] ++ indentLines (encsep "[ " ", " "]" [ "(PredicateRef \"" <> predicateRef_name ref <> "\" " <> showt (predicateRef_version ref) <> ")" | pred <- preds , let ref = predicateDefRef pred ]) where encsep start _ end [] = [start <> end] encsep start mid end xs = zipWith (<>) (start : repeat mid) xs ++ [end] header :: NameSpaces -> [NameSpaces] -> Text header here deps = Text.unlines $ [ "-- @" <> "generated" , "{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, DataKinds #-}" , "{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}" , "{-# LANGUAGE UndecidableInstances #-}" -- UndecidableInstances is needed for the RecordFields type instances , "import qualified Data.ByteString" , "import qualified Data.Default" , "import qualified Data.Text" , "" -- we should use qualified imports as far as possible to avoid -- clashing with Thrift-generated code , "import qualified Glean.Types as Glean" , "import qualified Glean.Typed as Glean" , "import qualified Glean.Query.Angle as Angle" , "import qualified Glean.Angle.Types as Angle" , "" ] ++ -- import dependencies map importSchema deps ++ -- inject this instance into the Builtin schema, because there's no -- other good place to put it. (case here of ["builtin"] -> [ "" , "type instance Angle.SumFields (Prelude.Maybe t) =" , " 'Angle.TField \"nothing\" Unit (" , " 'Angle.TField \"just\" t" , " 'Angle.TNoFields)" ] _ -> []) importSchema :: NameSpaces -> Text importSchema ns = "import qualified Glean.Schema." <> upperSquashNS <> ".Types" where upperSquashNS = Text.concat (map cap1 ns) -- Check against hardcoded list of what glean.h provides provided :: Text -> Bool provided = (`elem` known) where known = [] -- ----------------------------------------------------------------------------- indent :: Text -> Text indent t = if Text.null t then t else " " <> t indentLines :: [Text] -> [Text] indentLines = map indent -- | These names are for predicates/struct/union of Maybe-tree haskellTypeName :: (NameSpaces, Text) -> Text haskellTypeName (ns, x) = haskellThriftName (ns, cap1 x) fieldName :: (NameSpaces, Text) -> Text fieldName (ns, x) = haskellThriftName (ns, low1 x) -- | apply stupid and wrong heuristic paren :: Text -> Text paren inner = let needsParen = Text.any (' ' ==) inner && (Text.take 1 inner `notElem` [ "(", "[" ]) in if needsParen then "(" <> inner <> ")" else inner -- | Slightly pretty breaking of long lines at "=" (=@) :: Text -> Text -> Text (=@) lhs rhs = if Text.length lhs + Text.length rhs + 3 + 4 <= 80 then lhs <> " = " <> rhs else let pre = Text.takeWhile (' '==) lhs in lhs <> " =\n " <> pre <> indent rhs infixr 5 =@ optionalize :: Text -> Text optionalize name = let inner = fromMaybe name (Text.stripPrefix "inner " name) in "Prelude.Maybe" <>! inner die :: HasCallStack => String -> String -> a die x y = error ("Error in genSchemaHS in " <> x <> " : " <> y) (<>!) :: Text -> Text -> Text (<>!) a b = a <> " " <> paren b infixr 6 <>! genericParamNames :: Text -> Int -> [Text] genericParamNames base n = map mk [1..n] where mk i = base <> Text.pack (show i) localOrExternal :: NameSpaces -> Text -> (NameSpaces, Text) localOrExternal here name = if null ns then (here,x) else (ns,x) where (ns,x) = splitDot name shareTypeDef :: Bool -> NameSpaces -> ResolvedType' s -> M Text shareTypeDef genSub here t = do (no, name) <- nameThisType t case no of New | genSub -> do let tref = TypeRef (joinDot (here,name)) 0 pushDefs =<< genType tref t _otherwise -> return () return (haskellTypeName (localOrExternal here name)) haskellTy :: NameSpaces -> ResolvedType' s -> M Text haskellTy = haskellTy_ PredName True -- | how to render predicate types in haskellTy data PredTy = PredName | PredKey haskellTy_ :: PredTy -> Bool -- ^ generate nested type definitions -> NameSpaces -> ResolvedType' s -> M Text haskellTy_ withId genSub here t = case t of -- Leafs ByteTy{} -> return "Glean.Byte" NatTy{} -> return "Glean.Nat" BooleanTy{} -> return "Prelude.Bool" StringTy{} -> return "Data.Text.Text" ArrayTy ByteTy -> return "Data.ByteString.ByteString" ArrayTy tInner -> do inner <- haskellTy_ PredName genSub here tInner return $ "[" <> inner <> "]" RecordTy{} -> shareTypeDef genSub here t SumTy{} -> shareTypeDef genSub here t SetTy tInner -> do inner <- haskellTy_ PredName genSub here tInner return $ "[" <> inner <> "]" MaybeTy ty -> do inner <- haskellTy_ PredName genSub here ty return (optionalize inner) -- References PredicateTy _ pred -> do let wrap = case withId of PredName -> id PredKey -> ("Glean.KeyType " <>) wrap . haskellTypeName <$> predicateName pred NamedTy _ typeRef -> haskellTypeName <$> typeName typeRef EnumeratedTy _ -> shareTypeDef genSub here t TyVar{} -> error "haskellTy_: TyVar" HasTy{} -> error "haskellTy_: HasTy" HasKey{} -> error "haskellTy_: HasKey" ElementsOf{} -> error "haskellTy_: ElementsOf" genPredicate :: ResolvedPredicateDef -> M [Text] genPredicate PredicateDef{..} | provided (predicateRef_name predicateDefRef) = return [] | otherwise = do pName <- predicateName predicateDefRef let here = fst pName name = haskellTypeName pName -- e.g. Clang_File withPredicateDefHint (snd pName) $ do let appendName suffix = let (ns, x) = pName in (ns, x <> suffix) glean_name = predicateRef_name predicateDefRef -- e.g. clang.File has_value = predicateDefValueType /= unitT field_id = fieldName $ appendName "_id" -- field name for Id name_key = appendName "_key" -- type of Key name_value = appendName "_value" -- type of Value (type_key, define_key) <- if shouldNameKeyType predicateDefKeyType then define_kt here predicateDefKeyType name_key else do ty <- haskellTy_ PredName True here predicateDefKeyType return (ty, []) (type_value, define_value) <- if not has_value then return ("Unit", []) else do define_kt here predicateDefValueType name_value let extra = define_key ++ define_value let ver = predicateRef_version predicateDefRef inst cls body = "instance " <> cls <> " " <> name <> " where" : indentLines body def_Predicate = inst "Glean.Predicate" $ ["type KeyType " <> name =@ type_key] ++ ["type ValueType " <> name =@ type_value | has_value] ++ [ "getName _proxy " =@ "Glean.PredicateRef " <> Text.pack (show glean_name) <> -- adds quotes, does escaping showt ver , "getId = Glean.IdOf . Glean.Fid . " <> field_id , "mkFact (Glean.IdOf (Glean.Fid x)) k " <> (if has_value then "v" else "_") <> " = " <> name <> " x k" <> (if has_value then " v" else "") , "getFactKey = " <> fieldName name_key , if has_value then "getFactValue = " <> fieldName name_value else "getFactValue _ = Prelude.Just ()" ] def_Type = inst "Glean.Type" [ "buildRtsValue b = Glean.buildRtsValue b . Glean.getId" , "decodeRtsValue = Glean.decodeRef" , "decodeAsFact = Glean.decodeFact" , "sourceType = Glean.predicateSourceType" ] return $ extra ++ map myUnlines [def_Predicate, def_Type] -- Make the thriftTy type text, and the needed [Text] blocks define_kt :: HasCallStack => NameSpaces -> ResolvedType -> (NameSpaces, Text) -> M (Text, [Text]) define_kt here typ name_kt = case typ of ByteTy{} -> leaf NatTy{} -> leaf StringTy{} -> leaf RecordTy [] -> leaf RecordTy _fields -> alias typ ArrayTy{} -> alias typ SetTy{} -> alias typ SumTy [] -> leaf SumTy _fields -> alias typ MaybeTy{} -> alias typ PredicateTy{} -> leaf NamedTy{} -> alias typ BooleanTy{} -> leaf _other -> die "define_kt" (show typ) where gname = joinDot name_kt leaf = (,) <$> return (haskellTypeName name_kt) <*> return [] alias t = do ref <- haskellTy here (NamedTy () (TypeRef gname 0)) def <- genType (TypeRef gname 0) t return (ref,def) genType :: TypeRef -> ResolvedType' s -> M [Text] genType TypeRef{..} ty | provided typeRef_name = return [] | otherwise = case ty of RecordTy fields -> structDef typeRef_name typeRef_version fields SumTy fields -> unionDef typeRef_name typeRef_version fields EnumeratedTy vals -> enumDef typeRef_name typeRef_version vals _ -> return [] structDef :: Name -> Version -> [ResolvedFieldDef' s] -> M [Text] structDef ident ver fields = do let typeRef = TypeRef ident ver sName@(here,root) <- typeName typeRef let name = haskellTypeName sName withTypeDefHint root $ do let fieldParamNames = genericParamNames "x" (length fields) makeTypeName (FieldDef p tField) = withRecordFieldHint p (haskellTy_ PredKey True here tField) spaced = Text.intercalate " " fieldParamNames nameAndParams = if null fieldParamNames then name else name <> " " <> spaced encodeMe = case length fields of 0 -> [ "buildRtsValue _b " <> name <> " = Prelude.return ()" ] _ -> ("buildRtsValue b " <> paren nameAndParams <> " = do") : indentLines (map ("Glean.buildRtsValue b " <>) fieldParamNames) decodeMe = case length fields of 0 -> [ "decodeRtsValue = Prelude.pure " <> name] n -> "decodeRtsValue = " <> name : indent "<$> Glean.decodeRtsValue" : indentLines (replicate (pred n) "<*> Glean.decodeRtsValue") tys <- mapM makeTypeName fields let def_Type = "instance Glean.Type " <> name <> " where" : indentLines ( encodeMe <> decodeMe <> [sourceTypeDef ident ver] ) def_RecordFields = [ emitFieldTypes "Angle.RecordFields" name (zip fields tys) ] return $ map myUnlines [def_Type, def_RecordFields] unionDef :: Name -> Version -> [ResolvedFieldDef' s] -> M [Text] unionDef ident ver fields = do let typeRef = TypeRef ident ver uName@(here,root) <- typeName typeRef let name = haskellTypeName uName withTypeDefHint root $ do let shortConNames = map fieldDefName fields conNames = map toConName shortConNames toConName shortName = cap1 (prefix <> shortName) where prefix = name <> "_" makeTypeName pred gen (FieldDef p tField) = withUnionFieldHint p (haskellTy_ pred gen here tField) -- walk over the fields to generate nested types first mapM_ (makeTypeName PredName True) fields keyTypeNames <- mapM (makeTypeName PredKey False) fields let def_Type = case conNames of [] -> "instance Glean.Type " <> name <> " where" : indentLines [ "buildRtsValue _ _ = Prelude.return ()" , "decodeRtsValue = Prelude.error $ \"decodeRtsValue\" <> " <> name ] _ -> let emptyCon = toConName "EMPTY" builds = mkBuildEmpty conNames emptyCon ++ concat (zipWith mkBuild [0..] conNames) decodes = "decodeRtsValue = Glean.sumD" : indentLines ( "(Prelude.pure " <> emptyCon <> ")" : asArray (map mkDecode conNames) ) in concat [ ["instance Glean.Type " <> name <> " where"] , indentLines builds , indentLines decodes , indentLines [sourceTypeDef ident ver] ] def_SumFields = [ emitFieldTypes "Angle.SumFields" name (zip fields keyTypeNames) ] return $ map myUnlines $ [def_Type, def_SumFields] where mkBuildEmpty constructors emptyCon = let index = length constructors in "buildRtsValue b " <> emptyCon <> "=" : indentLines [ "Glean.buildRtsSelector b " <> Text.pack (show index) ] mkBuild :: Int -> Text -> [Text] mkBuild i c = "buildRtsValue b (" <> c <> " x) = do" : indentLines [ "Glean.buildRtsSelector b " <> Text.pack (show i) , "Glean.buildRtsValue b x" ] mkDecode :: Text -> Text mkDecode c = "Glean.mapD " <> c asArray :: [Text] -> [Text] asArray = \case [] -> ["[]"] (x:xs) -> concat [ ["[ " <> x] , [", " <> e | e <- xs] , ["]"] ] enumDef :: Name -> Version -> [Name] -> M [Text] enumDef ident ver eVals = do let typeRef = TypeRef ident ver eName@(_,root) <- typeName typeRef let name = haskellTypeName eName withTypeDefHint root $ do let def_Type = "instance Glean.Type " <> name <> " where" : indentLines [ "buildRtsValue = Glean.thriftEnum_buildRtsValue " , "decodeRtsValue = Glean.thriftEnumD " , sourceTypeDef ident ver ] def_SumFields = [ emitFieldTypes "Angle.SumFields" name [ (FieldDef n unitT, "Glean.Schema.Builtin.Types.Unit") | n <- eVals ] ] def_AngleEnum = "instance Angle.AngleEnum " <> name <> " where": indentLines [ "type AngleEnumTy " <> name <> " = " <> name , "enumName v = Text.pack (Prelude.drop " <> showt (Text.length root + 1) <> " (Prelude.show v))"] return $ map myUnlines [def_Type, def_SumFields, def_AngleEnum] sourceTypeDef :: Name -> Version -> Text sourceTypeDef name version = "sourceType _ = Angle.NamedTy () " <> paren sourceRef where sourceRef = Text.unwords [ "Angle.SourceRef" , Text.pack (show name) , paren ("Prelude.Just " <> showt version) ] emitFieldTypes :: Text -> Text -> [(ResolvedFieldDef' s, Text)] -> Text emitFieldTypes family name fields = "type instance " <> family <> " " <> name <> " = " <> go fields where go [] = "'Angle.TNoFields" go ((FieldDef name _, ty) : rest) = "'Angle.TField \"" <> name <> "\" (" <> ty <> ") (" <> go rest <> ")"