packages feed

code-conjure-0.0.2: src/Conjure/Engine.hs

-- |
-- Module      : Conjure.Engine
-- Copyright   : (c) 2021 Rudy Matela
-- License     : 3-Clause BSD  (see the file LICENSE)
-- Maintainer  : Rudy Matela <rudy@matela.com.br>
--
-- An internal module of 'Conjure',
-- a library for Conjuring function implementations
-- from tests or partial definitions.
-- (a.k.a.: functional inductive programming)
{-# LANGUAGE CPP, RecordWildCards, TupleSections #-}
module Conjure.Engine
  ( module Data.Express
  , module Data.Express.Fixtures
  , module Test.Speculate.Engine
  , module Test.Speculate.Reason
  , Args(..)
  , args
  , conjure
  , conjureWith
  , conjpure
  , conjpureWith
  , candidateExprs
  , ifFor
  )
where

import Data.Express
import Data.Express.Fixtures hiding ((-==-))
import qualified Data.Ratio
import Test.LeanCheck.Error (errorToTrue, errorToFalse, errorToNothing)
import Test.Speculate hiding ((===), Args(..), args)
import Test.Speculate.Reason
import Test.Speculate.Engine
import Test.Speculate.Expr
import System.IO

import Conjure.Expr
import Conjure.Conjurable

-- | Arguments to be passed to 'conjureWith' or 'conjpureWith'.
--   See 'args' for the defaults.
data Args = Args
  { maxTests         :: Int  -- ^ defaults to 60
  , maxSize          :: Int  -- ^ defaults to 9, keep greater than maxEquationSize
  , maxEquationSize  :: Int  -- ^ defaults to 5, keep smaller than maxSize
  , maxRecursionSize :: Int  -- ^ defaults to 60
  }

-- | Default arguments to conjure.
--
-- * 60 tests
-- * functions of up to 9 symbols
-- * pruning with equations up to size 5
-- * recursion up to 60 symbols.
args :: Args
args = Args
  { maxTests          =  60
  , maxSize           =   9
  , maxEquationSize   =   5
  , maxRecursionSize  =  60
  }

-- | Like 'conjure' but in the pure world.
--
-- Returns a triple whose:
--
-- 1. first element is the number of candidates considered
--
-- 2. second element is the number of defined points in the given function
--
-- 3. third element is a list of implementations encoded as 'Expr's
--    paired with the number of matching points.
conjpure :: Conjurable f => String -> f -> [Expr] -> (Int,Int,[(Int,Expr)])
conjpure =  conjpureWith args

-- | Like 'conjpure' but allows setting options through 'Args' and 'args'.
conjpureWith :: Conjurable f => Args -> String -> f -> [Expr] -> (Int,Int,[(Int,Expr)])
conjpureWith Args{..} nm f es  =  (length candidates,totalDefined,) $ sortBy compareResult
  [ (ffxx .=. re, ffxx -==- e)
  | e <- candidates
  , apparentlyTerminates rrff e
  , let re = recursexpr maxRecursionSize vffxx e
  , ffxx ?=? re
  ]
  where
  totalDefined  =  ffxx .=. ffxx
  candidates  =  filter (\e -> typ e == typ ffxx)
              .  concat
              .  take maxSize
              $  candidateExprs nm f maxEquationSize (===) [es]
  ffxx   =  canonicalApplication nm f
  vffxx  =  canonicalVarApplication nm f
  rrff   =  var nm f

  (===), (?=?) :: Expr -> Expr -> Bool
  e1 === e2  =  isReallyTrue      (e1 -==- e2)
  e1 ?=? e2  =  isTrueWhenDefined (e1 -==- e2)

  e1 .=. e2  =  countTrue         (e1 -==- e2)
  (-==-)  =  mkEquation  eqs
    where
    eqs  =  value "==" ((==) :: Bool -> Bool -> Bool)
         :  es

  isTrueWhenDefined  =  all (errorToTrue  . eval False) . gs
  isReallyTrue       =  all (errorToFalse . eval False) . gs
  countTrue        =  count (errorToFalse . eval False) . gs

  gs :: Expr -> [Expr]
  gs  =  take maxTests . grounds (tiersFor f)

-- | Conjures an implementation of a partially defined function.
--
-- Takes a 'String' with the name of a function,
-- a partially-defined function from a conjurable type,
-- and a list of building blocks encoded as 'Expr's.
--
-- For example, given:
--
-- > square :: Int -> Int
-- > square 0  =  0
-- > square 1  =  1
-- > square 2  =  4
-- >
-- > background :: [Expr]
-- > background =
-- >   [ val (0::Int)
-- >   , val (1::Int)
-- >   , value "+" ((+) :: Int -> Int -> Int)
-- >   , value "*" ((*) :: Int -> Int -> Int)
-- >   , value "==" ((==) :: Int -> Int -> Bool)
-- > ]
--
-- The conjure function does the following:
--
-- > > conjure "square" square background
-- > square :: Int -> Int
-- > -- looking through 815 candidates, 100% match, 3/3 assignments
-- > square x  =  x * x
--
-- The background is defined with 'val', 'value' and 'ifFor'.
conjure :: Conjurable f => String -> f -> [Expr] -> IO ()
conjure  =  conjureWith args

-- | Like 'conjure' but allows setting options through 'Args' and 'args'.
conjureWith :: Conjurable f => Args -> String -> f -> [Expr] -> IO ()
conjureWith args nm f es  =  do
  print (var nm f)
  putStr $ "-- looking through " ++ show ncs ++ " candidates"
  hFlush stdout
  case rs of
    []    -> putStrLn $ "\ncannot conjure"
    ((n,e):_) -> do putStrLn $ ", " ++ showMatch n
                    putStrLn $ showEq e
--  nes -> putStrLn . unlines $ "":[showEq e ++ "  -- " ++ show n | (n,e) <- nes]
  putStrLn ""
  where
  (ncs,t,rs)  =  conjpureWith args nm f es
  showMatch n  =  show (n % t) ++ "% match, " ++ show n ++ "/" ++ show t ++ " assignments"
  showEq eq  =  showExpr (lhs eq) ++ "  =  " ++ showExpr (rhs eq)

candidateExprs :: Conjurable f
               => String -> f
               -> Int
               -> (Expr -> Expr -> Bool)
               -> [[Expr]]
               -> [[Expr]]
candidateExprs nm f sz (===) ess  =  expressionsT $ [ef:exs] \/ ess
  where
  (ef:exs)  =  unfoldApp $ canonicalVarApplication nm f
  thy  =  theoryFromAtoms (===) sz $ [nub (b_:map holeAsTypeOf exs)] \/ ess
  expressionsT ds  =  filterT (\e -> count (== ef) (vars e) <= 1)
                   $  filterT (isRootNormalE thy)
                   $  ds \/ (delay $ productMaybeWith ($$) es es)
    where
    es = expressionsT ds

lhs, rhs :: Expr -> Expr
lhs (((Value "==" _) :$ e) :$ _)  =  e
rhs (((Value "==" _) :$ _) :$ e)  =  e

compareResult :: (Int,Expr) -> (Int,Expr) -> Ordering
compareResult (n1,e1) (n2,e2)  =  n2 `compare` n1
                               <> e1 `compareSimplicity` e2

(%) :: Int -> Int -> Int
x % y  =  x * 100 `div` y