-- | -- Module : Conjure.Expr -- Copyright : (c) 2021 Rudy Matela -- License : 3-Clause BSD (see the file LICENSE) -- Maintainer : Rudy Matela -- -- This internal module reexports 'Data.Express' along with a few other -- utilities. {-# LANGUAGE CPP, TupleSections #-} module Conjure.Expr ( module Data.Express , module Data.Express.Fixtures , rehole , (>$$<) , funToVar , recursexpr , apparentlyTerminates , mayNotEvaluateArgument , applicationOld , compareSimplicity , ifFor , primitiveHoles , primitiveApplications , valuesBFS , holesBFS , fillBFS , showEq , lhs , rhs , ($$**) , ($$|<) , possibleHoles , isDeconstructionE , revaluate , reval , useMatches , enumerateAppsFor , enumerateFillings , module Conjure.Utils ) where import Conjure.Utils import Data.Express import Data.Express.Utils.Typeable import Data.Express.Fixtures hiding ((-==-)) import Data.Dynamic import Control.Applicative ((<$>)) -- for GHC <= 7.8 import Test.LeanCheck (mapT, filterT, (\/), delay, productWith, productMaybeWith) import Test.LeanCheck.Tiers (products) import Test.LeanCheck.Utils.Types (A, B, C, D, E, F) -- | /O(n)/. -- Compares the simplicity of two 'Expr's. -- An expression /e1/ is /strictly simpler/ than another expression /e2/ -- if the first of the following conditions to distingish between them is: -- -- 1. /e1/ is smaller in size\/length than /e2/, -- e.g.: @x + y < x + (y + z)@; -- -- 2. or, /e1/ has less variable occurrences than /e2/, -- -- 3. or, /e1/ has fewer distinct constants than /e2/, -- e.g.: @1 + 1 < 0 + 1@. -- -- They're otherwise considered of equal complexity, -- e.g.: @x + y@ and @y + z@. -- -- > > (xx -+- yy) `compareComplexity` (xx -+- (yy -+- zz)) -- > LT -- -- > > (xx -+- yy) `compareComplexity` (xx -+- xx) -- > EQ -- -- > > (xx -+- xx) `compareComplexity` (one -+- xx) -- > GT -- -- > > (one -+- one) `compareComplexity` (zero -+- one) -- > LT -- -- > > (xx -+- yy) `compareComplexity` (yy -+- zz) -- > EQ -- -- > > (zero -+- one) `compareComplexity` (one -+- zero) -- > EQ compareSimplicity :: Expr -> Expr -> Ordering compareSimplicity = (compare `on` length . values) <> (compare `on` length . vars) <> (compare `on` length . nubConsts) -- | Makes the function in an application a variable funToVar :: Expr -> Expr funToVar (ef :$ ex) = funToVar ef :$ ex funToVar ef@(Value nm _) = nm `varAsTypeOf` ef -- | Expands recursive calls on an expression -- until the given size limit is reached. -- -- > > recursexpr 6 (ff xx) (ff xx) -- > f x :: Int -- -- > > recursexpr 6 (ff xx) (one -+- ff xx) -- > 1 + (1 + (1 + (1 + f x))) :: Int -- -- > > recursexpr 6 (ff xx) (if' pp one (xx -*- ff xx)) -- > (if p then 1 else x * (if p then 1 else x * f x)) :: Int -- -- > > recursexpr 6 (ff xx) (if' pp one (xx -*- ff (gg xx))) -- > (if p then 1 else x * (if p then 1 else g x * f (g (g x)))) :: Int recursexpr :: Int -> Expr -> Expr -> Expr recursexpr sz epat = re where err = error "recursexpr: pattern must contain an application of variables" (erf:vs) = unfoldApp epat re e' | not (all isVar (erf:vs)) = err | e == e' || size e > sz = e | otherwise = re e where e = re1 e' re1 e = case unfoldApp e of [e] -> e (ef:exs) | ef == erf -> e' //- zip vs exs | otherwise -> foldApp (map re1 (ef:exs)) -- recursive call _only_ under an if -- future-work: guess short-circuit operators -- | Checks if the given recursive call apparently terminates. -- The first argument indicates the functional variable indicating the -- recursive call. -- -- > > apparentlyTerminates ffE (ff xx) -- > False -- -- > > apparentlyTerminates ffE (if' pp zero (ff xx)) -- > True -- -- This function only allows recursion in the else clause: -- -- > > apparentlyTerminates ffE (if' pp (ff xx) zero) -- > False -- -- Of course, recursive calls as the condition are not allowed: -- -- > > apparentlyTerminates ffE (if' (odd' (ff xx)) zero zero) -- > False apparentlyTerminates :: Expr -> Expr -> Bool apparentlyTerminates eRecursiveCall = at where at (e1 :$ e2) = (mayNotEvaluateArgument e1 || at e2) && at e1 at e = e /= eRecursiveCall -- | Checks if the given functional expression may refrain from evaluating its -- next argument. -- -- -- > > mayNotEvaluateArgument (plus :$ xx) -- > False -- -- > > mayNotEvaluateArgument (andE :$ pp) -- > True -- -- This returns false for non-funcional value even if it involves an -- application which may not evaluate its argument. -- -- > > mayNotEvaluateArgument (andE :$ pp :$ qq) -- > False -- -- This currently works by checking if the function is an if, '&&' or '||'. mayNotEvaluateArgument :: Expr -> Bool mayNotEvaluateArgument (Value "if" ce :$ _ :$ _) = True mayNotEvaluateArgument (Value "&&" ce :$ _) = True mayNotEvaluateArgument (Value "||" ce :$ _) = True mayNotEvaluateArgument _ = False applicationOld :: Expr -> [Expr] -> Maybe Expr applicationOld ff es = appn ff where appn ff | isFun ff = case [e | Just (_ :$ e) <- (map (ff $$)) es] of [] -> Nothing -- could not find type representative in es (e:_) -> appn (ff :$ holeAsTypeOf e) | otherwise = Just ff -- | Creates an if 'Expr' of the type of the given proxy. -- -- > > ifFor (undefined :: Int) -- > if :: Bool -> Int -> Int -> Int -- -- > > ifFor (undefined :: String) -- > if :: Bool -> [Char] -> [Char] -> [Char] -- -- You need to provide this as part of your building blocks on the primitives -- if you want recursive functions to be considered and produced. ifFor :: Typeable a => a -> Expr ifFor a = value "if" (\p x y -> if p then x else y `asTypeOf` a) -- | Application cross-product between lists of Exprs (>$$<) :: [Expr] -> [Expr] -> [Expr] exs >$$< eys = catMaybes [ex $$ ey | ex <- exs, ey <- eys] -- TODO: move >$$< into Data.Express? primitiveHoles :: [Expr] -> [Expr] primitiveHoles prims = sort $ ph hs where hs = nub $ map holeAsTypeOf prims ph = iterateUntil (==) ps ps es = nub $ es ++ sq es sq es = nub $ map holeAsTypeOf $ es >$$< es -- FIXME: the function above is quite inefficient. -- Should run fast for a small number of types, -- but if this number increases runtime may start -- to become significant. primitiveApplications :: [Expr] -> [[Expr]] primitiveApplications prims = takeWhile (not . null) $ iterate (>$$< primitiveHoles prims) prims -- lists terminal values in BFS order valuesBFS :: Expr -> [Expr] valuesBFS = concat . bfs where bfs :: Expr -> [[Expr]] bfs (ef :$ ex) = [] : mzip (bfs ef) (bfs ex) bfs e = [[e]] -- lists holes in BFS order holesBFS :: Expr -> [Expr] holesBFS = filter isHole . valuesBFS fillBFS :: Expr -> Expr -> Expr fillBFS e e' = fst (f e) where f :: Expr -> (Expr,Maybe Int) f (ef :$ ex) = case (mf, mx) of (Nothing, Nothing) -> (ef :$ ex, Nothing) (Just lf, Nothing) -> (ef' :$ ex, Just $ lf+1) (Nothing, Just lx) -> (ef :$ ex', Just $ lx+1) (Just lf, Just lx) | lf <= lx -> (ef' :$ ex, Just $ lf+1) | otherwise -> (ef :$ ex', Just $ lx+1) where (ef', mf) = f ef (ex', mx) = f ex f e | isHole e && typ e == typ e' = (e', Just 0) | otherwise = (e, Nothing) -- TODO: move BFS functions into Express? showEq :: Expr -> String showEq (((Value "==" _) :$ lhs) :$ rhs) = showExpr lhs ++ " = " ++ showExpr rhs showEq e = "not an Eq: " ++ show e lhs, rhs :: Expr -> Expr lhs (((Value "==" _) :$ e) :$ _) = e rhs (((Value "==" _) :$ _) :$ e) = e -- Debug: application that always works ($$**) :: Expr -> Expr -> Maybe Expr e1 $$** e2 = Just $ e1 :$ e2 -- Debug: application that works for the correct kinds ($$|<) :: Expr -> Expr -> Maybe Expr e1 $$|< e2 = if isFunTy t1 && tyArity (argumentTy t1) == tyArity t2 then Just $ e1 :$ e2 else Nothing where t1 = ktyp e1 t2 = ktyp e2 ktyp :: Expr -> TypeRep ktyp (e1 :$ e2) = resultTy (ktyp e1) ktyp e = typ e possibleHoles :: [Expr] -> [Expr] possibleHoles = nubSort . ph . nubSort . map holeAsTypeOf where ph hs = case nubSort $ hs ++ [holeAsTypeOf hfx | hf <- hs, hx <- hs, Just hfx <- [hf $$ hx]] of hs' | hs' == hs -> hs | otherwise -> ph hs' nubSort = nub . sort -- TODO: this is O(n^2), make this O(n log n) -- -- Expression enumeration -- -- enumerateAppsFor :: Expr -> (Expr -> Bool) -> [Expr] -> [[Expr]] enumerateAppsFor h keep es = for h where hs :: [Expr] hs = possibleHoles es for :: Expr -> [[Expr]] for h = filter (\e -> typ h == typ e) es : apps where apps = foldr (\/) [] [ filterT keep $ fliproductWith (:$) (for hf) (for hx) | hf <- hs , hx <- hs , Just hfx <- [hf $$ hx] , typ h == typ hfx ] enumerateFillings :: Expr -> [[Expr]] -> [[Expr]] enumerateFillings e = mapT (fill e) . products . replicate (length $ holes e) -- Like 'isDeconstruction' but lifted over the 'Expr' type. isDeconstructionE :: [Expr] -> Expr -> Expr -> Bool -- [a] -> (a -> Bool) -> (a -> a) -> Bool isDeconstructionE [] _ _ = error "isDeconstructionE: empty list of test values" isDeconstructionE es ez ed | all isIllTyped [f :$ e | e <- es, f <- [ez,ed]] = error "isDeconstructionE: types do not match" isDeconstructionE es ez ed = isDeconstruction es (eval False . (ez :$)) (ed :$) recursiveToDynamic :: (Expr,Expr) -> Int -> Expr -> Maybe Dynamic recursiveToDynamic (efxs, ebody) n = fmap (\(_,_,d) -> d) . re (n * size ebody) n where (ef':exs') = unfoldApp efxs rev :: Typeable a => Int -> Int -> Expr -> Maybe (Int, Int, a) rev m n e = case re m n e of Nothing -> Nothing Just (m,n,d) -> case fromDynamic d of Nothing -> Nothing Just x -> Just (m, n, x) re :: Int -> Int -> Expr -> Maybe (Int, Int, Dynamic) re m n _ | n <= 0 = error "recursiveToDynamic: recursion limit reached" re m n _ | m <= 0 = error "recursiveToDynamic: evaluation limit reached" re m n (Value "if" _ :$ ec :$ ex :$ ey) = case rev m n ec of Nothing -> Nothing Just (m,n,True) -> re m n ex Just (m,n,False) -> re m n ey re m n (Value "||" _ :$ ep :$ eq) = case rev m n ep of Nothing -> Nothing Just (m,n,True) -> (m,n,) <$> toDynamic (val True) Just (m,n,False) -> re m n eq re m n (Value "&&" _ :$ ep :$ eq) = case rev m n ep of Nothing -> Nothing Just (m,n,True) -> re m n eq Just (m,n,False) -> (m,n,) <$> toDynamic (val False) re m n e = case unfoldApp e of [] -> error "recursiveToDynamic: empty application unfold" -- should never happen [e] -> (m-1,n,) <$> toDynamic e (ef:exs) | ef == ef' -> re m (n-1) $ ebody //- zip exs' exs | otherwise -> foldl ($$) (re m n ef) exs Just (m,n,d1) $$ e2 = case re m n e2 of Nothing -> Nothing Just (m', n', d2) -> (m',n',) <$> dynApply d1 d2 _ $$ _ = Nothing revaluate :: Typeable a => (Expr,Expr) -> Int -> Expr -> Maybe a revaluate dfn n e = recursiveToDynamic dfn n e >>= fromDynamic reval :: Typeable a => (Expr,Expr) -> Int -> a -> Expr -> a reval dfn n x = fromMaybe x . revaluate dfn n -- | like 'productWith' but prefers enumerating from the second tiers first fliproductWith :: (a->b->c) -> [[a]] -> [[b]] -> [[c]] fliproductWith _ [] _ = [] fliproductWith _ _ [] = [] fliproductWith f xss (ys:yss) = map (** ys) xss \/ delay (productWith f xss yss) where xs ** ys = [x `f` y | x <- xs, y <- ys] -- | -- -- > useMatches [xx,yy] [xx,yy] = [[(xx,xx), (yy,yy)]] -- > useMatches [xx,yy] [yy,xx] = [[(xx,xx), (yy,yy)]] -- > useMatches [yy,xx] [xx,yy] = [[(yy,yy), (xx,xx)]] -- > useMatches [xx,yy] [xx,xx] = [] -- > useMatches [xx,yy] [abs' xx, abs' yy] = [[(xx,abs' xx), (yy, abs' yy)]] -- > useMatches [xx-:-xxs, yy-:-yys] [abs' xx, abs' yy] -- > = [(xx-:-xxs, abs' xx), (yy-:-yys, abs' yy)] useMatches :: [Expr] -> [Expr] -> [[(Expr,Expr)]] useMatches [] [] = [[]] useMatches [] es = [] -- no matches when lists have different lengths useMatches es [] = [] -- no matches when lists have different lengths useMatches (e:es) es' = concat [ map ((e,e'):) (useMatches es es') | (e',es') <- choicesThat (\e' _ -> any (`elem` vars e') (vars e)) es' ] rehole :: Expr -> Expr rehole (e1 :$ e2) = rehole e1 :$ rehole e2 rehole e | isVar e = "" `varAsTypeOf` e | otherwise = e instance Express A where expr = val instance Express B where expr = val instance Express C where expr = val instance Express D where expr = val instance Express E where expr = val instance Express F where expr = val