diff options
Diffstat (limited to 'src/Pages/TH.hs')
| -rw-r--r-- | src/Pages/TH.hs | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/Pages/TH.hs b/src/Pages/TH.hs new file mode 100644 index 0000000..0cc93db --- /dev/null +++ b/src/Pages/TH.hs @@ -0,0 +1,218 @@ +{-# LANGUAGE LinearTypes #-} +{-# LANGUAGE TemplateHaskellQuotes #-} +module Pages.TH where + +import Data.ByteString (ByteString) +import Data.ByteString qualified as BS +import Data.Char (toUpper) +import Data.List.NonEmpty (NonEmpty(..)) +import Data.List.NonEmpty qualified as NE +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.Encoding qualified as TE +import Data.Text.Builder.Linear.Buffer +import Language.Haskell.TH +import Text.Mustache qualified as M +import Text.Mustache.Types qualified as M + +import EscapeXML + + +data DataMap + = VQInt + | VQBool + | VQText + | VQUnit -- () + | VQList DataMap -- ^ embedded map is of a list element + | VQMaybe DataMap + | VQObject [(Text, (Exp -> Exp, DataMap))] -- ^ field name, projection function, submap + + +makeDataMap :: Name -> Q DataMap +makeDataMap name = makeDataMapTy (ConT name) + +makeDataMapTy :: Type -> Q DataMap +makeDataMapTy (ConT tyname) + | tyname == ''Int = return VQInt + | tyname == ''Bool = return VQBool + | tyname == ''Text = return VQText +makeDataMapTy (TupleT 0) = return VQUnit +makeDataMapTy (AppT ListT ty) = VQList <$> makeDataMapTy ty +makeDataMapTy (AppT (ConT name) ty) | name == ''Maybe = VQMaybe <$> makeDataMapTy ty + +makeDataMapTy topty = do + case splitApps topty [] of + (ConT tyname, tyargs) -> + reify tyname >>= \case + TyConI (DataD [] _ tyvars _ [RecC conname fields] _) -> + let n = length fields + in VQObject <$> sequence + [do dm <- makeDataMapTy (subst (Map.fromList (zip (map tyvarName tyvars) tyargs)) fieldty) + f <- projectField conname i n + return (T.pack (nameBase name), (f, dm)) + | ((name, _bang, fieldty), i) <- zip fields [0..]] + _ -> fail $ "Not a suitable data type: " ++ show tyname + _ -> fail $ "Not a straightforward type: " ++ show topty + where + projectField :: Name -> Int -> Int -> Q (Exp -> Exp) + projectField conname i n = do + varname <- newName ('x' : show (i + 1)) + let pats = replicate i WildP ++ [VarP varname] ++ replicate (n-i-1) WildP + return $ \e -> + CaseE e [Match (ConP conname [] pats) (NormalB (VarE varname)) []] + + splitApps :: Type -> [Type] -> (Type, [Type]) + splitApps (AppT t1 t2) args = splitApps t1 (t2 : args) + splitApps t args = (t, args) + + tyvarName :: TyVarBndr flag -> Name + tyvarName (PlainTV n _) = n + tyvarName (KindedTV n _ _) = n + + subst :: Map Name Type -> Type -> Type + subst _ t@ConT{} = t + subst _ t@ListT = t + subst _ t@TupleT{} = t + subst mp (VarT n) | Just t <- Map.lookup n mp = t + subst mp (AppT t1 t2) = AppT (subst mp t1) (subst mp t2) + subst _ t = error $ "Unsupported type: " ++ show t + +makeRender :: M.Template -> Name -> Q [Dec] +makeRender (M.Template tplname nodes _) dataname = do + dm <- makeDataMap dataname + let funname = mkName ("renderPage" ++ toPascalCase tplname) + argname <- newName "dat" + bufname <- newName "buf" + body <- renderNodes ((dm, VarE argname) :| []) (DuplicableExp (VarE bufname)) nodes + let body' = VarE 'runBufferBS `AppE` LamE [VarP bufname] body + return + [SigD funname (ArrowT `AppT` ConT dataname `AppT` ConT ''ByteString) + ,FunD funname + [Clause [BangP (VarP argname)] (NormalB body') []]] + +data ExpensiveExp + = ExpensiveExp Exp + | DuplicableExp Exp + +expensive :: ExpensiveExp -> Exp +expensive (ExpensiveExp e) = e +expensive (DuplicableExp e) = e + +makeDuplicable :: ExpensiveExp -> (Exp -> Q Exp) -> Q Exp +makeDuplicable (ExpensiveExp e) k = do + varname <- newName "a" + caseE (pure e) + [match (varP varname) (normalB (k (VarE varname))) []] +makeDuplicable (DuplicableExp e) k = k e + +-- | This is just '(|>)', but with a @NOINLINE@ pragma. This is because GHC +-- eagerly inlines '(|>)', and that function contains some conditionals, and as +-- a result the generated Core for the render function contains a whole forest +-- of join points. By avoiding inlining this function, the Core becomes a +-- straight line of @case@s that contains join points only when there is +-- genuinely control flow going on, i.e. when substituting a @Maybe@ or when +-- looping over a list. +{-# NOINLINE noinlineAppText #-} +noinlineAppText :: Buffer %1-> Text -> Buffer +noinlineAppText = (|>) + +-- | All input expressions must be duplicable. +renderNodes :: NonEmpty (DataMap, Exp) -> ExpensiveExp -> [M.Node Text] -> Q (Exp {- Buffer -}) +renderNodes _ buf [] = return (expensive buf) +renderNodes dats buf (node : nodes) = do + let app buf' name e = InfixE (Just buf') (VarE name) (Just e) + + buf' <- case node of + M.TextBlock t -> + let bytes = BS.unpack (TE.encodeUtf8 t) + in return $ app (expensive buf) '(|>#) (LitE (StringPrimL bytes)) + M.Variable esc named -> + case search named dats of + Just (VQInt, e) -> return $ app (expensive buf) '(|>$) e + Just (VQText, e) -> + return $ app (expensive buf) 'noinlineAppText (if esc then VarE 'escapeXML `AppE` e else e) + Just (_, _) -> fail $ "Strange value in variable reference " ++ show named + Nothing -> fail $ "Variable not found: " ++ show named + M.Section named nodes' -> + case search named dats of + Just (VQBool, e) -> + makeDuplicable buf $ \buf' -> + condE (pure e) + (renderNodes ((VQBool, e) NE.<| dats) (DuplicableExp buf') nodes') + (pure buf') + Just (VQText, e) -> + makeDuplicable buf $ \buf' -> + condE [| T.null $(pure e) |] + (pure buf') + (renderNodes ((VQText, e) NE.<| dats) (DuplicableExp buf') nodes') + Just (VQList dm, e) -> do + bvar <- newName "b" + vvar <- newName "v" + varE 'foldlIntoBuffer + `appE` lamE [varP bvar, varP vvar] + (renderNodes ((dm, VarE vvar) NE.<| dats) (DuplicableExp (VarE bvar)) nodes') + `appE` pure (expensive buf) + `appE` pure e + Just (VQMaybe dm, e) -> do + xvar <- newName "x" + makeDuplicable buf $ \buf' -> + caseE (pure e) + [match (conP 'Nothing []) (normalB (pure buf')) [] + ,match (conP 'Just [varP xvar]) + (normalB (renderNodes ((dm, VarE xvar) NE.<| dats) (DuplicableExp buf') nodes')) + []] + Just pair@(VQObject _, _) -> do + renderNodes (pair NE.<| dats) buf nodes' + Just (VQInt, _) -> fail $ "Int value in section scrutinee " ++ show named + Just (VQUnit, _) -> fail $ "() value in section scrutinee " ++ show named + Nothing -> fail $ "Section scrutinee not found: " ++ show named + M.InvertedSection named nodes' -> + case search named dats of + Just (VQBool, e) -> + makeDuplicable buf $ \buf' -> + condE (pure e) + (pure buf') + (renderNodes dats (DuplicableExp buf') nodes') + Just (VQText, e) -> + makeDuplicable buf $ \buf' -> + condE [| T.null $(pure e) |] + (renderNodes dats (DuplicableExp buf') nodes') + (pure buf') + Just (VQMaybe _, e) -> + makeDuplicable buf $ \buf' -> + caseE (pure e) + [match (conP 'Nothing []) (normalB (renderNodes dats (DuplicableExp buf') nodes')) [] + ,match (conP 'Just [wildP]) (normalB (pure buf')) []] + Just (VQList _, e) -> + makeDuplicable buf $ \buf' -> + caseE (pure e) + [match (listP []) (normalB (renderNodes dats (DuplicableExp buf') nodes')) [] + ,match wildP (normalB (pure buf')) []] + Just (VQObject _, _) -> fail $ "Object value in inverted section scrutinee " ++ show named + Just (VQInt, _) -> fail $ "Int value in inverted section scrutinee " ++ show named + Just (VQUnit, _) -> fail $ "() value in inverted section scrutinee " ++ show named + Nothing -> fail $ "Inverted section scrutinee not found: " ++ show named + M.Partial _ _ -> fail $ "TODO partials" + + renderNodes dats (ExpensiveExp buf') nodes + +search :: M.DataIdentifier -> NonEmpty (DataMap, Exp) -> Maybe (DataMap, Exp) +search M.Implicit (value :| _) = Just value +search (M.NamedData []) (value :| _) = Just value +search (M.NamedData (key : keys)) ((dm, e) :| dats) = + case dm of + VQObject fields -> + case lookup key fields of + Just (proj, dm') -> search (M.NamedData keys) ((dm', proj e) :| []) + Nothing -> search (M.NamedData (key : keys)) =<< NE.nonEmpty dats + _ -> + search (M.NamedData (key : keys)) =<< NE.nonEmpty dats + +toPascalCase :: String -> String +toPascalCase "" = "" +toPascalCase (c:cs) = toUpper c : go cs + where go ('-':cs') = toPascalCase cs' + go (c':cs') = c' : go cs' + go "" = "" |
