Files
tricu/src/Main.hs
2026-06-22 17:26:21 -05:00

679 lines
23 KiB
Haskell

module Main where
import Check (checkFile, checkFileWithStore, instrumentIOContinuations)
import ContentStore
import ContentStore.Bundle
import Module.Manifest
import System.Exit (die)
import Eval (evalTricu, mainResult, result)
import FileEval
( ContractMode(..)
, LoadedSource(..)
, defaultStorePath
, evaluateFileWithContextWithStoreAndMode
, evaluateFileWithStore
, loadFileWithStoreMode
, compileFileWithStore
)
import IODriver (IOPermissions(..), runIO)
import Parser (parseTricu)
import REPL (repl, replWithStore)
import Research (T, EvaluatedForm(..), Env, formatT, exportDag)
import Wire (encodeBundle, defaultExportNames, Bundle(..))
import Control.Monad (foldM, forM, unless, when)
import Data.Char (isAlphaNum)
import Data.List (sortOn)
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 qualified Data.Map as Map
import System.Directory (createDirectoryIfMissing, getHomeDirectory)
import System.FilePath (takeBaseName, (</>))
-- ---------------------------------------------------------------------------
-- CLI argument types
-- ---------------------------------------------------------------------------
data AppArgs = AppArgs
{ globalStore :: Maybe FilePath
, appCommand :: TricuArgs
} deriving (Show)
data TricuArgs
= Repl
| Check
{ checkInput :: FilePath
, checkStore :: Maybe FilePath
}
| Eval
{ evalFiles :: [FilePath]
, evalStore :: Maybe FilePath
, evalFormat :: EvaluatedForm
, evalOutput :: FilePath
, evalUnchecked :: Bool
, evalIo :: Bool
, evalAllowRead :: [FilePath]
, evalAllowWrite :: [FilePath]
, evalAllowReadAll :: Bool
, evalAllowWriteAll :: Bool
, evalUnsafeIo :: Bool
}
| ArboricxCompile
{ compileInput :: FilePath
, compileStore :: Maybe FilePath
, compileOutput :: FilePath
, compileNames :: [String]
}
| ArboricxImport
{ importFile :: FilePath
, importStore :: Maybe FilePath
, importModule :: Maybe String
}
| ArboricxExport
{ exportTargets :: [String]
, exportModules :: [String]
, exportOutput :: FilePath
, exportNames :: [String]
, exportStore :: Maybe FilePath
, exportAll :: Bool
, exportSplit :: Bool
, dag :: Bool
}
| StoreAliasList
{ storeAliasKind :: AliasKind
, storePathOpt :: Maybe FilePath
}
| StoreAliasGet
{ storeAliasKind :: AliasKind
, storeAliasName :: String
, storePathOpt :: 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
"number" -> Right Number
"string" -> Right StringLit
_ -> Left $ "Unknown format: " ++ s ++ ". Expected: tree, fsl, ast, ternary, ascii, decode, number, string"
checkParser :: Parser TricuArgs
checkParser = Check
<$> argument str (metavar "FILE")
<*> optional (option str
( long "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path for module import resolution"
))
evalParser :: Parser TricuArgs
evalParser = Eval
<$> many (argument str (metavar "FILE..."))
<*> optional (option str
( long "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path for module import resolution"
))
<*> option readEvaluatedForm
( long "format"
<> short 'f'
<> metavar "FORM"
<> value Tree
<> help "Output format: tree, fsl, ast, ternary, ascii, decode, number, string"
)
<*> option str
( long "output"
<> short 'o'
<> metavar "FILE"
<> value ""
<> help "Write output to file instead of stdout"
)
<*> switch
( long "unchecked"
<> help "Evaluate as untyped code: ignore View Contract annotations and do not publish unchecked view refs"
)
<*> 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)"
))
<*> switch
( long "allow-read-all"
<> help "Allow reading from any path"
)
<*> switch
( long "allow-write-all"
<> help "Allow writing to any path"
)
<*> switch
( long "unsafe-io"
<> help "Allow unrestricted read and write access"
)
compileParser :: Parser TricuArgs
compileParser = ArboricxCompile
<$> option str
( long "file"
<> short 'f'
<> metavar "FILE"
<> value ""
<> help "Input .tri source file"
)
<*> optional (option str
( long "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path for module import resolution"
))
<*> 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)"
))
importParser :: Parser TricuArgs
importParser = ArboricxImport
<$> option str
( long "file"
<> short 'f'
<> metavar "FILE"
<> value ""
<> help "Bundle file to import"
)
<*> optional (option str
( long "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path"
))
<*> optional (option str
( long "module"
<> short 'm'
<> metavar "NAME"
<> help "Module alias to create for the imported bundle (defaults to bundle file basename)"
))
exportParser :: Parser TricuArgs
exportParser = ArboricxExport
<$> many (option str
( long "target"
<> short 't'
<> metavar "TARGET"
<> help "Target hash or name (repeatable)"
))
<*> many (option str
( long "module"
<> short 'm'
<> metavar "MODULE"
<> help "Module alias or manifest hash to export (repeatable; bundle export only)"
))
<*> 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 "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path"
))
<*> switch
( long "all"
<> help "Export all name aliases that point at tree-term objects"
)
<*> switch
( long "split"
<> help "Write one single-export bundle per export; --output is treated as a directory"
)
<*> switch
( long "dag"
<> help "Export as a topologically-sorted DAG node table instead of a bundle"
)
aliasKindReader :: ReadM AliasKind
aliasKindReader = eitherReader $ \s -> case s of
"names" -> Right NameAlias
"name" -> Right NameAlias
"modules" -> Right ModuleAlias
"module" -> Right ModuleAlias
"packages" -> Right PackageAlias
"package" -> Right PackageAlias
_ -> Left "alias kind must be one of: names, modules, packages"
storePathParser :: Parser (Maybe FilePath)
storePathParser = optional (option str
( long "store"
<> short 's'
<> metavar "PATH"
<> help "Content-addressed store path"
))
aliasKindParser :: Parser AliasKind
aliasKindParser = option aliasKindReader
( long "kind"
<> short 'k'
<> metavar "KIND"
<> value NameAlias
<> help "Alias kind: names, modules, packages (default: names)"
)
storeAliasListParser :: Parser TricuArgs
storeAliasListParser = StoreAliasList
<$> aliasKindParser
<*> storePathParser
storeAliasGetParser :: Parser TricuArgs
storeAliasGetParser = StoreAliasGet
<$> aliasKindParser
<*> argument str (metavar "NAME")
<*> storePathParser
versionStr :: String
versionStr = "tricu " ++ showVersion version
tricuParser :: Parser AppArgs
tricuParser = AppArgs
<$> optional (option str
( long "store"
<> metavar "PATH"
<> help "Global content-addressed store path used by commands and the REPL unless a subcommand overrides it"
))
<*> ((subparser topCommands <|> pure Repl)
<**> infoOption versionStr (long "version" <> help "Show version"))
where
topCommands = mconcat
[ command "check" (info (checkParser <**> helper)
(progDesc "Check View Contract annotations and report ok or diagnostics"))
, 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"))
, command "store" (info (storeParser <**> helper)
(progDesc "Inspect and manage the content-addressed store"))
]
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"))
]
storeParser :: Parser TricuArgs
storeParser = subparser $ mconcat
[ command "alias" (info (storeAliasParser <**> helper)
(progDesc "Inspect workspace aliases"))
]
storeAliasParser :: Parser TricuArgs
storeAliasParser = subparser $ mconcat
[ command "list" (info (storeAliasListParser <**> helper)
(progDesc "List aliases by kind"))
, command "get" (info (storeAliasGetParser <**> helper)
(progDesc "Resolve an alias by kind and name"))
]
-- ---------------------------------------------------------------------------
-- Entry point
-- ---------------------------------------------------------------------------
main :: IO ()
main = do
appArgs <- execParser $ info (tricuParser <**> helper)
( fullDesc
<> progDesc "Exploring Tree Calculus"
<> header versionStr
)
let mGlobalStore = globalStore appArgs
args = applyGlobalStore mGlobalStore (appCommand appArgs)
case args of
Repl -> runReplWithStore mGlobalStore
Check {} -> runCheck args
Eval {} -> runEval args
ArboricxCompile {} -> runCompile args
ArboricxImport {} -> runImport args
ArboricxExport {} -> runExport args
StoreAliasList {} -> runStoreAliasList args
StoreAliasGet {} -> runStoreAliasGet args
-- ---------------------------------------------------------------------------
-- Command runners
-- ---------------------------------------------------------------------------
applyGlobalStore :: Maybe FilePath -> TricuArgs -> TricuArgs
applyGlobalStore mGlobal args = case args of
Repl -> Repl
Check {} -> args { checkStore = preferLocal (checkStore args) }
Eval {} -> args { evalStore = preferLocal (evalStore args) }
ArboricxCompile {} -> args { compileStore = preferLocal (compileStore args) }
ArboricxImport {} -> args { importStore = preferLocal (importStore args) }
ArboricxExport {} -> args { exportStore = preferLocal (exportStore args) }
StoreAliasList {} -> args { storePathOpt = preferLocal (storePathOpt args) }
StoreAliasGet {} -> args { storePathOpt = preferLocal (storePathOpt args) }
where
preferLocal local = case local of
Just _ -> local
Nothing -> mGlobal
runRepl :: IO ()
runRepl = runReplWithStore Nothing
runReplWithStore :: Maybe FilePath -> IO ()
runReplWithStore mStore = do
putStrLn "Welcome to the tricu REPL"
putStrLn "You may exit with `CTRL+D` or the `!exit` command."
case mStore of
Nothing -> repl
Just store -> replWithStore (StorePath store)
runCheck :: TricuArgs -> IO ()
runCheck opts = do
output <- case checkStore opts of
Nothing -> checkFile (checkInput opts)
Just storePath -> checkFileWithStore (StorePath storePath) (checkInput opts)
putStrLn output
evaluateCheckedIOFile :: StorePath -> ContractMode -> Env -> FilePath -> IO Env
evaluateCheckedIOFile store mode env filePath = do
loaded <- loadFileWithStoreMode mode store filePath
checkedAst <- case instrumentIOContinuations (loadedAst loaded) of
Left err -> die err
Right asts -> pure asts
viewEnv <- evaluateFileWithStore (Just store) "./lib/view.tri"
pure $ evalTricu (Map.unions [viewEnv, loadedImports loaded, env]) checkedAst
runEval :: TricuArgs -> IO ()
runEval opts = do
let files = evalFiles opts
form = evalFormat opts
out = evalOutput opts
resultT <- case files of
[] -> do
input <- getContents
let env = evalTricu Map.empty (parseTricu input)
return $ result env
_ -> do
mStoreOpt <- traverse (pure . StorePath) (evalStore opts)
let contractMode = if evalUnchecked opts then IgnoreContracts else EnforceContracts
finalEnv <- if evalIo opts && contractMode == EnforceContracts
then do
store <- maybe defaultStorePath pure mStoreOpt
foldM (evaluateCheckedIOFile store contractMode) Map.empty files
else foldM (evaluateFileWithContextWithStoreAndMode contractMode mStoreOpt) Map.empty files
return $ mainResult finalEnv
finalT <- if evalIo opts
then do
let perms = IOPermissions
{ allowRead = evalAllowRead opts
, allowWrite = evalAllowWrite opts
, allowReadAll = evalUnsafeIo opts || evalAllowReadAll opts
, allowWriteAll = evalUnsafeIo opts || evalAllowWriteAll opts
}
result <- runIO perms resultT
case result of
Left err -> die $ "IO error: " ++ err
Right val -> pure val
else return resultT
writeOutput out (formatT form finalT)
runCompile :: TricuArgs -> IO ()
runCompile opts = do
let input = compileInput opts
out = compileOutput opts
names = compileNames opts
mStore = StorePath <$> compileStore 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
compileFileWithStore mStore input out nameTexts
runImport :: TricuArgs -> IO ()
runImport opts = do
let file = importFile opts
when (null file) $ die "tricu arboricx import: input file is required"
store <- resolveStorePath (importStore opts)
bundleData <- BL.readFile file
roots <- unpackBundleToStore store (BL.toStrict bundleData)
mapM_ (\(name, root) ->
writeAlias store NameAlias name (treeTermRef root)) roots
let manifest = ModuleManifest []
[ ModuleExport
name
(treeTermRef root)
"arboricx.abi.tree.v1"
Nothing
Nothing
| (name, root) <- roots
]
moduleName = T.pack $ maybe (takeBaseName file) id (importModule opts)
manifestHash <- putManifest store manifest
writeAlias store ModuleAlias moduleName (ObjectRef (unDomain manifestDomain) manifestHash)
putStrLn $ "Imported " ++ show (length roots) ++ " root(s):"
mapM_ (\(name, root) -> putStrLn $ " " ++ T.unpack name ++ " -> " ++ T.unpack root) roots
putStrLn $ "Created module alias " ++ T.unpack moduleName ++ " -> " ++ T.unpack manifestHash
runExport :: TricuArgs -> IO ()
runExport opts =
if dag opts
then runExportDag opts
else runExportBundle opts
runExportBundle :: TricuArgs -> IO ()
runExportBundle opts = do
let targets = exportTargets opts
modules = exportModules opts
out = exportOutput opts
names = exportNames opts
allFlag = exportAll opts
splitFlag = exportSplit opts
when (null out) $ die "tricu arboricx export: --output is required"
when (null targets && null modules && not allFlag) $
die "tricu arboricx export: at least one --target, --module, or --all is required"
when (splitFlag && not (null names)) $
die "tricu arboricx export --split: --name is not supported; split bundles use their export names"
store <- resolveStorePath (exportStore opts)
allEntries <- if allFlag then resolveAllNameExports store else pure []
targetRoots <- mapM (resolveStoreTarget store) targets
moduleRoots <- concat <$> mapM (resolveModuleExports store) modules
let targetEntries = zip (defaultExportNames (length targetRoots)) targetRoots
entries = allEntries ++ targetEntries ++ moduleRoots
expNames = if null names then map fst entries else map T.pack names
when (null entries) $
die "tricu arboricx export: no tree-term exports found"
when (length expNames /= length entries) $
die "tricu arboricx export: number of --name values must match number of exported roots"
if splitFlag
then runExportBundleSplit store out (zip expNames (map snd entries))
else do
bundle <- packBundleFromStore store (zip expNames (map snd entries))
let bundleData = encodeBundle bundle
BL.writeFile out (BL.fromStrict bundleData)
putStrLn $ "Exported bundle with " ++ show (length entries) ++ " export(s) to " ++ out
putStrLn $ " nodes: " ++ show (Seq.length (bundleNodes bundle))
putStrLn $ " size: " ++ show (BS.length bundleData) ++ " bytes"
runExportBundleSplit :: StorePath -> FilePath -> [(T.Text, ObjectHash)] -> IO ()
runExportBundleSplit store outDir entries = do
createDirectoryIfMissing True outDir
written <- forM (zip [0 :: Int ..] entries) $ \(i, (name, root)) -> do
bundle <- packBundleFromStore store [(name, root)]
let bundleData = encodeBundle bundle
path = outDir </> splitBundleFileName i name
BL.writeFile path (BL.fromStrict bundleData)
pure (path, Seq.length (bundleNodes bundle), BS.length bundleData)
putStrLn $ "Exported " ++ show (length written) ++ " split bundle(s) to " ++ outDir
mapM_ (\(path, nodeCount, byteCount) ->
putStrLn $ " " ++ path ++ " (nodes: " ++ show nodeCount ++ ", size: " ++ show byteCount ++ " bytes)") written
runStoreAliasList :: TricuArgs -> IO ()
runStoreAliasList opts = do
store <- resolveStorePath (storePathOpt opts)
aliases <- listAliases store (storeAliasKind opts)
mapM_ (\(name, ref) -> putStrLn $ T.unpack name ++ " -> " ++ formatObjectRef ref) aliases
runStoreAliasGet :: TricuArgs -> IO ()
runStoreAliasGet opts = do
store <- resolveStorePath (storePathOpt opts)
mRef <- readAlias store (storeAliasKind opts) (T.pack $ storeAliasName opts)
case mRef of
Nothing -> die $ "alias not found: " ++ storeAliasName opts
Just ref -> putStrLn $ storeAliasName opts ++ " -> " ++ formatObjectRef ref
runExportDag :: TricuArgs -> IO ()
runExportDag opts = do
let targets = exportTargets opts
modules = exportModules opts
out = exportOutput opts
unless (null modules) $
die "tricu arboricx export --dag: --module is only supported for bundle export"
case targets of
[target] -> do
store <- resolveStorePath (exportStore opts)
root <- resolveStoreTarget store target
maybeTerm <- getTreeTerm store root
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"
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
resolveStorePath :: Maybe FilePath -> IO StorePath
resolveStorePath (Just path) = return (StorePath path)
resolveStorePath Nothing = do
home <- getHomeDirectory
return (StorePath (home </> ".tricu" </> "store"))
treeTermRef :: ObjectHash -> ObjectRef
treeTermRef = ObjectRef (unDomain treeTermDomain)
resolveStoreTarget :: StorePath -> String -> IO ObjectHash
resolveStoreTarget store target = do
mAlias <- readAlias store NameAlias (T.pack target)
let root = maybe (T.pack target) objectRefHash mAlias
mTree <- getTreeTerm store root
case mTree of
Just _ -> return root
Nothing -> die $ "Term not found in store: " ++ target
resolveAllNameExports :: StorePath -> IO [(T.Text, ObjectHash)]
resolveAllNameExports store = do
aliases <- sortOn fst <$> listAliases store NameAlias
fmap concat $ mapM exportAlias aliases
where
exportAlias (name, ref)
| objectRefKind ref /= unDomain treeTermDomain = pure []
| otherwise = do
mTree <- getTreeTerm store (objectRefHash ref)
case mTree of
Nothing -> die $ "Name alias tree term not found: " ++ T.unpack name
Just _ -> pure [(name, objectRefHash ref)]
resolveModuleExports :: StorePath -> String -> IO [(T.Text, ObjectHash)]
resolveModuleExports store moduleTarget = do
manifestHash <- resolveModuleManifestHash store moduleTarget
mManifest <- getManifest store manifestHash
manifest <- case mManifest of
Nothing -> die $ "Module manifest not found in store: " ++ moduleTarget
Just value -> return value
mapM exportEntry (moduleManifestExports manifest)
where
exportEntry ex = do
let ref = moduleExportObject ex
unless (objectRefKind ref == unDomain treeTermDomain) $
die $ "Unsupported module export object kind for " ++ T.unpack (moduleExportName ex) ++ ": " ++ T.unpack (objectRefKind ref)
mTree <- getTreeTerm store (objectRefHash ref)
case mTree of
Nothing -> die $ "Module export tree term not found: " ++ T.unpack (moduleExportName ex)
Just _ -> return (moduleExportName ex, objectRefHash ref)
resolveModuleManifestHash :: StorePath -> String -> IO ObjectHash
resolveModuleManifestHash store moduleTarget = do
mAlias <- readAlias store ModuleAlias (T.pack moduleTarget)
case mAlias of
Just ref -> do
unless (objectRefKind ref == unDomain manifestDomain) $
die $ "Module alias does not point at a module manifest: " ++ moduleTarget
return (objectRefHash ref)
Nothing -> return (T.pack moduleTarget)
formatObjectRef :: ObjectRef -> String
formatObjectRef ref = T.unpack (objectRefKind ref) ++ " " ++ T.unpack (objectRefHash ref)
splitBundleFileName :: Int -> T.Text -> FilePath
splitBundleFileName i name = show i ++ "-" ++ sanitize (T.unpack name) ++ ".arboricx"
where
sanitize [] = "export"
sanitize xs = case map safeChar xs of
[] -> "export"
ys -> ys
safeChar c
| isAlphaNum c || c == '-' || c == '_' || c == '.' = c
| otherwise = '_'
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