{-# 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 _ = []