{-
 -   The Lambda Shell, an interactive environment for evaluating pure untyped lambda terms.
 -   Copyright (C) 2005-2011, Robert Dockins
 -
 -   This program is free software; you can redistribute it and/or modify
 -   it under the terms of the GNU General Public License as published by
 -   the Free Software Foundation; either version 2 of the License, or
 -   (at your option) any later version.
 -
 -   This program is distributed in the hope that it will be useful,
 -   but WITHOUT ANY WARRANTY; without even the implied warranty of
 -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 -   GNU General Public License for more details.
 -
 -   You should have received a copy of the GNU General Public License
 -   along with this program; if not, write to the Free Software
 -   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 -}

{- |
 This module defines the pure lambda calculus and
 some associated operations.

 Lambda terms are represented with de Brujin indicies.  Lambdas
 are annotated with a label for the variable that is used when
 displaying.  Lambda terms may be references to let-bindings;
 these are unfolded in explicit reduction steps.  Let bindings are
 non-recursive; that is, the bound name is not in scope during
 the definition.
-}

module Lambda (
-- * Type Definitions
  Bindings
, ReductionStrategy

-- * Lamda Term Datatype
, PureLambda (..)

-- * Comparison Functions
, alphaEq
, normalEq

-- * Auxilary Functions
, lookupBinding
, lookupBindingM
, printLam
, showLam
, lamSubst
, lamShift
, unfoldTop

-- * Reduction Strategies
, lamReduceNull
, lamReduceWHNF
, lamReduceHNF
, lamReduceNF
, lamStrictNF

-- * Evaluation Functions
, lamEvalF
, lamEval
, lamEvalCount
, lamEvalTrace

) where

import Data.List
import qualified Env as Env
import qualified Data.Map as Map
import Control.Monad (MonadPlus (..))
import Control.Monad.Identity

type Bindings a l = Map.Map String (Maybe (PureLambda a l))

lookupBinding :: String -> Bindings a l -> Maybe (PureLambda a l)
lookupBinding name b  = runIdentity (lookupBindingM name b)

lookupBindingM :: Monad m => String -> Bindings a l -> m (Maybe (PureLambda a l))
lookupBindingM name b =
   case Map.lookup name b of
      Just x -> return x
      Nothing -> fail (concat ["'",name,"' not bound"])

----------------------------------------------------------------
-- | The type of lambda terms;
--   they are polymorphic in an annotation type \'a\' and the type
--   of labels \'l\'.

data PureLambda a l
   = Lam a l (PureLambda a l)
   | App a (PureLambda a l) (PureLambda a l)
   | Var a Int
   | Binding a String
 deriving (Show)

------------------------------------------------------------------
-- | Alpha equivalance on lambda terms.  Defined in the usual
--   way, except that bindings are compared by name.

alphaEq      :: PureLambda a l
             -> PureLambda a l
             -> Bool

alphaEq (Lam _ _ t1)   (Lam _ _ t2)   = alphaEq t1 t2
alphaEq (App _ x1 y1)  (App _ x2 y2)  = alphaEq x1 x2 && alphaEq y1 y2
alphaEq (Var _ i1)     (Var _ i2)     = i1 == i2
alphaEq (Binding _ n1) (Binding _ n2) = n1 == n2
alphaEq _              _              = False


-------------------------------------------------------------------
-- | Defines an eqivalance predicate on normalizing terms, where
--   terms with alpha-equivalant normal forms are in the relation.
--   This function will diverge for non-normalizing terms.

normalEq     :: Bindings a l    -- ^ Let bindings in scope
             -> PureLambda a l
             -> PureLambda a l
             -> Bool

normalEq binds t1 t2 =
    let n1 = lamEval binds True lamReduceNF t1
        n2 = lamEval binds True lamReduceNF t2
    in alphaEq n1 n2

-------------------------------------------------------------------
-- | Show a lambda term, minimizing parenthises and disambiguating
--   variables in nested scopes with identical labels.

printLam :: Bindings a String -> PureLambda a String -> String
printLam binds lam = showLam binds lam []

showLam :: Bindings a String -> PureLambda a String -> ShowS
showLam binds = showLam_ (Env.empty (Map.keysSet binds)) TopContext 0


data LamContext
   = TopContext
   | AppLeft
   | AppRight
 deriving (Eq)


showLam_     :: Env.Env
             -> LamContext
             -> Int
             -> PureLambda a String
             -> ShowS

showLam_ env c x lam = case lam of

  Binding _ name -> showLambdas env x . showString name

  Var _ v        -> showLambdas env x . showString (Env.lookup v env)

  Lam _ label t  ->
       showParen (c /= TopContext)
       ( showLam_ (Env.insert label env) TopContext (x+1) t )

  App _ t1 t2    ->
       showParen (c == AppRight)
       ( showLambdas env x
       . showLam_ env AppLeft 0 t1
       . showChar ' '
       . showLam_ env AppRight 0 t2
       )


showLambdas :: Env.Env
            -> Int
            -> ShowS

showLambdas env 0 = id
showLambdas env x =
    ( showChar '\\'
    . showString
          (concat . intersperse " " . map (\i -> Env.lookup i env) $ [x-1, x-2 .. 0])
    . showString ". "
    )


-----------------------------------------------------------------------------
-- shifts all free variables by a specified amount
-- ancillary function for substitution

lamShift     :: Int
             -> Int
             -> PureLambda a l
             -> PureLambda a l

lamShift c d v@(Var a x)
   | x >= c    = Var a (x+d)
   | otherwise = v

lamShift c d (Lam a label t)  = Lam a label (lamShift (c+1) d t)
lamShift c d (App a t1 t2)    = App a (lamShift c d t1) (lamShift c d t2)
lamShift c d b@(Binding _ _)  = b


------------------------------------------------------------------------------
-- | Capture-avoiding substitution;
--   substitute \'s\' into \'t\', replacing all free variables with index 0.

lamSubst     :: PureLambda a l -- ^ s
             -> PureLambda a l -- ^ t
             -> PureLambda a l

lamSubst s t = lamShift 0 (-1) (lamSubst' (lamShift 0 1 s) 0 0 t)



lamSubst'    :: PureLambda a l
             -> Int
             -> Int
             -> PureLambda a l
             -> PureLambda a l

lamSubst' s var c v@(Var _ x)
   | x == (var+c) = lamShift 0 c s
   | otherwise    = v

lamSubst' s var c (Lam a label t)  = Lam a label (lamSubst' s var (c+1) t)
lamSubst' s var c (App a t1 t2)    = App a (lamSubst' s var c t1) (lamSubst' s var c t2)
lamSubst' s var c b@(Binding _ _)  = b


-------------------------------------------------------------------------------------
-- | The type of reduction strategies.

type ReductionStrategy a l
     = Bindings a l
    -> Bool
    -> PureLambda a l
    -> Maybe (PureLambda a l)


-------------------------------------------------------------------------------------
-- | The \'null\' reduction strategy, which does no reduction
lamReduceNull :: ReductionStrategy a l
lamReduceNull _ _ _ = Nothing

-------------------------------------------------------------------------------------
-- | Single-step normal order reduction to Weak Head Normal Form (WHNF)

lamReduceWHNF :: ReductionStrategy a l

lamReduceWHNF b unfold (App _ (Lam _ _ t1) t2) = Just (lamSubst t2 t1)
lamReduceWHNF b unfold (App a t1 t2)           = lamReduceWHNF b True t1   >>= \t1' -> return (App a t1' t2)
lamReduceWHNF b unfold (Lam a l t)             = Nothing
lamReduceWHNF b unfold (Var _ _)               = Nothing
lamReduceWHNF b unfold (Binding a name)        = if unfold then lookupBinding name b else Nothing


-------------------------------------------------------------------------------------
-- | Single-step normal order reduction to Head Normal Form (HNF)

lamReduceHNF :: ReductionStrategy a l

lamReduceHNF b unfold (App _ (Lam _ _ t1) t2)  = Just (lamSubst t2 t1)
lamReduceHNF b unfold (App a t1 t2)            = lamReduceHNF b True t1   >>= \t1' -> return (App a t1' t2)
lamReduceHNF b unfold (Lam a l t)              = lamReduceHNF b unfold t  >>= \t'  -> return (Lam a l t')
lamReduceHNF b unfold (Var _ _)                = Nothing
lamReduceHNF b unfold (Binding a name)         = if unfold then lookupBinding name b else Nothing



--------------------------------------------------------------------------------------
-- | Single-step normal order reduction to Normal Form (NF)

lamReduceNF :: ReductionStrategy a l

lamReduceNF b unfold (App _ (Lam _ _ t1) t2)   = Just (lamSubst t2 t1)
lamReduceNF b unfold (App a t1 t2)             = (lamReduceNF b True t1   >>= \t1' -> return (App a t1' t2))
                                                   `mplus`
                                                 (lamReduceNF b unfold t2 >>= \t2' -> return (App a t1 t2'))
lamReduceNF b unfold (Lam a l t)               = lamReduceNF b unfold t   >>= \t'  -> return (Lam a l t')
lamReduceNF b unfold (Var _ _)                 = Nothing
lamReduceNF b unfold (Binding a name)          = if unfold then lookupBinding name b else Nothing



---------------------------------------------------------------------------------------
-- | Single-step applicative order reduction to Normal Form (NF)

lamStrictNF :: ReductionStrategy a l

lamStrictNF b unfold (App a (Lam al l t1) t2)  = (lamStrictNF b True t2 >>= \t2' -> return (App a (Lam al l t1) t2'))
                                                   `mplus`
                                                 (Just (lamSubst t2 t1))
lamStrictNF b unfold (App a t1 t2)             = (lamStrictNF b True t1   >>= \t1' -> return (App a t1' t2))
                                                   `mplus`
                                                 (lamStrictNF b unfold t2 >>= \t2' -> return (App a t1 t2'))
lamStrictNF b unfold (Lam a l t)               = lamStrictNF b unfold t   >>= \t'  -> return (Lam a l t')
lamStrictNF b unfold (Var _ _)                 = Nothing
lamStrictNF b unfold (Binding a name)          = if unfold then lookupBinding name b else Nothing



---------------------------------------------------------------------------------------
-- | Helper for various kinds of evaluation.  Applies the function \'z\' if
--   the evaluation strategy has terminated, and applies \'f\' to the
--   reduced term otherwise.

lamEvalF     :: Bindings a l             -- ^ A set of bindings for unfolding
             -> Bool                     -- ^ Apply full unfolding?
             -> ReductionStrategy a l    -- ^ Reduction strategy to use
             -> (PureLambda a l -> b)    -- ^ f
             -> (PureLambda a l -> b)    -- ^ z
             -> PureLambda a l           -- ^ The term to reduce
             -> b

lamEvalF b unfold reduce f z x =
   case reduce b unfold x of
        Just x' -> f x'
        Nothing -> z x




-------------------------------------------------------------------------------------
-- | Big-step reduction; that is, apply the reduction strategy until
--   it fails to reduce any futher.

lamEval     :: Bindings a l            -- ^ A set of bindings for unfolding
             -> Bool                   -- ^ Apply full unfolding ?
             -> ReductionStrategy a l  -- ^ Reduction strategy to use
             -> PureLambda a l         -- ^ The term to reduce
             -> PureLambda a l         -- ^ The evaluated term

lamEval bind unfold red = eval
  where evalF  = lamEvalF bind unfold red
        eval x = evalF eval id x



-------------------------------------------------------------------------------------
-- | Big-step reduction that counts the number of reductions performed

lamEvalCount :: Bindings a l           -- ^ A set of bindings for unfolding
             -> Bool                   -- ^ Apply full unfolding ?
             -> ReductionStrategy a l  -- ^ Reduction strategy to use
             -> PureLambda a l         -- ^ The term to reduce
             -> (PureLambda a l,Integer) -- ^ The evaluated term and reduction count

lamEvalCount bind unfold red = eval 0
  where evalF     = lamEvalF bind unfold red
        eval n x  = evalF (\t -> eval (succ n) t) (\t -> (t,n)) x


-------------------------------------------------------------------------------------
-- | Traced evaluation; the result is a list of the reduction
--   steps taken by the given reduction stragegy.  A non-terminating
--   term (under the given strategy) will result in an infinite list.
--   For a normalizing term, the last element in the list will be the
--   normal  form.

lamEvalTrace :: Bindings a l          -- ^ A set of bindings for unfolding
             -> Bool                  -- ^ Apply full unfolding ?
             -> ReductionStrategy a l -- ^ Reduction strategy to use
             -> PureLambda a l        -- ^ The term to reduce
             -> [PureLambda a l]      -- ^ The list of intermediate reductions

lamEvalTrace bind unfold red = eval
  where evalF  = lamEvalF bind unfold red
        eval x = evalF ((x:) . eval) (:[]) x





-----------------------------------------------------------------------------------------
-- | If a lambda term is just a let binding, this function will unfold it; otherwise
--   it will return the term unchanged.  It will result in bottom if the term is not bound.

unfoldTop     :: Bindings () String
              -> PureLambda () String
              -> PureLambda () String

unfoldTop binds (Binding a x) = maybe (Binding a x) id $
                                  Map.findWithDefault (error $ concat ["'",x,"' not bound"]) x binds
unfoldTop binds x             = x
