-- {-# OPTIONS_GHC -cpp -DDEBUG #-}
{-# OPTIONS_GHC -cpp #-}

-----------------------------------------------------------------------------
-- |
-- Module      : Berp.Base.ControlStack
-- Copyright   : (c) 2010 Bernie Pope
-- License     : BSD-style
-- Maintainer  : florbitous@gmail.com
-- Stability   : experimental
-- Portability : ghc
--
-- Operations on the control stack.
--
-----------------------------------------------------------------------------

#include "BerpDebug.h"

module Berp.Base.ControlStack 
   ( isEmpty, isProcedureCall, isExceptionHandler, isWhileLoop, isGeneratorCall
   , unwind, unwindPastWhileLoop, unwindUpToWhileLoop, push, pop, nullifyTopHandler 
   , unwindYieldContext, dumpStack, getControlStack, setControlStack
   )
   where

import Control.Monad.State
import Data.Maybe (maybe)
import Berp.Base.SemanticTypes (ControlStack (..), Eval, EvalState (..), Object (..))
import Berp.Base.LiftedIO as LIO (writeIORef, putStrLn)
import {-# SOURCE #-} Berp.Base.StdTypes.None (none)

isEmpty :: ControlStack -> Bool
isEmpty EmptyStack = True
isEmpty _ = False

isProcedureCall :: ControlStack -> Bool
isProcedureCall (ProcedureCall {}) = True
isProcedureCall _ = False

isExceptionHandler :: ControlStack -> Bool
isExceptionHandler (ExceptionHandler {}) = True
isExceptionHandler _ = False

isWhileLoop :: ControlStack -> Bool
isWhileLoop (WhileLoop {}) = True
isWhileLoop _ = False

isGeneratorCall :: ControlStack -> Bool
isGeneratorCall (GeneratorCall {}) = True
isGeneratorCall _ = False

{- Unwind the control stack and execute any "finally" exception handlers
   that we pass along the way. Returns the stack with the most recently popped 
   element remaining.
-}
unwind :: (ControlStack -> Bool) -> Eval ControlStack 
unwind pred = do
   stack <- gets control_stack
   unwindFrame stack
   where
   unwindFrame :: ControlStack -> Eval ControlStack 
   -- XXX should be an exception
   unwindFrame EmptyStack = error $ "unwindFrame: empty control stack" 
   unwindFrame stack@(ExceptionHandler { exception_finally = maybeFinally }) = do
      pop
      maybe (return none) id maybeFinally
      if pred stack
         then return stack 
         else unwind pred 
   unwindFrame stack
      | pred stack = pop >> return stack 
      | otherwise = pop >> unwind pred 

unwindYieldContext :: Eval Object -> Eval (Object -> Eval Object) 
unwindYieldContext continuation = do
   stack <- gets control_stack
   let (generatorYield, generatorObj, newStack, context) = unwindYieldWorker stack
   LIO.writeIORef (object_continuation generatorObj) continuation 
   LIO.writeIORef (object_stack_context generatorObj) context
   setControlStack newStack
   return generatorYield 
   where
   unwindYieldWorker :: ControlStack -> (Object -> Eval Object, Object, ControlStack, ControlStack -> ControlStack) 
   -- XXX this should be an exception
   unwindYieldWorker EmptyStack = error "unwindYieldWorker: empty control stack"
   unwindYieldWorker (ProcedureCall {}) = error "unwindYieldWorker: procedure call"
   unwindYieldWorker (ExceptionHandler handler finally tail) =
      (yield, obj, stack, ExceptionHandler handler finally . context)
      where
      (yield, obj, stack, context) = unwindYieldWorker tail
   unwindYieldWorker (WhileLoop start end tail) =
      (yield, obj, stack, WhileLoop start end . context)
      where
      (yield, obj, stack, context) = unwindYieldWorker tail
   unwindYieldWorker (GeneratorCall yield obj tail) = (yield, obj, tail, id)

unwindPastWhileLoop :: Eval ControlStack
unwindPastWhileLoop = do
   stack <- unwindUpToWhileLoop
   pop
   return stack

unwindUpToWhileLoop :: Eval ControlStack
unwindUpToWhileLoop = do
   stack <- gets control_stack
   unwindFrame stack
   where
   unwindFrame :: ControlStack -> Eval ControlStack 
   -- XXX should be an exception, should mention continue/break called outside of loop
   unwindFrame EmptyStack = error $ "unwindUpToWhileLoop: empty control stack"
   unwindFrame (ExceptionHandler { exception_finally = maybeFinally }) = do
      pop
      maybe (return none) id maybeFinally
      unwindUpToWhileLoop
   unwindFrame stack@(WhileLoop {}) = return stack
   -- XXX should be an exception which mentions continue/break called outside of loop
   unwindFrame (ProcedureCall {}) = error $ "unwindUpToWhileLoop: procedure call"
   unwindFrame (GeneratorCall {}) = error $ "unwindUpToWhileLoop: generator call"

pop :: Eval ()
pop = do
   stack <- gets control_stack
   case stack of
      -- should be an exception
      EmptyStack -> error "pop: empty stack" 
      _other -> setControlStack $ control_stack_tail stack

push :: (ControlStack -> ControlStack) -> Eval ()
push frame = do
   stack <- gets control_stack
   setControlStack (frame stack) 

setControlStack :: ControlStack -> Eval ()
setControlStack stack = modify $ \state -> state { control_stack = stack }

getControlStack :: Eval ControlStack
getControlStack = gets control_stack

-- assumes top of stack is an exception handler
nullifyTopHandler :: Eval ()
nullifyTopHandler = do
   IF_DEBUG(dumpStack)
   stack <- gets control_stack
   case stack of
     ExceptionHandler {} -> 
        setControlStack $ stack { exception_handler = Nothing } 
     _other -> error $ "nullifyTopHandler: top of stack is not an exception handler: " ++ show stack

dumpStack :: Eval ()
dumpStack = do
   LIO.putStrLn "--- Bottom of stack ---"
   stack <- gets control_stack
   mapStackM printer stack
   LIO.putStrLn "--- Top of stack ---"
   where
   printer :: ControlStack -> Eval ()
   printer (ProcedureCall {}) = LIO.putStrLn "ProcedureCall" 
   printer (ExceptionHandler {}) = LIO.putStrLn "ExceptionHandler" 
   printer (WhileLoop {}) = LIO.putStrLn "WhileLoop" 
   printer (GeneratorCall {}) = LIO.putStrLn "GeneratorCall" 
   printer (EmptyStack {}) = LIO.putStrLn "EmptyStack" 

mapStackM :: Monad m => (ControlStack -> m ()) -> ControlStack -> m ()
mapStackM _f EmptyStack = return ()
mapStackM f stack = f stack >> mapStackM f (control_stack_tail stack)