packages feed

zeolite-lang-0.18.0.0: src/Module/ParseMetadata.hs

{- -----------------------------------------------------------------------------
Copyright 2020-2021 Kevin P. Barry

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
----------------------------------------------------------------------------- -}

-- Author: Kevin P. Barry [ta0kira@gmail.com]

module Module.ParseMetadata (
  ConfigFormat,
  autoReadConfig,
  autoWriteConfig,
) where

import Control.Applicative.Permutations
import Control.Monad (when)

import Base.CompilerError
import Cli.CompileOptions
import Cli.Programs (VersionHash(..))
import Module.CompileMetadata
import Parser.Common
import Parser.Procedure ()
import Parser.TextParser
import Parser.TypeCategory ()
import Parser.TypeInstance ()
import Text.Regex.TDFA
import Types.Procedure (Expression,MacroName)
import Types.TypeCategory (FunctionName(..),Namespace(..))
import Types.TypeInstance (CategoryName(..))


class ConfigFormat a where
  readConfig :: TextParser a
  writeConfig :: CollectErrorsM m => a -> m [String]

autoReadConfig :: (ConfigFormat a, ErrorContextM m) => String -> String -> m a
autoReadConfig f s = runTextParser (between optionalSpace endOfDoc readConfig) f s

autoWriteConfig ::  (ConfigFormat a, CollectErrorsM m) => a -> m String
autoWriteConfig = fmap unlines . writeConfig

structOpen :: TextParser ()
structOpen = sepAfter (string_ "{")

structClose :: TextParser ()
structClose = sepAfter (string_ "}")

indents :: [String] -> [String]
indents = map indent

indent :: String -> String
indent = ("  " ++)

prependFirst :: String -> [String] -> [String]
prependFirst s0 (s:ss) = (s0 ++ s):ss
prependFirst s0 _      = [s0]

validateCategoryName :: ErrorContextM m => CategoryName -> m ()
validateCategoryName c =
    when (not $ show c =~ "^[A-Z][A-Za-z0-9]*$") $
      compilerErrorM $ "Invalid category name: \"" ++ show c ++ "\""

validateFunctionName :: ErrorContextM m => FunctionName -> m ()
validateFunctionName f =
    when (not $ show f =~ "^[a-z][A-Za-z0-9]*$") $
      compilerErrorM $ "Invalid function name: \"" ++ show f ++ "\""

validateHash :: ErrorContextM m => VersionHash -> m ()
validateHash h =
    when (not $ show h =~ "^[A-Za-z0-9]+$") $
      compilerErrorM $ "Version hash must be a hex string: \"" ++ show h ++ "\""

parseHash :: TextParser VersionHash
parseHash = labeled "version hash" $ sepAfter (fmap VersionHash $ some hexDigitChar)

maybeShowNamespace :: ErrorContextM m => String -> Namespace -> m [String]
maybeShowNamespace l (StaticNamespace ns) = do
  when (not $ ns =~ "^[A-Za-z][A-Za-z0-9_]*$") $
    compilerErrorM $ "Invalid category namespace: \"" ++ ns ++ "\""
  return [l ++ " " ++ ns]
maybeShowNamespace _ _ = return []

parseNamespace :: TextParser Namespace
parseNamespace = labeled "namespace" $ do
  b <- lowerChar
  e <- sepAfter $ many (alphaNumChar <|> char '_')
  return $ StaticNamespace (b:e)

parseQuoted :: TextParser String
parseQuoted = labeled "quoted string" $ do
  string_ "\""
  ss <- manyTill stringChar (string_ "\"")
  optionalSpace
  return ss

parseList :: TextParser a -> TextParser [a]
parseList p = labeled "list" $ do
  sepAfter (string_ "[")
  xs <- manyTill (sepAfter p) (string_ "]")
  optionalSpace
  return xs

parseOptional :: String -> a -> TextParser a -> Permutation (TextParser) a
parseOptional l def p = toPermutationWithDefault def $ do
    try $ sepAfter (string_ l)
    p

parseRequired :: String -> TextParser a -> Permutation (TextParser) a
parseRequired l p = toPermutation $ do
    try $ sepAfter (string_ l)
    p

instance ConfigFormat CompileMetadata where
  readConfig = runPermutation $ CompileMetadata
    <$> parseRequired "version_hash:"       parseHash
    <*> parseRequired "path:"               parseQuoted
    <*> parseOptional "public_namespace:"   NoNamespace parseNamespace
    <*> parseOptional "private_namespace:"  NoNamespace parseNamespace
    <*> parseRequired "public_deps:"        (parseList parseQuoted)
    <*> parseRequired "private_deps:"       (parseList parseQuoted)
    <*> parseRequired "public_categories:"  (parseList sourceParser)
    <*> parseRequired "private_categories:" (parseList sourceParser)
    <*> parseRequired "public_subdirs:"     (parseList parseQuoted)
    <*> parseRequired "private_subdirs:"    (parseList parseQuoted)
    <*> parseRequired "public_files:"       (parseList parseQuoted)
    <*> parseRequired "private_files:"      (parseList parseQuoted)
    <*> parseRequired "test_files:"         (parseList parseQuoted)
    <*> parseRequired "hxx_files:"          (parseList parseQuoted)
    <*> parseRequired "cxx_files:"          (parseList parseQuoted)
    <*> parseRequired "binaries:"           (parseList parseQuoted)
    <*> parseRequired "libraries:"          (parseList parseQuoted)
    <*> parseRequired "link_flags:"         (parseList parseQuoted)
    <*> parseRequired "object_files:"       (parseList readConfig)
  writeConfig (CompileMetadata h p ns1 ns2 is is2 cs1 cs2 ds1 ds2 ps xs ts hxx cxx bs ls lf os) = do
    validateHash h
    ns1' <- maybeShowNamespace "public_namespace:"  ns1
    ns2' <- maybeShowNamespace "private_namespace:" ns2
    mapCompilerM_ validateCategoryName cs1
    mapCompilerM_ validateCategoryName cs2
    os' <- fmap concat $ mapCompilerM writeConfig os
    return $ [
        "version_hash: " ++ show h,
        "path: " ++ show p
      ] ++ ns1' ++ ns2' ++ [
        "public_deps: ["
      ] ++ indents (map show is) ++ [
        "]",
        "private_deps: ["
      ] ++ indents (map show is2) ++ [
        "]",
        "public_categories: ["
      ] ++ indents (map show cs1) ++ [
        "]",
        "private_categories: ["
      ] ++ indents (map show cs2) ++ [
        "]",
        "public_subdirs: ["
      ] ++ indents (map show ds1) ++ [
        "]",
        "private_subdirs: ["
      ] ++ indents (map show ds2) ++ [
        "]",
        "public_files: ["
      ] ++ indents (map show ps) ++ [
        "]",
        "private_files: ["
      ] ++ indents (map show xs) ++ [
        "]",
        "test_files: ["
      ] ++ indents (map show ts) ++ [
        "]",
        "hxx_files: ["
      ] ++ indents (map show hxx) ++ [
        "]",
        "cxx_files: ["
      ] ++ indents (map show cxx) ++ [
        "]",
        "binaries: ["
      ] ++ indents (map show bs) ++ [
        "]",
        "libraries: ["
      ] ++ indents (map show ls) ++ [
        "]",
        "link_flags: ["
      ] ++ indents (map show lf) ++ [
        "]",
        "object_files: ["
      ] ++ indents os' ++ [
        "]"
      ]

instance ConfigFormat ObjectFile where
  readConfig = category <|> other where
    category = do
      sepAfter (string_ "category_object")
      structOpen
      o <- runPermutation $ CategoryObjectFile
        <$> parseRequired "category:" readConfig
        <*> parseRequired "requires:" (parseList readConfig)
        <*> parseRequired "files:"    (parseList parseQuoted)
      structClose
      return o
    other = do
      sepAfter (string_ "other_object")
      structOpen
      f <- runPermutation $ OtherObjectFile
        <$> parseRequired "file:" parseQuoted
      structClose
      return f
  writeConfig (CategoryObjectFile c rs fs) = do
    category <- writeConfig c
    requires <- fmap concat $ mapCompilerM writeConfig rs
    return $ [
        "category_object {"
      ] ++ indents ("category: " `prependFirst` category) ++ [
        indent "requires: ["
      ] ++ (indents . indents) requires ++ [
        indent "]",
        indent "files: ["
      ] ++ (indents . indents) (map show fs) ++ [
        indent "]",
        "}"
      ]
  writeConfig (OtherObjectFile f) = do
    return $ [
        "other_object {",
        indent ("file: " ++ show f),
        "}"
      ]

instance ConfigFormat CategoryIdentifier where
  readConfig = category <|> unresolved where
    category = do
      sepAfter (string_ "category")
      structOpen
      i <- runPermutation $ CategoryIdentifier
        <$> parseRequired "path:"      parseQuoted
        <*> parseRequired "name:"      sourceParser
        <*> parseOptional "namespace:" NoNamespace parseNamespace
      structClose
      return i
    unresolved = do
      sepAfter (string_ "unresolved")
      structOpen
      c <- runPermutation $ UnresolvedCategory
        <$> parseRequired "name:" sourceParser
      structClose
      return c
  writeConfig (CategoryIdentifier p c ns) = do
    validateCategoryName c
    namespace <- maybeShowNamespace "namespace:" ns
    return $ [
        "category {",
        indent $ "name: " ++ show c
      ] ++ indents namespace ++ [
        indent $ "path: " ++ show p,
        "}"
      ]
  writeConfig (UnresolvedCategory c) = do
    validateCategoryName c
    return $ ["unresolved { " ++ "name: " ++ show c ++ " " ++ "}"]

instance ConfigFormat ModuleConfig where
  readConfig = runPermutation $ ModuleConfig
    <$> parseOptional "root:"           "" parseQuoted
    <*> parseRequired "path:"              parseQuoted
    <*> parseOptional "expression_map:" [] (parseList parseExprMacro)
    <*> parseOptional "public_deps:"    [] (parseList parseQuoted)
    <*> parseOptional "private_deps:"   [] (parseList parseQuoted)
    <*> parseOptional "extra_files:"    [] (parseList readConfig)
    <*> parseOptional "include_paths:"  [] (parseList parseQuoted)
    <*> parseRequired "mode:"              readConfig
  writeConfig (ModuleConfig p d em is is2 es ep m) = do
    es' <- fmap concat $ mapCompilerM writeConfig es
    m' <- writeConfig m
    when (not $ null em) $ compilerErrorM "Only empty expression maps are allowed when writing"
    return $ [
        "root: " ++ show p,
        "path: " ++ show d,
        "expression_map: [",
        -- NOTE: expression_map isn't output because that would require making
        -- all Expression serializable.
        "]",
        "public_deps: ["
      ] ++ indents (map show is) ++ [
        "]",
        "private_deps: ["
      ] ++ indents (map show is2) ++ [
        "]",
        "extra_files: ["
      ] ++ indents es' ++ [
        "]",
        "include_paths: ["
      ] ++ indents (map show ep) ++ [
        "]"
      ] ++ "mode: " `prependFirst` m'

instance ConfigFormat ExtraSource where
  readConfig = category <|> other where
    category = do
      sepAfter (string_ "category_source")
      structOpen
      s <- runPermutation $ CategorySource
        <$> parseRequired "source:"        parseQuoted
        <*> parseOptional "categories:" [] (parseList sourceParser)
        <*> parseOptional "requires:"   [] (parseList sourceParser)
      structClose
      return s
    other = do
      f <- parseQuoted
      return (OtherSource f)
  writeConfig (CategorySource f cs ds) = do
    mapCompilerM_ validateCategoryName cs
    mapCompilerM_ validateCategoryName ds
    return $ [
        "category_source {",
        indent ("source: " ++ show f),
        indent "categories: ["
      ] ++ (indents . indents . map show) cs ++ [
        indent "]",
        indent "requires: ["
      ] ++ (indents . indents . map show) ds ++ [
        indent "]",
        "}"
      ]
  writeConfig (OtherSource f) = return [show f]

instance ConfigFormat CompileMode where
  readConfig = labeled "compile mode" $ binary <|> incremental where
    binary = do
      sepAfter (string_ "binary")
      structOpen
      b <- runPermutation $ CompileBinary
        <$> parseRequired "category:"               sourceParser
        <*> parseRequired "function:"               sourceParser
        <*> parseOptional "link_mode:"  LinkDynamic parseLinkerMode
        <*> parseOptional "output:"     ""          parseQuoted
        <*> parseOptional "link_flags:" []          (parseList parseQuoted)
      structClose
      return b
    incremental = do
      sepAfter (string_ "incremental")
      structOpen
      lf <- runPermutation $ CompileIncremental
        <$> parseOptional "link_flags:" [] (parseList parseQuoted)
      structClose
      return lf
  writeConfig (CompileBinary c f lm o lf) = do
    validateCategoryName c
    validateFunctionName f
    lm' <- showLinkerMode lm
    return $ [
        "binary {",
        indent ("category: " ++ show c),
        indent ("function: " ++ show f),
        indent ("link_mode: " ++ lm'),
        indent ("output: " ++ show o),
        indent ("link_flags: [")
      ] ++ (indents . indents) (map show lf) ++ [
        indent "]",
        "}"
      ]
  writeConfig (CompileIncremental lf) = do
    return $ [
        "incremental {",
        indent ("link_flags: [")
      ] ++ (indents . indents) (map show lf) ++ [
        indent "]",
        "}"
      ]
  writeConfig _ = compilerErrorM "Invalid compile mode"

parseLinkerMode :: TextParser LinkerMode
parseLinkerMode = labeled "linker mode" $ static <|> dynamic where
  static  = sepAfter (string_ "static")  >> return LinkStatic
  dynamic = sepAfter (string_ "dynamic") >> return LinkDynamic

showLinkerMode :: ErrorContextM m => LinkerMode -> m String
showLinkerMode LinkStatic  = return "static"
showLinkerMode LinkDynamic = return "dynamic"

parseExprMacro :: TextParser (MacroName,Expression SourceContext)
parseExprMacro = do
  sepAfter (string_ "expression_macro")
  structOpen
  e <- runPermutation $ (,)
    <$> parseRequired "name:"       sourceParser
    <*> parseRequired "expression:" sourceParser
  structClose
  return e