urbit/pkg/hs-urbit/lib/Noun/TH.hs

215 lines
6.8 KiB
Haskell
Raw Normal View History

{-
2019-06-27 01:51:30 +03:00
Generate FromNoun and ToNoun instances.
-}
2019-07-12 04:16:40 +03:00
module Noun.TH (deriveNoun) where
2019-07-12 22:24:44 +03:00
import ClassyPrelude hiding (fromList)
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
2019-07-12 22:24:44 +03:00
import Noun.Convert
2019-06-28 00:28:58 +03:00
import RIO (decodeUtf8Lenient)
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
2019-06-27 01:51:30 +03:00
= Tup ConInfo
| Enu [Name]
| Sum [ConInfo]
deriving (Eq, Ord, Show)
typeShape :: Name -> Q Shape
typeShape tyName = do
2019-06-27 01:51:30 +03:00
cs <- reify tyName >>= \case
TyConI (DataD _ nm [] _ cs _) -> pure $ unpackCon <$> cs
TyConI (NewtypeD _ nm [] _ c _) -> pure $ [unpackCon c]
TyConI (DataD _ nm _ _ cs _) -> fail "Type variables are unsupported"
TyConI (NewtypeD _ nm _ _ c _) -> fail "Type variables are unsupported"
TyConI _ -> fail badSynonym
_ -> fail "not type"
2019-06-27 01:51:30 +03:00
let allEmpty = all (null . snd) cs
if allEmpty
2019-06-27 01:51:30 +03:00
then pure $ Enu $ fst <$> cs
else
case cs of
2019-06-27 01:51:30 +03:00
[] -> pure $ Enu []
[c] -> pure $ Tup c
cs -> pure $ Sum cs
where
badSynonym = "deriveFunctor: tyCon may not be a type synonym."
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
2019-06-27 01:51:30 +03:00
body <- typeShape tyName <&> \case Tup con -> tupToNoun con
Enu cons -> enumToAtom cons
Sum cons -> sumToNoun cons
[d|
2019-06-27 01:51:30 +03:00
instance ToNoun $(conT tyName) where
toNoun = $(pure body)
|]
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
deriveFromNoun :: Name -> Q [Dec]
2019-06-27 02:27:37 +03:00
deriveFromNoun tyName = do
body <- typeShape tyName <&> \case Tup con -> tupFromNoun con
Enu cons -> enumFromAtom cons
Sum cons -> sumFromNoun cons
let tag = LitE $ StringL $ nameStr tyName
2019-06-27 02:27:37 +03:00
[d|
2019-06-27 02:27:37 +03:00
instance FromNoun $(conT tyName) where
parseNoun = named $(pure tag) . $(pure body)
2019-06-27 02:27:37 +03:00
|]
2019-06-27 02:27:37 +03:00
enumFromAtom :: [Name] -> Exp
2019-06-27 03:58:55 +03:00
enumFromAtom nms = 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")
getCord = BindS (VarP c) $ AppE (VarE 'parseNoun) (VarE x)
2019-06-27 02:27:37 +03:00
examine = NoBindS $ CaseE (VarE c) (matches ++ [fallback])
matches = mkMatch <$> nms
fallback = Match WildP (NormalB $ AppE (VarE 'fail) matchFail) []
body = DoE [getCord, examine]
matchFail = LitE $ StringL ("Expected one of: " <> possible)
possible = intercalate " " (('%':) . tagString <$> nms)
mkMatch n = Match (ConP 'Cord [LitP (tagLit n)])
(NormalB $ AppE (VarE 'pure) (ConE n))
[]
applyE :: Exp -> [Exp] -> Exp
applyE e [] = e
applyE e (a:as) = applyE (AppE e a) as
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
2019-06-28 00:28:58 +03:00
unexpectedTag :: [Name] -> Exp -> Exp
2019-06-27 03:58:55 +03:00
unexpectedTag expected got =
2019-06-28 00:28:58 +03:00
applyE (VarE 'mappend) [LitE (StringL prefix), got]
2019-06-27 03:58:55 +03:00
where
possible = intercalate " " (('%':) . tagString <$> expected)
2019-06-28 00:28:58 +03:00
prefix = "Expected one of: " <> possible <> " but got %"
2019-06-27 03:58:55 +03:00
2019-06-27 02:27:37 +03:00
sumFromNoun :: [ConInfo] -> Exp
2019-06-27 03:58:55 +03:00
sumFromNoun cons = LamE [VarP x] (DoE [getHead, getTag, examine])
where
(x, h, t, c) = (mkName "x", mkName "h", mkName "t", mkName "c")
getHead = BindS (TupP [VarP h, VarP t])
$ AppE (VarE 'parseNoun) (VarE x)
getTag = BindS (ConP 'Cord [VarP c])
$ AppE (VarE 'parseNoun) (VarE h)
examine = NoBindS
$ CaseE (VarE c) (matches ++ [fallback])
matches = mkMatch <$> cons
mkMatch = \(n, tys) -> let body = AppE (tupFromNoun (n, tys)) (VarE t)
in Match (LitP $ tagLit n) (NormalB body) []
fallback = Match WildP (NormalB $ AppE (VarE 'fail) matchFail) []
2019-06-28 00:28:58 +03:00
matchFail = unexpectedTag (fst <$> cons)
$ AppE (VarE 'unpack)
$ AppE (VarE 'decodeUtf8Lenient)
$ VarE c
2019-06-27 01:51:30 +03:00
--------------------------------------------------------------------------------
2019-06-27 02:27:37 +03:00
tagString :: Name -> String
tagString = hsToHoon . nameStr
nameStr :: Name -> String
nameStr (Name (OccName n) _) = n
2019-06-27 02:27:37 +03:00
tagLit :: Name -> Lit
tagLit = StringL . tagString
2019-06-27 01:51:30 +03:00
tagNoun :: Name -> Exp
tagNoun = AppE (VarE 'toNoun)
. AppE (ConE 'Cord)
. LitE
. StringL
. hsToHoon
. nameStr
where
nameStr :: Name -> String
nameStr (Name (OccName n) _) = n
2019-06-27 01:51:30 +03:00
tagTup :: Name -> [Name] -> Exp
tagTup c args = AppE (VarE 'toNoun) $ TupE (tagNoun c : 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
--------------------------------------------------------------------------------
2019-06-27 01:51:30 +03:00
enumToAtom :: [Name] -> Exp
enumToAtom cons =
LamCaseE $ cons <&> \nm ->
Match (ConP nm []) (NormalB $ tagNoun nm) []
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 :: [ConInfo] -> Exp
sumToNoun cons = LamCaseE (cons <&> mkMatch)
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 = tagTup nm 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