{-# OPTIONS_HADDOCK not-home #-} {-# LANGUAGE TemplateHaskell #-} module Pinchot.Terminalize where import Control.Monad (join) import Data.Sequence (Seq) import qualified Data.Sequence as Seq import Data.Foldable (foldlM, toList) import Data.Map (Map) import qualified Data.Map as Map import qualified Language.Haskell.TH as T import Pinchot.NonEmpty (NonEmpty(NonEmpty)) import qualified Pinchot.NonEmpty as NonEmpty import Pinchot.Types import Pinchot.Rules -- | For all the given rules and their ancestors, creates -- declarations that reduce the rule and all its ancestors to -- terminal symbols. Each rule gets a declaration named -- @t'RULE_NAME@ where @RULE_NAME@ is the name of the rule. The -- type of the declaration is either -- -- Production a -> Seq (t, a) -- -- or -- -- Production a -> NonEmpty (t, a) -- -- where @Production@ is the production corresponding to the given -- 'Rule', @t@ is the terminal token type (often 'Char'), and @a@ is -- arbitrary metadata about each token (often 'Loc'). 'NonEmpty' is -- returned for productions that must always contain at least one -- terminal symbol; for those that can be empty, 'Seq' is returned. -- -- Example: "Pinchot.Examples.Terminalize". terminalizers :: Qualifier -- ^ Qualifier for the module containing the data types created -- from the 'Rule's -> Seq (Rule t) -> T.Q [T.Dec] terminalizers qual = fmap concat . traverse (terminalizer qual) . toList . families -- | For the given rule, creates declarations that reduce the rule -- to terminal symbols. No ancestors are handled. Each rule gets a -- declaration named @t'RULE_NAME@ where @RULE_NAME@ is the name of -- the rule. The -- type of the declaration is either -- -- Production a -> Seq (t, a) -- -- or -- -- Production a -> NonEmpty (t, a) -- -- where @Production@ is the production corresponding to the given -- 'Rule', @t@ is the terminal token type (often 'Char'), and @a@ is -- arbitrary metadata about each token (often 'Loc'). 'NonEmpty' is -- returned for productions that must always contain at least one -- terminal symbol; for those that can be empty, 'Seq' is returned. terminalizer :: Qualifier -- ^ Qualifier for the module containing the data types created -- from the 'Rule's -> Rule t -> T.Q [T.Dec] terminalizer qual rule@(Rule nm _ _) = sequence [sig, expn] where declName = "t'" ++ nm anyType = T.varT (T.mkName "a") charType = T.varT (T.mkName "t") sig | atLeastOne rule = T.sigD (T.mkName declName) . T.forallT [T.PlainTV (T.mkName "t") , T.PlainTV (T.mkName "a")] (return []) $ [t| $(T.conT (quald qual nm)) $(charType) $(anyType) -> NonEmpty ($(charType), $(anyType)) |] | otherwise = T.sigD (T.mkName declName) . T.forallT [ T.PlainTV (T.mkName "t") , T.PlainTV (T.mkName "a")] (return []) $ [t| $(T.conT (quald qual nm)) $(charType) $(anyType) -> Seq ($(charType), $(anyType)) |] expn = T.valD (T.varP $ T.mkName declName) (T.normalB (terminalizeRuleExp qual rule)) [] -- | For the given rule, returns an expression that has type of -- either -- -- Production a -> Seq (t, a) -- -- or -- -- Production a -> NonEmpty (t, a) -- -- where @Production@ is the production corresponding to the given -- 'Rule', and @t@ is the terminal token type. 'NonEmpty' is -- returned for productions that must always contain at least one -- terminal symbol; for those that can be empty, 'Seq' is returned. -- -- Example: 'Pinchot.Examples.Terminalize.terminalizeAddress'. terminalizeRuleExp :: Qualifier -> Rule t -> T.Q T.Exp terminalizeRuleExp qual rule@(Rule nm _ _) = do let allRules = family rule lkp <- ruleLookupMap allRules let mkDec r@(Rule rn _ _) = let expn = terminalizeSingleRule qual lkp r decName = lookupName lkp rn in T.valD (T.varP decName) (T.normalB expn) [] T.letE (fmap mkDec . toList $ allRules) (T.varE (lookupName lkp nm)) -- | Creates a 'Map' where each key is the name of the 'Rule' and -- each value is a name corresponding to that 'Rule'. No -- ancestors are used. ruleLookupMap :: Foldable c => c (Rule t) -> T.Q (Map RuleName (T.Name)) ruleLookupMap = foldlM f Map.empty where f mp (Rule nm _ _) = do name <- T.newName $ "rule" ++ nm return $ Map.insert nm name mp lookupName :: Map RuleName T.Name -> RuleName -> T.Name lookupName lkp n = case Map.lookup n lkp of Nothing -> error $ "lookupName: name not found: " ++ n Just r -> r -- | For the given rule, returns an expression that has type -- of either -- -- Production a -> Seq (t, a) -- -- or -- -- Production a -> NonEmpty (t, a) -- -- where @Production@ is the production corresponding to the given -- 'Rule', and @t@ is the terminal token type. 'NonEmpty' is -- returned for productions that must always contain at least one -- terminal symbol; for those that can be empty, 'Seq' is returned. -- Gets no ancestors. terminalizeSingleRule :: Qualifier -- ^ Module qualifier for module containing the generated types -- corresponding to all 'Rule's -> Map RuleName T.Name -- ^ For a given Rule, looks up the name of the expression that -- will terminalize that rule. -> Rule t -> T.Q T.Exp terminalizeSingleRule qual lkp rule@(Rule nm _ ty) = case ty of Terminal _ -> do x <- T.newName "x" let pat = T.conP (quald qual nm) [T.varP x] [| \ $(pat) -> NonEmpty.singleton $(T.varE x) |] NonTerminal b1 bs -> do x <- T.newName "x" let fTzn | atLeastOne rule = terminalizeProductAtLeastOne | otherwise = terminalizeProductAllowsZero tzr (Branch name sq) = fmap (\(pat, expn) -> T.match pat (T.normalB expn) []) (fTzn qual lkp name sq) m1 <- tzr b1 ms <- traverse tzr . toList $ bs T.lamE [T.varP x] (T.caseE (T.varE x) (m1 : ms)) Wrap (Rule inner _ _) -> do x <- T.newName "x" let pat = T.conP (quald qual nm) [T.varP x] [| \ $(pat) -> $(T.varE (lookupName lkp inner)) $(T.varE x) |] Record rs -> do (pat, expn) <- fTzr qual lkp nm rs [| \ $(pat) -> $(expn) |] where fTzr | atLeastOne rule = terminalizeProductAtLeastOne | otherwise = terminalizeProductAllowsZero Opt r@(Rule inner _ _) -> do x <- T.newName "x" let pat = T.conP (quald qual nm) [T.varP x] [| \ $(pat) -> maybe Seq.empty $(convert (T.varE (lookupName lkp inner))) $(T.varE x) |] where convert expn | atLeastOne r = [| NonEmpty.flatten . $(expn) |] | otherwise = expn Star r@(Rule inner _ _) -> do x <- T.newName "x" let pat = T.conP (quald qual nm) [T.varP x] convert e | atLeastOne r = [| NonEmpty.flatten . $(e) |] | otherwise = e [| \ $(pat) -> join . fmap $(convert (T.varE (lookupName lkp inner))) $ $(T.varE x) |] Plus r@(Rule inner _ _) | atLeastOne r -> do x <- T.newName "x" let pat = T.conP (quald qual nm) [T.varP x] [| \ $(pat) -> let getTermNonEmpty = $(T.varE (lookupName lkp inner)) getTerms (NonEmpty e1 es) = join . fmap getTermNonEmpty $ NonEmpty.NonEmpty e1 es in getTerms $(T.varE x) |] | otherwise -> do x <- T.newName "x" [| let getTermSeq = $(T.varE (lookupName lkp inner)) getTerms (NonEmpty e1 es) = getTermSeq e1 `mappend` (join (fmap getTermSeq es)) in getTerms $(T.varE x) |] terminalizeProductAllowsZero :: Qualifier -> Map RuleName T.Name -> String -- ^ Rule name or branch name, as applicable -> Seq (Rule t) -> T.Q (T.PatQ, T.ExpQ) terminalizeProductAllowsZero qual lkp name bs = do pairs <- fmap toList . traverse (terminalizeProductRule lkp) $ bs let pat = T.conP (quald qual name) (fmap (fst . snd) pairs) body = case pairs of [] -> [| Seq.empty |] x:xs -> foldl f start xs where f acc trip = [| $(acc) `mappend` $(procTrip trip) |] start = procTrip x procTrip (rule, (_, expn)) | atLeastOne rule = [| NonEmpty.flatten $(expn) |] | otherwise = expn return (pat, body) terminalizeProductAtLeastOne :: Qualifier -> Map RuleName T.Name -> String -- ^ Rule name or branch name, as applicable -> Seq (Rule t) -> T.Q (T.PatQ, T.ExpQ) terminalizeProductAtLeastOne qual lkp name bs = do pairs <- fmap toList . traverse (terminalizeProductRule lkp) $ bs let pat = T.conP (quald qual name) (fmap (fst . snd) pairs) body = [| ( $(leadSeq) `NonEmpty.prependSeq` $(firstNonEmpty)) `NonEmpty.appendSeq` $(trailSeq) |] where (leadRules, lastRules) = span (not . atLeastOne . fst) pairs (firstNonEmptyRule, trailRules) = case lastRules of [] -> error $ "terminalizeProductAtLeastOne: failure 1: " ++ name x:xs -> (x, xs) leadSeq = case fmap (snd . snd) leadRules of [] -> [| Seq.empty |] x:xs -> foldl f x xs where f acc expn = [| $(acc) `mappend` $(expn) |] firstNonEmpty = [| $(snd . snd $ firstNonEmptyRule) |] trailSeq = foldl f [| Seq.empty |] trailRules where f acc (rule, (_, expn)) | atLeastOne rule = [| $(acc) `mappend` NonEmpty.flatten $(expn) |] | otherwise = [| $(acc) `mappend` $(expn) |] return (pat, body) terminalizeProductRule :: Map RuleName T.Name -> Rule t -> T.Q (Rule t, (T.Q T.Pat, T.Q T.Exp)) terminalizeProductRule lkp r@(Rule nm _ _) = do x <- T.newName $ "terminalizeProductRule'" ++ nm let getTerms = [| $(T.varE (lookupName lkp nm)) $(T.varE x) |] return (r, (T.varP x, getTerms)) -- | Examines a rule to determine whether when terminalizing it will -- always return at least one terminal symbol. atLeastOne :: Rule t -> Bool -- ^ True if the rule will always have at least one terminal -- symbol. atLeastOne (Rule _ _ ty) = case ty of Terminal _ -> True NonTerminal b1 bs -> branchAtLeastOne b1 && all branchAtLeastOne bs where branchAtLeastOne (Branch _ rs) = any atLeastOne rs Wrap r -> atLeastOne r Record rs -> any atLeastOne rs Opt _ -> False Star _ -> False Plus r -> atLeastOne r