feat(haskell): CLI rewrite

This commit is contained in:
2026-05-11 15:29:12 -05:00
parent ea748b2e5e
commit e0b1e95729
8 changed files with 441 additions and 330 deletions

View File

@@ -40,8 +40,15 @@ serializeNameList :: [Text] -> Text
serializeNameList = T.intercalate "," . nub . sort serializeNameList = T.intercalate "," . nub . sort
initContentStore :: IO Connection initContentStore :: IO Connection
initContentStore = do initContentStore = initContentStoreWithPath Nothing
dbPath <- getContentStorePath
-- | Initialise a content store with an explicit path, or fall back
-- to the environment variable / default location.
initContentStoreWithPath :: Maybe FilePath -> IO Connection
initContentStoreWithPath mPath = do
dbPath <- case mPath of
Just p -> return p
Nothing -> getContentStorePath
createDirectoryIfMissing True (takeDirectory dbPath) createDirectoryIfMissing True (takeDirectory dbPath)
conn <- open dbPath conn <- open dbPath
setupDatabase conn setupDatabase conn

View File

@@ -93,6 +93,43 @@ evalAST mconn selectedVersions ast = do
resolvedEnv <- resolveTermsFromStore mconn selectedVersions varNames resolvedEnv <- resolveTermsFromStore mconn selectedVersions varNames
return $ evalASTSync resolvedEnv ast return $ evalASTSync resolvedEnv ast
-- | Evaluate a single AST term using a local environment augmented by
-- lazily-resolved store terms.
evalASTWithEnv :: Maybe Connection -> Env -> TricuAST -> IO T
evalASTWithEnv mconn localEnv ast = do
let varNames = collectVarNames ast
storeEnv <- resolveTermsFromStore mconn Map.empty varNames
let combinedEnv = Map.union localEnv storeEnv
return $ evalASTSync combinedEnv ast
-- | Store-aware version of 'evalSingle'.
evalSingleWithStore :: Maybe Connection -> Env -> TricuAST -> IO Env
evalSingleWithStore mconn env term
| SDef name [] body <- term = do
res <- evalASTWithEnv mconn env body
case Map.lookup name env of
Just existingValue
| existingValue == res -> return env
| otherwise -> return $ Map.insert "!result" res (Map.insert name res env)
Nothing -> return $ Map.insert "!result" res (Map.insert name res env)
| otherwise = do
res <- evalASTWithEnv mconn env term
return $ Map.insert "!result" res env
-- | Store-aware version of 'evalTricu'. Does not preload the entire
-- content store; terms are resolved on demand as variables are
-- encountered.
evalTricuWithStore :: Maybe Connection -> Env -> [TricuAST] -> IO Env
evalTricuWithStore mconn env x = go env (reorderDefs env x)
where
go env' [] = return env'
go env' [def] = do
updatedEnv <- evalSingleWithStore mconn env' def
return $ Map.insert "!result" (result updatedEnv) updatedEnv
go env' (def:xs) = do
updatedEnv <- evalSingleWithStore mconn env' def
evalTricuWithStore mconn updatedEnv xs
collectVarNames :: TricuAST -> [(String, Maybe String)] collectVarNames :: TricuAST -> [(String, Maybe String)]
collectVarNames = go [] collectVarNames = go []
where where

View File

@@ -2,21 +2,22 @@ module FileEval
( preprocessFile ( preprocessFile
, evaluateFile , evaluateFile
, evaluateFileWithContext , evaluateFileWithContext
, evaluateFileWithStore
, evaluateFileResult , evaluateFileResult
, compileFile , compileFile
) where ) where
import Eval (evalTricu) import Eval (evalTricu, evalTricuWithStore)
import Lexer import Lexer
import Parser import Parser
import Research import Research
import ContentStore (initContentStore, storeTerm, hashTerm) import ContentStore (newContentStore, storeTerm, hashTerm)
import Database.SQLite.Simple (Connection)
import Wire (exportNamedBundle, defaultExportNames) import Wire (exportNamedBundle, defaultExportNames)
import Control.Monad (forM_) import Control.Monad (forM_)
import Data.List (partition) import Data.List (partition)
import Data.Maybe (mapMaybe) import Data.Maybe (mapMaybe)
import System.Environment (setEnv)
import System.FilePath (takeDirectory, normalise, (</>)) import System.FilePath (takeDirectory, normalise, (</>))
import System.Exit (die) import System.Exit (die)
import Database.SQLite.Simple (close) import Database.SQLite.Simple (close)
@@ -79,6 +80,18 @@ evaluateFileWithContext env filePath = do
ast <- preprocessFile filePath ast <- preprocessFile filePath
pure $ evalTricu env ast pure $ evalTricu env ast
-- | File evaluation that lazily resolves missing names from the
-- content store instead of pre-loading the entire store into memory.
evaluateFileWithStore :: Maybe Connection -> Env -> FilePath -> IO Env
evaluateFileWithStore mconn env filePath = do
contents <- readFile filePath
let tokens = lexTricu contents
case parseProgram tokens of
Left err -> errorWithoutStackTrace (handleParseError err)
Right _ast -> do
ast <- preprocessFile filePath
evalTricuWithStore mconn env ast
preprocessFile :: FilePath -> IO [TricuAST] preprocessFile :: FilePath -> IO [TricuAST]
preprocessFile p = preprocessFile' Set.empty p p preprocessFile p = preprocessFile' Set.empty p p
@@ -181,9 +194,8 @@ compileFile inputPath outputPath maybeNames = do
$ map (\(n,t) -> (T.pack n, t)) compiledTerms $ map (\(n,t) -> (T.pack n, t)) compiledTerms
compiledNames :: [T.Text] = Map.keys compiledMap compiledNames :: [T.Text] = Map.keys compiledMap
compiledTermsList :: [T] = Map.elems compiledMap compiledTermsList :: [T] = Map.elems compiledMap
-- Create a temp content store -- Create a temp in-memory content store
setEnv "TRICU_DB_PATH" "/tmp/tricu-compile.db" conn <- newContentStore
conn <- initContentStore
-- Store each term in the temp store under its requested name -- Store each term in the temp store under its requested name
forM_ (zip compiledNames compiledTermsList) $ \(n, t) -> forM_ (zip compiledNames compiledTermsList) $ \(n, t) ->
storeTerm conn [T.unpack n] t storeTerm conn [T.unpack n] t

View File

@@ -1,280 +1,374 @@
module Main where module Main where
import ContentStore (initContentStore, loadEnvironment, loadTerm, resolveExportTarget) import ContentStore (initContentStoreWithPath, loadEnvironment, loadTerm, resolveExportTarget)
import System.Exit (die) import System.Exit (die)
import Server (runServer) import Server (runServerWithPath)
import Eval (evalTricu, mainResult, result) import Eval (evalTricu, evalTricuWithStore, mainResult, result)
import FileEval import FileEval (evaluateFileWithContext, evaluateFileWithStore, compileFile)
import Parser (parseTricu) import Parser (parseTricu)
import REPL import REPL (repl)
import Research import Research (T, EvaluatedForm(..), Env, formatT, exportDag)
import Wire import Wire (exportNamedBundle, defaultExportNames, importBundle)
import Control.Monad (foldM) import Control.Monad (foldM, unless, when)
import Data.Text (Text, unpack) import Data.Text (unpack, pack)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Version (showVersion) import Data.Version (showVersion)
import Paths_tricu (version) import Paths_tricu (version)
import System.Console.CmdArgs import Options.Applicative
import System.Environment (lookupEnv)
import System.IO (hPutStrLn, stderr)
import Text.Megaparsec ()
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import Database.SQLite.Simple (close) import Database.SQLite.Simple (Connection, close)
import qualified Data.Map as Map import qualified Data.Map as Map
import System.Environment (lookupEnv)
-- ---------------------------------------------------------------------------
-- CLI argument types
-- ---------------------------------------------------------------------------
data TricuArgs data TricuArgs
= Repl = Repl
| Evaluate { file :: [FilePath], form :: EvaluatedForm, outFile :: FilePath } | Eval
| TDecode { file :: [FilePath] } { evalFiles :: [FilePath]
| Compile { inputFile :: FilePath, outFile :: FilePath, names :: [String] } , evalFormat :: EvaluatedForm
| Export { hash :: String, exportNameOpt :: String, outFile :: FilePath, names :: [String] } , evalOutput :: FilePath
| Import { inFile :: FilePath } , evalDb :: Maybe FilePath
| Serve { host :: String, port :: Int } }
| ExportDag { target :: String, outFile :: FilePath } | ArboricxCompile
deriving (Show, Data, Typeable) { 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)
replMode :: TricuArgs -- ---------------------------------------------------------------------------
replMode = Repl -- optparse-applicative parsers
&= help "Start interactive REPL" -- ---------------------------------------------------------------------------
&= auto
&= name "repl"
evaluateMode :: TricuArgs readEvaluatedForm :: ReadM EvaluatedForm
evaluateMode = Evaluate readEvaluatedForm = eitherReader $ \s -> case s of
{ file = def &= help "Input file path(s) for evaluation.\n \ "tree" -> Right Tree
\ Defaults to stdin." "fsl" -> Right FSL
&= name "f" &= typ "FILE" "ast" -> Right AST
, form = TreeCalculus &= typ "FORM" "ternary" -> Right Ternary
&= help "Optional output form: (tree|fsl|ast|ternary|ascii|decode).\n \ "ascii" -> Right Ascii
\ Defaults to tricu-compatible `t` tree form." "decode" -> Right Decode
&= name "t" _ -> Left $ "Unknown format: " ++ s ++ ". Expected: tree, fsl, ast, ternary, ascii, decode"
, outFile = def &= help "Optional output file path. Defaults to stdout."
&= name "o" &= typ "FILE"
}
&= help "Evaluate tricu and return the result of the final expression."
&= explicit
&= name "eval"
decodeMode :: TricuArgs evalParser :: Parser TricuArgs
decodeMode = TDecode evalParser = Eval
{ file = def <$> many (argument str (metavar "FILE..."))
&= help "Optional input file path to attempt decoding.\n \ <*> option readEvaluatedForm
\ Defaults to stdin." ( long "format"
&= name "f" &= typ "FILE" <> short 'f'
} <> metavar "FORM"
&= help "Decode a Tree Calculus value into a string representation." <> value Tree
&= explicit <> help "Output format: tree, fsl, ast, ternary, ascii, decode"
&= name "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"
))
exportMode :: TricuArgs compileParser :: Parser TricuArgs
exportMode = Export compileParser = ArboricxCompile
{ hash = def &= help "Hash or stored term name(s) to export (comma-separated)." <$> option str
&= name "h" &= typ "HASH_OR_NAME" ( long "file"
, exportNameOpt = def &= help "Export name (legacy; use -n NAME for full control)." <> short 'f'
&= name "n" &= typ "NAME" <> metavar "FILE"
, outFile = def &= help "Output file path for the bundle." &= name "o" &= typ "FILE" <> value ""
, names = def &= help "Export name(s) for the bundle manifest (comma-separated or repeated -n)." <> help "Input .tri source file"
&= typ "NAME" )
} <*> option str
&= help "Export a Merkle bundle from the content store." ( long "output"
&= explicit <> short 'o'
&= name "export" <> 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"
))
importMode :: TricuArgs importParser :: Parser TricuArgs
importMode = Import importParser = ArboricxImport
{ inFile = def &= help "Path to the bundle file to import." <$> option str
&= name "f" &= typ "FILE" ( long "file"
} <> short 'f'
&= help "Import a Merkle bundle into the content store." <> metavar "FILE"
&= explicit <> value ""
&= name "import" <> help "Bundle file to import"
)
<*> optional (option str
( long "db"
<> short 'd'
<> metavar "PATH"
<> help "Content store database path"
))
compileMode :: TricuArgs exportParser :: Parser TricuArgs
compileMode = Compile exportParser = ArboricxExport
{ inputFile = def &= help "Path to the tricu source file (.tri) to compile." <$> many (option str
&= name "f" &= typ "FILE" ( long "target"
, outFile = def &= help "Output bundle file path (.tri.bundle)." <> short 't'
&= name "o" &= typ "FILE" <> metavar "TARGET"
, names = def &= help "Definition name(s) to export as bundle roots (comma-separated or repeated -x). Defaults to 'main'." <> help "Target hash or name (repeatable)"
&= name "x" &= typ "NAME" ))
} <*> option str
&= help "Compile a tricu source file into a standalone Arboricx portable bundle." ( long "output"
&= explicit <> short 'o'
&= name "compile" <> 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"
)
serveMode :: TricuArgs serveParser :: Parser TricuArgs
serveMode = Serve serveParser = ArboricxServe
{ host = "127.0.0.1" &= help "Host to bind the server to." &= name "h" &= typ "HOST" <$> option str
, port = 8787 &= help "HTTP port to listen on." &= name "p" &= typ "PORT" ( long "host"
} <> metavar "HOST"
&= help "Start a read-only HTTP server for exporting Arboricx bundles." <> value "127.0.0.1"
&= explicit <> help "Host to bind the server to"
&= name "server" )
<*> 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"
))
exportDagMode :: TricuArgs versionStr :: String
exportDagMode = ExportDag versionStr = "tricu " ++ showVersion version
{ target = def &= help "Stored term name or hash to export as a DAG node table."
&= name "t" &= typ "NAME_OR_HASH" tricuParser :: Parser TricuArgs
, outFile = def &= help "Optional output file path. Defaults to stdout." tricuParser = (subparser topCommands <|> pure Repl)
&= name "o" &= typ "FILE" <**> infoOption versionStr (long "version" <> help "Show version")
} where
&= help "Export a term's Merkle DAG as a topologically-sorted node table for host embedding." topCommands = mconcat
&= explicit [ command "eval" (info (evalParser <**> helper)
&= name "export-dag" (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 :: IO ()
main = do main = do
let versionStr = "tricu Evaluator and REPL " ++ showVersion version args <- execParser $ info (tricuParser <**> helper)
cmdArgsParsed <- cmdArgs $ modes [replMode, evaluateMode, decodeMode, compileMode, exportMode, importMode, serveMode, exportDagMode] ( fullDesc
&= help "tricu: Exploring Tree Calculus" <> progDesc "Exploring Tree Calculus"
&= program "tricu" <> header versionStr
&= summary versionStr )
&= versionArg [explicit, name "version", summary versionStr] case args of
case cmdArgsParsed of Repl -> runRepl
Repl -> do Eval {} -> runEval args
putStrLn "Welcome to the tricu REPL" ArboricxCompile {} -> runCompile args
putStrLn "You may exit with `CTRL+D` or the `!exit` command." ArboricxImport {} -> runImport args
repl ArboricxExport {} -> runExport args
Evaluate { file = filePaths, form = outputForm, outFile = evalOutFile } -> do ArboricxServe {} -> runServe args
maybeDbPath <- lookupEnv "TRICU_DB_PATH"
evalResult <- case filePaths of -- ---------------------------------------------------------------------------
[] -> do -- Command runners
initialEnv <- case maybeDbPath of -- ---------------------------------------------------------------------------
Just _ -> do
conn <- initContentStore runRepl :: IO ()
env <- loadEnvironment conn runRepl = do
close conn putStrLn "Welcome to the tricu REPL"
return env putStrLn "You may exit with `CTRL+D` or the `!exit` command."
Nothing -> return Map.empty repl
input <- getContents
pure $ runTricuTEnv initialEnv input runEval :: TricuArgs -> IO ()
filePaths@(_:_) -> do runEval opts = do
initialEnv <- case maybeDbPath of let files = evalFiles opts
Just _ -> do form = evalFormat opts
conn <- initContentStore out = evalOutput opts
env <- loadEnvironment conn mconn <- case evalDb opts of
close conn Just dbPath -> Just <$> initContentStoreWithPath (Just dbPath)
return env Nothing -> do
Nothing -> return Map.empty mDbPath <- lookupEnv "TRICU_DB_PATH"
finalEnv <- foldM evaluateFileWithContext initialEnv filePaths case mDbPath of
pure $ mainResult finalEnv Just _ -> Just <$> initContentStoreWithPath Nothing
let fRes = formatT outputForm evalResult Nothing -> return Nothing
if null evalOutFile resultT <- case files of
then putStr fRes [] -> do
else writeFile evalOutFile fRes input <- getContents
TDecode { file = filePaths } -> do env <- evalTricuWithStore mconn Map.empty (parseTricu input)
value <- case filePaths of return $ result env
[] -> getContents _ -> do
(filePath:_) -> readFile filePath finalEnv <- foldM (evaluateFileWithStore mconn) Map.empty files
putStrLn $ decodeResult $ result $ evalTricu Map.empty $ parseTricu value return $ mainResult finalEnv
Export { hash = hashStr, exportNameOpt = legacyName, names = namesArg, outFile = outFilePath } -> do case mconn of
conn <- initContentStore Just conn -> close conn
let hashList = T.split (== ',') (T.pack hashStr) Nothing -> return ()
hashes <- mapM (\h -> do writeOutput out (formatT form resultT)
(resolvedHash, _) <- resolveExportTarget conn (T.unpack h)
return resolvedHash) hashList runCompile :: TricuArgs -> IO ()
-- Merge legacy -n and new -n (names); names wins when non-empty runCompile opts = do
let allNames = if null namesArg let input = compileInput opts
then if null legacyName then [] else [legacyName] out = compileOutput opts
else namesArg names = compileNames opts
let expNames = if null allNames when (null out) $ die "tricu arboricx compile: --output is required"
then defaultExportNames (length hashes) when (null input) $ die "tricu arboricx compile: input file is required"
else map T.pack allNames let nameTexts = if null names then [] else map T.pack names
let exports = zip expNames hashes compileFile input out nameTexts
bundleData <- exportNamedBundle conn exports
BL.writeFile outFilePath (BL.fromStrict bundleData) runImport :: TricuArgs -> IO ()
putStrLn $ "Exported bundle with " ++ show (length exports) ++ " export(s) to " ++ outFilePath runImport opts = do
close conn let file = importFile opts
Import { inFile = importFile } -> do when (null file) $ die "tricu arboricx import: input file is required"
conn <- initContentStore withContentStore (importDb opts) $ \conn -> do
bundleData <- BL.readFile importFile bundleData <- BL.readFile file
roots <- importBundle conn (BL.toStrict bundleData) roots <- importBundle conn (BL.toStrict bundleData)
putStrLn $ "Imported " ++ show (length roots) ++ " root(s):" putStrLn $ "Imported " ++ show (length roots) ++ " root(s):"
mapM_ (\r -> putStrLn $ " " ++ unpack r) roots mapM_ (\r -> putStrLn $ " " ++ unpack r) roots
close conn
Compile { inputFile = compileInputFile, outFile = compileOutFile, names = namesArg } -> runExport :: TricuArgs -> IO ()
let exportNames = if null namesArg then [] else map T.pack namesArg runExport opts =
in compileFile compileInputFile compileOutFile exportNames if dag opts
Serve { host = hostStr, port = portNum } -> do then runExportDag opts
putStrLn $ "Starting Arboricx bundle server on " ++ hostStr ++ ":" ++ show portNum else runExportBundle opts
putStrLn $ " GET /bundle/hash/:hash -- primary endpoint"
putStrLn $ " GET /bundle/name/:name -- convenience endpoint" runExportBundle :: TricuArgs -> IO ()
putStrLn $ " Content-Type: application/vnd.arboricx.bundle" runExportBundle opts = do
runServer hostStr portNum let targets = exportTargets opts
ExportDag { target = targetName, outFile = dagOutFile } -> do out = exportOutput opts
conn <- initContentStore names = exportNames opts
maybeTerm <- loadTerm conn targetName when (null out) $ die "tricu arboricx export: --output is required"
close conn when (null targets) $ die "tricu arboricx export: at least one --target is required"
withContentStore (exportDb opts) $ \conn -> do
hashes <- mapM (\t -> do
(h, _) <- resolveExportTarget conn t
return h) targets
let expNames = if null names
then defaultExportNames (length hashes)
else map T.pack names
when (length expNames /= length hashes) $
die "tricu arboricx export: number of --name values must match number of TARGETs"
let exports = zip expNames hashes
bundleData <- exportNamedBundle conn exports
BL.writeFile out (BL.fromStrict bundleData)
putStrLn $ "Exported bundle with " ++ show (length exports) ++ " export(s) to " ++ out
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 case maybeTerm of
Nothing -> die $ "Term not found: " ++ targetName Nothing -> die $ "Term not found: " ++ target
Just term -> do Just term -> do
let (rootIdx, nodes) = exportDag term let (rootIdx, nodes) = Research.exportDag term
output = unlines $ show rootIdx : map (\(tag, refs) -> unwords (tag : map show refs)) nodes output = unlines $
if null dagOutFile show rootIdx :
then putStr output map (\(tag, refs) -> unwords (tag : map show refs)) nodes
else writeFile dagOutFile output writeOutput out output
[] -> die "tricu arboricx export --dag: exactly one --target is required"
_ -> die "tricu arboricx export --dag: exactly one --target is required"
runTricu :: String -> String runServe :: TricuArgs -> IO ()
runTricu = formatT TreeCalculus . runTricuT 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
runTricuT :: String -> T -- ---------------------------------------------------------------------------
runTricuT input = -- Helpers
let asts = parseTricu input -- ---------------------------------------------------------------------------
finalEnv = evalTricu Map.empty asts
in result finalEnv
runTricuEnv :: Env -> String -> String withContentStore :: Maybe FilePath -> (Connection -> IO a) -> IO a
runTricuEnv env = formatT TreeCalculus . runTricuTEnv env 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 -> String -> T
runTricuTEnv env input = runTricuTEnv env input =
let asts = parseTricu input let asts = parseTricu input
finalEnv = evalTricu env asts finalEnv = evalTricu env asts
in result finalEnv in result finalEnv
runTricuWithEnvT :: String -> (Env, T)
runTricuWithEnvT input =
let asts = parseTricu input
finalEnv = evalTricu Map.empty asts
in (finalEnv, result finalEnv)
runTricuWithEnv :: String -> (Env, String)
runTricuWithEnv input =
let asts = parseTricu input
finalEnv = evalTricu Map.empty asts
res = result finalEnv
in (finalEnv, formatT TreeCalculus res)
runTricuEnvWithEnvT :: Env -> String -> (Env, T)
runTricuEnvWithEnvT env input =
let asts = parseTricu input
finalEnv = evalTricu env asts
in (finalEnv, result finalEnv)
runTricuEnvWithEnv :: Env -> String -> (Env, String)
runTricuEnvWithEnv env input =
let asts = parseTricu input
finalEnv = evalTricu env asts
res = result finalEnv
in (finalEnv, formatT TreeCalculus res)
chooseExportName :: String -> String -> [Text] -> IO Text
chooseExportName explicitName input storedNames
| not (null explicitName) = return $ T.pack explicitName
| Just firstName <- firstNonEmpty storedNames = return firstName
| otherwise = do
hPutStrLn stderr $
"No stored name found for export target " ++ input ++ "; using export name 'root'. "
++ "Use export -n NAME to preserve a semantic name."
return "root"
firstNonEmpty :: [Text] -> Maybe Text
firstNonEmpty = go
where
go [] = Nothing
go (x:xs)
| T.null x = go xs
| otherwise = Just x

View File

@@ -130,7 +130,7 @@ repl = do
handleOutput :: REPLState -> InputT IO () handleOutput :: REPLState -> InputT IO ()
handleOutput state = do handleOutput state = do
let formats = [Decode, TreeCalculus, FSL, AST, Ternary, Ascii] let formats = [Decode, Tree, FSL, AST, Ternary, Ascii]
outputStrLn "Available output formats:" outputStrLn "Available output formats:"
mapM_ (\(i, f) -> outputStrLn $ show (i :: Int) ++ ". " ++ show f) mapM_ (\(i, f) -> outputStrLn $ show (i :: Int) ++ ". " ++ show f)
(zip [1..] formats) (zip [1..] formats)

View File

@@ -8,8 +8,6 @@ import Data.Map ()
import Data.Text (Text, replace) import Data.Text (Text, replace)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Word (Word8) import Data.Word (Word8)
import System.Console.CmdArgs (Data, Typeable)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
@@ -55,8 +53,8 @@ data LToken
deriving (Eq, Show, Ord) deriving (Eq, Show, Ord)
-- Output formats -- Output formats
data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode data EvaluatedForm = Tree | FSL | AST | Ternary | Ascii | Decode
deriving (Show, Data, Typeable) deriving (Show)
-- Environment containing previously evaluated TC terms -- Environment containing previously evaluated TC terms
type Env = Map.Map String T type Env = Map.Map String T
@@ -243,7 +241,7 @@ toList _ = Left "Invalid Tree Calculus list"
-- Outputs -- Outputs
formatT :: EvaluatedForm -> T -> String formatT :: EvaluatedForm -> T -> String
formatT TreeCalculus = toSimpleT . show formatT Tree = toSimpleT . show
formatT FSL = show formatT FSL = show
formatT AST = show . toAST formatT AST = show . toAST
formatT Ternary = toTernaryString formatT Ternary = toTernaryString
@@ -289,7 +287,7 @@ decodeResult tc =
(_, _, Right n) -> show n (_, _, Right n) -> show n
(_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]" (_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]"
(_, Right [], _) -> "[]" (_, Right [], _) -> "[]"
_ -> formatT TreeCalculus tc _ -> formatT Tree tc
where where
isCommonChar c = isCommonChar c =
let n = fromEnum c let n = fromEnum c

View File

@@ -1,15 +1,15 @@
module Server module Server
( runServer ( runServer
, runServerWithPath
) where ) where
import ContentStore (initContentStore, nameToTerm, hashToTerm, listStoredTerms, import ContentStore (initContentStore, initContentStoreWithPath, nameToTerm, hashToTerm, listStoredTerms,
parseNameList, StoredTerm(..), termHash) parseNameList, StoredTerm(..), termHash)
import Database.SQLite.Simple (close) import Database.SQLite.Simple (Connection, close)
import Wire (exportNamedBundle) import Wire (exportNamedBundle)
import Control.Monad (when) import Control.Monad (when, void)
import Data.Maybe (catMaybes) import Data.Maybe (catMaybes)
import Control.Monad (void)
import Network.HTTP.Types (Header, Status, status200, status400, status404, status405, hContentType) import Network.HTTP.Types (Header, Status, status200, status400, status404, status405, hContentType)
import Network.Wai import Network.Wai
@@ -25,48 +25,36 @@ import qualified Data.Text as T
-- | Start an HTTP server that serves Arboricx bundles from the -- | Start an HTTP server that serves Arboricx bundles from the
-- local content store. -- local content store.
--
-- This is a read-only export surface. Clients fetch bundle bytes
-- and independently inspect / verify / run them. The server does
-- not execute bundles.
--
-- Bind host defaults to @127.0.0.1@.
--
-- Endpoints
-- ---------
-- GET /health - 200 "ok"
-- GET /bundle/name/:name - export single term by name
-- GET /bundle/hash/:hash - export single term by hash
-- GET /bundle/roots?n=...&h=... - export multiple roots (n=name, h=hash)
-- GET /terms - plain-text listing (debug)
--
runServer :: String -> Int -> IO () runServer :: String -> Int -> IO ()
runServer hostStr port = runServer = runServerWithPath Nothing
runSettings settings app
-- | Start an HTTP server with an explicit database path.
runServerWithPath :: Maybe FilePath -> String -> Int -> IO ()
runServerWithPath mDbPath hostStr port =
runSettings settings (app mkConn)
where where
mkConn = initContentStoreWithPath mDbPath
settings = setPort port $ setHost (fromString hostStr) defaultSettings settings = setPort port $ setHost (fromString hostStr) defaultSettings
-- | WAI application backed by the content store. -- | WAI application backed by the content store.
-- Uses the same database path as @eval@ mode (env var app :: IO Connection -> Application
-- @TRICU_DB_PATH@ or the default location). app mkConn request respond = case (requestMethod request, pathInfo request) of
app :: Application
app request respond = case (requestMethod request, pathInfo request) of
("GET", ["health"]) -> ("GET", ["health"]) ->
respond $ healthResponse respond $ healthResponse
("GET", ["bundle", "roots"]) -> ("GET", ["bundle", "roots"]) ->
rootsHandler request respond rootsHandler mkConn request respond
("GET", ["bundle", "name", nameText]) -> do ("GET", ["bundle", "name", nameText]) -> do
body <- nameHandler nameText body <- nameHandler mkConn nameText
respond body respond body
("GET", ["bundle", "hash", hashText]) -> do ("GET", ["bundle", "hash", hashText]) -> do
body <- hashHandler hashText body <- hashHandler mkConn hashText
respond body respond body
("GET", ["terms"]) -> do ("GET", ["terms"]) -> do
body <- termsResponse body <- termsResponse mkConn
respond body respond body
("POST", _) -> ("POST", _) ->
@@ -85,18 +73,9 @@ healthResponse :: Response
healthResponse = responseLBS status200 [] "ok" healthResponse = responseLBS status200 [] "ok"
-- | GET /bundle/roots?n=root&n=helper&h=abc123... -- | GET /bundle/roots?n=root&n=helper&h=abc123...
-- Resolve multiple named roots (by stored term name or raw hash) rootsHandler :: IO Connection -> Request -> (Response -> IO a) -> IO a
-- and return a single bundle containing all of them. rootsHandler mkConn request respond = do
-- conn <- mkConn
-- Query parameters:
-- - @n=<name>@ — one or more stored term names (resolved via nameToTerm)
-- - @h=<hash>@ — one or more full Merkle hashes (validated as 16-64 hex chars)
--
-- The bundle manifest receives all resolved (name, hash) pairs as roots
-- and exports. The node section is the union of all reachable nodes.
rootsHandler :: Request -> (Response -> IO a) -> IO a
rootsHandler request respond = do
conn <- initContentStore
let qs = queryString request let qs = queryString request
nParams = catMaybes [v | (k, v) <- qs, map toLower (unpack k) == "n"] nParams = catMaybes [v | (k, v) <- qs, map toLower (unpack k) == "n"]
hParams = catMaybes [v | (k, v) <- qs, map toLower (unpack k) == "h"] hParams = catMaybes [v | (k, v) <- qs, map toLower (unpack k) == "h"]
@@ -133,14 +112,9 @@ rootsHandler request respond = do
(fromStrict bundleData) (fromStrict bundleData)
-- | GET /bundle/name/:name -- | GET /bundle/name/:name
-- Resolve a stored term name, export it as an Arboricx bundle, nameHandler :: IO Connection -> Text -> IO Response
-- and return the raw bundle bytes. nameHandler mkConn nameText = do
-- conn <- mkConn
-- Sets @Content-Type@ and @X-Arboricx-Root-Hash@ headers.
-- Returns 404 when the name does not resolve to any stored term.
nameHandler :: Text -> IO Response
nameHandler nameText = do
conn <- initContentStore
stored <- nameToTerm conn nameText stored <- nameToTerm conn nameText
case stored of case stored of
Nothing -> do Nothing -> do
@@ -155,19 +129,13 @@ nameHandler nameText = do
return $ responseLBS status200 (bundleHeaders th cd) (fromStrict bundleData) return $ responseLBS status200 (bundleHeaders th cd) (fromStrict bundleData)
-- | GET /bundle/hash/:hash -- | GET /bundle/hash/:hash
-- Resolve a full Merkle hash and export the root as an Arboricx hashHandler :: IO Connection -> Text -> IO Response
-- bundle. hashHandler mkConn hashText =
--
-- - Malformed hash (non-hex or < 16 chars): 400
-- - Well-formed but absent: 404
-- - Present: 200 with bundle bytes
hashHandler :: Text -> IO Response
hashHandler hashText =
let raw = T.pack (dropWhile (== '#') (T.unpack hashText)) let raw = T.pack (dropWhile (== '#') (T.unpack hashText))
in if not (T.all isHexDigit raw) || T.length raw < 16 in if not (T.all isHexDigit raw) || T.length raw < 16
then return $ responseLBS status400 [] "400 Bad Request: invalid hash" then return $ responseLBS status400 [] "400 Bad Request: invalid hash"
else do else do
conn <- initContentStore conn <- mkConn
stored <- hashToTerm conn raw stored <- hashToTerm conn raw
case stored of case stored of
Nothing -> do Nothing -> do
@@ -183,10 +151,9 @@ hashHandler hashText =
(fromStrict bundleData) (fromStrict bundleData)
-- | GET /terms -- | GET /terms
-- Plain-text listing of all stored terms (debugging only). termsResponse :: IO Connection -> IO Response
termsResponse :: IO Response termsResponse mkConn = do
termsResponse = do conn <- mkConn
conn <- initContentStore
terms <- listStoredTerms conn terms <- listStoredTerms conn
close conn close conn
let lines' = [ names <> " " <> hash <> " " <> T.pack (show created) let lines' = [ names <> " " <> hash <> " " <> T.pack (show created)
@@ -212,14 +179,12 @@ bundleHeaders root cd =
, ("Content-Disposition", encodeUtf8 cd) , ("Content-Disposition", encodeUtf8 cd)
] ]
-- | Pick the first stored name, falling back to "root" when names are empty.
firstOrRoot :: Text -> Text firstOrRoot :: Text -> Text
firstOrRoot names = firstOrRoot names =
case parseNameList names of case parseNameList names of
[] -> "root" [] -> "root"
(x:_) -> x (x:_) -> x
-- | Sanitise a string to a safe filename prefix.
safeFileName :: String -> String safeFileName :: String -> String
safeFileName = map go safeFileName = map go
where where

View File

@@ -17,7 +17,6 @@ executable tricu
hs-source-dirs: hs-source-dirs:
src src
default-extensions: default-extensions:
DeriveDataTypeable
LambdaCase LambdaCase
MultiWayIf MultiWayIf
OverloadedStrings OverloadedStrings
@@ -41,7 +40,7 @@ executable tricu
, base16-bytestring , base16-bytestring
, base64-bytestring , base64-bytestring
, bytestring , bytestring
, cmdargs , optparse-applicative
, containers , containers
, cryptonite , cryptonite
, directory , directory
@@ -82,7 +81,6 @@ test-suite tricu-tests
main-is: Spec.hs main-is: Spec.hs
hs-source-dirs: test, src hs-source-dirs: test, src
default-extensions: default-extensions:
DeriveDataTypeable
LambdaCase LambdaCase
MultiWayIf MultiWayIf
OverloadedStrings OverloadedStrings
@@ -93,7 +91,7 @@ test-suite tricu-tests
, base16-bytestring , base16-bytestring
, base64-bytestring , base64-bytestring
, bytestring , bytestring
, cmdargs , optparse-applicative
, containers , containers
, cryptonite , cryptonite
, directory , directory