Compare commits

...

1 Commits

Author SHA1 Message Date
813e880ed7 Vibe coded initial content store
Dropped networking features
2025-04-25 12:43:52 -05:00
11 changed files with 1391 additions and 204 deletions

View File

@ -1,5 +1,5 @@
!import "base.tri" !Local
!import "list.tri" List
!import "list.tri" !Local
match_ = y (self value patterns :
triage
@ -17,8 +17,8 @@ match_ = y (self value patterns :
patterns)
match = (value patterns :
match_ value (List.map (sublist :
pair (List.head sublist) (List.head (List.tail sublist)))
match_ value (map (sublist :
pair (head sublist) (head (tail sublist)))
patterns))
otherwise = const (t t)

415
src/ContentStore.hs Normal file
View File

@ -0,0 +1,415 @@
module ContentStore where
import Research
import Parser
import Control.Exception (catch, IOException)
import Control.Monad (foldM, forM)
import Control.Monad.IO.Class (liftIO)
import Crypto.Hash (hash, SHA256, Digest)
import Data.ByteString (ByteString)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Database.SQLite.Simple
import System.Directory (createDirectoryIfMissing, getXdgDirectory, XdgDirectory(..))
import System.FilePath ((</>), takeDirectory)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map as Map
import qualified Data.Serialize as Cereal
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Database.SQLite.Simple.FromRow (FromRow(..), field)
import Network.HTTP.Simple (httpJSON, parseRequest, getResponseBody, getResponseStatus)
import Network.HTTP.Types.Status (statusCode)
import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), Value)
import qualified Data.Aeson as Aeson
import Network.Wai (Application, responseLBS, requestMethod, pathInfo, lazyRequestBody)
import Network.Wai.Handler.Warp (run)
import Network.HTTP.Types (status200, status404, status400, methodGet, methodPost)
import System.Environment (lookupEnv)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.List (intercalate)
data StoredTerm = StoredTerm
{ termHash :: Text
, termNames :: Text
, termData :: ByteString
, termMetadata :: Text
, termCreatedAt :: Integer
, termTags :: Text
} deriving (Show)
instance FromRow StoredTerm where
fromRow = StoredTerm <$> field <*> field <*> field <*> field <*> field <*> field
initContentStore :: IO Connection
initContentStore = do
dbPath <- getContentStorePath
createDirectoryIfMissing True (takeDirectory dbPath)
conn <- open dbPath
execute_ conn "CREATE TABLE IF NOT EXISTS terms (\
\hash TEXT PRIMARY KEY, \
\names TEXT, \
\term_data BLOB, \
\metadata TEXT, \
\created_at INTEGER DEFAULT (strftime('%s','now')), \
\tags TEXT DEFAULT '')"
execute_ conn "CREATE INDEX IF NOT EXISTS terms_names_idx ON terms(names)"
execute_ conn "CREATE INDEX IF NOT EXISTS terms_tags_idx ON terms(tags)"
return conn
getContentStorePath :: IO FilePath
getContentStorePath = do
dataDir <- getXdgDirectory XdgData "tricu"
return $ dataDir </> "content-store.db"
instance Cereal.Serialize T where
put Leaf = Cereal.putWord8 0
put (Stem t) = do
Cereal.putWord8 1
Cereal.put t
put (Fork a b) = do
Cereal.putWord8 2
Cereal.put a
Cereal.put b
get = do
tag <- Cereal.getWord8
case tag of
0 -> return Leaf
1 -> Stem <$> Cereal.get
2 -> Fork <$> Cereal.get <*> Cereal.get
_ -> fail $ "Invalid tag for T: " ++ show tag
serializeTerm :: T -> ByteString
serializeTerm = LBS.toStrict . Cereal.encodeLazy
deserializeTerm :: ByteString -> Either String T
deserializeTerm = Cereal.decodeLazy . LBS.fromStrict
hashTerm :: T -> Text
hashTerm = T.pack . show . (hash :: ByteString -> Digest SHA256) . serializeTerm
storeTerm :: Connection -> [String] -> T -> IO Text
storeTerm conn names term = do
let termBS = serializeTerm term
termHashText = hashTerm term
namesText = T.pack $ intercalate "," names
metadataText = T.pack "{}"
tagsText = T.pack ""
existing <- query conn
"SELECT hash FROM terms WHERE hash = ?"
(Only termHashText) :: IO [Only Text]
case existing of
[] -> execute conn
"INSERT INTO terms (hash, names, term_data, metadata, tags) VALUES (?, ?, ?, ?, ?)"
(termHashText, namesText, termBS, metadataText, tagsText)
_ -> execute conn
"UPDATE terms SET names = ?, metadata = ? WHERE hash = ?"
(namesText, metadataText, termHashText)
return termHashText
getTermByHash :: Connection -> Text -> IO (Maybe StoredTerm)
getTermByHash conn hashText = do
results <- query conn
"SELECT hash, names, term_data, metadata, created_at, tags FROM terms WHERE hash = ?"
(Only hashText)
case results of
[term] -> return $ Just term
_ -> return Nothing
getTermByName :: Connection -> Text -> IO (Maybe StoredTerm)
getTermByName conn nameText = do
results <- query conn
"SELECT hash, names, term_data, metadata, created_at, tags FROM terms WHERE names = ? ORDER BY created_at DESC LIMIT 1"
(Only nameText)
case results of
[term] -> return $ Just term
_ -> return Nothing
listStoredTerms :: Connection -> IO [StoredTerm]
listStoredTerms conn = do
query_ conn "SELECT hash, names, term_data, metadata, created_at, tags FROM terms ORDER BY created_at DESC"
storeEnvironment :: Connection -> Env -> IO [(String, Text)]
storeEnvironment conn env = do
let defs = Map.toList $ Map.delete "!result" env
-- Group definitions by their tree form
let groupedDefs = Map.toList $ Map.fromListWith (++) [(term, [name]) | (name, term) <- defs]
-- Store each unique tree form with all its names
forM groupedDefs $ \(term, namesList) -> do
-- namesList is [String], which is what storeTerm expects
hash <- storeTerm conn namesList term
-- Return the first name and hash for backward compatibility
return (head namesList, hash)
-- API types for remote term lookup
data TermResponse = TermResponse {
respHash :: Text,
respNames :: Text,
respData :: ByteString,
respMetadata :: Text,
respCreatedAt :: Integer,
respTags :: Text
}
instance FromJSON TermResponse where
parseJSON = Aeson.withObject "TermResponse" $ \v -> TermResponse
<$> v .: "hash"
<*> v .: "names"
<*> (TE.encodeUtf8 <$> v .: "data")
<*> v .: "metadata"
<*> v .: "created_at"
<*> v .: "tags"
instance ToJSON StoredTerm where
toJSON term = object [
"hash" .= termHash term,
"names" .= termNames term,
"data" .= TE.decodeUtf8 (termData term),
"metadata" .= termMetadata term,
"created_at" .= termCreatedAt term,
"tags" .= termTags term
]
instance FromJSON StoredTerm where
parseJSON = Aeson.withObject "StoredTerm" $ \v -> StoredTerm
<$> v .: "hash"
<*> v .: "names"
<*> (TE.encodeUtf8 <$> v .: "data")
<*> v .: "metadata"
<*> v .: "created_at"
<*> v .: "tags"
-- Try to fetch a term from a remote server if it's not found locally
fetchRemoteTerm :: String -> String -> IO (Maybe StoredTerm)
fetchRemoteTerm serverUrl identifier = do
let isFullHash = '#' `elem` identifier && length (drop 1 identifier) == 64
isName = not ('#' `elem` identifier)
if not (isFullHash || isName)
then do
putStrLn "Cannot fetch remote term: incomplete hash provided"
return Nothing
else do
let url = if isFullHash
then serverUrl ++ "/term/hash/" ++ drop 1 identifier
else serverUrl ++ "/term/name/" ++ identifier
request <- parseRequest url
response <- httpJSON request `catch` \(_ :: IOException) -> do
putStrLn $ "Failed to connect to remote server: " ++ serverUrl
return undefined -- This will be caught by the pattern match below
case statusCode (getResponseStatus response) of
200 -> do
let termResp = getResponseBody response :: TermResponse
return $ Just $ StoredTerm
(respHash termResp)
(respNames termResp)
(respData termResp)
(respMetadata termResp)
(respCreatedAt termResp)
(respTags termResp)
_ -> return Nothing
-- Modified loadTerm to try remote lookup if term not found locally
loadTerm :: Connection -> String -> IO (Maybe T)
loadTerm conn identifier = do
result <- loadLocalTerm conn identifier
case result of
Just t -> return $ Just t
Nothing -> do
serverUrl <- lookupEnv "TRICU_SERVER"
case serverUrl of
Just url -> do
putStrLn $ "Term not found locally, trying remote server: " ++ url
remoteTerm <- fetchRemoteTerm url identifier
case remoteTerm of
Just term -> do
-- Store the term locally for future use
execute conn
"INSERT INTO terms (hash, names, term_data, metadata, created_at, tags) VALUES (?, ?, ?, ?, ?, ?)"
(termHash term, termNames term, termData term, termMetadata term, termCreatedAt term, termTags term)
deserializeStoredTerm term
Nothing -> return Nothing
Nothing -> return Nothing
where
loadLocalTerm conn identifier = do
if '#' `elem` identifier
then do
let hashText = T.pack $ drop 1 identifier
storedTerm <- getTermByHash conn hashText
case storedTerm of
Just term -> deserializeStoredTerm term
Nothing -> return Nothing
else do
storedTerm <- getTermByName conn (T.pack identifier)
case storedTerm of
Just term -> deserializeStoredTerm term
Nothing -> return Nothing
deserializeStoredTerm term =
case deserializeTerm (termData term) of
Right t -> return $ Just t
Left err -> do
putStrLn $ "Error deserializing term: " ++ err
return Nothing
-- Server implementation
runServer :: Int -> IO ()
runServer port = do
conn <- initContentStore
putStrLn $ "Starting tricu server on port " ++ show port
run port (app conn)
app :: Connection -> Application
app conn req respond = do
case (requestMethod req, pathInfo req) of
-- Get term by hash
(method, ["term", "hash", hash]) | method == methodGet -> do
result <- getTermByHash conn hash
case result of
Just term -> respond $ responseLBS status200 [("Content-Type", "application/json")]
$ Aeson.encode term
Nothing -> respond $ responseLBS status404 [("Content-Type", "application/json")]
$ "{\"error\": \"Term not found\"}"
-- Get term by name
(method, ["term", "name", name]) | method == methodGet -> do
result <- getTermByName conn name
case result of
Just term -> respond $ responseLBS status200 [("Content-Type", "application/json")]
$ Aeson.encode term
Nothing -> respond $ responseLBS status404 [("Content-Type", "application/json")]
$ "{\"error\": \"Term not found\"}"
-- Store a new term
(method, ["term"]) | method == methodPost -> do
body <- lazyRequestBody req
case Aeson.decode body :: Maybe StoredTerm of
Just term -> do
-- Update the timestamp
currentTime <- round <$> getPOSIXTime
let updatedTerm = term { termCreatedAt = currentTime }
execute conn
"INSERT INTO terms (hash, names, term_data, metadata, created_at, tags) VALUES (?, ?, ?, ?, ?, ?)"
(termHash updatedTerm, termNames updatedTerm, termData updatedTerm, termMetadata updatedTerm, termCreatedAt updatedTerm, termTags updatedTerm)
respond $ responseLBS status200 [("Content-Type", "application/json")]
$ "{\"status\": \"success\", \"hash\": \"" <> LBS.fromStrict (TE.encodeUtf8 (termHash updatedTerm)) <> "\"}"
Nothing -> respond $ responseLBS status400 [("Content-Type", "application/json")]
$ "{\"error\": \"Invalid request format\"}"
-- List all terms
(method, ["terms"]) | method == methodGet -> do
terms <- listStoredTerms conn
respond $ responseLBS status200 [("Content-Type", "application/json")]
$ Aeson.encode terms
-- Get terms by tag
(method, ["terms", "tag", tag]) | method == methodGet -> do
terms <- getTermsByTag conn tag
respond $ responseLBS status200 [("Content-Type", "application/json")]
$ Aeson.encode terms
-- Default response for unknown routes
_ -> respond $ responseLBS status404 [("Content-Type", "application/json")]
$ "{\"error\": \"Not found\"}"
loadEnvironment :: Connection -> IO Env
loadEnvironment conn = do
terms <- listStoredTerms conn
foldM addTermToEnv Map.empty terms
where
addTermToEnv env term =
case deserializeTerm (termData term) of
Right t -> do
-- Split the names and add each one to the environment
let namesList = T.splitOn "," (termNames term)
return $ foldl (\e name -> Map.insert (T.unpack name) t e) env namesList
Left _ -> return env
getTermVersions :: Connection -> String -> IO [(Text, T, Integer)]
getTermVersions conn name = do
let nameText = T.pack name
results <- query conn
"SELECT hash, term_data, created_at FROM terms WHERE names LIKE ? ORDER BY created_at DESC"
(Only $ "%" <> nameText <> "%")
catMaybes <$> mapM (\(hash, termData, timestamp) ->
case deserializeTerm termData of
Right t -> return $ Just (hash, t, timestamp)
Left err -> do
putStrLn $ "Error deserializing term: " ++ err
return Nothing) results
-- Set a tag for a term
setTag :: Connection -> Text -> Text -> IO ()
setTag conn hash tagValue = do
-- Check if the term exists
exists <- termExists conn hash
if exists
then do
-- Get current tags
currentTags <- query conn "SELECT tags FROM terms WHERE hash = ?" (Only hash) :: IO [Only Text]
case currentTags of
[Only tags] -> do
let tagsList = filter (not . T.null) $ T.splitOn "," tags
newTags = if tagValue `elem` tagsList
then tags -- Tag already exists
else if T.null tags
then tagValue
else tags <> "," <> tagValue
-- Update tags
execute conn "UPDATE terms SET tags = ? WHERE hash = ?" (newTags, hash)
_ -> putStrLn $ "Term with hash " ++ T.unpack hash ++ " not found"
else
putStrLn $ "Term with hash " ++ T.unpack hash ++ " does not exist"
-- Check if a term exists
termExists :: Connection -> Text -> IO Bool
termExists conn hash = do
results <- query conn "SELECT 1 FROM terms WHERE hash = ? LIMIT 1" (Only hash) :: IO [[Int]]
return $ not (null results)
-- Get all tags for a term
getTagsForTerm :: Connection -> Text -> IO [Text]
getTagsForTerm conn hash = do
tags <- query conn "SELECT tags FROM terms WHERE hash = ?" (Only hash) :: IO [Only Text]
case tags of
[Only tagsText] -> return $ filter (not . T.null) $ T.splitOn "," tagsText
_ -> return []
-- Get all terms with a specific tag
getTermsByTag :: Connection -> Text -> IO [StoredTerm]
getTermsByTag conn tagValue = do
-- Use LIKE with pattern matching to find terms with the tag
-- We need to match ",tag," or "tag," or ",tag" or just "tag" if it's the only tag
let pattern = "%," <> tagValue <> ",%"
pattern2 = tagValue <> ",%"
pattern3 = "%," <> tagValue
exactMatch = tagValue
query conn "SELECT hash, names, term_data, metadata, created_at, tags \
\FROM terms \
\WHERE tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ? \
\ORDER BY created_at DESC"
(pattern, pattern2, pattern3, exactMatch)
-- Get all terms with any tag
getTaggedTerms :: Connection -> IO [StoredTerm]
getTaggedTerms conn = do
query_ conn "SELECT hash, names, term_data, metadata, created_at, tags \
\FROM terms \
\WHERE tags != '' \
\ORDER BY created_at DESC"

View File

@ -1,35 +1,42 @@
module Eval where
import ContentStore
import Parser
import Research
import Control.Monad (forM_, foldM)
import Data.List (partition, (\\))
import Data.Map (Map)
import Database.SQLite.Simple
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.List (foldl')
evalSingle :: Env -> TricuAST -> Env
evalSingle env term
| SDef name [] body <- term
= case Map.lookup name env of
Just existingValue
| existingValue == evalAST env body -> env
| otherwise -> errorWithoutStackTrace $
"Unable to rebind immutable identifier: " ++ name
Nothing ->
let res = evalAST env body
in Map.insert "!result" res (Map.insert name res env)
| existingValue == evalASTSync env body -> env
| otherwise
-> let res = evalASTSync env body
in Map.insert "!result" res (Map.insert name res env)
Nothing
-> let res = evalASTSync env body
in Map.insert "!result" res (Map.insert name res env)
| SApp func arg <- term
= let res = apply (evalAST env func) (evalAST env arg)
in Map.insert "!result" res env
| SVar name <- term
= let res = apply (evalASTSync env func) (evalASTSync env arg)
in Map.insert "!result" res env
| SVar name Nothing <- term
= case Map.lookup name env of
Just v -> Map.insert "!result" v env
Nothing ->
errorWithoutStackTrace $ "Variable `" ++ name ++ "` not defined\n\
\This error should never occur here. Please report this as an issue."
Just v -> Map.insert "!result" v env
Nothing -> errorWithoutStackTrace $ "Variable " ++ name ++ " not defined"
| SVar name (Just hash) <- term
= errorWithoutStackTrace $ "Hash-specific variable lookup not supported in local evaluation: " ++ name ++ "#" ++ hash
| otherwise
= Map.insert "!result" (evalAST env term) env
= let res = evalASTSync env term
in Map.insert "!result" res env
evalTricu :: Env -> [TricuAST] -> Env
evalTricu env x = go env (reorderDefs env x)
@ -41,23 +48,107 @@ evalTricu env x = go env (reorderDefs env x)
go env (x:xs) =
evalTricu (evalSingle env x) xs
evalAST :: Env -> TricuAST -> T
evalAST env term
| SLambda _ _ <- term = evalAST env (elimLambda term)
| SVar name <- term = evalVar name
| TLeaf <- term = Leaf
| TStem t <- term = Stem (evalAST env t)
| TFork t u <- term = Fork (evalAST env t) (evalAST env u)
| SApp t u <- term = apply (evalAST env t) (evalAST env u)
| SStr s <- term = ofString s
| SInt n <- term = ofNumber n
| SList xs <- term = ofList (map (evalAST env) xs)
| SEmpty <- term = Leaf
| otherwise = errorWithoutStackTrace "Unexpected AST term"
where
evalVar name = Map.findWithDefault
(errorWithoutStackTrace $ "Variable " ++ name ++ " not defined")
name env
-- Pure evaluation function that doesn't depend on IO
evalASTSync :: Env -> TricuAST -> T
evalASTSync env term = case term of
SLambda _ _ -> evalASTSync env (elimLambda term)
SVar name Nothing -> case Map.lookup name env of
Just v -> v
Nothing -> errorWithoutStackTrace $ "Variable " ++ name ++ " not defined"
SVar name (Just hash) ->
-- In pure evaluation, we can only look up variables that are in the environment
case Map.lookup (name ++ "#" ++ hash) env of
Just v -> v
Nothing -> errorWithoutStackTrace $
"Variable " ++ name ++ " with hash " ++ hash ++ " not found in environment"
TLeaf -> Leaf
TStem t -> Stem (evalASTSync env t)
TFork t u -> Fork (evalASTSync env t) (evalASTSync env u)
SApp t u -> apply (evalASTSync env t) (evalASTSync env u)
SStr s -> ofString s
SInt n -> ofNumber n
SList xs -> ofList (map (evalASTSync env) xs)
SEmpty -> Leaf
_ -> errorWithoutStackTrace $ "Unexpected AST term: " ++ show term
-- IO wrapper that resolves terms from the database before evaluation
evalAST :: Maybe Connection -> Map.Map String T.Text -> TricuAST -> IO T
evalAST mconn selectedVersions ast = do
-- First, collect all variable names that need to be resolved
let varNames = collectVarNames ast
-- Resolve all needed variables from the database
resolvedEnv <- resolveTermsFromStore mconn selectedVersions varNames
-- Now perform pure evaluation with the resolved environment
return $ evalASTSync resolvedEnv ast
-- Helper to collect all variable names from an AST
collectVarNames :: TricuAST -> [(String, Maybe String)]
collectVarNames = go []
where
go acc (SVar name mhash) = (name, mhash) : acc
go acc (SApp t u) = go (go acc t) u
go acc (SLambda vars body) =
-- Filter out bound variables
let boundVars = Set.fromList vars
collected = go [] body
in acc ++ filter (\(name, _) -> not $ Set.member name boundVars) collected
go acc (TStem t) = go acc t
go acc (TFork t u) = go (go acc t) u
go acc (SList xs) = foldl' go acc xs
go acc _ = acc
-- Resolve terms from the content store
resolveTermsFromStore :: Maybe Connection -> Map.Map String T.Text -> [(String, Maybe String)] -> IO Env
resolveTermsFromStore Nothing _ _ = return Map.empty
resolveTermsFromStore (Just conn) selectedVersions varNames = do
-- Process each variable and build the environment
foldM (\env (name, mhash) -> do
term <- resolveTermFromStore conn selectedVersions name mhash
case term of
Just t -> return $ Map.insert (getVarKey name mhash) t env
Nothing -> return env
) Map.empty varNames
where
getVarKey name Nothing = name
getVarKey name (Just hash) = name ++ "#" ++ hash
-- Helper to resolve a single term from the store
resolveTermFromStore :: Connection -> Map.Map String T.Text -> String -> Maybe String -> IO (Maybe T)
resolveTermFromStore conn selectedVersions name mhash = case mhash of
-- If a specific hash is provided in the code, use that
Just hashPrefix -> do
-- Find all terms with this name
versions <- getTermVersions conn name
-- Filter for versions that match the hash prefix
let matchingVersions = filter (\(hash, _, _) ->
T.isPrefixOf (T.pack hashPrefix) hash) versions
case matchingVersions of
[] -> return Nothing
[(_, term, _)] -> return $ Just term
_ -> return Nothing -- Ambiguous hash prefix
-- No hash specified, fall back to the selectedVersions or normal lookup
Nothing -> case Map.lookup name selectedVersions of
-- If we have a selected version, use that hash
Just hash -> do
mterm <- getTermByHash conn hash
case mterm of
Just term -> case deserializeTerm (termData term) of
Right t -> return $ Just t
Left _ -> return Nothing
Nothing -> return Nothing
-- Otherwise, try to load by name
Nothing -> do
versions <- getTermVersions conn name
case versions of
[] -> return Nothing
[(_, term, _)] -> return $ Just term
_ -> return $ Just $ (\(_, t, _) -> t) $ head versions -- Use most recent version
elimLambda :: TricuAST -> TricuAST
elimLambda = go
@ -71,7 +162,7 @@ elimLambda = go
| application term = applicationResult term
| otherwise = term
etaReduction (SLambda [v] (SApp f (SVar x))) = v == x && not (isFree v f)
etaReduction (SLambda [v] (SApp f (SVar x Nothing))) = v == x && not (isFree v f)
etaReduction _ = False
etaReduceResult (SLambda [_] (SApp f _)) = f
@ -96,18 +187,12 @@ elimLambda = go
application _ = False
applicationResult (SApp f g) = SApp (elimLambda f) (elimLambda g)
toSKI x (SVar y)
| x == y = _I
| otherwise = SApp _K (SVar y)
toSKI x t@(SApp n u)
| not (isFree x t) = SApp _K t
| otherwise = SApp (SApp _S (toSKI x n)) (toSKI x u)
toSKI x (SList xs)
| not (isFree x (SList xs)) = SApp _K (SList xs)
| otherwise = SList (map (toSKI x) xs)
toSKI x t
| not (isFree x t) = SApp _K t
| otherwise = errorWithoutStackTrace "Unhandled toSKI conversion"
toSKI x (SVar y Nothing)
| x == y = _I
| otherwise = SApp _K (SVar y Nothing)
toSKI x (SApp m n) = SApp (SApp _S (toSKI x m)) (toSKI x n)
toSKI x (SLambda [y] body) = toSKI x (toSKI y body)
toSKI _ term = SApp _K term
-- Combinators and special forms
_S = parseSingle "t (t (t t t)) t"
@ -115,26 +200,20 @@ elimLambda = go
_I = parseSingle "t (t (t t)) t"
_B = parseSingle "t (t (t t (t (t (t t t)) t))) (t t)"
_TRI = parseSingle "t (t (t t (t (t (t t t))))) t"
-- Pattern bodies
triageBody a b c = SApp (SApp TLeaf (SApp (SApp TLeaf (SVar a)) (SVar b))) (SVar c)
composeBody f g x = SApp (SVar f) (SApp (SVar g) (SVar x))
triageBody a b c = SApp (SApp TLeaf (SApp (SApp TLeaf (SVar a Nothing)) (SVar b Nothing))) (SVar c Nothing)
composeBody f g x = SApp (SVar f Nothing) (SVar g Nothing)
isFree :: String -> TricuAST -> Bool
isFree x = Set.member x . freeVars
freeVars :: TricuAST -> Set.Set String
freeVars (SVar v ) = Set.singleton v
freeVars (SList s ) = foldMap freeVars s
freeVars (SLambda v b ) = foldr Set.delete (freeVars b) v
freeVars (SApp f a ) = freeVars f <> freeVars a
freeVars (TFork l r ) = freeVars l <> freeVars r
freeVars (SDef _ _ b) = freeVars b
freeVars (TStem t ) = freeVars t
freeVars (SInt _ ) = Set.empty
freeVars (SStr _ ) = Set.empty
freeVars TLeaf = Set.empty
freeVars _ = Set.empty
freeVars (SVar v Nothing) = Set.singleton v
freeVars (SVar v (Just _)) = Set.singleton v -- Hash doesn't affect free variables
freeVars (SApp t u) = Set.union (freeVars t) (freeVars u)
freeVars (SLambda vs body) = Set.difference (freeVars body) (Set.fromList vs)
freeVars _ = Set.empty
reorderDefs :: Env -> [TricuAST] -> [TricuAST]
reorderDefs env defs
@ -215,3 +294,34 @@ mainResult :: Env -> T
mainResult r = case Map.lookup "main" r of
Just a -> a
Nothing -> errorWithoutStackTrace "No valid definition for `main` found."
-- Evaluate an AST with an explicit environment
evalWithEnv :: Env -> Maybe Connection -> Map.Map String T.Text -> TricuAST -> IO T
evalWithEnv env mconn selectedVersions ast = do
-- First collect all variable names that might need to be resolved
let varNames = findVarNames ast
-- For each name not in the environment, try to resolve it from the store
resolvedEnv <- case mconn of
Just conn -> foldM (\e name ->
if Map.member name e
then return e
else do
mterm <- resolveTermFromStore conn selectedVersions name Nothing
case mterm of
Just term -> return $ Map.insert name term e
Nothing -> return e
) env varNames
Nothing -> return env
-- Now evaluate with the resolved environment
return $ evalASTSync resolvedEnv ast
-- Helper to find all variable names in an AST
findVarNames :: TricuAST -> [String]
findVarNames ast = case ast of
SVar name _ -> [name]
SApp a b -> findVarNames a ++ findVarNames b
SLambda args body -> findVarNames body \\ args
SDef name args body -> name : (findVarNames body \\ args)
_ -> []

View File

@ -109,9 +109,9 @@ nsDefinition moduleName other =
nsBody moduleName other
nsBody :: String -> TricuAST -> TricuAST
nsBody moduleName (SVar name)
| isPrefixed name = SVar name
| otherwise = SVar (nsVariable moduleName name)
nsBody moduleName (SVar name mhash)
| isPrefixed name = SVar name mhash
| otherwise = SVar (nsVariable moduleName name) mhash
nsBody moduleName (SApp func arg) =
SApp (nsBody moduleName func) (nsBody moduleName arg)
nsBody moduleName (SLambda args body) =
@ -122,18 +122,16 @@ nsBody moduleName (TFork left right) =
TFork (nsBody moduleName left) (nsBody moduleName right)
nsBody moduleName (TStem subtree) =
TStem (nsBody moduleName subtree)
nsBody moduleName (SDef name args body)
| isPrefixed name = SDef name args (nsBody moduleName body)
| otherwise = SDef (nsVariable moduleName name)
args (nsBody moduleName body)
nsBody moduleName (SDef name args body) =
SDef (nsVariable moduleName name) args (nsBodyScoped moduleName args body)
nsBody _ other = other
nsBodyScoped :: String -> [String] -> TricuAST -> TricuAST
nsBodyScoped moduleName args body = case body of
SVar name ->
SVar name mhash ->
if name `elem` args
then SVar name
else nsBody moduleName (SVar name)
then SVar name mhash
else nsBody moduleName (SVar name mhash)
SApp func arg ->
SApp (nsBodyScoped moduleName args func) (nsBodyScoped moduleName args arg)
SLambda innerArgs innerBody ->
@ -141,13 +139,11 @@ nsBodyScoped moduleName args body = case body of
SList items ->
SList (map (nsBodyScoped moduleName args) items)
TFork left right ->
TFork (nsBodyScoped moduleName args left)
(nsBodyScoped moduleName args right)
TFork (nsBodyScoped moduleName args left) (nsBodyScoped moduleName args right)
TStem subtree ->
TStem (nsBodyScoped moduleName args subtree)
SDef name innerArgs innerBody ->
SDef (nsVariable moduleName name) innerArgs
(nsBodyScoped moduleName (args ++ innerArgs) innerBody)
SDef (nsVariable moduleName name) innerArgs (nsBodyScoped moduleName (args ++ innerArgs) innerBody)
other -> other
isPrefixed :: String -> Bool

View File

@ -35,6 +35,7 @@ tricuLexer = do
[ try lnewline
, try namespace
, try dot
, try identifierWithHash
, try identifier
, try keywordT
, try integerLiteral
@ -56,12 +57,25 @@ lexTricu input = case runParser tricuLexer "" input of
keywordT :: Lexer LToken
keywordT = string "t" *> notFollowedBy alphaNumChar $> LKeywordT
identifierWithHash :: Lexer LToken
identifierWithHash = do
first <- lowerChar <|> char '_'
rest <- many $ letterChar
<|> digitChar <|> char '_' <|> char '-' <|> char '?'
<|> char '$' <|> char '@' <|> char '%'
char '#'
hash <- some (alphaNumChar <|> char '-')
let name = first : rest
if name == "t" || name == "!result"
then fail "Keywords (`t`, `!result`) cannot be used with a hash"
else return (LIdentifierWithHash name hash)
identifier :: Lexer LToken
identifier = do
first <- lowerChar <|> char '_'
rest <- many $ letterChar
<|> digitChar <|> char '_' <|> char '-' <|> char '?'
<|> char '$' <|> char '#' <|> char '@' <|> char '%'
<|> char '$' <|> char '@' <|> char '%'
let name = first : rest
if name == "t" || name == "!result"
then fail "Keywords (`t`, `!result`) cannot be used as an identifier"

View File

@ -5,6 +5,7 @@ import FileEval
import Parser (parseTricu)
import REPL
import Research
import ContentStore (runServer)
import Control.Monad (foldM)
import Control.Monad.IO.Class (liftIO)
@ -19,6 +20,7 @@ data TricuArgs
= Repl
| Evaluate { file :: [FilePath], form :: EvaluatedForm }
| TDecode { file :: [FilePath] }
| Serve { port :: Int }
deriving (Show, Data, Typeable)
replMode :: TricuArgs
@ -52,10 +54,20 @@ decodeMode = TDecode
&= explicit
&= name "decode"
serveMode :: TricuArgs
serveMode = Serve
{ port = 8080
&= help "Port to run the server on (default: 8080)"
&= name "p" &= typ "PORT"
}
&= help "Start a tricu server that provides term lookup via HTTP API"
&= explicit
&= name "serve"
main :: IO ()
main = do
let versionStr = "tricu Evaluator and REPL " ++ showVersion version
args <- cmdArgs $ modes [replMode, evaluateMode, decodeMode]
args <- cmdArgs $ modes [replMode, evaluateMode, decodeMode, serveMode]
&= help "tricu: Exploring Tree Calculus"
&= program "tricu"
&= summary versionStr
@ -65,7 +77,7 @@ main = do
putStrLn "Welcome to the tricu REPL"
putStrLn "You may exit with `CTRL+D` or the `!exit` command."
putStrLn "Try typing `!` with tab completion for more commands."
repl Map.empty
repl
Evaluate { file = filePaths, form = form } -> do
result <- case filePaths of
[] -> runTricuT <$> getContents
@ -80,6 +92,10 @@ main = do
[] -> getContents
(filePath:_) -> readFile filePath
putStrLn $ decodeResult $ result $ evalTricu Map.empty $ parseTricu value
Serve { port = port } -> do
putStrLn $ "Active on port " ++ show port
putStrLn "Press Ctrl+C to stop the server"
runServer port
-- Simple interfaces
@ -125,4 +141,4 @@ runTricuEnvWithEnv env input =
let asts = parseTricu input
finalEnv = evalTricu env asts
res = result finalEnv
in (finalEnv, formatT TreeCalculus res)
in (finalEnv, formatT TreeCalculus res)

View File

@ -249,7 +249,7 @@ parseGroupedItemM = do
parseSingleItemM :: ParserM TricuAST
parseSingleItemM = do
token <- satisfyM (\case LIdentifier _ -> True; LKeywordT -> True; _ -> False)
if | LIdentifier name <- token -> pure (SVar name)
if | LIdentifier name <- token -> pure (SVar name Nothing)
| token == LKeywordT -> pure TLeaf
| otherwise -> fail "Unexpected token in list item"
@ -258,16 +258,25 @@ parseVarM = do
token <- satisfyM (\case
LNamespace _ -> True
LIdentifier _ -> True
LIdentifierWithHash _ _ -> True
_ -> False)
case token of
LNamespace ns -> do
_ <- satisfyM (== LDot)
LIdentifier name <- satisfyM (\case LIdentifier _ -> True; _ -> False)
pure $ SVar (ns ++ "." ++ name)
pure $ SVar (ns ++ "." ++ name) Nothing
LIdentifier name
| name == "t" || name == "!result" ->
fail ("Reserved keyword: " ++ name ++ " cannot be assigned.")
| otherwise -> pure (SVar name)
| otherwise -> pure (SVar name Nothing)
LIdentifierWithHash name hash ->
if name == "t" || name == "!result"
then fail ("Reserved keyword: " ++ name ++ " cannot be assigned.")
else pure (SVar name (Just hash))
_ -> fail "Unexpected token while parsing variable"
parseIntLiteralM :: ParserM TricuAST
@ -275,7 +284,7 @@ parseIntLiteralM = do
let intL = (\case LIntegerLiteral _ -> True; _ -> False)
token <- satisfyM intL
if | LIntegerLiteral value <- token ->
pure (SInt value)
pure (SInt (fromIntegral value))
| otherwise ->
fail "Unexpected token while parsing integer literal"

View File

@ -5,6 +5,17 @@ import FileEval
import Lexer
import Parser
import Research
import ContentStore
import Control.Concurrent (forkIO, threadDelay, killThread, ThreadId)
import Control.Monad (forever, void, when, forM, forM_, foldM, unless)
import Data.ByteString (ByteString)
import Data.Maybe (isNothing, isJust, fromJust, catMaybes)
import Database.SQLite.Simple (Connection, Only(..), query, query_, execute, execute_, open)
import System.Directory (doesFileExist, createDirectoryIfMissing)
import System.FSNotify
import System.FilePath (takeDirectory, (</>))
import Text.Read (readMaybe)
import Control.Exception (IOException, SomeException, catch
, displayException)
@ -14,17 +25,37 @@ import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
import Data.Char (isSpace, isUpper)
import Data.List (dropWhile, dropWhileEnd, isPrefixOf)
import Data.List ((\\), dropWhile, dropWhileEnd, isPrefixOf, nub, sortBy, groupBy, intercalate)
import Data.Version (showVersion)
import Paths_tricu (version)
import System.Console.Haskeline
import System.Console.ANSI (setSGR, SGR(..), ConsoleLayer(..), ColorIntensity(..),
Color(..), ConsoleIntensity(..), clearFromCursorToLineEnd)
import qualified Data.Map as Map
import qualified Data.Text as T
import qualified Data.Text.IO as T
repl :: Env -> IO ()
repl env = runInputT settings (withInterrupt (loop env Decode))
import Control.Concurrent (forkIO, threadDelay)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Time (UTCTime, getCurrentTime, diffUTCTime)
import Control.Concurrent.MVar (MVar, newMVar, putMVar, takeMVar)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
data REPLState = REPLState
{ replForm :: EvaluatedForm
, replContentStore :: Maybe Connection
, replWatchedFile :: Maybe FilePath
, replSelectedVersions :: Map.Map String T.Text
, replWatcherThread :: Maybe ThreadId
}
repl :: IO ()
repl = do
conn <- initContentStore
runInputT settings (withInterrupt (loop (REPLState Decode (Just conn) Nothing Map.empty Nothing)))
where
settings :: Settings IO
settings = Settings
@ -39,49 +70,70 @@ repl env = runInputT settings (withInterrupt (loop env Decode))
where
commands = [ "!exit"
, "!output"
, "!definitions"
, "!import"
, "!clear"
, "!save"
, "!reset"
, "!version"
, "!help"
, "!definitions"
, "!watch"
, "!unwatch"
, "!refresh"
, "!versions"
, "!select"
, "!tag"
]
loop :: Env -> EvaluatedForm -> InputT IO ()
loop env form = handle (interruptHandler env form) $ do
loop :: REPLState -> InputT IO ()
loop state = handle (\Interrupt -> interruptHandler state Interrupt) $ do
minput <- getInputLine "tricu < "
case minput of
Nothing -> outputStrLn "Exiting tricu"
Nothing -> return ()
Just s
| strip s == "" -> loop env form
| strip s == "" -> loop state
| strip s == "!exit" -> outputStrLn "Exiting tricu"
| strip s == "!clear" -> do
liftIO $ putStr "\ESC[2J\ESC[H"
loop env form
loop state
| strip s == "!reset" -> do
outputStrLn "Environment reset to initial state"
loop Map.empty form
| strip s == "!version" -> do
outputStrLn "Selected versions reset"
loop state { replSelectedVersions = Map.empty }
| strip s == "!help" -> do
outputStrLn $ "tricu version " ++ showVersion version
loop env form
| "!save" `isPrefixOf` strip s -> handleSave env form
| strip s == "!output" -> handleOutput env form
| strip s == "!definitions" -> do
let defs = Map.keys $ Map.delete "!result" env
if null defs
then outputStrLn "No definitions discovered."
else do
outputStrLn "Available definitions:"
mapM_ outputStrLn defs
loop env form
| "!import" `isPrefixOf` strip s -> handleImport env form
| take 2 s == "--" -> loop env form
outputStrLn "Available commands:"
outputStrLn " !exit - Exit the REPL"
outputStrLn " !clear - Clear the screen"
outputStrLn " !reset - Reset selected versions"
outputStrLn " !help - Show tricu version and available commands"
outputStrLn " !output - Change output format (tree|fsl|ast|ternary|ascii|decode)"
outputStrLn " !definitions - List all defined terms in the content store"
outputStrLn " !import - Import definitions from file"
outputStrLn " !watch - Watch a file for changes"
outputStrLn " !unwatch - Stop watching file"
outputStrLn " !refresh - Refresh from content store"
outputStrLn " !versions - Show all versions of a term"
outputStrLn " !select - Select a specific version of a term"
outputStrLn " !tag - Add or update a tag for a term"
loop state
| strip s == "!output" -> handleOutput state
| strip s == "!definitions" -> handleDefinitions state
| "!import" `isPrefixOf` strip s -> handleImport state
| "!watch" `isPrefixOf` strip s -> handleWatch state
| strip s == "!unwatch" -> handleUnwatch state
| strip s == "!refresh" -> handleRefresh state
| "!versions" `isPrefixOf` strip s -> handleVersions state
| "!select" `isPrefixOf` strip s -> handleSelect state
| "!tag" `isPrefixOf` strip s -> handleTag state
| take 2 s == "--" -> loop state
| otherwise -> do
newEnv <- liftIO $ processInput env s form `catch` errorHandler env
loop newEnv form
-- Process the input with error handling
result <- liftIO $ catch
(processInput state s)
(errorHandler state)
loop result
handleOutput :: Env -> EvaluatedForm -> InputT IO ()
handleOutput env currentForm = do
handleOutput :: REPLState -> InputT IO ()
handleOutput state = do
let formats = [Decode, TreeCalculus, FSL, AST, Ternary, Ascii]
outputStrLn "Available output formats:"
mapM_ (\(i, f) -> outputStrLn $ show i ++ ". " ++ show f)
@ -97,94 +149,636 @@ repl env = runInputT settings (withInterrupt (loop env Decode))
case result of
Nothing -> do
outputStrLn "Invalid selection. Keeping current output format."
loop env currentForm
loop state
Just newForm -> do
outputStrLn $ "Output format changed to: " ++ show newForm
loop env newForm
loop state { replForm = newForm }
handleImport :: Env -> EvaluatedForm -> InputT IO ()
handleImport env form = do
res <- runMaybeT $ do
let fset = setComplete completeFilename defaultSettings
path <- MaybeT $ runInputT fset $
getInputLineWithInitial "File path to load < " ("", "")
handleDefinitions :: REPLState -> InputT IO ()
handleDefinitions state = case replContentStore state of
Nothing -> do
liftIO $ printError "Content store not initialized"
loop state
Just conn -> do
terms <- liftIO $ listStoredTerms conn
if null terms
then do
liftIO $ printWarning "No terms in content store."
loop state
else do
liftIO $ do
printSuccess $ "Content store contains " ++ show (length terms) ++ " terms:"
text <- MaybeT $ liftIO $ handle (\e -> do
putStrLn $ "Error reading file: " ++ displayException (e :: IOException)
return Nothing
) $ Just <$> readFile (strip path)
-- Calculate the maximum width of names
let maxNameWidth = maximum $ map (length . intercalate ", " . map T.unpack . T.splitOn "," . termNames) terms
case parseProgram (lexTricu text) of
Left err -> do
lift $ outputStrLn $ "Parse error: " ++ handleParseError err
MaybeT $ return Nothing
Right ast -> do
ns <- MaybeT $ runInputT defaultSettings $
getInputLineWithInitial "Namespace (or !Local for no namespace) < " ("", "")
-- Process each term and display its names
forM_ terms $ \term -> do
let namesList = T.splitOn "," (termNames term)
hash = termHash term
namesStr = intercalate ", " (map T.unpack namesList)
padding = replicate (maxNameWidth - length namesStr) ' '
liftIO $ do
putStr " "
printVariable namesStr
putStr padding
putStr " [hash: "
displayColoredHash hash
putStrLn "]"
-- Show tags if any
tags <- getTagsForTerm conn hash
unless (null tags) $ displayTags tags
let name = strip ns
if (name /= "!Local" && (null name || not (isUpper (head name)))) then do
lift $ outputStrLn "Namespace must start with an uppercase letter"
MaybeT $ return Nothing
else do
prog <- liftIO $ preprocessFile (strip path)
let code = case name of
"!Local" -> prog
_ -> nsDefinitions name prog
env' = evalTricu env code
return env'
case res of
loop state
handleImport :: REPLState -> InputT IO ()
handleImport state = do
let fset = setComplete completeFilename defaultSettings
filename <- runInputT fset $ getInputLineWithInitial "File to import: " ("", "")
case filename of
Nothing -> loop state
Just f -> do
let cleanFilename = strip f
exists <- liftIO $ doesFileExist cleanFilename
if not exists
then do
liftIO $ printError $ "File not found: " ++ cleanFilename
loop state
else importFile state cleanFilename
importFile :: REPLState -> String -> InputT IO ()
importFile state cleanFilename = do
code <- liftIO $ readFile cleanFilename
case replContentStore state of
Nothing -> do
outputStrLn "Import cancelled"
loop env form
Just env' ->
loop (Map.delete "!result" env') form
liftIO $ printError "Content store not initialized"
loop state
Just conn -> do
-- Parse the entire file content at once
let asts = parseTricu code
-- First, evaluate the file using the standard evaluation pipeline
-- This will handle dependencies correctly
env <- liftIO $ evaluateFile cleanFilename
-- Now store all the definitions from the environment
liftIO $ do
printSuccess $ "Importing file: " ++ cleanFilename
-- Get all definitions from the environment (excluding result)
let defs = Map.toList $ Map.delete "!result" env
-- Store each definition
importedCount <- foldM (\count (name, term) -> do
-- Check if this tree form already exists with other names
let hashValue = hashTerm term
existingTerm <- getTermByHash conn hashValue
-- Determine the names to store
namesList <- case existingTerm of
Just existingTerm -> do
let existingNames = T.splitOn "," (termNames existingTerm)
if T.pack name `elem` existingNames
then return $ map T.unpack existingNames -- Name already exists
else return $ map T.unpack existingNames ++ [name] -- Add new name
Nothing -> return [name] -- New term
-- Store with all names
hash <- storeTerm conn namesList term
printSuccess $ "Stored definition: " ++ name ++ " with hash " ++ T.unpack hash
return (count + 1)
) 0 defs
printSuccess $ "Imported " ++ show importedCount ++ " definitions successfully"
loop state
interruptHandler :: Env -> EvaluatedForm -> Interrupt -> InputT IO ()
interruptHandler env form _ = do
outputStrLn "Interrupted with CTRL+C\n\
\You can use the !exit command or CTRL+D to exit"
loop env form
processAST :: Connection -> REPLState -> TricuAST -> IO REPLState
processAST conn state ast = do
case ast of
SDef name [] body -> do
-- Evaluate the body using the existing evaluation pipeline
result <- evalAST (Just conn) (replSelectedVersions state) body
-- Check if this tree form already exists with other names
let hashValue = hashTerm result
existingTerm <- getTermByHash conn hashValue
-- Determine the names to store
names <- case existingTerm of
Just term -> do
let existingNames = T.splitOn "," (termNames term)
if T.pack name `elem` existingNames
then return $ T.unpack (termNames term) -- Name already exists
else return $ T.unpack (termNames term) ++ "," ++ name -- Add new name
Nothing -> return name -- New term
-- Store with all names
hash <- storeTerm conn [names] result
processInput :: Env -> String -> EvaluatedForm -> IO Env
processInput env input form = do
let asts = parseTricu input
newEnv = evalTricu env asts
case Map.lookup "!result" newEnv of
Just r -> do
putStrLn $ "tricu > " ++ formatT form r
Nothing -> pure ()
return newEnv
putStr "tricu > "
printSuccess "Stored definition: "
printVariable name
putStr " with hash "
displayColoredHash hash
putStrLn ""
errorHandler :: Env -> SomeException -> IO (Env)
errorHandler env e = do
putStrLn $ "Error: " ++ show e
return env
return state
_ -> return state
handleWatch :: REPLState -> InputT IO ()
handleWatch state = do
-- Get the default scratch file path
dbPath <- liftIO $ getContentStorePath
let filepath = takeDirectory dbPath </> "scratch.tri"
let dirPath = takeDirectory filepath
-- Ensure the directory exists
liftIO $ createDirectoryIfMissing True dirPath
-- Create the file if it doesn't exist
fileExists <- liftIO $ doesFileExist filepath
unless fileExists $ liftIO $ do
-- Create a simple template file
writeFile filepath "-- tricu scratch file\n\n"
outputStrLn $ "Using scratch file: " ++ filepath
-- Stop any existing watcher
when (isJust (replWatcherThread state)) $ do
outputStrLn "Stopping previous file watch"
liftIO $ killThread (fromJust $ replWatcherThread state)
outputStrLn $ "Starting to watch file: " ++ filepath
outputStrLn "Press Ctrl+C to stop watching and return to REPL"
-- First, process the file immediately
liftIO $ processWatchedFile filepath (replContentStore state) (replSelectedVersions state) (replForm state)
-- Create a reference time for debouncing
lastProcessedRef <- liftIO $ newIORef =<< getCurrentTime
-- Start a new file watcher in a separate thread
watcherId <- liftIO $ forkIO $ withManager $ \mgr -> do
-- Watch for changes in the file
stopAction <- watchDir mgr dirPath (\event -> eventPath event == filepath) $ \event -> do
-- Implement debouncing to prevent multiple rapid triggers
now <- getCurrentTime
lastProcessed <- readIORef lastProcessedRef
-- Only process if at least 500ms have passed since last processing
when (diffUTCTime now lastProcessed > 0.5) $ do
putStrLn $ "\nFile changed: " ++ filepath
processWatchedFile filepath (replContentStore state) (replSelectedVersions state) (replForm state)
writeIORef lastProcessedRef now
-- Keep the watcher alive
forever $ threadDelay 1000000
-- Enter a blocking loop that can be interrupted with Ctrl+C
watchLoop state { replWatchedFile = Just filepath, replWatcherThread = Just watcherId }
handleUnwatch :: REPLState -> InputT IO ()
handleUnwatch state = case replWatchedFile state of
Nothing -> do
outputStrLn "No file is currently being watched"
loop state
Just path -> do
outputStrLn $ "Stopped watching " ++ path
-- Kill the watcher thread if it exists
when (isJust (replWatcherThread state)) $ do
liftIO $ killThread (fromJust $ replWatcherThread state)
loop state { replWatchedFile = Nothing, replWatcherThread = Nothing }
handleRefresh :: REPLState -> InputT IO ()
handleRefresh state = case replContentStore state of
Nothing -> do
outputStrLn "Content store not initialized"
loop state
Just conn -> do
outputStrLn "Environment refreshed from content store"
loop state
handleVersions :: REPLState -> InputT IO ()
handleVersions state = case replContentStore state of
Nothing -> do
liftIO $ printError "Content store not initialized"
loop state
Just conn -> do
liftIO $ printPrompt "Term name: "
name <- getInputLine ""
case name of
Nothing -> loop state
Just n -> do
versions <- liftIO $ getTermVersions conn (strip n)
if null versions
then liftIO $ printError $ "No versions found for term: " ++ n
else do
liftIO $ do
printKeyword "Versions of "
printVariable (strip n)
putStrLn ":"
forM_ (zip [1..] versions) $ \(i, (hash, _, ts)) -> do
-- Get tags for this version
tags <- getTagsForTerm conn hash
-- Display version number
putStr $ show (i :: Int) ++ ". "
-- Display hash with color
displayColoredHash hash
-- Display timestamp
putStr $ " (" ++ formatTimestamp ts ++ ")"
-- Display tags if any
unless (null tags) $ do
putStr " ["
printKeyword "Tags: "
forM_ (zip [0..] tags) $ \(j, tag) -> do
printTag (T.unpack tag)
when (j < length tags - 1) $ putStr ", "
putStr "]"
putStrLn ""
loop state
handleSelect :: REPLState -> InputT IO ()
handleSelect state = case replContentStore state of
Nothing -> do
liftIO $ printError "Content store not initialized"
loop state
Just conn -> do
liftIO $ printPrompt "Term name: "
name <- getInputLine ""
case name of
Nothing -> loop state
Just n -> do
let cleanName = strip n
versions <- liftIO $ getTermVersions conn cleanName
if null versions
then do
liftIO $ printError $ "No versions found for term: " ++ cleanName
loop state
else do
liftIO $ do
printKeyword "Versions of "
printVariable cleanName
putStrLn ":"
forM_ (zip [1..] versions) $ \(i, (hash, _, ts)) -> do
-- Get tags for this version
tags <- getTagsForTerm conn hash
-- Display version number
putStr $ show (i :: Int) ++ ". "
-- Display hash with color
displayColoredHash hash
-- Display timestamp
putStr $ " (" ++ formatTimestamp ts ++ ")"
-- Display tags if any
unless (null tags) $ do
putStr " ["
printKeyword "Tags: "
forM_ (zip [0..] tags) $ \(j, tag) -> do
printTag (T.unpack tag)
when (j < length tags - 1) $ putStr ", "
putStr "]"
putStrLn ""
liftIO $ printPrompt "Select version (or press Enter to cancel): "
choice <- getInputLine ""
case choice >>= readMaybe of
Just idx | idx > 0 && idx <= length versions -> do
let (hash, _, _) = versions !! (idx - 1)
let newState = state { replSelectedVersions =
Map.insert cleanName hash (replSelectedVersions state) }
liftIO $ do
printSuccess "Selected version "
displayColoredHash hash
putStr " for term "
printVariable cleanName
putStrLn ""
loop newState
_ -> loop state
handleTag :: REPLState -> InputT IO ()
handleTag state = case replContentStore state of
Nothing -> do
liftIO $ printError "Content store not initialized"
loop state
Just conn -> do
-- Get the hash
liftIO $ printPrompt "Term hash (or name): "
hash <- getInputLine ""
case hash of
Nothing -> loop state
Just h -> do
let hashText = T.pack (strip h)
-- If input is a name, try to get the most recent version's hash
finalHash <- if T.any (== '#') hashText
then return hashText
else do
versions <- liftIO $ getTermVersions conn (strip h)
if null versions
then do
liftIO $ printError $ "No versions found for term: " ++ h
return hashText
else do
let (mostRecentHash, _, _) = head versions
return mostRecentHash
-- Show existing tags
tags <- liftIO $ getTagsForTerm conn finalHash
unless (null tags) $ do
liftIO $ do
printKeyword "Existing tags:"
displayTags tags
-- Get the tag value
liftIO $ printPrompt "Tag: "
tagValue <- getInputLine ""
case tagValue of
Nothing -> loop state
Just tv -> do
-- Set the tag
liftIO $ do
setTag conn finalHash (T.pack (strip tv))
printSuccess $ "Tag set to '"
printTag (strip tv)
putStr "' for term with hash "
displayColoredHash (T.take 8 finalHash)
putStrLn ""
loop state
interruptHandler :: REPLState -> Interrupt -> InputT IO ()
interruptHandler state _ = do
liftIO $ do
printWarning "Interrupted with CTRL+C"
printWarning "You can use the !exit command or CTRL+D to exit"
loop state
-- Simple error handler that returns the original state
errorHandler :: REPLState -> SomeException -> IO REPLState
errorHandler state e = do
printError $ "Error: " ++ displayException e
return state
-- Process input and return the new state
processInput :: REPLState -> String -> IO REPLState
processInput state input = do
let asts = parseTricu input
case asts of
[] -> return state
_ -> case replContentStore state of
Nothing -> do
printError "Content store not initialized"
return state
Just conn -> do
-- First, check for any variables that have multiple versions and auto-select the most recent
newState <- foldM (\s ast -> do
let varNames = findVarNames ast
foldM (\s' name -> do
-- Skip if already selected
if Map.member name (replSelectedVersions s')
then return s'
else do
versions <- getTermVersions conn name
if length versions > 1
then do
let (hash, _, _) = head versions -- Most recent version
printWarning $ "Multiple versions of '" ++ name ++ "' found:"
forM_ (zip [1..] versions) $ \(i, (h, _, ts)) -> do
putStr $ show (i :: Int) ++ ". "
displayColoredHash (T.take 8 h)
putStrLn $ " (" ++ formatTimestamp ts ++ ")"
printWarning "Please `!select` a version"
printWarning $ "For now, using the most recent version of '" ++ name ++ "'"
-- Auto-select the most recent version
return s' { replSelectedVersions = Map.insert name hash (replSelectedVersions s') }
else return s'
) s varNames
) state asts
-- Process each AST node
forM_ asts $ \ast -> do
case ast of
SDef name [] body -> do
-- Store the definition in the database
result <- evalAST (Just conn) (replSelectedVersions newState) body
hash <- storeTerm conn [name] result
putStr "tricu > "
printSuccess "Stored definition: "
printVariable name
putStr " with hash "
displayColoredHash hash
putStrLn ""
putStr "tricu > "
printResult $ formatT (replForm newState) result
putStrLn ""
_ -> do
-- Evaluate the expression
result <- evalAST (Just conn) (replSelectedVersions newState) ast
putStr "tricu > "
printResult $ formatT (replForm newState) result
putStrLn ""
return newState
strip :: String -> String
strip = dropWhileEnd isSpace . dropWhile isSpace
handleSave :: Env -> EvaluatedForm -> InputT IO ()
handleSave env form = do
let fset = setComplete completeFilename defaultSettings
path <- runInputT fset $
getInputLineWithInitial "File to save < " ("", "")
-- Add a new watchLoop function that handles watching mode
watchLoop :: REPLState -> InputT IO ()
watchLoop state = handle (\Interrupt -> do
outputStrLn "\nStopped watching file"
-- Kill the watcher thread when interrupted
when (isJust (replWatcherThread state)) $ do
liftIO $ killThread (fromJust $ replWatcherThread state)
loop state { replWatchedFile = Nothing, replWatcherThread = Nothing }) $ do
-- This is a blocking loop that does nothing but wait for Ctrl+C
liftIO $ threadDelay 1000000
watchLoop state
case path of
Nothing -> do
outputStrLn "Save cancelled"
loop env form
Just p -> do
let definitions = Map.toList $ Map.delete "!result" env
filepath = strip p
-- Add a helper function to process a watched file
processWatchedFile :: FilePath -> Maybe Connection -> Map.Map String T.Text -> EvaluatedForm -> IO ()
processWatchedFile filepath mconn selectedVersions outputForm = do
content <- readFile filepath
let asts = parseTricu content
outputStrLn "Starting save..."
liftIO $ writeFile filepath ""
outputStrLn "File created..."
forM_ definitions $ \(name, value) -> do
let content = name ++ " = " ++ formatT TreeCalculus value ++ "\n"
outputStrLn $ "Writing definition: " ++ name ++ " with length " ++ show (length content)
liftIO $ appendFile filepath content
outputStrLn $ "Saved " ++ show (length definitions) ++ " definitions to " ++ p
-- Process each AST node
forM_ asts $ \ast -> case ast of
SDef name [] body -> do
-- Evaluate the body using the database
result <- evalAST mconn selectedVersions body
loop env form
-- Store the result in the database
case mconn of
Just conn -> do
hash <- storeTerm conn [name] result
putStrLn $ "tricu > Stored definition: " ++ name ++ " with hash " ++ T.unpack hash
Nothing -> putStrLn "Content store not initialized"
-- Display the result
putStrLn $ "tricu > " ++ name ++ " = " ++ formatT outputForm result
_ -> do
-- Evaluate the expression using the database
result <- evalAST mconn selectedVersions ast
putStrLn $ "tricu > Result: " ++ formatT outputForm result
putStrLn $ "tricu > Processed file: " ++ filepath
-- Helper function to find all variable names in an AST
findVarNames :: TricuAST -> [String]
findVarNames ast = case ast of
SVar name _ -> [name]
SApp a b -> findVarNames a ++ findVarNames b
SLambda args body -> findVarNames body \\ args
SDef name args body -> name : (findVarNames body \\ args)
_ -> []
-- Add this helper function to format timestamps
formatTimestamp :: Integer -> String
formatTimestamp ts = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" (posixSecondsToUTCTime (fromIntegral ts))
-- Helper function to display a hash with the first 8 chars highlighted
displayColoredHash :: T.Text -> IO ()
displayColoredHash hash = do
let (prefix, rest) = T.splitAt 8 hash
-- Set color to bright cyan for the first 8 chars
setSGR [SetColor Foreground Vivid Cyan]
putStr $ T.unpack prefix
-- Reset to normal color for the rest
setSGR [SetColor Foreground Dull White]
putStr $ T.unpack rest
-- Reset all attributes
setSGR [Reset]
-- Helper function to display a hash with the first 8 chars highlighted in a string
coloredHashString :: T.Text -> String
coloredHashString hash =
"\ESC[1;36m" ++ T.unpack (T.take 8 hash) ++
"\ESC[0;37m" ++ T.unpack (T.drop 8 hash) ++
"\ESC[0m"
-- Color helper functions
withColor :: ColorIntensity -> Color -> IO () -> IO ()
withColor intensity color action = do
setSGR [SetColor Foreground intensity color]
action
setSGR [Reset]
printColored :: ColorIntensity -> Color -> String -> IO ()
printColored intensity color text = withColor intensity color $ putStr text
printlnColored :: ColorIntensity -> Color -> String -> IO ()
printlnColored intensity color text = withColor intensity color $ putStrLn text
-- Specialized color functions for different elements
printSuccess :: String -> IO ()
printSuccess = printlnColored Vivid Green
printError :: String -> IO ()
printError = printlnColored Vivid Red
printWarning :: String -> IO ()
printWarning = printlnColored Vivid Yellow
printPrompt :: String -> IO ()
printPrompt = printColored Vivid Blue
printVariable :: String -> IO ()
printVariable = printColored Vivid Magenta
printTag :: String -> IO ()
printTag = printColored Vivid Yellow
printKeyword :: String -> IO ()
printKeyword = printColored Vivid Blue
printResult :: String -> IO ()
printResult = printColored Dull White
-- Helper function to display tags with color
displayTags :: [T.Text] -> IO ()
displayTags [] = return ()
displayTags tags = do
putStr " Tags: "
forM_ (zip [0..] tags) $ \(i, tag) -> do
printTag (T.unpack tag)
when (i < length tags - 1) $ putStr ", "
putStrLn ""
initContentStore :: IO Connection
initContentStore = do
dbPath <- getContentStorePath
createDirectoryIfMissing True (takeDirectory dbPath)
conn <- open dbPath
execute_ conn "CREATE TABLE IF NOT EXISTS terms (\
\hash TEXT PRIMARY KEY, \
\names TEXT, \
\term_data BLOB, \
\metadata TEXT, \
\created_at INTEGER DEFAULT (strftime('%s','now')), \
\tags TEXT DEFAULT '')"
execute_ conn "CREATE INDEX IF NOT EXISTS terms_names_idx ON terms(names)"
execute_ conn "CREATE INDEX IF NOT EXISTS terms_tags_idx ON terms(tags)"
return conn
storeTerm :: Connection -> [String] -> T -> IO T.Text
storeTerm conn names term = do
let termBS = serializeTerm term
termHashText = hashTerm term
namesText = T.pack $ intercalate "," names
metadataText = T.pack "{}"
tagsText = T.pack ""
existing <- query conn
"SELECT hash FROM terms WHERE hash = ?"
(Only termHashText) :: IO [Only T.Text]
case existing of
[] -> execute conn
"INSERT INTO terms (hash, names, term_data, metadata, tags) VALUES (?, ?, ?, ?, ?)"
(termHashText, namesText, termBS, metadataText, tagsText)
_ -> execute conn
"UPDATE terms SET names = ?, metadata = ? WHERE hash = ?"
(namesText, metadataText, termHashText)
return termHashText
getTermByName :: Connection -> T.Text -> IO (Maybe StoredTerm)
getTermByName conn nameText = do
results <- query conn
"SELECT hash, names, term_data, metadata, created_at, tags FROM terms WHERE names LIKE ? ORDER BY created_at DESC LIMIT 1"
(Only $ "%" <> nameText <> "%")
case results of
[term] -> return $ Just term
_ -> return Nothing
listStoredTerms :: Connection -> IO [StoredTerm]
listStoredTerms conn = do
query_ conn "SELECT hash, names, term_data, metadata, created_at, tags FROM terms ORDER BY created_at DESC"

View File

@ -14,7 +14,7 @@ data T = Leaf | Stem T | Fork T T
-- Abstract Syntax Tree for tricu
data TricuAST
= SVar String
= SVar String (Maybe String) -- Variable name and optional hash prefix
| SInt Integer
| SStr String
| SList [TricuAST]
@ -30,11 +30,11 @@ data TricuAST
-- Lexer Tokens
data LToken
= LKeywordT
| LIdentifier String
= LIdentifier String
| LIdentifierWithHash String String
| LKeywordT
| LNamespace String
| LIntegerLiteral Integer
| LStringLiteral String
| LImport String String
| LAssign
| LColon
| LDot
@ -42,9 +42,10 @@ data LToken
| LCloseParen
| LOpenBracket
| LCloseBracket
| LStringLiteral String
| LIntegerLiteral Int
| LNewline
| LImport String String
deriving (Show, Eq, Ord)
deriving (Eq, Show, Ord)
-- Output formats
data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode
@ -54,7 +55,7 @@ data EvaluatedForm = TreeCalculus | FSL | AST | Ternary | Ascii | Decode
type Env = Map.Map String T
-- Tree Calculus Reduction Rules
{-
{-
The t operator is left associative.
1. t t a b -> a
2. t (t a) b c -> a c (b c)

View File

@ -12,7 +12,6 @@ import Control.Monad.IO.Class (liftIO)
import Data.List (isInfixOf)
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Text.Megaparsec (runParser)
import qualified Data.Map as Map
@ -33,7 +32,7 @@ tests = testGroup "Tricu Tests"
, providedLibraries
, fileEval
, modules
, demos
-- , demos
, decoding
]
@ -103,7 +102,7 @@ parser = testGroup "Parser Tests"
, testCase "Parse function definitions" $ do
let input = "x = (a b c : a)"
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a"))))
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a" Nothing))))
parseSingle input @?= expect
, testCase "Parse nested Tree Calculus terms" $ do
@ -123,7 +122,7 @@ parser = testGroup "Parser Tests"
, testCase "Parse function with applications" $ do
let input = "f = (x : t x)"
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x")))
expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x" Nothing)))
parseSingle input @?= expect
, testCase "Parse nested lists" $ do
@ -170,17 +169,17 @@ parser = testGroup "Parser Tests"
, testCase "Parse lambda abstractions" $ do
let input = "(a : a)"
expect = (SLambda ["a"] (SVar "a"))
expect = (SLambda ["a"] (SVar "a" Nothing))
parseSingle input @?= expect
, testCase "Parse multiple arguments to lambda abstractions" $ do
let input = "x = (a b : a)"
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a")))
expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a" Nothing)))
parseSingle input @?= expect
, testCase "Grouping T terms with parentheses in function application" $ do
let input = "x = (a : a)\nx (t)"
expect = [SDef "x" [] (SLambda ["a"] (SVar "a")),SApp (SVar "x") TLeaf]
expect = [SDef "x" [] (SLambda ["a"] (SVar "a" Nothing)),SApp (SVar "x" Nothing) TLeaf]
parseTricu input @?= expect
, testCase "Comments 1" $ do

View File

@ -21,18 +21,36 @@ executable tricu
LambdaCase
MultiWayIf
OverloadedStrings
ScopedTypeVariables
ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC
build-depends:
base >=4.7
, aeson
, ansi-terminal
, base64-bytestring
, bytestring
, cereal
, cmdargs
, containers
, cryptonite
, directory
, exceptions
, filepath
, fsnotify
, haskeline
, http-conduit
, http-types
, megaparsec
, mtl
, sqlite-simple
, tasty
, tasty-hunit
, text
, time
, transformers
, wai
, warp
, zlib
other-modules:
Eval
FileEval
@ -51,20 +69,35 @@ test-suite tricu-tests
LambdaCase
MultiWayIf
OverloadedStrings
ScopedTypeVariables
build-depends:
base
base >=4.7
, aeson
, ansi-terminal
, base64-bytestring
, bytestring
, cereal
, cmdargs
, containers
, cryptonite
, directory
, exceptions
, filepath
, fsnotify
, haskeline
, http-conduit
, http-types
, megaparsec
, mtl
, sqlite-simple
, tasty
, tasty-hunit
, tasty-quickcheck
, text
, time
, transformers
, wai
, warp
, zlib
default-language: Haskell2010
other-modules:
Eval