From f322b738747b1aef0f6443ab384c11d14ae9cb6c Mon Sep 17 00:00:00 2001 From: Murray Date: Thu, 14 Jun 2018 16:42:14 +0100 Subject: [PATCH] init --- .gitignore | 1 + Bin.hs | 102 +++++++++++++++++++++++++++ Diag.hs | 118 +++++++++++++++++++++++++++++++ LICENSE | 13 ++++ Main.hs | 93 ++++++++++++++++++++++++ Schema.hs | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Type.hs | 157 +++++++++++++++++++++++++++++++++++++++++ baler.cabal | 17 +++++ stack.yaml | 3 + 9 files changed, 704 insertions(+) create mode 100644 .gitignore create mode 100644 Bin.hs create mode 100644 Diag.hs create mode 100644 LICENSE create mode 100644 Main.hs create mode 100644 Schema.hs create mode 100644 Type.hs create mode 100644 baler.cabal create mode 100644 stack.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ee1bf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.stack-work diff --git a/Bin.hs b/Bin.hs new file mode 100644 index 0000000..be9d1dd --- /dev/null +++ b/Bin.hs @@ -0,0 +1,102 @@ + +module Bin (encode, decode) where + +import Data.Word +import Data.Bits +import Data.Monoid +import Control.Monad (replicateM) +import qualified Data.ByteString.Lazy as B +import Data.Binary.Get +import Data.ByteString.Builder + +import Type + +encodeun :: Word64 -> Builder +encodeun v + | v <= 240 = w8 v + | v <= 2287 = let v' = v - 240 in w8 ((v' `shiftR` 8) + 241) <> w8 (v' .&. 0xff) + | v <= 67823 = let v' = v - 2288 in word8 249 <> w8 (v' `shiftR` 8) <> w8 (v' .&. 0xff) + | otherwise = words 0 v mempty + where + words :: Word8 -> Word64 -> Builder -> Builder + words l 0 b = word8 (247 + l) <> b + words l v' b = words (l + 1) (v' `shiftR` 8) (w8 (0xff .&. v') <> b) + + w8 = word8 . fromIntegral + +decodeun' :: Word8 -> Get Word64 +decodeun' a0 + | a0 <= 240 = return $ fromIntegral a0 + | a0 <= 248 = getWord8 >>= (\a1 -> return $ 240 + 256 * (fromIntegral a0 - 241) + fromIntegral a1) + | a0 == 249 = do + a1 <- getWord8 + a2 <- getWord8 + return $ 2288 + 256 * (fromIntegral a1) + (fromIntegral a2) + | otherwise = getBytes (a0 - 247) 0 + where + getBytes :: Word8 -> Word64 -> Get Word64 + getBytes 0 n = return n + getBytes l n = getWord8 >>= (\b -> getBytes (l-1) ((n `shiftL` 8) + (fromIntegral b))) + +decodeun :: Get Word64 +decodeun = getWord8 >>= decodeun' + +encode' :: TypeV -> Builder +encode' (U8v w) = word8 w +encode' (U16v w) = word16BE w +encode' (U32v w) = word32BE w +encode' (U64v w) = word64BE w +encode' (I8v i) = int8 i +encode' (I16v i) = int16BE i +encode' (I32v i) = int32BE i +encode' (I64v i) = int64BE i +encode' (F32v f) = floatBE f +encode' (F64v d) = doubleBE d +encode' (UVv w) = encodeun w +encode' (Tuplev x) = foldMap (encode' . snd) x +encode' (Unionv _ n x) = encode' (UVv n) + <> encode' x +encode' (Arrayv x) = encode' (UVv $ fromIntegral $ length x) + <> foldMap encode' x + +encode :: TypeV -> B.ByteString +encode = toLazyByteString . encode' + +nth :: [a] -> Word64 -> Maybe a +nth (x:_) 0 = Just x +nth (_:xs) n = nth xs (n - 1) +nth _ _ = Nothing + +dec :: (Maybe String, Raw) -> Get (Maybe String, TypeV) +dec (a, b) = (,) a <$> decode' b + +decode' :: Raw -> Get TypeV +decode' U8 = U8v <$> getWord8 +decode' U16 = U16v <$> getWord16be +decode' U32 = U32v <$> getWord32be +decode' U64 = U64v <$> getWord64be +decode' I8 = I8v <$> getInt8 +decode' I16 = I16v <$> getInt16be +decode' I32 = I32v <$> getInt32be +decode' I64 = I64v <$> getInt64be +decode' F32 = F32v <$> getFloatbe +decode' F64 = F64v <$> getDoublebe +decode' UV = UVv <$> decodeun +decode' (NameR _) = undefined +decode' (TupleR t) = Tuplev <$> (mapM dec t) +decode' (UnionR u) = do + n <- decodeun + case u `nth` n of + Nothing -> fail "union index out of bounds" + Just (annotation, r) -> Unionv annotation n <$> decode' r +decode' (ArrayR a) = Arrayv <$> + (decodeun >>= return . fromIntegral >>= + flip replicateM (decode' a)) + +decode :: Raw -> B.ByteString -> Either String TypeV +decode spec bs = + case runGetOrFail (decode' spec) bs of + Right (i, _, t) -> if B.null i + then Right t + else Left "too much input" + Left (_, _, e) -> Left e diff --git a/Diag.hs b/Diag.hs new file mode 100644 index 0000000..2166582 --- /dev/null +++ b/Diag.hs @@ -0,0 +1,118 @@ + +module Diag (diag) where + +import Data.Word +import Codec.Binary.UTF8.String as UTF8 + +import Text.Parsec +import Text.Parsec.String + +import Type + +valid :: (Num a) => (Int -> Integer -> Bool) -> String -> Int -> String -> Parser a +valid out kind bits s = + let n = read s in + if out bits n + then fail $ concat [ s, + " does not fit in ", kind, + " number of ", show bits, + " bits" + ] + else return $ fromIntegral n + +tonat :: (Num a) => Int -> String -> Parser a +tonat = valid uout "an unsigned" + +toint :: (Num a) => Int -> String -> Parser a +toint = valid iout "a signed" + +uout :: Int -> Integer -> Bool +uout bits n = n >= 2 ^ bits || n < 0 + +iout :: Int -> Integer -> Bool +iout bits n = n >= 2 ^ (bits-1) || n < - (2 ^ (bits-1)) + +size :: Bool -> String -> Parser TypeV +size validInt n = char '\'' >> if validInt + then unsigned <|> signed <|> floating + else floating + where + unsigned = char 'u' >> choice [ + char '8' >> U8v <$> tonat 8 n, + string "16" >> U16v <$> tonat 16 n, + string "32" >> U32v <$> tonat 32 n, + string "64" >> U64v <$> tonat 64 n + ] + signed = char 'i' >> choice [ + char '8' >> I8v <$> toint 8 n, + string "16" >> I16v <$> toint 16 n, + string "32" >> I32v <$> toint 32 n, + string "64" >> I64v <$> toint 64 n + ] + floating = char 'f' >> choice [ + string "32" >> return (F32v (read n)), + string "64" >> return (F64v (read n)) + ] + +num :: Parser TypeV +num = do + i <- integer "a number" + s <- suffix + case s of + "" -> size True i <|> (UVv <$> tonat 64 i) + s' -> let f = i ++ s' in (size False f) + where + (<:>) a b = (:) <$> a <*> b + digits = many1 digit + plus = char '+' >> digits + minus = char '-' <:> digits + integer = plus <|> minus <|> digits + suffix = (++) <$> d <*> e + d = option "" $ char '.' <:> digits + e = option "" $ oneOf "eE" <:> integer + +ws :: Parser () +ws = spaces "" + +annotation :: Parser (Maybe String) +annotation = optionMaybe (try note) "an annotation" + where + word = many1 $ alphaNum <|> oneOf "._-<>?!" + note = word <* char ':' <* ws + +tuple :: Parser TypeV +tuple = Tuplev <$> (open >> many pair <* close) + where + pair = (,) <$> annotation <*> diag' + open = char '{' <* ws + close = char '}' <* ws + +union :: Word64 -> Parser TypeV +union n = char '@' >> ws >> Unionv Nothing n <$> diag' + +unionum :: Parser TypeV +unionum = do + n <- num <* ws + case n of + UVv n' -> union n' <|> return (UVv n') + _ -> return n + +array :: Parser TypeV +array = Arrayv <$> (open >> many diag' <* close) + where + open = char '[' <* ws + close = char ']' <* ws + +str :: Parser TypeV +str = Arrayv . f <$> str' + where + open = char '"' + close = char '"' <* ws + str' = open >> many (noneOf "\"\n") <* close + f s = U8v <$> UTF8.encode s + +diag' :: Parser TypeV +diag' = choice [ unionum, tuple, array, str ] + +diag :: String -> String -> Either ParseError TypeV +diag name input = parse (ws >> diag' <* eof) name input diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4eae954 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2018 Murray + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/Main.hs b/Main.hs new file mode 100644 index 0000000..05f07e9 --- /dev/null +++ b/Main.hs @@ -0,0 +1,93 @@ + +import System.Environment +import System.Exit + +import Control.Monad + +import qualified Data.ByteString.Lazy as B + +import Schema +import Diag +import Bin + +readFrom :: String -> IO String +readFrom "-" = getContents +readFrom f = readFile f + +bytesFrom :: FilePath -> IO B.ByteString +bytesFrom "-" = B.getContents +bytesFrom f = B.readFile f + +arg :: String -> String -> [String] -> String +arg _ "" [a] = a +arg def switch (s:v:args') + | switch == s = v + | otherwise = arg def switch args' +arg def _ _ = def + +display :: String -> String +display "-" = "stdin" +display s = s + +check :: [String] -> IO () +check args = + let input = arg "-" "" args + incdir = arg "." "-i" args in do + str <- readFrom input + r <- schema incdir (display input) str + case r of + Left e -> die (show e) + Right s -> print s + +bin2diag :: [String] -> IO () +bin2diag args = + let schemaFile = arg "-" "-s" args + incdir = arg "." "-i" args + input = arg "-" "" args in do + when (schemaFile == input) $ + die "cannot read schema and data from the same file, see `help`" + str <- readFrom schemaFile + r <- schema incdir (display schemaFile) str + case r of + Left e -> die (show e) + Right s -> do + bs <- bytesFrom input + case decode s bs of + Left e -> die e + Right d -> print d + +diag2bin :: [String] -> IO () +diag2bin args = + let input = arg "-" "" args in do + str <- readFrom input + case diag (display input) str of + Left e -> die (show e) + Right d -> B.putStr $ encode d + +help :: IO () +help = do + prog <- getProgName + putStrLn $ "\ +\usage: \n\ +\ " ++ prog ++ " [-i dir] [file]\n\ +\ " ++ prog ++ " encode [file]\n\ +\ " ++ prog ++ " decode [-s file] [-i dir] [file]\n\ +\ " ++ prog ++ " help\n\ +\\n\ +\ -s file file containing the schema\n\ +\ -i dir include search path\n\ +\\n\ +\file always defaults to stdin" + +main :: IO () +main = do + args <- getArgs + case args of + ("encode":args') -> diag2bin args' + ("decode":args') -> bin2diag args' + ("help":_) -> help + ("-h":_) -> help + ("-help":_) -> help + ("--help":_) -> help + args' -> check args' + diff --git a/Schema.hs b/Schema.hs new file mode 100644 index 0000000..b256c45 --- /dev/null +++ b/Schema.hs @@ -0,0 +1,200 @@ + +module Schema (schema) where + +import Control.Monad +import Control.Monad.Trans + +import Data.Maybe +import Data.Char (isDigit) + +import Text.Parsec +import Text.Parsec.Pos + +import Data.HashMap.Strict as Map +import Data.HashSet as Set + +import Type + +type Includes = HashSet String +type Bindings = HashMap String ([String], Raw) +type Arguments = HashMap String Raw +type ParseState = (Includes, Bindings) +type Parser = ParsecT String ParseState IO + +afst :: (a -> c) -> (a, b) -> (c, b) +afst f (a, b) = (f a, b) + +asnd :: (b -> c) -> (a, b) -> (a, c) +asnd f (a, b) = (a, f b) + +putInclude :: String -> Parser () +putInclude s = modifyState (afst (Set.insert s)) + +putBinding :: String -> ([String], Raw) -> Parser () +putBinding s p = modifyState (asnd (Map.insert s p)) + +getIncludes :: Parser Includes +getIncludes = fst <$> getState + +fb :: Raw -> Parser (Maybe ([String], Raw)) +fb = return . Just . (,) [] + +fbx :: Raw -> Parser (Maybe ([String], Raw)) +fbx = return . Just . (,) ["x"] + +findBinding :: String -> Parser (Maybe ([String], Raw)) +findBinding "u8" = fb U8 +findBinding "u16" = fb U16 +findBinding "u32" = fb U32 +findBinding "u64" = fb U64 +findBinding "i8" = fb I8 +findBinding "i16" = fb I16 +findBinding "i32" = fb I32 +findBinding "i64" = fb I64 +findBinding "f32" = fb F32 +findBinding "f64" = fb F64 +findBinding "uv" = fb UV +findBinding "none" = fb $ UnionR [] +findBinding "void" = fb $ TupleR [] +findBinding "bool" = fb $ + UnionR [ + (Just "false", TupleR []), + (Just "true", TupleR []) + ] +findBinding "maybe" = fbx $ + UnionR [ + (Just "nothing", TupleR []), + (Just "just", NameR "x") + ] +findBinding "map" = + return . Just . (,) ["k", "v"] $ + ArrayR $ + TupleR [ + (Just "key", NameR "k"), + (Just "value", NameR "v") + ] +findBinding "string" = fb $ ArrayR U8 +findBinding "utf8" = fb $ ArrayR U8 +findBinding s = if all isDigit s + then fbx $ TupleR (replicate (read s) (Nothing, NameR "x")) + else Map.lookup s . snd <$> getState + +comment :: Parser () +comment = char ';' >> manyTill anyChar newline >> return () + +ignore' :: Parser () +ignore' = void space <|> comment "" + +ignore :: Parser () +ignore = skipMany ignore' + +ignore1 :: Parser () +ignore1 = skipMany1 ignore' + +lexeme :: Parser p -> Parser p +lexeme p = p <* ignore1 + +word :: Parser String +word = many1 $ alphaNum <|> oneOf "._-<>?!" + +annotation :: Parser String +annotation = lexeme (word <* char ':') "an annotation" + +keyword :: String -> Parser () +keyword = void . try . lexeme . string + +subst :: Arguments -> Raw -> Raw +subst args r = case r of + NameR n -> fromJust (Map.lookup n args) + TupleR t -> TupleR $ fmap subst' t + UnionR u -> UnionR $ fmap subst' u + ArrayR a -> ArrayR $ subst args a + base -> base + where + subst' (a, b) = (a, subst args b) + +typep :: Either [String] Arguments -> Parser Raw +typep names = choice [ + keyword "tuple" >> TupleR <$> block, + keyword "union" >> UnionR <$> block, + keyword "array" >> ArrayR <$> typep names, + flip label "a type name" $ do + n <- word + case names of + Left params -> + if elem n params + then ignore1 >> return (NameR n) + else beta n + Right args -> ignore1 >> return (fromJust $ Map.lookup n args) + ] + where + block = manyTill noted (keyword "end") + noted = (,) <$> optionMaybe (try $ annotation) <*> typep names + + beta n = do + b <- findBinding n + case b of + Nothing -> fail $ "could not find binding of " ++ n + Just (params, raw) -> do + ignore1 + args <- collect params Map.empty + return $ subst args raw + + collect [] m = return m + collect (p:ps) m = do + arg <- typep names + collect ps (Map.insert p arg m) + +binding :: Parser () +binding = do + keyword "let" + w <- word + b <- findBinding w + when (isJust b) $ + fail $ "binding " ++ w ++ " already exists" + ignore1 + p <- manyTill name (keyword "be") + t <- typep (Left p) + putBinding w (p, t) + where + name = lexeme word "a parameter" + +include :: String -> Parser () +include path = do + keyword "include" + f <- quoted + is <- getIncludes + when (Set.member f is) $ + fail "Recusive or duplicate file inclusion detected" + putInclude f + + oldInput <- getInput + oldPos <- getPosition + + setInput =<< (lift $ readFile (filepath f)) + setPosition $ initialPos f + context path >> eof + + setInput oldInput + setPosition oldPos + ignore1 + where + quoted = char '"' + >> many (noneOf "\"\n") + <* char '"' "a quoted filename" + filepath fn = path ++ "/" ++ fn + +context :: String -> Parser () +context path = ignore >> many (include path) >> many binding >> return () + +schema' :: String -> Parser Raw +schema' path = context path >> typep (Left []) <* eof + +schema :: String -> SourceName -> String -> IO (Either ParseError Raw) +schema path name input = + runParserT + (schema' path) + (Set.empty, Map.empty) + name + input + diff --git a/Type.hs b/Type.hs new file mode 100644 index 0000000..b1f97c3 --- /dev/null +++ b/Type.hs @@ -0,0 +1,157 @@ + +module Type where + +import Data.Word +import Data.Bits ((.|.), (.&.), shiftL) +import Data.Char (chr, isPrint) + +import Data.Int + +data Raw + = U8 | U16 | U32 | U64 + | I8 | I16 | I32 | I64 + | F32 | F64 + | UV + | NameR String + | TupleR [(Maybe String, Raw)] + | UnionR [(Maybe String, Raw)] + | ArrayR Raw + +ss :: String -> ShowS +ss = showString + +showIndent :: Int -> ShowS +showIndent i = ss (replicate (i * 2) ' ') + +showAnnotated :: Int -> (Int -> a -> ShowS) -> (Maybe String, a) -> ShowS +showAnnotated i f (Nothing, r) = f i r +showAnnotated i f (Just a, r) = ss a . ss ": " . f i r + +showItem :: Int -> (Int -> a -> ShowS) -> (Maybe String, a) -> ShowS +showItem i f p = showIndent i . showAnnotated i f p + +showBlock :: Int -> (Int -> a -> ShowS) + -> String -> [(Maybe String, a)] -> String + -> ShowS +showBlock _ _ start [] end = ss start . showChar ' ' . ss end +showBlock i f start l end = + let i' = i + 1 in + ss start . showChar '\n' + . foldl (\s p -> s . showItem i' f p . showChar '\n') (ss "") l + . showIndent i . ss end + +showType :: Int -> Raw -> ShowS +showType _ U8 = ss "u8" +showType _ U16 = ss "u16" +showType _ U32 = ss "u32" +showType _ U64 = ss "u64" +showType _ I8 = ss "i8" +showType _ I16 = ss "i16" +showType _ I32 = ss "i32" +showType _ I64 = ss "i64" +showType _ F32 = ss "f32" +showType _ F64 = ss "f64" +showType _ UV = ss "uv" +showType _ (NameR s) = ss s +showType i (TupleR l) = showBlock i showType "tuple" l "end" +showType i (UnionR l) = showBlock i showType "union" l "end" +showType i (ArrayR t) = ss "array\n" . showIndent (i + 1) . showType (i + 1) t + +instance Show Raw where + showsPrec _ a = showType 0 a + +data TypeV + = U8v Word8 | U16v Word16 | U32v Word32 | U64v Word64 + | I8v Int8 | I16v Int16 | I32v Int32 | I64v Int64 + | F32v Float | F64v Double + | UVv Word64 + | Tuplev [(Maybe String, TypeV)] + | Unionv (Maybe String) Word64 TypeV + | Arrayv [TypeV] + +sp :: (Show a) => a -> ShowS +sp = showsPrec 0 + +unv :: TypeV -> Word8 +unv (U8v v) = v +unv _ = undefined + +showArray :: Int -> [TypeV] -> ShowS +showArray i l = + let i' = i + 1 in + ss "[\n" + . foldl (\s v -> s . showIndent i' . showDiag i' v . showChar '\n') + (ss "") + l + . showIndent i . ss "]" + +showDiag :: Int -> TypeV -> ShowS +showDiag _ (U8v w) = sp w . ss "'u8" +showDiag _ (U16v w) = sp w . ss "'u16" +showDiag _ (U32v w) = sp w . ss "'u32" +showDiag _ (U64v w) = sp w . ss "'u64" +showDiag _ (I8v i) = sp i . ss "'i8" +showDiag _ (I16v i) = sp i . ss "'i16" +showDiag _ (I32v i) = sp i . ss "'i32" +showDiag _ (I64v i) = sp i . ss "'i64" +showDiag _ (F32v f) = sp f . ss "'f32" +showDiag _ (F64v f) = sp f . ss "'f64" +showDiag _ (UVv w) = sp w +showDiag i (Tuplev t) = showBlock i showDiag "{" t "}" +showDiag i (Unionv s n v) = + ss (maybe "" (flip (++) ": ") s) + . sp n . showChar '@' . showDiag i v +showDiag _ (Arrayv []) = ss "[ ]" +showDiag i (Arrayv l@(U8v f:r)) = + let str = utf8decode $ f : fmap unv r in + case str of + Just s -> showChar '"' . ss s . showChar '"' + Nothing -> showArray i l +showDiag i (Arrayv l) = showArray i l + +instance Show TypeV where + showsPrec _ a = showDiag 0 a + +(<:<) :: Char -> Maybe String -> Maybe String +(<:<) _ Nothing = Nothing +(<:<) c (Just s) + | isPrint c = Just (c:s) + | otherwise = Nothing + +-- Adapted from the utf8-string package +utf8decode :: [Word8] -> Maybe String +utf8decode [] = Just "" +utf8decode (c:cs) + | c < 0x80 = chr (fromEnum c) <:< utf8decode cs + | c < 0xc0 = Nothing + | c < 0xe0 = multi1 + | c < 0xf0 = multi_byte 2 0xf 0x800 + | c < 0xf8 = multi_byte 3 0x7 0x10000 + | c < 0xfc = multi_byte 4 0x3 0x200000 + | c < 0xfe = multi_byte 5 0x1 0x4000000 + | otherwise = Nothing + where + multi1 = case cs of + c1 : ds | c1 .&. 0xc0 == 0x80 -> + let d = ((fromEnum c .&. 0x1f) `shiftL` 6) .|. fromEnum (c1 .&. 0x3f) in + if d >= 0x000080 + then toEnum d <:< utf8decode ds + else Nothing + _ -> Nothing + + multi_byte :: Int -> Word8 -> Int -> Maybe [Char] + multi_byte i mask overlong = aux i cs (fromEnum (c .&. mask)) + where + aux :: Int -> [Word8] -> Int -> Maybe String + aux 0 rs acc + | overlong <= acc && acc <= 0x10ffff && + (acc < 0xd800 || 0xdfff < acc) && + (acc < 0xfffe || 0xffff < acc) = chr acc <:< utf8decode rs + | otherwise = Nothing + + aux n (r:rs) acc + | r .&. 0xc0 == 0x80 = aux (n-1) rs + $ shiftL acc 6 .|. fromEnum (r .&. 0x3f) + + aux _ rs _ = Nothing + diff --git a/baler.cabal b/baler.cabal new file mode 100644 index 0000000..733b575 --- /dev/null +++ b/baler.cabal @@ -0,0 +1,17 @@ +name: baler +version: 0.1.0.0 +synopsis: Minimal binary serialization format +homepage: https://github.com/ii8/baler +license: ISC +license-file: LICENSE +author: Murray +maintainer: murray.calavera@gmail.com +category: Codec +build-type: Simple +cabal-version: >=1.10 + +executable baler + main-is: Main.hs + other-modules: Type, Schema, Diag, Bin + build-depends: base, binary, bytestring, mtl, parsec, unordered-containers, utf8-string + default-language: Haskell2010 diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..02ebe7d --- /dev/null +++ b/stack.yaml @@ -0,0 +1,3 @@ +resolver: lts-11.13 +packages: +- .