{-# LANGUAGE DuplicateRecordFields, OverloadedLists, InstanceSigs #-} {-# OPTIONS_GHC -Wall #-} module Rubyhs.References ( References(entries) , FQN(..) , references , Env(..) , Result(..) , Context(..) , graph , prettyContext , Node(..) ) where import Frelude -- import Data.Language.Ruby hiding (context) import qualified Data.Language.Ruby as Ruby import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import qualified Data.Aeson.Encoding.Internal as Aeson import Data.Kind import Control.Monad.State import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson import Data.Set (Set) import qualified Data.Set as Set import qualified Data.Text as Text import Data.Coerce import Data.Graph (Graph, Vertex, Forest) import qualified Data.Graph as Graph import Data.Hashable import qualified Data.Char as Char span :: G node key -> Forest node span (g, f, _) = fmap ((\(x, _, _) -> x) . f) <$> Graph.dff g type G node key = (Graph, Vertex -> (node, key, [key]), key -> Maybe Vertex) graph :: Ruby.Begin -> Forest Node graph b = span $ Graph.graphFromEdges $ go <$> toList ys where go :: (Node, Set Node) -> (Node, Text, [Text]) go (x, xs) = (x, prettyContext x, prettyContext <$> toList xs) Result ys = references b newtype Namespace = Namespace [Text] deriving stock instance Show Namespace deriving stock instance Eq Namespace deriving stock instance Ord Namespace instance IsList Namespace where type Item Namespace = Text fromList = coerce toList = coerce deriving newtype instance Semigroup Namespace deriving newtype instance Monoid Namespace instance FromJSON Namespace where parseJSON = Aeson.withText "Namespace" $ \t -> Namespace <$> (traverse pure $ Text.splitOn "::" t) instance ToJSON Namespace where toJSON = Aeson.String . showNamespace showNamespace :: Namespace -> Text showNamespace = Text.intercalate "::" . coerce -- Names are in reverse order. data FQN = FQN { namespace :: Namespace , name :: Text } data Node = NodeDef FQN | NodeModule Namespace beginsWithCapital :: Text -> Bool beginsWithCapital = Char.isUpper . Text.head deriving stock instance Show Node deriving stock instance Eq Node deriving stock instance Ord Node instance FromJSON Node where parseJSON = Aeson.withText "Node" $ nodeParser nodeParser :: Text -> Aeson.Parser Node nodeParser = \t -> case Text.splitOn "." t of [mods,fun] -> fmap NodeDef (FQN <$> Aeson.parseJSON (Aeson.String mods) <*> pure fun) [mods] -> if beginsWithCapital mods then NodeModule <$> Aeson.parseJSON (Aeson.String mods) else pure $ NodeDef $ FQN mempty mods _ -> empty instance Aeson.FromJSONKey Node where fromJSONKey = Aeson.FromJSONKeyTextParser nodeParser instance Aeson.ToJSONKey Node where toJSONKey = Aeson.ToJSONKeyText prettyContext (\n -> Aeson.text $ prettyContext n) instance ToJSON Node where toJSON = \case NodeDef q -> Aeson.toJSON q NodeModule m -> Aeson.toJSON m instance Hashable Node where hashWithSalt n a = hashWithSalt @Text n $ prettyContext a deriving stock instance Show FQN deriving stock instance Eq FQN deriving stock instance Ord FQN instance Pretty Namespace where pretty = pretty . showNamespace instance Aeson.ToJSON FQN where toJSON = Aeson.String . prettyFQN instance Aeson.ToJSONKey FQN where instance Pretty FQN where pretty = pretty . prettyFQN prettyFQN :: FQN -> Text prettyFQN FQN{name, namespace} = case namespace of [] -> name _ -> convertString (showNamespace namespace) <> "." <> name newtype Context = Context Node deriving stock instance Eq Context deriving stock instance Ord Context deriving newtype instance ToJSON Context -- HACK: Not really a semigroup as is evident from the implementation. instance Semigroup Context where Context c0 <> Context c1 = case (c0, c1) of (NodeModule n0, NodeModule n1) -> Context $ NodeModule $ n0 <> n1 (NodeModule n0, NodeDef (FQN n1 f)) -> Context $ NodeDef $ FQN (n0 <> n1) f (NodeDef{}, NodeModule{}) -> error "Cannot append module to function context." (NodeDef{}, NodeDef{}) -> error "Cannot append function to function context." class Monad m => MyMonad (m :: Type -> Type) where declaration :: Node -> m () application :: Node -> m () getContext :: m Context writeContext :: Context -> m () data Env = Env { declarations :: Set Node , applications :: HashMap Node (Set Node) , context :: Context } newtype Result = Result (HashMap Node (Set Node)) -- HACK: FromJSON and ToJSON don't agree. deriving newtype instance FromJSON Result instance ToJSON Result where toJSON (Result a) = Aeson.Object $ fromList $ go <$> toList a where go :: (Node, Set Node) -> (Text, Aeson.Value) go (x, y) = (prettyContext x, Aeson.toJSON y) prettyContext :: Node -> Text prettyContext = \case NodeDef fun -> prettyFQN fun NodeModule ns -> showNamespace ns instance MyMonad (State Env) where declaration q = modify go where go :: Env -> Env go env@Env{declarations} = env { declarations = Set.insert q declarations } application n = do Context c <- getContext let go env@Env{applications} = env { applications = HashMap.insertWith mappend c (Set.singleton n) applications } modify go getContext = gets Rubyhs.References.context writeContext q = modify go where go env = env { Rubyhs.References.context = q } locally :: MyMonad m => m a -> m a locally act = do old <- getContext res <- act writeContext old pure res class References a where entries :: MyMonad m => a -> m () references :: Ruby.Begin -> Result references q = Result $ HashMap.unionWith mappend applications declarations' where Env{declarations,applications} = execState (entries @_ @(State Env) q) emptyEnv emptyEnv = Env mempty mempty (Context $ NodeModule mempty) declarations' = fromList $ (\x -> (x, mempty)) <$> toList declarations instance References Ruby.Begin where entries :: forall m . MyMonad m => Ruby.Begin -> m () entries (Ruby.Begin defs) = traverse_ (locally . entries) defs instance References Ruby.Statement where entries = \case Ruby.StmtModule m -> entries m Ruby.StmtDef f -> entries f Ruby.StmtDefs f -> entries f Ruby.StmtSend s -> entries s Ruby.StmtConst c -> entries c Ruby.StmtBlock b -> entries b Ruby.StmtCasgn c -> entries c Ruby.StmtArray a -> entries a Ruby.StmtSym s -> entries s Ruby.StmtStr s -> entries s Ruby.StmtLvasgn a -> entries a Ruby.StmtLvar a -> entries a Ruby.StmtIvar a -> entries a Ruby.StmtSelf s -> entries s Ruby.StmtCbase s -> entries s Ruby.StmtNil n -> entries n Ruby.StmtAnything a -> entries a instance References Ruby.Block where entries Ruby.Block{send,args,begin} = do entries send entries args entries begin instance References Ruby.Args where entries = const $ pure () -- TODO: We have to make a "declaration" for the constant here as -- well! instance References Ruby.Casgn where entries Ruby.Casgn{rhs} = entries rhs instance References Ruby.Sym where entries _ = pure () instance References Ruby.Str where entries _ = pure () instance References Ruby.Lvasgn where entries _ = pure () instance References Ruby.Lvar where entries _ = pure () instance References Ruby.Ivar where entries _ = pure () instance References Ruby.Self where entries _ = pure () instance References Ruby.Cbase where entries _ = pure () instance References Ruby.Nil where entries _ = pure () instance References Ruby.Array where entries Ruby.Array{statements} = traverse_ entries statements instance References Ruby.Anything where entries = const $ pure () instance References Ruby.Const where entries con = application $ NodeModule $ constToNamespace con updateContext :: MyMonad m => (Context -> Context) -> m () updateContext f = getContext >>= \c -> writeContext (f c) instance References Ruby.Module where entries Ruby.Module{name, begin} = do updateContext $ (<>) $ Context $ NodeModule $ constToNamespace name c <- getContext >>= \case Context (NodeModule c) -> pure c _ -> error "..." declaration $ NodeModule c entries begin atomToNode :: MyMonad m => Ruby.Atom -> m Node atomToNode (Ruby.Atom name) = do namespace <- getContext >>= \case Context (NodeModule c) -> pure c -- We could allow this and just say that the function defined in -- another function sits in the same context as the surrounding -- function. Context NodeDef{} -> error "Cannot have a function declaration in a function context" pure $ NodeDef $ FQN namespace name instance References Ruby.Def where entries Ruby.Def{atom, begin} = do node <- atomToNode atom declaration node locally $ do writeContext (Context node) entries begin instance References Ruby.Defs where -- TODO: The field `context` is also relevant here! entries Ruby.Defs{atom, begin} = entries $ Ruby.Def{atom,begin,args=error "hack"} qual :: Namespace -> Ruby.Atom -> FQN qual namespace (Ruby.Atom name) = FQN { namespace , name } instance References Ruby.Send where entries Ruby.Send{context, atom, args} = do application $ NodeDef $ qual (statementToNamespace context) atom traverse_ entries args statementToNamespace :: Ruby.Statement -> Namespace statementToNamespace = go mempty where go acc = \case Ruby.StmtConst c -> acc <> constToNamespace c -- The nil-case and cbase-case should produce different results, -- surely. `Namespace` may not be a good representation. Ruby.StmtNil{} -> acc Ruby.StmtCbase{} -> acc -- The send-, ivar- and lvar- case cannot be handled because of the way -- we've defined `Namespace`. Ruby.StmtSend{} -> acc Ruby.StmtIvar{} -> acc Ruby.StmtLvar{} -> acc _ -> error "Can only build namespaces from sequences of `const` statements" constToNamespace :: Ruby.Const -> Namespace constToNamespace Ruby.Const{context, atom} = statementToNamespace context <> [k] where Ruby.Atom k = atom