Compare commits
1 Commits
main
...
vibe-codin
Author | SHA1 | Date | |
---|---|---|---|
813e880ed7 |
@ -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
415
src/ContentStore.hs
Normal 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"
|
226
src/Eval.hs
226
src/Eval.hs
@ -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)
|
||||
_ -> []
|
||||
|
@ -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
|
||||
|
16
src/Lexer.hs
16
src/Lexer.hs
@ -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"
|
||||
|
22
src/Main.hs
22
src/Main.hs
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
802
src/REPL.hs
802
src/REPL.hs
@ -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"
|
||||
|
@ -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)
|
||||
|
13
test/Spec.hs
13
test/Spec.hs
@ -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
|
||||
|
37
tricu.cabal
37
tricu.cabal
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user