-- -*- coding: utf-8; mode: haskell; -*- -- File: library/Language/Ninja/AST/Expr.hs -- -- License: -- Copyright 2017 Awake Security -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UndecidableInstances #-} -- | -- Module : Language.Ninja.AST.Expr -- Copyright : Copyright 2017 Awake Security -- License : Apache-2.0 -- Maintainer : opensource@awakesecurity.com -- Stability : experimental -- -- This module contains a type representing a string that potentially contains -- variable references in the parsed Ninja AST, along with any supporting or -- related types. -- -- @since 0.1.0 module Language.Ninja.AST.Expr ( -- * @Expr@ Expr (..) , _Exprs, _Lit, _Var , askVar, askExpr, addBind, addBinds , normalizeExpr , ExprConstraint ) where import Control.Arrow (second) import qualified Control.Lens as Lens import Data.Foldable (asum) import Data.Maybe (fromMaybe) import Data.Monoid (Endo (Endo, appEndo), (<>)) import Flow ((.>), (|>)) import Data.Text (Text) import qualified Data.Text as Text import Control.DeepSeq (NFData) import Data.Data (Data) import Data.Hashable (Hashable) import GHC.Generics (Generic) import qualified Test.QuickCheck as QC import Test.QuickCheck.Instances () import qualified Test.SmallCheck.Series as SC import GHC.Exts (Constraint) import Data.Aeson ((.:), (.=)) import qualified Data.Aeson as Aeson import qualified Language.Ninja.AST.Env as AST import qualified Language.Ninja.Misc as Misc -------------------------------------------------------------------------------- -- | An expression containing variable references in the Ninja language. -- -- @since 0.1.0 data Expr ann = -- | Sequencing of expressions. -- -- @since 0.1.0 Exprs !ann ![Expr ann] | -- | A literal string. -- -- @since 0.1.0 Lit !ann !Text | -- | A variable reference. -- -- @since 0.1.0 Var !ann !Text deriving (Eq, Show, Generic, Data, Functor, Foldable, Traversable) -- | A prism for the 'Exprs' constructor. -- -- @since 0.1.0 {-# INLINE _Exprs #-} _Exprs :: Lens.Prism' (Expr ann) (ann, [Expr ann]) _Exprs = Lens.prism' (uncurry Exprs) $ \case (Exprs ann es) -> Just (ann, es) _ -> Nothing -- | A prism for the 'Lit' constructor. -- -- @since 0.1.0 {-# INLINE _Lit #-} _Lit :: Lens.Prism' (Expr ann) (ann, Text) _Lit = Lens.prism' (uncurry Lit) $ \case (Lit ann text) -> Just (ann, text) _ -> Nothing -- | A prism for the 'Var' constructor. -- -- @since 0.1.0 {-# INLINE _Var #-} _Var :: Lens.Prism' (Expr ann) (ann, Text) _Var = Lens.prism' (uncurry Var) $ \case (Var ann name) -> Just (ann, name) _ -> Nothing -- | Evaluate the given 'Expr' in the given context (@'Env' 'Text' 'Text'@). -- -- @since 0.1.0 askExpr :: AST.Env Text Text -> Expr ann -> Text askExpr e (Exprs _ xs) = Text.concat (map (askExpr e) xs) askExpr _ (Lit _ x) = x askExpr e (Var _ x) = askVar e x -- | Look up the given variable in the given context, returning the empty string -- if the variable was not found. -- -- @since 0.1.0 askVar :: AST.Env Text Text -> Text -> Text askVar e x = fromMaybe Text.empty (AST.askEnv e x) -- | Add a binding with the given name ('Text') and value ('Expr') to the -- given context. -- -- @since 0.1.0 addBind :: Text -> Expr ann -> AST.Env Text Text -> AST.Env Text Text addBind k v e = AST.addEnv k (askExpr e v) e -- | Add bindings from a list. Note that this function evaluates all the -- right-hand-sides first, and then adds them all to the environment. -- -- For example: -- -- >>> :set -XOverloadedStrings -- >>> let binds = [("x", Lit () "5"), ("y", Var () "x")] -- >>> AST.headEnv (addBinds binds AST.makeEnv) -- fromList [("x","5"),("y","")] -- -- @since 0.1.0 addBinds :: [(Text, Expr ann)] -> AST.Env Text Text -> AST.Env Text Text addBinds bs e = map (second (askExpr e) .> uncurry AST.addEnv .> Endo) bs |> mconcat |> (\endo -> appEndo endo e) -- | Normalize an 'Expr' by recursively flattening any 'Exprs' nodes, removing -- empty 'Lit' nodes, combining adjacent 'Lit' nodes, and pulling out the -- interior of the top-level 'Exprs' node if it has only one subnode. -- -- The number of 'Exprs' nodes in the output is guaranteed to be 0 or 1. -- -- If it is 0, then there is exactly one node of any type in the output. -- -- The output is thus isomorphic to @(Maybe ann, [(ann, Either Text Text)])@, -- where the @Maybe ann@ represents the annotation of the top-level 'Exprs' -- node if it exists. -- -- @since 0.1.0 normalizeExpr :: forall ann. (Monoid ann) => Expr ann -> Expr ann normalizeExpr = flatten .> removeEmpty .> combineAdj .> listToExpr where flatten :: Expr ann -> [Expr ann] flatten (Exprs _ es) = concatMap flatten es flatten owise = [owise] removeEmpty :: [Expr ann] -> [Expr ann] removeEmpty [] = [] removeEmpty (Lit _ "" : rest) = removeEmpty rest removeEmpty (owise : rest) = owise : removeEmpty rest combineAdj :: [Expr ann] -> [Expr ann] combineAdj = (\case [] -> [] (Lit annX x : Lit annY y : rest) -> (Lit (annX <> annY) (x <> y)) |> (\e -> combineAdj (e : rest)) (owise : rest) -> owise : combineAdj rest) listToExpr :: [Expr ann] -> Expr ann listToExpr [e] = e listToExpr es = Exprs (mconcat (map (Lens.view Misc.annotation) es)) es -- | The usual definition for 'Lens.Plated'. -- -- @since 0.1.0 instance (Data ann) => Lens.Plated (Expr ann) -- | The usual definition for 'Misc.Annotated'. -- -- @since 0.1.0 instance Misc.Annotated Expr where annotation' f = Lens.lens (helper .> fst) (helper .> snd) where helper (Exprs ann es) = (ann, \x -> Exprs x (map (fmap f) es)) helper (Lit ann text) = (ann, \x -> Lit x text) helper (Var ann name) = (ann, \x -> Var x name) -- | Converts 'Exprs' to @{ann: …, exprs: […]}@, 'Lit' to @{ann: …, lit: […]}@, -- and 'Var' to @{ann: …, var: …}@. -- -- @since 0.1.0 instance (Aeson.ToJSON ann) => Aeson.ToJSON (Expr ann) where toJSON (Exprs ann es) = Aeson.object ["ann" .= ann, "exprs" .= es] toJSON (Lit ann text) = Aeson.object ["ann" .= ann, "lit" .= text] toJSON (Var ann name) = Aeson.object ["ann" .= ann, "var" .= name] -- | Inverse of the 'Aeson.ToJSON' instance. -- -- @since 0.1.0 instance (Aeson.FromJSON ann) => Aeson.FromJSON (Expr ann) where parseJSON = Aeson.withObject "Expr" $ \o -> do ann <- o .: "ann" asum [ Exprs ann <$> (o .: "exprs") , Lit ann <$> (o .: "lit") , Var ann <$> (o .: "var") ] -- | Reasonable 'QC.Arbitrary' instance for 'Expr'. -- -- @since 0.1.0 instance forall ann. (QC.Arbitrary ann) => QC.Arbitrary (Expr ann) where arbitrary = QC.sized go where go :: Int -> QC.Gen (Expr ann) go n | n <= 0 = [ litG (QC.resize litLength QC.arbitrary) , varG (QC.resize varLength QC.arbitrary) ] |> QC.oneof go n = [ go 0 , do width <- (`mod` maxWidth) <$> QC.arbitrary let subtree = go (n `div` lossRate) Exprs <$> QC.arbitrary <*> QC.vectorOf width subtree ] |> QC.oneof litG, varG :: QC.Gen Text -> QC.Gen (Expr ann) litG g = Lit <$> QC.arbitrary <*> g varG g = Var <$> QC.arbitrary <*> g litLength, varLength, lossRate, maxWidth :: Int litLength = 10 varLength = 10 maxWidth = 5 lossRate = 2 -- | Default 'Hashable' instance via 'Generic'. -- -- @since 0.1.0 instance (Hashable ann) => Hashable (Expr ann) -- | Default 'NFData' instance via 'Generic'. -- -- @since 0.1.0 instance (NFData ann) => NFData (Expr ann) -- | Default 'SC.Serial' instance via 'Generic'. -- -- @since 0.1.0 instance ( Monad m, ExprConstraint (SC.Serial m) ann ) => SC.Serial m (Expr ann) -- | Default 'SC.CoSerial' instance via 'Generic'. -- -- @since 0.1.0 instance ( Monad m, ExprConstraint (SC.CoSerial m) ann ) => SC.CoSerial m (Expr ann) -- | The set of constraints required for a given constraint to be automatically -- computed for a 'Expr'. -- -- @since 0.1.0 type ExprConstraint (c :: * -> Constraint) (ann :: *) = (c Text, c ann) --------------------------------------------------------------------------------