{-# OPTIONS_HADDOCK not-home #-} {-# LANGUAGE TemplateHaskell #-} module Pinchot.Terminalize where import Control.Monad (join) import Data.Sequence (Seq) import Data.Sequence.NonEmpty (NonEmptySeq) import qualified Data.Sequence.NonEmpty as NonEmpty 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.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) -> NonEmptySeq ($(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 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) ms <- traverse tzr . toList $ bs T.lamE [T.varP x] (T.caseE (T.varE x) 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.nonEmptySeqToSeq . $(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.nonEmptySeqToSeq . $(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.NonEmptySeq e1 es) = join . fmap getTermNonEmpty $ NonEmpty.NonEmptySeq e1 es in getTerms $(T.varE x) |] | otherwise -> do x <- T.newName "x" [| let getTermSeq = $(T.varE (lookupName lkp inner)) getTerms (NonEmpty.NonEmptySeq 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.nonEmptySeqToSeq $(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.nonEmptySeqToSeq $(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 bs -> 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