packages feed

vintage-basic-1.0: src/Language/VintageBasic/BasicMonad.hs

{-# LANGUAGE FlexibleContexts, Rank2Types #-}

-- | This monad provides runtime support for variable assignment,
-- user-defined functions, I/O, and a jump table.

module Language.VintageBasic.BasicMonad where

import Prelude hiding (lookup)
import Control.Monad.CPST
import Control.Monad.CPST.DurableTraps
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Trans
import Data.HashTable
import Data.IORef
import Data.Array.IO
import Data.Maybe
import Data.Time
import IO.IOStream
import Language.VintageBasic.Printer(printVarName)
import Language.VintageBasic.Result
import Language.VintageBasic.Syntax
import System.Random

-- | Values in BASIC programs.
data Val = FloatVal Float | IntVal Int | StringVal String
    deriving (Eq,Show,Ord)

instance Typeable Val where
    typeOf (FloatVal  _) = FloatType
    typeOf (IntVal    _) = IntType
    typeOf (StringVal _) = StringType

-- | The default variable value (before assignment) for each BASIC type.
defVal :: ValType -> Val
defVal FloatType  = FloatVal 0
defVal IntType    = IntVal 0
defVal StringType = StringVal ""

-- | The store of BASIC variables.
data BasicStore = BasicStore {
    scalarTable :: HashTable VarName (IORef Val),
      -- ^ scalar variable assignments
    arrayTable :: HashTable VarName ([Int], IOArray Int Val),
      -- ^ array variable values (@[Int]@ lists the bound of each dimension)
    fnTable :: HashTable VarName (IORef ([Val] -> Code Val))
      -- ^ user-defined function (@DEF FN@) definitions
}

-- | The default array bounds.
defBounds :: [Int]
defBounds = [11] -- one dimension, 0-10

-- | The common way to convert Float to Int values in BASIC is to round down.
floatToInt :: Float -> Int
floatToInt = floor

data BasicState = BasicState {
    inputStream :: IOStream,  -- ^ where @INPUT@ comes from
    outputStream :: IOStream, -- ^ where @PRINT@ output goes
    lineNumber :: Int,        -- ^ for error reporting
    outputColumn :: Int,      -- ^ for the @TAB()@ function
    prevRandomVal :: Float,   -- ^ the previously generated random value
    randomGen :: StdGen,      -- ^ random number generator
    dataStrings :: [String]   -- ^ the strings extracted from all @DATA@ statements
}

type BasicRT = ReaderT BasicStore (StateT BasicState IO)
type Basic o = CPST o BasicRT
type BasicCont o i = Cont o BasicRT i
type BasicExcep o i = Excep o BasicRT i
type Code a = Basic (BasicExcep Result ()) a
type Program = Code (BasicExcep Result ())
type BasicExceptionHandler = ExceptionHandler Result BasicRT

-- | An exception handler used to print error messages at the outermost
-- level of execution.
errorDumper :: BasicExceptionHandler
errorDumper x _ _ continue = do
    case x of
        Pass -> return ()
        _    -> printString $ (show x ++ "\n")
    continue False

-- | Runs an interpreted BASIC program using the supplied input and output streams.
runProgram
    :: IOStream -- ^ input stream
    -> IOStream -- ^ output stream
    -> Program  -- ^ the BASIC program, in lazy interpreted form
    -> IO ()
runProgram inputHandle outputHandle prog = do
    vFlush outputHandle
    st <- new (==) (hashString . printVarName)
    at <- new (==) (hashString . printVarName)
    ft <- new (==) (hashString . printVarName)
    let store = BasicStore st at ft
    let state = (BasicState inputHandle outputHandle 0 0 0 (mkStdGen 0) [])
    runStateT (runReaderT (runCPST (catchC errorDumper prog)) store) state
    return ()

-- | Raises a runtime error if the assertion is False.
assert :: Bool -> RuntimeError -> Code ()
assert cond err = if cond then return () else raiseRuntimeError err

raiseRuntimeException :: RuntimeException -> Code ()
raiseRuntimeException rte = do
    state <- get
    raiseCC (LabeledRuntimeException (lineNumber state) rte)
    return ()

raiseRuntimeError :: RuntimeError -> Code ()
raiseRuntimeError err = raiseRuntimeException (RuntimeError err)

extractFloatOrFail :: RuntimeError -> Val -> Code Float
extractFloatOrFail _   (FloatVal fv) = return fv
extractFloatOrFail err _             = raiseRuntimeError err >> return 0

-- | Get the value of a scalar variable.
getScalarVar :: VarName -> Basic o Val
getScalarVar vn = do
    store <- ask
    liftIO $ do
        maybeVarRef <- lookup (scalarTable store) vn
        case maybeVarRef of
            Nothing -> return (defVal (typeOf vn))
            (Just varRef) -> readIORef varRef

-- | Set the value of a scalar variable.
setScalarVar :: VarName -> Val -> Basic o ()
setScalarVar vn val = do
    store <- ask
    liftIO $ do
        maybeVarRef <- lookup (scalarTable store) vn
        case maybeVarRef of
            Nothing -> do
                ref <- newIORef val
                insert (scalarTable store) vn ref
            (Just varRef) -> writeIORef varRef val

-- | BASIC indices range from zero to an upper bound.
-- We're storing 1+ that bound, to make computations easier.
indicesAreWithinBounds :: [Int] -> [Int] -> Bool
indicesAreWithinBounds bounds indices =
    and $ (zipWith (>) bounds indices) ++ (map (>=0) indices)

-- | Given the array's bounds and indices, compute the linear index
-- into the storage array.
arrIndex :: [Int] -> [Int] -> Int
arrIndex bounds indices =
    let scales = tail $ scanr (*) 1 bounds
        in sum $ zipWith (*) scales indices

-- | Create a new array in the store, with the specified name and bounds.
dimArray :: VarName -> [Int] -> Code ([Int], (IOArray Int Val))
dimArray vn bounds = do
    store <- ask
    maybeArray <- liftIO $ lookup (arrayTable store) vn
    assert (isNothing maybeArray) ReDimensionedArrayError
    arr <- liftIO $ newArray (0, product bounds - 1) (defVal (typeOf vn))
    liftIO $ insert (arrayTable store) vn (bounds, arr)
    return (bounds, arr)

-- | Look up an array from the store. Indices are provided just to check
-- validity.
lookupArray :: VarName -> [Int] -> Code ([Int], (IOArray Int Val))
lookupArray vn indices = do
    store <- ask
    maybeArray <- liftIO $ lookup (arrayTable store) vn
    (bounds, arr) <- case maybeArray of
        Nothing -> dimArray vn defBounds
        (Just (bounds, arr)) -> return (bounds, arr)
    assert (length bounds == length indices) MismatchedArrayDimensionsError
    assert (indicesAreWithinBounds bounds indices) OutOfArrayBoundsError
    return (bounds, arr)

-- | Get a value from an array variable.
getArrVar :: VarName -> [Int] -> Code Val
getArrVar vn indices = do
    (bounds, arr) <- lookupArray vn indices
    liftIO $ readArray arr (arrIndex bounds indices)

-- | Set a value in an array variable.
setArrVar :: VarName -> [Int] -> Val -> Code ()
setArrVar vn indices val = do
    (bounds, arr) <- lookupArray vn indices
    liftIO $ writeArray arr (arrIndex bounds indices) val

-- | Retrieve the code for a user-defined function from the store.
getFn :: VarName -> Code ([Val] -> Code Val)
getFn vn = do
    store <- ask
    maybeVarRef <- liftIO $ lookup (fnTable store) vn
    assert (isJust maybeVarRef) (UndefinedFunctionError vn)
    liftIO $ readIORef (fromJust maybeVarRef)

-- | Define a user-defined function in the store.
setFn :: VarName -> ([Val] -> Code Val) -> Code ()
setFn vn fn = do
    store <- ask
    liftIO $ do
        maybeVarRef <- lookup (fnTable store) vn
        case maybeVarRef of
            Nothing -> do
                ref <- newIORef fn
                insert (fnTable store) vn ref
            (Just varRef) -> writeIORef varRef fn

-- | Set the current line number in the BASIC state.
setLineNumber :: Int -> Basic o ()
setLineNumber lineNum = modify (\state -> state { lineNumber = lineNum })

-- | The width of print zones, used for commas in a @PRINT@ statement.
zoneWidth :: Int
zoneWidth = 14

-- | Output a string, updating the current output column in the state.
printString :: String -> Basic o ()
printString s = do
    state <- get
    let startCol = outputColumn state
    liftIO $ vPutStr (outputStream state) s >> vFlush (outputStream state)
    put (state { outputColumn = endCol startCol s })

-- | Given a starting column and a string, returns the ending column,
-- were that string to be output.
endCol :: Int -> String -> Int    
endCol startCol "" = startCol
endCol _ ('\n' : s) = endCol 0 s
endCol _ ('\r' : s) = endCol 0 s
endCol startCol (_ : s) = endCol (startCol + 1) s

-- | Retrieves the current output column from the state.
getOutputColumn :: Code Int
getOutputColumn = do
    state <- get
    return $ outputColumn state

-- | Gets an input line, raising an exception if we are at EOF.
getString :: Code String
getString = do
    state <- get
    liftIO $ vFlush (outputStream state)
    eof <- liftIO $ vIsEOF (inputStream state)
    assert (not eof) EndOfInputError
    liftIO $ vGetLine (inputStream state)

-- | The number of seconds that have elapsed since midnight (local time).
secondsSinceMidnight :: Code Int
secondsSinceMidnight = do
    zonedTime <- liftIO getZonedTime
    return (floor $ toRational $ timeOfDayToTime $ localTimeOfDay $
      zonedTimeToLocalTime zonedTime)

-- | Seeds the BASIC random number generator using an integer.
seedRandom :: Int -> Code ()
seedRandom i = modify (\state -> state { randomGen = (mkStdGen i) })

-- | Seeds the BAISC random number generator based on the number of
-- seconds since midnight.
seedRandomFromTime :: Code ()
seedRandomFromTime = secondsSinceMidnight >>= seedRandom

-- | Get the previously generated random number from the state.
-- Used for @RND(0)@.
getPrevRandom :: Code Float
getPrevRandom = do
    state <- get
    return (prevRandomVal state)

-- | Generate a new random number.
getRandom :: Code Float
getRandom = do
    state <- get
    let (rVal, rGen) = random (randomGen state)
    put (state { prevRandomVal = rVal, randomGen = rGen })
    return rVal

-- | Set the list of @DATA@ statement strings in the store.
setDataStrings :: [String] -> Code ()
setDataStrings ds = modify (\store -> store { dataStrings = ds })

-- | @READ@ the next @DATA@ value from the data strings.
readData :: Code String
readData = do
    state <- get
    let ds = dataStrings state
    assert (not (null ds)) OutOfDataError
    put (state { dataStrings = tail ds })
    return (head ds)