409 lines
13 KiB
Haskell
409 lines
13 KiB
Haskell
module Main where
|
|
|
|
import ContentStore (initContentStoreWithPath, loadEnvironment, loadTerm, loadTree, resolveExportTarget)
|
|
import System.Exit (die)
|
|
import Server (runServerWithPath)
|
|
import Eval (evalTricu, evalTricuWithStore, mainResult, result)
|
|
import FileEval (evaluateFileWithContext, evaluateFileWithStore, compileFile)
|
|
import IODriver (IOPermissions(..), checkIOSentinel, runIO)
|
|
import Parser (parseTricu)
|
|
import REPL (repl)
|
|
import Research (T, EvaluatedForm(..), Env, formatT, exportDag)
|
|
import Wire (buildBundle, encodeBundle, importBundle, defaultExportNames, Bundle(..))
|
|
|
|
import Control.Monad (foldM, unless, when)
|
|
import Data.Text (unpack, pack)
|
|
import qualified Data.Text as T
|
|
import Data.Version (showVersion)
|
|
import Paths_tricu (version)
|
|
import Options.Applicative
|
|
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.Sequence as Seq
|
|
import Database.SQLite.Simple (Connection, close)
|
|
|
|
import qualified Data.Map as Map
|
|
import System.Environment (lookupEnv)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- CLI argument types
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
data TricuArgs
|
|
= Repl
|
|
| Eval
|
|
{ evalFiles :: [FilePath]
|
|
, evalFormat :: EvaluatedForm
|
|
, evalOutput :: FilePath
|
|
, evalDb :: Maybe FilePath
|
|
, evalIo :: Bool
|
|
, evalAllowRead :: [FilePath]
|
|
, evalAllowWrite :: [FilePath]
|
|
}
|
|
| ArboricxCompile
|
|
{ compileInput :: FilePath
|
|
, compileOutput :: FilePath
|
|
, compileNames :: [String]
|
|
, compileDb :: Maybe FilePath
|
|
}
|
|
| ArboricxImport
|
|
{ importFile :: FilePath
|
|
, importDb :: Maybe FilePath
|
|
}
|
|
| ArboricxExport
|
|
{ exportTargets :: [String]
|
|
, exportOutput :: FilePath
|
|
, exportNames :: [String]
|
|
, exportDb :: Maybe FilePath
|
|
, dag :: Bool
|
|
}
|
|
| ArboricxServe
|
|
{ serveHost :: String
|
|
, servePort :: Int
|
|
, serveDb :: Maybe FilePath
|
|
}
|
|
deriving (Show)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- optparse-applicative parsers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
readEvaluatedForm :: ReadM EvaluatedForm
|
|
readEvaluatedForm = eitherReader $ \s -> case s of
|
|
"tree" -> Right Tree
|
|
"fsl" -> Right FSL
|
|
"ast" -> Right AST
|
|
"ternary" -> Right Ternary
|
|
"ascii" -> Right Ascii
|
|
"decode" -> Right Decode
|
|
_ -> Left $ "Unknown format: " ++ s ++ ". Expected: tree, fsl, ast, ternary, ascii, decode"
|
|
|
|
evalParser :: Parser TricuArgs
|
|
evalParser = Eval
|
|
<$> many (argument str (metavar "FILE..."))
|
|
<*> option readEvaluatedForm
|
|
( long "format"
|
|
<> short 'f'
|
|
<> metavar "FORM"
|
|
<> value Tree
|
|
<> help "Output format: tree, fsl, ast, ternary, ascii, decode"
|
|
)
|
|
<*> option str
|
|
( long "output"
|
|
<> short 'o'
|
|
<> metavar "FILE"
|
|
<> value ""
|
|
<> help "Write output to file instead of stdout"
|
|
)
|
|
<*> optional (option str
|
|
( long "db"
|
|
<> short 'd'
|
|
<> metavar "PATH"
|
|
<> help "Content store database path"
|
|
))
|
|
<*> switch
|
|
( long "io"
|
|
<> help "Interpret the result as an IO action tree and execute it"
|
|
)
|
|
<*> many (option str
|
|
( long "allow-read"
|
|
<> metavar "PATH"
|
|
<> help "Allow reading from PATH prefix (repeatable)"
|
|
))
|
|
<*> many (option str
|
|
( long "allow-write"
|
|
<> metavar "PATH"
|
|
<> help "Allow writing to PATH prefix (repeatable)"
|
|
))
|
|
|
|
compileParser :: Parser TricuArgs
|
|
compileParser = ArboricxCompile
|
|
<$> option str
|
|
( long "file"
|
|
<> short 'f'
|
|
<> metavar "FILE"
|
|
<> value ""
|
|
<> help "Input .tri source file"
|
|
)
|
|
<*> option str
|
|
( long "output"
|
|
<> short 'o'
|
|
<> metavar "FILE"
|
|
<> value ""
|
|
<> help "Output bundle file path (required)"
|
|
)
|
|
<*> many (option str
|
|
( long "name"
|
|
<> short 'n'
|
|
<> metavar "NAME"
|
|
<> help "Definition name(s) to export as bundle roots (repeatable)"
|
|
))
|
|
<*> optional (option str
|
|
( long "db"
|
|
<> short 'd'
|
|
<> metavar "PATH"
|
|
<> help "Content store database path"
|
|
))
|
|
|
|
importParser :: Parser TricuArgs
|
|
importParser = ArboricxImport
|
|
<$> option str
|
|
( long "file"
|
|
<> short 'f'
|
|
<> metavar "FILE"
|
|
<> value ""
|
|
<> help "Bundle file to import"
|
|
)
|
|
<*> optional (option str
|
|
( long "db"
|
|
<> short 'd'
|
|
<> metavar "PATH"
|
|
<> help "Content store database path"
|
|
))
|
|
|
|
exportParser :: Parser TricuArgs
|
|
exportParser = ArboricxExport
|
|
<$> many (option str
|
|
( long "target"
|
|
<> short 't'
|
|
<> metavar "TARGET"
|
|
<> help "Target hash or name (repeatable)"
|
|
))
|
|
<*> option str
|
|
( long "output"
|
|
<> short 'o'
|
|
<> metavar "FILE"
|
|
<> value ""
|
|
<> help "Output file path (required for bundle export)"
|
|
)
|
|
<*> many (option str
|
|
( long "name"
|
|
<> short 'n'
|
|
<> metavar "NAME"
|
|
<> help "Export name(s) for the bundle manifest (repeatable)"
|
|
))
|
|
<*> optional (option str
|
|
( long "db"
|
|
<> short 'd'
|
|
<> metavar "PATH"
|
|
<> help "Content store database path"
|
|
))
|
|
<*> switch
|
|
( long "dag"
|
|
<> help "Export as a topologically-sorted DAG node table instead of a bundle"
|
|
)
|
|
|
|
serveParser :: Parser TricuArgs
|
|
serveParser = ArboricxServe
|
|
<$> option str
|
|
( long "host"
|
|
<> metavar "HOST"
|
|
<> value "127.0.0.1"
|
|
<> help "Host to bind the server to"
|
|
)
|
|
<*> option auto
|
|
( long "port"
|
|
<> short 'p'
|
|
<> metavar "PORT"
|
|
<> value 8787
|
|
<> help "HTTP port to listen on"
|
|
)
|
|
<*> optional (option str
|
|
( long "db"
|
|
<> short 'd'
|
|
<> metavar "PATH"
|
|
<> help "Content store database path"
|
|
))
|
|
|
|
versionStr :: String
|
|
versionStr = "tricu " ++ showVersion version
|
|
|
|
tricuParser :: Parser TricuArgs
|
|
tricuParser = (subparser topCommands <|> pure Repl)
|
|
<**> infoOption versionStr (long "version" <> help "Show version")
|
|
where
|
|
topCommands = mconcat
|
|
[ command "eval" (info (evalParser <**> helper)
|
|
(progDesc "Evaluate tricu source and print the result of the final expression"))
|
|
, command "arboricx" (info (arboricxParser <**> helper)
|
|
(progDesc "Arboricx bundle operations"))
|
|
]
|
|
|
|
arboricxParser :: Parser TricuArgs
|
|
arboricxParser = subparser $ mconcat
|
|
[ command "compile" (info (compileParser <**> helper)
|
|
(progDesc "Compile a .tri file into a standalone Arboricx bundle"))
|
|
, command "import" (info (importParser <**> helper)
|
|
(progDesc "Import an Arboricx bundle into the content store"))
|
|
, command "export" (info (exportParser <**> helper)
|
|
(progDesc "Export one or more terms from the content store"))
|
|
, command "serve" (info (serveParser <**> helper)
|
|
(progDesc "Start a read-only HTTP server for Arboricx bundles"))
|
|
]
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Entry point
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
main :: IO ()
|
|
main = do
|
|
args <- execParser $ info (tricuParser <**> helper)
|
|
( fullDesc
|
|
<> progDesc "Exploring Tree Calculus"
|
|
<> header versionStr
|
|
)
|
|
case args of
|
|
Repl -> runRepl
|
|
Eval {} -> runEval args
|
|
ArboricxCompile {} -> runCompile args
|
|
ArboricxImport {} -> runImport args
|
|
ArboricxExport {} -> runExport args
|
|
ArboricxServe {} -> runServe args
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Command runners
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
runRepl :: IO ()
|
|
runRepl = do
|
|
putStrLn "Welcome to the tricu REPL"
|
|
putStrLn "You may exit with `CTRL+D` or the `!exit` command."
|
|
repl
|
|
|
|
runEval :: TricuArgs -> IO ()
|
|
runEval opts = do
|
|
let files = evalFiles opts
|
|
form = evalFormat opts
|
|
out = evalOutput opts
|
|
mconn <- case evalDb opts of
|
|
Just dbPath -> Just <$> initContentStoreWithPath (Just dbPath)
|
|
Nothing -> do
|
|
mDbPath <- lookupEnv "TRICU_DB_PATH"
|
|
case mDbPath of
|
|
Just _ -> Just <$> initContentStoreWithPath Nothing
|
|
Nothing -> return Nothing
|
|
resultT <- case files of
|
|
[] -> do
|
|
input <- getContents
|
|
env <- evalTricuWithStore mconn Map.empty (parseTricu input)
|
|
return $ result env
|
|
_ -> do
|
|
finalEnv <- foldM (evaluateFileWithStore mconn) Map.empty files
|
|
return $ mainResult finalEnv
|
|
finalT <- if evalIo opts
|
|
then case checkIOSentinel resultT of
|
|
Right (1, action) -> do
|
|
let perms = IOPermissions (evalAllowRead opts) (evalAllowWrite opts)
|
|
runIO perms action
|
|
Right (v, _) -> die $ "Unsupported IO ABI version: " ++ show v
|
|
Left err -> die $ "IO mode requested but " ++ err
|
|
else return resultT
|
|
case mconn of
|
|
Just conn -> close conn
|
|
Nothing -> return ()
|
|
writeOutput out (formatT form finalT)
|
|
|
|
runCompile :: TricuArgs -> IO ()
|
|
runCompile opts = do
|
|
let input = compileInput opts
|
|
out = compileOutput opts
|
|
names = compileNames opts
|
|
when (null out) $ die "tricu arboricx compile: --output is required"
|
|
when (null input) $ die "tricu arboricx compile: input file is required"
|
|
let nameTexts = if null names then [] else map T.pack names
|
|
compileFile input out nameTexts
|
|
|
|
runImport :: TricuArgs -> IO ()
|
|
runImport opts = do
|
|
let file = importFile opts
|
|
when (null file) $ die "tricu arboricx import: input file is required"
|
|
withContentStore (importDb opts) $ \conn -> do
|
|
bundleData <- BL.readFile file
|
|
roots <- map T.unpack <$> importBundle conn (BL.toStrict bundleData)
|
|
putStrLn $ "Imported " ++ show (length roots) ++ " root(s):"
|
|
mapM_ (\r -> putStrLn $ " " ++ r) roots
|
|
|
|
runExport :: TricuArgs -> IO ()
|
|
runExport opts =
|
|
if dag opts
|
|
then runExportDag opts
|
|
else runExportBundle opts
|
|
|
|
runExportBundle :: TricuArgs -> IO ()
|
|
runExportBundle opts = do
|
|
let targets = exportTargets opts
|
|
out = exportOutput opts
|
|
names = exportNames opts
|
|
when (null out) $ die "tricu arboricx export: --output is required"
|
|
when (null targets) $ die "tricu arboricx export: at least one --target is required"
|
|
withContentStore (exportDb opts) $ \conn -> do
|
|
terms <- mapM (\t -> do
|
|
(h, _) <- resolveExportTarget conn t
|
|
maybeTree <- loadTree conn h
|
|
case maybeTree of
|
|
Nothing -> die $ "Term not found in store: " ++ t
|
|
Just tree -> return tree) targets
|
|
let expNames = if null names
|
|
then defaultExportNames (length terms)
|
|
else map T.pack names
|
|
when (length expNames /= length terms) $
|
|
die "tricu arboricx export: number of --name values must match number of TARGETs"
|
|
let namedTerms = zip expNames terms
|
|
bundle = buildBundle namedTerms
|
|
bundleData = encodeBundle bundle
|
|
BL.writeFile out (BL.fromStrict bundleData)
|
|
putStrLn $ "Exported bundle with " ++ show (length namedTerms) ++ " export(s) to " ++ out
|
|
putStrLn $ " nodes: " ++ show (Seq.length (bundleNodes bundle))
|
|
putStrLn $ " size: " ++ show (BS.length bundleData) ++ " bytes"
|
|
|
|
runExportDag :: TricuArgs -> IO ()
|
|
runExportDag opts = do
|
|
let targets = exportTargets opts
|
|
out = exportOutput opts
|
|
case targets of
|
|
[target] -> withContentStore (exportDb opts) $ \conn -> do
|
|
maybeTerm <- loadTerm conn target
|
|
case maybeTerm of
|
|
Nothing -> die $ "Term not found: " ++ target
|
|
Just term -> do
|
|
let (rootIdx, nodes) = Research.exportDag term
|
|
output = unlines $
|
|
show rootIdx :
|
|
map (\(tag, refs) -> unwords (tag : map show refs)) nodes
|
|
writeOutput out output
|
|
[] -> die "tricu arboricx export --dag: exactly one --target is required"
|
|
_ -> die "tricu arboricx export --dag: exactly one --target is required"
|
|
|
|
runServe :: TricuArgs -> IO ()
|
|
runServe opts = do
|
|
let hostStr = serveHost opts
|
|
portNum = servePort opts
|
|
putStrLn $ "Starting Arboricx bundle server on " ++ hostStr ++ ":" ++ show portNum
|
|
putStrLn $ " GET /bundle/hash/:hash -- primary endpoint"
|
|
putStrLn $ " GET /bundle/name/:name -- convenience endpoint"
|
|
putStrLn $ " Content-Type: application/vnd.arboricx.bundle"
|
|
runServerWithPath (serveDb opts) hostStr portNum
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Helpers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
withContentStore :: Maybe FilePath -> (Connection -> IO a) -> IO a
|
|
withContentStore mPath act = do
|
|
conn <- initContentStoreWithPath mPath
|
|
result <- act conn
|
|
close conn
|
|
return result
|
|
|
|
writeOutput :: FilePath -> String -> IO ()
|
|
writeOutput path content
|
|
| null path = putStr content
|
|
| otherwise = writeFile path content
|
|
|
|
runTricuTEnv :: Env -> String -> T
|
|
runTricuTEnv env input =
|
|
let asts = parseTricu input
|
|
finalEnv = evalTricu env asts
|
|
in result finalEnv
|