shrub/pkg/hs/urbit-king/lib/Ur/Noun/TH.hs

270 lines
9.1 KiB
Haskell
Raw Normal View History

2020-01-23 07:16:09 +03:00
{-|
Template Haskell Code to Generate FromNoun and ToNoun Instances
-}
2020-01-23 05:58:22 +03:00
module Ur.Noun.TH (deriveNoun, deriveToNoun, deriveFromNoun) where
2019-07-12 22:24:44 +03:00
import ClassyPrelude hiding (fromList)
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
2020-01-23 05:58:22 +03:00
import Ur.Noun.Convert
2020-01-23 05:58:22 +03:00
import Ur.Noun.Core (textToUtf8Atom)
2019-06-28 00:28:58 +03:00
2019-06-27 01:51:30 +03:00
import qualified Data.Char as C
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
2019-06-27 01:51:30 +03:00
type ConInfo = (Name, [Type])
data Shape
= Vod
| Tup ConInfo
| Sum [(String, Name)] [(String, ConInfo)]
deriving (Eq, Ord, Show)
typeShape :: Name -> Q ([TyVarBndr], Shape)
typeShape tyName = do
(vars, cs) <-
reify tyName >>= \case
TyConI (DataD _ nm vars _ cs _) -> pure (vars, unpackCon <$> cs)
TyConI (NewtypeD _ nm vars _ c _) -> pure (vars, [unpackCon c])
TyConI _ -> fail badSynonym
_ -> fail "not type"
let prefix = getPrefix (nameStr . fst <$> cs)
splits = splitFn ([], []) cs
splitFn (l, r) = \case
[] -> (l, r)
(n,[]) : cs -> splitFn (tagName prefix n:l, r) cs
conInf : cs -> splitFn (l, tagConInfo prefix conInf:r) cs
pure $ (vars,) $ case cs of
2020-01-23 07:16:09 +03:00
[] -> Vod
[c] -> Tup c
cs -> uncurry Sum splits
where
badSynonym = "deriveFunctor: tyCon may not be a type synonym."
tagConInfo :: Int -> ConInfo -> (String, ConInfo)
tagConInfo pre ci@(nm, _) = (tagString pre nm, ci)
tagName :: Int -> Name -> (String, Name)
tagName pre n = (tagString pre n, n)
tyStr = nameStr tyName
tyAbbrv = filter C.isUpper tyStr
typePrefixed = (tyStr `isPrefixOf`)
abbrvPrefixed = (tyAbbrv `isPrefixOf`)
getPrefix :: [String] -> Int
getPrefix cs | all typePrefixed cs = length tyStr
getPrefix cs | all abbrvPrefixed cs = length tyAbbrv
getPrefix _ = 0
2019-06-27 01:51:30 +03:00
unpackCon :: Con -> ConInfo
unpackCon = \case
NormalC nm bangTypes -> (nm, snd <$> bangTypes)
RecC nm varBangTypes -> (nm, varBangTypes <&> (\(_, _, t) -> t))
InfixC bangType1 nm bangType2 -> error "Infix Cnstrs are not supported"
ForallC tyVarBndrs ctx con -> error "Polymorphic tys are not supported"
GadtC nm bangTypes ty -> error "GADTs are not supported"
RecGadtC nm varBangTypes ty -> error "GADTs are not supported"
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
deriveNoun :: Name -> Q [Dec]
2019-06-27 01:51:30 +03:00
deriveNoun n = (<>) <$> deriveToNoun n <*> deriveFromNoun n
--------------------------------------------------------------------------------
deriveToNoun :: Name -> Q [Dec]
deriveToNoun tyName = do
(params, shape) <- typeShape tyName
let exp = case shape of Vod -> vodToNoun
Tup con -> tupToNoun con
-- Enu cons -> enumToAtom cons
Sum atoms cells -> sumToNoun atoms cells
params <- pure $ zip ['a' ..] params <&> \(n,_) -> mkName (singleton n)
let ty = foldl' (\acc v -> AppT acc (VarT v)) (ConT tyName) params
let overlap = Nothing
body = NormalB exp
ctx = params <&> \t -> AppT (ConT ''ToNoun) (VarT t)
inst = AppT (ConT ''ToNoun) ty
pure [InstanceD overlap ctx inst [ValD (VarP 'toNoun) body []]]
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
addErrTag :: String -> Exp -> Exp
addErrTag tag exp =
InfixE (Just $ AppE (VarE 'named) str) (VarE (mkName ".")) (Just exp)
where
str = LitE $ StringL tag
deriveFromNoun :: Name -> Q [Dec]
2019-06-27 02:27:37 +03:00
deriveFromNoun tyName = do
(params, shape) <- typeShape tyName
let exp = case shape of Vod -> vodFromNoun
Tup con -> tupFromNoun con
-- Enu cons -> enumFromAtom cons
Sum atoms cells -> sumFromNoun atoms cells
2019-06-27 02:27:37 +03:00
params <- pure $ zip ['a' ..] params <&> \(n,_) -> mkName (singleton n)
2019-06-27 02:27:37 +03:00
let ty = foldl' (\acc v -> AppT acc (VarT v)) (ConT tyName) params
let overlap = Nothing
body = NormalB (addErrTag (nameStr tyName) exp)
ctx = params <&> \t -> AppT (ConT ''FromNoun) (VarT t)
inst = AppT (ConT ''FromNoun) ty
pure [InstanceD overlap ctx inst [ValD (VarP 'parseNoun) body []]]
sumFromNoun :: [(String, Name)] -> [(String, ConInfo)] -> Exp
sumFromNoun [] cl = taggedFromNoun cl
sumFromNoun at [] = enumFromAtom at
sumFromNoun at cl = eitherParser (taggedFromNoun cl) (enumFromAtom at)
where
eitherParser :: Exp -> Exp -> Exp
eitherParser x y =
LamE [VarP n] $
InfixE (Just xCase) (VarE (mkName "<|>")) (Just yCase)
where
xCase = AppE x (VarE n)
yCase = AppE y (VarE n)
n = mkName "atomOrCell"
enumFromAtom :: [(String, Name)] -> Exp
enumFromAtom cons = LamE [VarP x] body
2019-06-27 02:27:37 +03:00
where
2019-06-27 03:58:55 +03:00
(x, c) = (mkName "x", mkName "c")
2019-07-22 21:10:27 +03:00
getTag = BindS (VarP c) $ AppE (VarE 'parseNounUtf8Atom) (VarE x)
2019-06-27 02:27:37 +03:00
examine = NoBindS $ CaseE (VarE c) (matches ++ [fallback])
matches = mkMatch <$> cons
2019-06-27 02:27:37 +03:00
fallback = Match WildP (NormalB $ AppE (VarE 'fail) matchFail) []
2019-07-22 21:10:27 +03:00
body = DoE [getTag, examine]
2019-06-27 02:27:37 +03:00
matchFail = LitE $ StringL ("Expected one of: " <> possible)
possible = intercalate " " (('%':) . fst <$> cons)
mkMatch = \(tag, nm) ->
2019-07-22 21:10:27 +03:00
Match (SigP (LitP $ StringL tag) (ConT ''Text))
(NormalB $ AppE (VarE 'pure) (ConE nm))
[]
2019-06-27 02:27:37 +03:00
applyE :: Exp -> [Exp] -> Exp
applyE e [] = e
applyE e (a:as) = applyE (AppE e a) as
vodFromNoun :: Exp
vodFromNoun = LamE [WildP] body
where
body = AppE (VarE 'fail)
$ LitE $ StringL "Can't FromNoun on uninhabited data type"
2019-06-27 02:27:37 +03:00
tupFromNoun :: ConInfo -> Exp
tupFromNoun (n, tys) = LamE [VarP x] body
where
x = mkName "x"
vars = mkName . singleton . fst <$> zip ['a'..] tys
body = DoE [getTup, convert]
convert = NoBindS $ AppE (VarE 'pure) $ applyE (ConE n) (VarE <$> vars)
getTup = BindS (TupP $ VarP <$> vars) $ AppE (VarE 'parseNoun) (VarE x)
2019-06-27 02:27:37 +03:00
unexpectedTag :: [String] -> Exp -> Exp
2019-06-27 03:58:55 +03:00
unexpectedTag expected got =
2019-07-22 21:10:27 +03:00
applyE (VarE 'mappend) [LitE (StringL prefix), AppE (VarE 'unpack) got]
2019-06-27 03:58:55 +03:00
where
possible = intercalate " " (('%':) <$> expected)
2019-06-28 00:28:58 +03:00
prefix = "Expected one of: " <> possible <> " but got %"
2019-06-27 03:58:55 +03:00
taggedFromNoun :: [(String, ConInfo)] -> Exp
taggedFromNoun cons = LamE [VarP n] (DoE [getHead, getTag, examine])
2019-06-27 03:58:55 +03:00
where
2019-07-22 21:10:27 +03:00
(n, h, t, c) = (mkName "noun", mkName "hed", mkName "tel", mkName "tag")
2019-06-27 03:58:55 +03:00
getHead = BindS (TupP [VarP h, VarP t])
$ AppE (VarE 'parseNoun) (VarE n)
2019-06-27 03:58:55 +03:00
2019-07-22 21:10:27 +03:00
getTag = BindS (SigP (VarP c) (ConT ''Text))
$ AppE (VarE 'parseNounUtf8Atom) (VarE h)
2019-06-27 03:58:55 +03:00
examine = NoBindS
$ CaseE (VarE c) (matches ++ [fallback])
matches = mkMatch <$> cons
mkMatch = \(tag, (n, tys)) ->
let body = AppE (addErrTag ('%':tag) (tupFromNoun (n, tys)))
(VarE t)
in Match (LitP $ StringL tag) (NormalB body) []
2019-06-27 03:58:55 +03:00
fallback = Match WildP (NormalB $ AppE (VarE 'fail) matchFail) []
2019-07-22 21:10:27 +03:00
matchFail = unexpectedTag (fst <$> cons) (VarE c)
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
tagString :: Int -> Name -> String
tagString prefix = hsToHoon . drop prefix . nameStr
nameStr :: Name -> String
nameStr (Name (OccName n) _) = n
2019-06-27 02:27:37 +03:00
tagNoun :: String -> Exp
2019-07-22 21:10:27 +03:00
tagNoun = AppE (VarE 'textToUtf8Atom)
2019-06-27 01:51:30 +03:00
. LitE
. StringL
tagTup :: String -> [Name] -> Exp
tagTup tag args = AppE (VarE 'toNoun) $ TupE (tagNoun tag : fmap VarE args)
2019-06-27 01:51:30 +03:00
tup :: [Name] -> Exp
tup = AppE (VarE 'toNoun) . TupE . fmap VarE
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
vodToNoun :: Exp
vodToNoun = LamCaseE []
2019-06-27 01:51:30 +03:00
tupToNoun :: ConInfo -> Exp
tupToNoun cons = LamCaseE [mkMatch cons]
where
mkMatch :: ConInfo -> Match
mkMatch (nm, tys) = Match (ConP nm params) (NormalB body) []
where vars = (zip tys ['a'..]) <&> (mkName . singleton . snd)
params = VarP <$> vars
body = tup vars
sumToNoun :: [(String, Name)] -> [(String, ConInfo)] -> Exp
sumToNoun a c =
LamCaseE (mixed <&> uncurry mkMatch)
2019-06-27 01:51:30 +03:00
where
mixed = mconcat [ a <&> \(x,y) -> (x, Left y)
, c <&> \(x,y) -> (x, Right y)
]
mkMatch :: String -> Either Name ConInfo -> Match
mkMatch tag = \case
Left nm -> Match (ConP nm []) (NormalB $ tagNoun tag) []
Right (nm, tys) -> Match (ConP nm params) (NormalB body) []
where vars = (zip tys ['a'..]) <&> (mkName . singleton . snd)
params = VarP <$> vars
body = tagTup tag vars
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
2019-06-27 01:51:30 +03:00
hsToHoon :: String -> String
hsToHoon = go []
where
2019-06-27 01:51:30 +03:00
go acc [] = intercalate "-" $ reverse acc
go acc (c:cs) = go (elem:acc) remain
where
head = C.toLower c
(tail, remain) = break C.isUpper cs
elem = head:tail