A cunning interpreter for the pure untyped λ-calculus.
at master 164 lines 5.4 kB view raw
1-- SPDX-FileCopyrightText: 2022 Severen Redwood <sev@severen.dev> 2-- SPDX-License-Identifier: GPL-3.0-or-later 3 4module Main (main) where 5 6import Control.Applicative (optional, (<**>)) 7import Control.Monad (unless) 8import Control.Monad.Trans.Class (lift) 9import Data.List (isPrefixOf, stripPrefix) 10import Data.String.Interpolate (i) 11import Data.Text qualified as T 12import Data.Version (showVersion) 13import Effectful (Eff, IOE, MonadIO, liftIO, runEff, (:>)) 14import Effectful.State.Static.Local (State, evalState, get, modify, put) 15import Options.Applicative 16 ( execParser, 17 fullDesc, 18 header, 19 help, 20 helper, 21 info, 22 long, 23 metavar, 24 short, 25 strArgument, 26 switch, 27 ) 28import Paths_sly qualified as P (version) 29import Sly.Eval 30 ( Bindings, 31 Program (Program, bindings, terms), 32 fileToProgram, 33 runProgram, 34 stringToProgram, 35 ) 36import Sly.Syntax (Name (..), astShow) 37import System.Console.Haskeline 38 ( InputT, 39 defaultSettings, 40 getInputLine, 41 runInputT, 42 ) 43import System.Random (initStdGen, uniformR) 44import Text.Megaparsec (errorBundlePretty) 45 46data Options = Options {file :: Maybe String, version :: Bool} 47 48versionString :: String 49versionString = "sly v" <> showVersion P.version 50 51-- NOTE: This should not conflict with valid program syntax. 52 53-- | The prefix used for REPL commands. 54commandPrefix :: String 55commandPrefix = ":" 56 57outputStr :: (MonadIO m) => String -> m () 58outputStr = liftIO . putStr 59 60outputStrLn :: (MonadIO m) => String -> m () 61outputStrLn = liftIO . putStrLn 62 63main :: IO () 64main = (runEff . run) =<< execParser opts 65 where 66 opts = 67 info 68 (parser <**> helper) 69 (fullDesc <> header "sly - An interpreter for the pure untyped λ-calculus.") 70 parser = do 71 file <- optional $ strArgument (metavar "FILE") 72 version <- 73 switch 74 (long "version" <> short 'V' <> help "Print version and exit") 75 pure Options {..} 76 77run :: (IOE :> es) => Options -> Eff es () 78run opts = do 79 if opts.version 80 then outputStrLn versionString 81 else maybe repl runFile opts.file 82 83runFile :: (IOE :> es) => String -> Eff es () 84runFile path = do 85 result <- liftIO $ fileToProgram path 86 case result of 87 Left bundle -> outputStr (errorBundlePretty bundle) 88 Right program -> mapM_ (liftIO . print) (runProgram program) 89 90repl :: (IOE :> es) => Eff es () 91repl = do 92 let adjectives = ["cunning", "crafty", "guileful", "shrewd"] :: [String] 93 94 stdGen <- liftIO initStdGen 95 let (n, _) = uniformR (0, length adjectives - 1) stdGen 96 outputStrLn 97 [i|Welcome to #{versionString}, the #{adjectives !! n} λ-calculus interpreter!|] 98 outputStrLn "Type :quit or press C-d to exit." 99 100 evalState mempty $ runInputT defaultSettings loop 101 where 102 loop :: (State Bindings :> es, IOE :> es) => InputT (Eff es) () 103 loop = do 104 getInputLine "~> " >>= \case 105 Nothing -> return () 106 Just input 107 | commandPrefix `isPrefixOf` input -> do 108 shouldQuit <- lift $ runCommand (tail input) 109 unless shouldQuit loop 110 | otherwise -> do 111 case stringToProgram (T.pack input) of 112 Left bundle -> outputStr (errorBundlePretty bundle) 113 Right program -> do 114 lift $ modify (<> program.bindings) 115 bindings <- lift get 116 mapM_ (liftIO . print) $ 117 runProgram Program {bindings, terms = program.terms} 118 loop 119 120-- TODO: Refactor this to be less ad-hoc. 121 122-- | Run an interpreter command. 123runCommand :: (State Bindings :> es, IOE :> es) => String -> Eff es Bool 124runCommand input 125 | command `elem` ["q", "quit"] = return True 126 | command == "parse", 127 Just term <- stripPrefix "parse " input = do 128 case stringToProgram (T.pack term) of 129 Left bundle -> outputStr (errorBundlePretty bundle) 130 Right program -> mapM_ (outputStrLn . astShow) program.terms 131 return False 132 | command == "load", 133 Just filepath <- stripPrefix "load " input = do 134 result <- liftIO $ fileToProgram filepath 135 case result of 136 Left bundle -> outputStr (errorBundlePretty bundle) 137 Right program -> do 138 mapM_ (liftIO . print) (runProgram program) 139 modify (<> program.bindings) 140 return False 141 | command `elem` ["b", "bindings"] = do 142 let format (Name n, t) = T.unpack n <> " := " <> show t 143 bindings <- get 144 mapM_ (outputStrLn . format) bindings 145 return False 146 | command == "clear" = put mempty >> return False 147 -- TODO: Generate this help string more robustly. 148 | command `elem` ["?", "h", "help"] = do 149 outputStrLn $ 150 "Commands available from the prompt:\n" 151 <> " :parse <term> show the parse tree for <term> in bracketed form\n" 152 <> " :load, :l <path> load a sly program, evaluating its terms and adding\n" 153 <> " its bindings to the environment\n" 154 <> " :bindings, :b show all bindings in the environment\n" 155 <> " :clear clear the environment\n" 156 <> " :help, :? view this list of commands\n" 157 <> " :quit, :q exit sly" 158 return False 159 | otherwise = do 160 outputStrLn $ "Unrecognised REPL command: " <> command 161 outputStrLn "Use :? for help." 162 return False 163 where 164 command = head $ words input