packages feed

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

{-# LANGUAGE FlexibleContexts, ParallelListComp, Rank2Types #-}

-- | The heart of the BASIC interpreter. It is implemented on top
-- of the BASIC monad.

module Language.VintageBasic.Interpreter(interpLines) where

import Control.Monad.CPST.DurableTraps
import Data.List
import Data.Maybe
import Language.VintageBasic.Builtins(Builtin(..))
import Language.VintageBasic.LexCommon(Tagged(..))
import Language.VintageBasic.BasicMonad
import Language.VintageBasic.Result
import Language.VintageBasic.Printer(printFloat)
import Language.VintageBasic.RuntimeParser(dataValsP,readFloat,trim)
import Language.VintageBasic.Syntax
import Text.ParserCombinators.Parsec(parse)
import Text.ParserCombinators.Parsec.Pos(sourceLine)

-- | Entry in the jump table, a lookup table that holds both
-- program code and @DATA@ statements, indexed by 'Label'.
data JumpTableEntry = JumpTableEntry {
    jtLabel :: Label,     -- ^ the starting line number
    jtProgram :: Program, -- ^ program source starting at that line
    jtData :: [String]    -- ^ @DATA@ strings starting at that line
}

type JumpTable = [JumpTableEntry]

programLookup :: JumpTable -> Label -> (Maybe Program)
programLookup jt lab = lookup lab [(jtLabel jte, jtProgram jte) | jte <- jt]

dataLookup :: JumpTable -> Label -> (Maybe [String])
dataLookup jt lab = lookup lab [(jtLabel jte, jtData jte) | jte <- jt]

-- | Lazily interprets a list of BASIC lines of code.

interpLines :: [Line] -> Program

-- Note that jumpTable and interpLine are mutually recursive.
-- The jumpTable contains interpreted code, which in turn calls
-- the jumpTable to look up code.  Since the jumpTable is a single
-- data structure, it memoizes interpreted code, making 'interpLines'
-- a just-in-time compiler.  (The only time code is reinterpreted
-- is following an @IF@ statement.)

interpLines progLines =
    let interpLine line@(Line lab stmts) =
            (lab, mapM_ (interpTS jumpTable) stmts, dataFromLine line)
        makeTableEntry (accumCode, accumData) (lab, codeSeg, lineData) =
            let accumCode' = codeSeg >> accumCode
                accumData' = lineData ++ accumData
                in ((accumCode', accumData'), JumpTableEntry lab accumCode' accumData')
        jumpTable = snd $ mapAccumR makeTableEntry (done, []) $ map interpLine progLines
    in do
        case jumpTable of
            ((JumpTableEntry _ prog dat) : _) -> do
                seedRandomFromTime
                setDataStrings dat
                prog
            [] -> done

-- Expression evaluation

boolToVal :: Bool -> Val
boolToVal t = if t then FloatVal (-1) else FloatVal 0

isNext :: Result -> Bool
isNext (LabeledRuntimeException _ (Next Nothing)) = True
isNext _ = False

isNextVar :: VarName -> Result -> Bool
isNextVar (VarName FloatType v1) (LabeledRuntimeException _ (Next (Just v2))) = v1==v2
isNextVar _ _ = False

isReturn :: Result -> Bool
isReturn (LabeledRuntimeException _ Return) = True
isReturn _ = False

liftFVOp1 :: (Float -> Float) -> Val -> Code Val
liftFVOp1 f (FloatVal v1) = return $ FloatVal $ f v1
liftFVOp1 _ _             = typeMismatch

liftFVBuiltin1 :: (Float -> Float) -> [Val] -> Code Val
liftFVBuiltin1 f [FloatVal v1] = return $ FloatVal $ f v1
liftFVBuiltin1 _ [_] = typeMismatch
liftFVBuiltin1 _ _ = wrongNumArgs

liftFVOp2 :: (Float -> Float -> Float) -> Val -> Val -> Code Val
liftFVOp2 f (FloatVal v1) (FloatVal v2) = return $ FloatVal $ f v1 v2
liftFVOp2 _ _             _             = typeMismatch

liftSVOp2 :: (String -> String -> String) -> Val -> Val -> Code Val
liftSVOp2 f (StringVal v1) (StringVal v2) = return $ StringVal $ f v1 v2
liftSVOp2 _ _              _              = typeMismatch

liftFSCmpOp2 :: (forall a. Ord a => a -> a -> Bool) -> Val -> Val -> Code Val
liftFSCmpOp2 f v1 v2 = do
    assert (typeOf v1 == typeOf v2) TypeMismatchError
    return $ boolToVal $ f v1 v2

valError :: RuntimeError -> Code Val
-- The return (FloatVal 0) will never be executed, but is needed to make the types work out.
valError err = raiseRuntimeError err >> return (FloatVal 0)

wrongNumArgs, typeMismatch, invalidArgument, divisionByZero :: Code Val
wrongNumArgs    = valError WrongNumberOfArgumentsError
typeMismatch    = valError TypeMismatchError
invalidArgument = valError InvalidArgumentError
divisionByZero  = valError DivisionByZeroError

-- | Evaluate a BASIC expression.
eval :: Expr -> Code Val
eval (LitX (FloatLit v)) = return (FloatVal v)
eval (LitX (StringLit v)) = return (StringVal v)
eval (VarX var) = getVar var
eval (FnX var xs) = do
    fn <- getFn var
    vals <- mapM eval xs
    fn vals
eval (MinusX x) = do
    val <- eval x
    liftFVOp1 negate val
eval (NotX x) = do
    val <- eval x
    liftFVOp1 (\v -> if v==0 then -1 else 0) val
eval (BinX op x1 x2) = do
    v1 <- eval x1
    v2 <- eval x2
    evalBinOp op v1 v2
eval (BuiltinX b xs) = do
    vs <- mapM eval xs
    evalBuiltin b vs
eval NextZoneX = do
    curCol <- getOutputColumn
    let numSpaces = zoneWidth - (curCol `mod` zoneWidth)
    return $ StringVal $ replicate numSpaces ' '
eval EmptySeparatorX = return $ StringVal ""
eval (ParenX x) = eval x

-- | Evaluate an expression with a binary operator.
evalBinOp :: BinOp -> Val -> Val -> Code Val
evalBinOp op =
    case op of
        AddOp -> \v1 v2 ->
            case (v1,v2) of
                (FloatVal _,  FloatVal _ ) -> liftFVOp2 (+) v1 v2
                (StringVal _, StringVal _) -> liftSVOp2 (++) v1 v2
                (_,           _          ) -> typeMismatch
        SubOp -> liftFVOp2 (-)
        MulOp -> liftFVOp2 (*)
        DivOp -> \v1 v2 ->
            case (v1,v2) of
                (FloatVal fv1, FloatVal fv2) ->
                    if fv2==0
                        then divisionByZero
                        else return $ FloatVal $ fv1/fv2
                (_,_) -> typeMismatch
        PowOp -> liftFVOp2 (**)
        EqOp -> liftFSCmpOp2 (==)
        NEOp -> liftFSCmpOp2 (/=)
        LTOp -> liftFSCmpOp2 (<)
        LEOp -> liftFSCmpOp2 (<=)
        GTOp -> liftFSCmpOp2 (>)
        GEOp -> liftFSCmpOp2 (>=)
        AndOp -> liftFVOp2 $ \v1 v2 -> if v1/=0 && v2/=0 then v1 else 0
        OrOp -> liftFVOp2 $ \v1 v2 -> if v1/=0 then v1 else v2

checkArgTypes :: [ValType] -> [Val] -> Code ()
checkArgTypes types vals = do
    if length types == length vals
        then
            if and [t == typeOf v | t <- types | v <- vals]
                then return ()
                else typeMismatch >> return ()
        else
            wrongNumArgs >> return ()

-- | Evaluate an expression with a builtin function.
evalBuiltin :: Builtin -> [Val] -> Code Val
evalBuiltin builtin args = case builtin of
    AbsBI -> liftFVBuiltin1 abs args
    AscBI -> do
        checkArgTypes [StringType] args
        let [StringVal v] = args
        if length v == 0
            then invalidArgument
            else return $ FloatVal (fromIntegral (fromEnum (head v)))
    AtnBI -> liftFVBuiltin1 atan args
    ChrBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal v] = args
        let iv = floatToInt v
        if iv < 0 || iv > 255
            then invalidArgument
            else return $ StringVal [toEnum iv]
    CosBI -> liftFVBuiltin1 cos args
    ExpBI -> liftFVBuiltin1 exp args
    IntBI -> liftFVBuiltin1 (fromIntegral . floatToInt) args
    LeftBI -> do
        checkArgTypes [StringType, FloatType] args
        let [StringVal sv, FloatVal fv] = args
        let iv = floatToInt fv
        if iv < 0
            then invalidArgument
            else return (StringVal (take iv sv))
    LenBI -> do
        checkArgTypes [StringType] args
        let [StringVal sv] = args in return (FloatVal (fromIntegral (length sv)))
    LogBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args in if fv <= 0 then invalidArgument else return (FloatVal (log fv))
    MidBI -> case args of
        [StringVal sv, FloatVal fv] ->
            let iv = floatToInt fv in
                if iv < 1
                    then invalidArgument
                    else return (StringVal (drop (iv-1) sv))
        [_, _] -> typeMismatch
        [StringVal sv, FloatVal fv1, FloatVal fv2] ->
            let iv1 = floatToInt fv1
                iv2 = floatToInt fv2
            in
                if iv1 < 1 || iv2 < 0
                    then invalidArgument
                    else return (StringVal (take iv2 (drop (iv1-1) sv)))
        [_, _, _] -> typeMismatch
        _ -> wrongNumArgs
    RightBI -> do
        checkArgTypes [StringType, FloatType] args
        let [StringVal sv, FloatVal fv] = args
        let iv = floatToInt fv
        if iv < 0
            then invalidArgument
            else return (StringVal (drop (length sv - iv) sv))
    RndBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args
        let iv = floatToInt fv
        if iv < 0
            then seedRandom iv
            else return ()
        rv <- if (iv == 0) then getPrevRandom else getRandom
        return (FloatVal rv)
    SgnBI -> liftFVBuiltin1 (\v -> if v < 0 then -1 else if v > 0 then 1 else 0) args
    SinBI -> liftFVBuiltin1 sin args
    SpcBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args
        let iv = floatToInt fv
        if iv < 0
           then invalidArgument
           else return (StringVal (replicate iv ' '))
    SqrBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args
        if fv < 0
            then invalidArgument
            else return (FloatVal (sqrt fv))
    StrBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args in return (StringVal (showVal (FloatVal fv)))
    TabBI -> do
        checkArgTypes [FloatType] args
        let [FloatVal fv] = args
        let destCol = floatToInt fv
        if (destCol < 0)
          then invalidArgument
          else do
            curCol <- getOutputColumn
            return $ StringVal $
                if curCol > destCol
                  then ""
                  else replicate (destCol - curCol) ' '
    TanBI -> liftFVBuiltin1 tan args
    ValBI -> do
        checkArgTypes [StringType] args
        let [StringVal sv] = args in return (FloatVal (maybe 0 id (readFloat (trim sv))))

-- | Interpret a tagged statement.
-- Sets the line number in the state, then passes the statement on to interpS.
interpTS :: JumpTable -> Tagged Statement -> Code ()
interpTS jumpTable (Tagged pos statement) = do
    setLineNumber (sourceLine pos)
    interpS jumpTable statement

-- | Interpret a single statement.
-- In the type of interpS, the first () signifies what is passed to a
-- resumed trap.  The second one represents what is returned by interpS.
interpS :: JumpTable -> Statement -> Code ()

interpS _ (RemS _) = return ()

interpS _ EndS = end

interpS _ StopS = end

interpS _ (DimS arrs) = mapM_ interpDim arrs

interpS _ (LetS var x) = do
    val <- eval x
    setVar var val

interpS _ (PrintS xs) = do
    mapM (\x -> eval x >>= printVal) xs
    if null xs || not (isPrintSeparator (last xs))
        then printString "\n"
        else return ()

interpS _ (InputS mPrompt vars) = do
    case mPrompt of
        Nothing -> return ()
        (Just ps) -> printString ps
    inputVars vars

interpS jumpTable (GotoS lab) = do
    let maybeCode = programLookup jumpTable lab
    assert (isJust maybeCode) (BadGotoTargetError lab)
    fromJust maybeCode >> end

interpS jumpTable (OnGotoS x labs) = interpComputed jumpTable GotoS x labs

interpS jumpTable (OnGosubS x labs) = interpComputed jumpTable GosubS x labs

interpS jumpTable (IfS x sts) = do
    v <- eval x
    fv <- extractFloatOrFail TypeMismatchError v
    if fv == 0
        then return ()
        else mapM_ (interpTS jumpTable) sts

-- Note that the loop condition isn't tested until a NEXT is reached.
-- This is an intentionally authentic feature.  In fact, were we to try to
-- test at the initial FOR, we wouldn't know which NEXT to jump to to skip
-- the loop - it is undecidable.
interpS _ (ForS control x1 x2 x3) = do
    assert (typeOf control == FloatType) TypeMismatchError
    v1 <- eval x1
    _ <- extractFloatOrFail TypeMismatchError v1
    setScalarVar control v1
    v2 <- eval x2
    lim <- extractFloatOrFail TypeMismatchError v2
    v3 <- eval x3
    step <- extractFloatOrFail TypeMismatchError v3
    trap $ \ x passOn resume continue ->
        if isNext x || isNextVar control x
            then do
                (FloatVal index) <- getScalarVar control
                let index' = index+step
                setScalarVar control (FloatVal index')
                if (step>=0 && index'<=lim)
                    || (step<0 && index'>=lim)
                    then continue True
                    else resume False
            else passOn True

interpS _ (NextS Nothing) = raiseRuntimeException (Next Nothing)
interpS _ (NextS (Just vars)) = mapM_ interpNextVar vars

interpS jumpTable (GosubS lab) =
    do let maybeCode = programLookup jumpTable lab
       assert (isJust maybeCode) (BadGosubTargetError lab)
       catchC gosubHandler (fromJust maybeCode)
       return ()
interpS _ ReturnS = raiseRuntimeException Return

interpS _ RandomizeS = seedRandomFromTime

interpS jumpTable (RestoreS maybeLab) = do
   case maybeLab of
       (Just lab) -> case dataLookup jumpTable lab of
           (Just ds) -> setDataStrings ds
           Nothing -> raiseRuntimeError (BadRestoreTargetError lab)
       Nothing -> if null jumpTable
           then return ()
           else setDataStrings (jtData (head jumpTable))

interpS _ (ReadS vars) = mapM_ interpRead vars

interpS _ (DataS _) = return ()

interpS _ (DefFnS vn params expr) = setFn vn $ \vals -> do
    assert (length params == length vals) WrongNumberOfArgumentsError
    assert
        (and [typeOf p == typeOf v | p <- params | v <- vals])
        TypeMismatchError
    stashedVals <- mapM getScalarVar params
    sequence_ $ zipWith setScalarVar params vals
    result <- eval expr
    sequence_ $ zipWith setScalarVar params stashedVals
    return result

gosubHandler :: BasicExceptionHandler
gosubHandler x passOn _ continue = if isReturn x then continue False else passOn True

-- | Interpret a computed @GOTO@ or @GOSUB@.
interpComputed :: JumpTable -> (Label -> Statement) -> Expr -> [Label] -> Code ()
interpComputed jumpTable cons x labs = do
    v <- eval x
    fv <- extractFloatOrFail TypeMismatchError v
    let i = floatToInt fv
    if i > 0 && i <= length labs
        then do
            let lab = labs !! (i-1)
            interpS jumpTable (cons lab)
        else
            return ()

-- | Interpret a @NEXT@ with a supplied variable.
interpNextVar :: VarName -> Code ()
interpNextVar (VarName FloatType v) = raiseRuntimeException (Next (Just v))
interpNextVar _ = raiseRuntimeError TypeMismatchError

-- | Execute an @INPUT@ statement with a list of input variables.
inputVars :: [Var] -> Code ()
inputVars vars = do
    printString "? "
    inText <- getString
    case parse dataValsP "" inText of
        (Right ivs) -> do
            let maybeVals = zipWith checkInput vars ivs
            if or (map isNothing maybeVals)
                then do
                    printString "!NUMBER EXPECTED - RETRY INPUT LINE\n"
                    inputVars vars
                else do
                    let vals = map fromJust maybeVals
                    sequence_ (zipWith setVar vars vals)
                    case compare (length vars) (length ivs) of
                        LT -> printString "!EXTRA INPUT IGNORED\n"
                        GT -> do
                            printString "?"
                            inputVars (drop (length vals) vars)
                        EQ -> return ()
        (Left _) -> error "Mismatched inputbuf in inputVars"

checkInput :: Var -> String -> Maybe Val
checkInput var s = case typeOf var of
    StringType -> Just (StringVal s)
    FloatType  -> case readFloat s of
        (Just v) -> Just (FloatVal v)
        _ -> Nothing
    IntType -> case readFloat s of
        (Just v) -> Just (IntVal (floatToInt v))
        _ -> Nothing

interpRead :: Var -> Code ()
interpRead var = do
    s <- readData
    case checkInput var s of
        Nothing -> raiseRuntimeError TypeMismatchError
        (Just val) -> setVar var val

getVar :: Var -> Code Val
getVar (ScalarVar vn) = do
    val <- getScalarVar vn
    coerceToExpression val
getVar (ArrVar vn xs) = do
    inds <- mapM eval xs
    is <- checkArrInds inds
    val <- getArrVar vn is
    coerceToExpression val

setVar :: Var -> Val -> Code ()
setVar (ScalarVar vn) val = do
    val' <- coerce vn val
    setScalarVar vn val'
setVar (ArrVar vn xs) val = do
    inds <- mapM eval xs
    is <- checkArrInds inds
    val' <- coerce vn val
    setArrVar vn is val'

-- | Coerce a value to the type it would have in an expression.
-- Specifically coerces ints to floats.
coerceToExpression :: Val -> Code Val
coerceToExpression val = case typeOf val of
    IntType -> coerce FloatType val
    _       -> return val

coerce :: Typeable a => a -> Val -> Code Val
coerce var val = case (typeOf var, val) of
    (IntType,    (FloatVal fv)) -> return $ IntVal (floatToInt fv)
    (FloatType,  (FloatVal _))  -> return val
    (FloatType,  (IntVal iv))   -> return $ FloatVal (fromIntegral iv)
    (IntType,    (IntVal _))    -> return val
    (StringType, (StringVal _)) -> return val
    (_,          _)             -> typeMismatch

interpDim :: (VarName, [Expr]) -> Code ()
interpDim (vn, xs) = do
    inds <- mapM eval xs
    is <- checkArrInds inds
    -- add 1, so that range is from 0 to user-specified bound
    let bounds = map (1+) is
    dimArray vn bounds
    return ()

checkArrInds :: [Val] -> Basic (BasicExcep Result ()) [Int]
checkArrInds indVals = do
    indFs <- mapM (extractFloatOrFail TypeMismatchError) indVals
    assert (and (map (>=0) indFs)) NegativeArrayDimError
    let inds = map floatToInt indFs
    return inds

showVal :: Val -> String
showVal (FloatVal fv) = printFloat fv
showVal (IntVal iv) = if iv > 0 then " " else "" ++ show iv
showVal (StringVal s) = s

printVal :: Val -> Basic o ()
printVal v =
  printString (showVal v ++ case v of
      (FloatVal _)  -> " "
      (IntVal _)    -> " "
      (StringVal _) -> ""
  )

-- | Extract the @DATA@ strings from a program line.
dataFromLine :: Line -> [String]
dataFromLine (Line _ stmts) = concat (map (dataFromStatement . getTaggedVal) stmts)

dataFromStatement :: Statement -> [String]
dataFromStatement (DataS s) =
    let (Right ss) = parse dataValsP "" s
    in ss
dataFromStatement _ = []