rearrange it all
Getty Ritter
2 years ago
| 13 | 13 | import qualified System.Console.GetOpt as Opt |
| 14 | 14 | import qualified System.Environment as Sys |
| 15 | 15 | |
| 16 | data Options = Options | |
| 17 | { optInputFile :: Maybe String, | |
| 18 | optUseDefaultTags :: Bool, | |
| 19 | optTagFile :: Maybe String | |
| 20 | } | |
| 21 | deriving (Eq, Show) | |
| 22 | ||
| 23 | opts :: [Opt.OptDescr (Options -> Options)] | |
| 24 | opts = | |
| 25 | [ Opt.Option | |
| 26 | ['n'] | |
| 27 | ["no-default-tags"] | |
| 28 | (Opt.NoArg (\o -> o {optUseDefaultTags = False})) | |
| 29 | "Do not include any default tags", | |
| 30 | Opt.Option | |
| 31 | ['t'] | |
| 32 | ["tags"] | |
| 33 | (Opt.ReqArg (\f o -> o {optTagFile = Just f}) "[file]") | |
| 34 | "The file of tag definitions to use" | |
| 35 | ] | |
| 36 | ||
| 37 | parseOpts :: IO Options | |
| 38 | parseOpts = do | |
| 39 | args <- Sys.getArgs | |
| 40 | let def = | |
| 41 | Options | |
| 42 | { optInputFile = Nothing, | |
| 43 | optUseDefaultTags = True, | |
| 44 | optTagFile = Nothing | |
| 45 | } | |
| 46 | case Opt.getOpt Opt.Permute opts args of | |
| 47 | (flags, [], []) -> | |
| 48 | return (foldl (flip id) def flags) | |
| 49 | (flags, [input], []) -> | |
| 50 | return (foldl (flip id) def flags) {optInputFile = Just input} | |
| 51 | (_, _, errors) -> | |
| 52 | error (unlines errors) | |
| 16 | -- | The main driver | |
| 17 | main :: IO () | |
| 18 | main = do | |
| 19 | -- parse command-line options | |
| 20 | options <- parseOpts | |
| 21 | -- read the source file | |
| 22 | telmlSource <- case optInputFile options of | |
| 23 | Nothing -> getContents | |
| 24 | Just f -> readFile f | |
| 25 | -- attempt to parse it | |
| 26 | let telml = case TeLML.parse telmlSource of | |
| 27 | Right str -> str | |
| 28 | Left err -> error err | |
| 29 | -- read the Lua source file, if provided | |
| 30 | luaSource <- case optTagFile options of | |
| 31 | Nothing -> return "" | |
| 32 | Just f -> BS.readFile f | |
| 33 | -- run everything needed in the Lua context (i.e. evaluating the | |
| 34 | -- source and then using it to interpret tags) | |
| 35 | result <- Lua.runEither (luaMain options luaSource telml) | |
| 36 | -- either print the result or print the error nicely | |
| 37 | case result of | |
| 38 | Right msg -> Text.putStr msg | |
| 39 | Left err -> putStrLn (Exn.displayException err) | |
| 40 | ||
| 41 | -- * Lua stuff | |
| 42 | ||
| 43 | -- | Everything in Lua should be in this monad, which includes our | |
| 44 | -- custom error type | |
| 45 | type LuaM r = Lua.LuaE Error r | |
| 46 | ||
| 47 | -- | Evaluate the provided Lua source code and then use it to | |
| 48 | -- interpret the `TeLML.Document`. | |
| 49 | luaMain :: Options -> BS.ByteString -> TeLML.Document -> LuaM Text.Text | |
| 50 | luaMain opts luaSource doc = do | |
| 51 | -- load the basic libraries so we have access to stuff like `ipairs` | |
| 52 | Lua.openbase | |
| 53 | Lua.pop 1 | |
| 54 | ||
| 55 | -- create the global `telml` table as a namespace for tags | |
| 56 | Lua.newtable | |
| 57 | Lua.setglobal "telml" | |
| 58 | ||
| 59 | -- evaluate the source file. (We don't care what it evaluates to.) | |
| 60 | _ <- Lua.dostring luaSource | |
| 61 | ||
| 62 | -- make sure that the user didn't do something funky like redefine | |
| 63 | -- the global `telml` to a string. | |
| 64 | telml <- Lua.getglobal "telml" | |
| 65 | if telml /= Lua.TypeTable | |
| 66 | then throw (RedefinedTable telml) | |
| 67 | else return () | |
| 68 | ||
| 69 | -- walk over the document, evaluating as we go | |
| 70 | handleDoc opts doc | |
| 71 | ||
| 72 | -- | Convert a `TeLML.Document` into a piece of `Text` | |
| 73 | handleDoc :: Options -> TeLML.Document -> LuaM Text.Text | |
| 74 | handleDoc opts = fmap mconcat . sequence . map (handleFrag opts) | |
| 75 | ||
| 76 | -- | Convert a `TeLML.Fragment` into a piece of `Text` with the | |
| 77 | -- relevant tag evaluation | |
| 78 | handleFrag :: Options -> TeLML.Fragment -> LuaM Text.Text | |
| 79 | handleFrag _ (TeLML.TextFrag text) = return text | |
| 80 | handleFrag opts (TeLML.TagFrag tag) = handleTag opts tag | |
| 81 | ||
| 82 | -- | Evaluate a tag in light of both the Lua source and the provided | |
| 83 | -- options | |
| 84 | handleTag :: Options -> TeLML.Tag -> LuaM Text.Text | |
| 85 | handleTag opts (TeLML.Tag n ps) = do | |
| 86 | -- evaluate the "arguments" first | |
| 87 | ps' <- mapM (handleDoc opts) ps | |
| 88 | -- look up the tag in the table | |
| 89 | Lua.pushstring (Text.encodeUtf8 n) | |
| 90 | -- check the type of the thing we've gotten out. (If it wasn't | |
| 91 | -- present in the table, we'll get `nil`.) | |
| 92 | typ <- Lua.gettable 1 | |
| 93 | case typ of | |
| 94 | Lua.TypeNil | |
| 95 | -- Defer to the standard tags by default | |
| 96 | | optUseDefaultTags opts -> standardTags n ps' | |
| 97 | -- ...but if the user opted out, then throw errors | |
| 98 | | otherwise -> throw (NoSuchTag n) | |
| 99 | -- if it's a function, then we can call it! | |
| 100 | Lua.TypeFunction -> do | |
| 101 | -- it's already on the stack, so now we need to add all the | |
| 102 | -- arguments to the stack. They're all strings, so push the | |
| 103 | -- appropriate bytestrings there | |
| 104 | mapM_ (Lua.pushstring . Text.encodeUtf8) ps' | |
| 105 | -- Call the function with the number of args we've passed, and | |
| 106 | -- expect a single return value | |
| 107 | Lua.call (Lua.NumArgs (fromIntegral (length ps'))) 1 | |
| 108 | -- look at the top thing on the stack to make sure it's a string | |
| 109 | -- (or convertible) | |
| 110 | result <- Lua.tostring 2 | |
| 111 | case result of | |
| 112 | -- if we got `Nothing`, then it's not a string; throw an error | |
| 113 | Nothing -> do | |
| 114 | actualtyp <- Lua.ltype 2 | |
| 115 | throw (NotAString n actualtyp) | |
| 116 | -- otherwise, it's a string, so pass it back down! | |
| 117 | Just r -> do | |
| 118 | Lua.pop 1 | |
| 119 | return (Text.decodeUtf8 r) | |
| 120 | -- if it's not `nil` _or_ a function, then produce an error about | |
| 121 | -- it | |
| 122 | _ -> throw (NotAFunction n typ) | |
| 123 | ||
| 124 | -- * Errors and error-handling | |
| 125 | ||
| 126 | -- We wrap the usual LuaHS error type in our own | |
| 53 | 127 | |
| 54 | 128 | data Error |
| 55 | 129 | = LuaError Lua.Exception |
| 57 | 131 | deriving (Show) |
| 58 | 132 | |
| 59 | 133 | instance Exn.Exception Error where |
| 134 | -- did you know this tries to use `show` by default? I really wish | |
| 135 | -- Haskell had something like the str/repr distinction or | |
| 136 | -- debug/display distinction. Trying to force every string | |
| 137 | -- representation into `show` is nonsense. | |
| 60 | 138 | displayException (LuaError err) = Exn.displayException err |
| 61 | 139 | displayException (TeLMLError err) = Exn.displayException err |
| 62 | 140 | |
| 141 | -- We need this in order to use our custom error type as a wrapper | |
| 142 | -- around Lua ones | |
| 63 | 143 | instance Lua.LuaError Error where |
| 64 | 144 | popException = do |
| 65 | 145 | err <- Lua.changeErrorType Lua.popException |
| 73 | 153 | |
| 74 | 154 | luaException = LuaError . Lua.luaException |
| 75 | 155 | |
| 76 | type LuaM r = Lua.LuaE Error r | |
| 77 | ||
| 156 | -- | A function that makes it easy to throw our own exceptions | |
| 78 | 157 | throw :: Exn.Exception e => e -> LuaM a |
| 79 | 158 | throw = |
| 80 | 159 | Lua.liftIO . Exn.throwIO . TeLMLError . Exn.toException |
| 81 | 160 | |
| 82 | main :: IO () | |
| 83 | main = do | |
| 84 | options <- parseOpts | |
| 85 | telmlSource <- case optInputFile options of | |
| 86 | Nothing -> getContents | |
| 87 | Just f -> readFile f | |
| 88 | let telml = case TeLML.parse telmlSource of | |
| 89 | Right str -> str | |
| 90 | Left err -> error err | |
| 91 | luaSource <- case optTagFile options of | |
| 92 | Nothing -> return "" | |
| 93 | Just f -> BS.readFile f | |
| 94 | result <- Lua.runEither (luaMain luaSource telml) | |
| 95 | case result of | |
| 96 | Right msg -> Text.putStr msg | |
| 97 | Left err -> putStrLn (Exn.displayException err) | |
| 98 | return () | |
| 99 | ||
| 100 | luaMain :: BS.ByteString -> TeLML.Document -> LuaM Text.Text | |
| 101 | luaMain luaSource doc = do | |
| 102 | Lua.openbase | |
| 103 | Lua.pop 1 | |
| 104 | ||
| 105 | Lua.newtable | |
| 106 | Lua.setglobal "telml" | |
| 107 | _ <- Lua.dostring luaSource | |
| 108 | telml <- Lua.getglobal "telml" | |
| 109 | if telml /= Lua.TypeTable | |
| 110 | then throw (RedefinedTable telml) | |
| 111 | else return () | |
| 112 | handleDoc doc | |
| 113 | ||
| 161 | -- ** Custom errors | |
| 162 | ||
| 163 | -- | This represents when a builtin tag expects a specific arity and | |
| 164 | -- we invoke it with the wrong one | |
| 114 | 165 | data BuiltinArityMismatch = BuiltinArityMismatch |
| 115 | 166 | { bamExpected :: Int, |
| 116 | 167 | bamProvided :: Int, |
| 129 | 180 | show (bamProvided bam) |
| 130 | 181 | ] |
| 131 | 182 | |
| 183 | -- | This is thrown when we can't find any tag with the relevant name | |
| 132 | 184 | data NoSuchTag = NoSuchTag {nstName :: Text.Text} deriving (Show) |
| 133 | 185 | |
| 134 | 186 | instance Exn.Exception NoSuchTag where |
| 135 | 187 | displayException nst = |
| 136 | 188 | "No such tag: `" ++ Text.unpack (nstName nst) ++ "`" |
| 137 | 189 | |
| 190 | -- | This is thrown when the Lua defined something with this name, but | |
| 191 | -- it wasn't actually a function we could call | |
| 138 | 192 | data NotAFunction = NotAFunction |
| 139 | 193 | {nafName :: Text.Text, nafActual :: Lua.Type} |
| 140 | 194 | deriving (Show) |
| 149 | 203 | " instead" |
| 150 | 204 | ] |
| 151 | 205 | |
| 206 | -- | This is thrown when we call a Lua function and it returns | |
| 207 | -- something which either isn't a string or isn't trivially | |
| 208 | -- convertable to a string (like a number). | |
| 152 | 209 | data NotAString = NotAString |
| 153 | 210 | {nasName :: Text.Text, nasActual :: Lua.Type} |
| 154 | 211 | deriving (Show) |
| 163 | 220 | " instead" |
| 164 | 221 | ] |
| 165 | 222 | |
| 223 | -- | This is thrown if the code for some reason tries to redefine | |
| 224 | -- `telml` into something that's not a table. | |
| 166 | 225 | data RedefinedTable = RedefinedTable |
| 167 | 226 | {rtType :: Lua.Type} |
| 168 | 227 | deriving (Show) |
| 175 | 234 | " instead" |
| 176 | 235 | ] |
| 177 | 236 | |
| 237 | -- | Print a Lua type nicely (for error message purposes) | |
| 178 | 238 | ppType :: Lua.Type -> String |
| 179 | 239 | ppType Lua.TypeNil = "nil" |
| 180 | 240 | ppType Lua.TypeBoolean = "boolean" |
| 187 | 247 | ppType Lua.TypeThread = "thread" |
| 188 | 248 | ppType Lua.TypeNone = "something unspeakable" |
| 189 | 249 | |
| 250 | -- * Tag definitions | |
| 251 | ||
| 252 | -- | Try to interpret this tag from the standard set, throwing an | |
| 253 | -- error if it doesn't exist | |
| 190 | 254 | standardTags :: Text.Text -> [Text.Text] -> LuaM Text.Text |
| 191 | 255 | standardTags n ps = |
| 192 | 256 | case n of |
| 224 | 288 | simpleTag _ [item] result = pure (result item) |
| 225 | 289 | simpleTag name items _ = throw (BuiltinArityMismatch 1 (length items) name) |
| 226 | 290 | |
| 227 | handleDoc :: TeLML.Document -> LuaM Text.Text | |
| 228 | handleDoc = fmap mconcat . sequence . map handleFrag | |
| 229 | ||
| 230 | handleFrag :: TeLML.Fragment -> LuaM Text.Text | |
| 231 | handleFrag (TeLML.TextFrag text) = return text | |
| 232 | handleFrag (TeLML.TagFrag tag) = handleTag tag | |
| 233 | ||
| 234 | handleTag :: TeLML.Tag -> LuaM Text.Text | |
| 235 | handleTag (TeLML.Tag n ps) = do | |
| 236 | -- evaluate the "arguments" first | |
| 237 | ps' <- mapM handleDoc ps | |
| 238 | Lua.pushstring (Text.encodeUtf8 n) | |
| 239 | typ <- Lua.gettable 1 | |
| 240 | case typ of | |
| 241 | Lua.TypeNil -> standardTags n ps' | |
| 242 | Lua.TypeFunction -> do | |
| 243 | mapM_ (Lua.pushstring . Text.encodeUtf8) ps' | |
| 244 | Lua.call (Lua.NumArgs (fromIntegral (length ps'))) 1 | |
| 245 | result <- Lua.tostring 2 | |
| 246 | case result of | |
| 247 | Nothing -> do | |
| 248 | actualtyp <- Lua.ltype 2 | |
| 249 | throw (NotAString n actualtyp) | |
| 250 | Just r -> do | |
| 251 | Lua.pop 1 | |
| 252 | return (Text.decodeUtf8 r) | |
| 253 | _ -> throw (NotAFunction n typ) | |
| 291 | -- * Options and option-parsing | |
| 292 | ||
| 293 | data Options = Options | |
| 294 | { optInputFile :: Maybe String, | |
| 295 | optUseDefaultTags :: Bool, | |
| 296 | optTagFile :: Maybe String | |
| 297 | } | |
| 298 | deriving (Eq, Show) | |
| 299 | ||
| 300 | optionDescriptions :: [Opt.OptDescr (Options -> Options)] | |
| 301 | optionDescriptions = | |
| 302 | [ Opt.Option | |
| 303 | ['n'] | |
| 304 | ["no-default-tags"] | |
| 305 | (Opt.NoArg (\o -> o {optUseDefaultTags = False})) | |
| 306 | "Do not include any default tags", | |
| 307 | Opt.Option | |
| 308 | ['t'] | |
| 309 | ["tags"] | |
| 310 | (Opt.ReqArg (\f o -> o {optTagFile = Just f}) "[file]") | |
| 311 | "The file of tag definitions to use" | |
| 312 | ] | |
| 313 | ||
| 314 | parseOpts :: IO Options | |
| 315 | parseOpts = do | |
| 316 | args <- Sys.getArgs | |
| 317 | let def = | |
| 318 | Options | |
| 319 | { optInputFile = Nothing, | |
| 320 | optUseDefaultTags = True, | |
| 321 | optTagFile = Nothing | |
| 322 | } | |
| 323 | case Opt.getOpt Opt.Permute optionDescriptions args of | |
| 324 | (flags, [], []) -> | |
| 325 | return (foldl (flip id) def flags) | |
| 326 | (flags, [input], []) -> | |
| 327 | return (foldl (flip id) def flags) {optInputFile = Just input} | |
| 328 | (_, _, errors) -> | |
| 329 | error (unlines errors) | |