A cunning interpreter for the pure untyped λ-calculus.
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