Files
tricu/src/Main.hs
James Eversole d6df01105c feat(haskell): Interaction Tree IO
oops, now we have purely modelled IO 🤷
2026-05-12 18:47:38 -05:00

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