fst-0.10.0.0: Main.hs
{-# LANGUAGE DoAndIfThenElse, FlexibleContexts, GeneralizedNewtypeDeriving #-}
{- |
fstStudio takes a program consisting of regular relations that denotes
the relation between two regular languages and constructs a
transducer. If a regular expression, not a relation, is given, then it
is interpreted as the identity relation. The syntax is very similar to
Xerox's finite state transducer syntax with two fundamental
differences: a distinction is made between functions (definitions) and
strings, and fststudio allows functional definitions.
[@\"a\"@] A symbol. Example: @[\"b\"]@ denotes the language @{\"b\"}@.
[@a@] A variable. A symbol without quotes is a variable.
[@\"a\":\"b\"@] Describes a relation between the symbol @a@ and @b@.
This relation is ordered and @a@ is said to be a part of the /upper
language/ and @b@ is said to be part of the /lower language/.
Example: @[\"a\":\"b\"]@ denotes the relation @{(\"a\",\"b\")}@.
[@0@] Epsilon symbol. The epsilon symbol denotes the string with no
symbols. Example: @[0]@ denotes the language @{\"\"}@.
[@?@] All symbol. The all symbol denotes the union of all symbols in
the alphabet. Example: @[?]@ and an alphabet @{a,b,c}@ denotes the
language @{\"a\",\"b\",\"c\"}@.
[@\"\"@] quotes cancel every special meaning of the symbols. Example:
@[\"? 0\"]@ denotes the language @{\"? 0\"}@.
[@\[A\]@] brackets are used to change the precedence of a regular
relation.
[@(A)@] parenthesis expresses optionality, and has the same meaning as
@[A|0]@.
[@A B@] Concatenation of the expressions or relations A and
B. Example: @[[a b] [c d]]@ denotes the language @{\"ac\", \"ad\", \"bc\",
\"bd\"}@
[@A^n@] Concatenation of @A@ /n/ times. @A^0@ is defined as the empty
string. Example: @[a]^3@ describes the language @{\"aaa\"}@.
[@A|B@] Union of the languages or relations @A@ and @B@. Example: @[a|b]@
describes the language @{\"a\",\"b\"}@.
[@A & B@] Intersection of the languages @A@ and @B@. Example: @[a b]
& [a]@ describes the language @{\"a\"}@.
[@A - B@] Minus of the languages @A@ and @B@, and has the same meaning as
@[A & B]@. Example: @[a b] - [a]@ describes the language @{\"b\"}@.
[@~A@] Describes the complement of an expression, and has the same
meaning as @[?* - A]@. Note that complement is always defined over
an alphabet. The expression @[A]@ is only unambiguous with respect to
an alphabet. Example: @[a]@ denotes the language that doesn't contain
the string @\"a\"@. If the alphabet is @{\"a\",\"b\"}@ then @[a]@
denotes the language @{\"\",\"b\",\"aa\",\"ba\",...}@.
[@A+@] Repetition (Kleenes plus). A concatenated with itself an
arbitrary number of times, including zero times. Example: @[a]+@ denotes
the infinite language @{\"a\",\"aa\",\"aaa\",...}@
[@A*@] Kleene’s star: @[A+ | 0]@. Example: @[a]*@ denotes the infinite
language @{\"\",\"a\",\"aa\",...}@
[@$A@] Containment. The set of strings where @A@ appear at least once
as a substring. Containment is the same thing as @[?* A ?*]@.
[@A .x. B@] Cross product of the languages @A@ and @B@. Example: @[[a b]
.x. c]@ describes the relations @{(\"a\",\"c\"), (\"b\",\"c\")}@.
[@A .o. B@] Composition of the relations @A@ and @B@. Example: @[a:b c:d]
.o. [d:e]@ describes the relation @{(\"c\",\"e\")}@.
The precedence of the operators is as follows, where 4 is the highest
precedence:
1. @.x.@ @.o.@
2. @&@ @-@
3. /Concatenation/
4. @~@ @^@ @*@ @+@ @$@
A file containing a program must end with @.fst@, and an input file
mustend with @.dat@. A program is a collection of functions defining
regular relations. A function with zero arguments is called a
definition or a macro. A definition, or a macro, can for example look
like this:
> <digits> ::= "1" | "2" | "3" | "4" | "5" |
> "6" | "7" | "8" | "9" | "0" ;
and a function can look like this:
> <swap,a,b> ::= b a ;
Note that strings are marked with quotes, and variables have no
quotes. Every program must contain a @\<main\>@ definition (a program
without one will result in a parse error).
> <main> ::= ... ;
The alphabet of a program is the symbols in the regular relation
defined in the program.
/Example program/
> <nickel> ::= ["n" .x. "c"^5];
> <dime> ::= ["d" .x. "c"^10];
> <quarter> ::= ["q" .x. "c"^25];
> <cent> ::= ["c" .x. "c"];
> <money> ::= [ <nickel> | <dime> | <quarter> | <cent>]*;
> <drink> ::= ["c"^65 .x. "PLONK"];
> <main> ::= [ <money> .o. <drink> ];
/Batch mode/
Usage: @fst FILE [Options]@. FILE must end with @.fst@, which defines
an FstStudio program, or @.net@, which defines a saved transducer. If
no options are given, then input is taken from standard input, the
transducer is applied down, and the output, if any, is produced on
standard output.
[@-u@] Apply the transducer up
[@-d@] Apply the transducer down
[@-i FILE@] Take input from FILE
[@-o FILE@] Write output to FILE
/Interactive mode - list of commands/
[@r REG@] Read a regular relation from standard input. If a regular
expression is typed, then it is interpreted as the identity relation.
[@b@] Build an epsilon-free, deterministic, minimal transducer from a
loaded/typed regular relation.
[@bn@] Build an epsilon-free, possibly non-deterministic, non-minimal
transducer from a load/typed regular relation.
[@m@] Minimize a built transducer.
[@det@] Determinize a built transducer.
[@s FILE@] Save to @FILE@. If @FILE@ ends with @.net@, then the built
transducer is saved. Any other suffix saves the produced output in the
system to @FILE@, if any.
[@l FILE@] Load from @FILE@. @FILE@ must end with @.fst@, @.net@ or
@.dat@. If @FILE@ ends with @.fst@, then a FstStudio program is loaded
into FstStudio. If @FILE@ ends with @.net@, then a transducer is loaded
into FstStudio. If @FILE@ ends with @.dat@, then input is loaded into
FstStudio.
[@l a | b@] Load and union two transducers. a and b must either be a
file ending with @.net@ or the symbol @*@, which refers to the interior
transducer. The produced transducer is possibly non-deterministic and
non-minimal.
[@l a b@] Load and concatenate two transducers. a and b must either be
ale ending with @.net@ or the symbol @*@, which refers to the interior
transducer. The produced transducer is possibly non-deterministicand
non-minimal.
[@l a*@] Load and apply Kleene’s star on a transducer. a must either
be a file ending with @.net@ or the symbol @*@, which refers to the
interior transducer. The produced transducer is possibly
non-deterministicand non-minimal.
[@l a .o. b@] Load and compose two transducers. a and b must either be
a file ending with @.net@ or the symbol @*@, which refers to the
interior transducer. The produced transducer is possibly
non-deterministic andnon-minimal.
[@vt@] View loaded/built transducer.
[@vr@] View loaded/typed regular relation.
[@vi@] View loaded input.
[@vo@] View produced output.
[@d@] Apply transducer down with loaded input.
[@u@] Apply transducer up with loaded input.
[@d SYMBOLS@] Apply tranducer down with @SYMBOLS@.
[@u SYMBOLS@] Apply transducer up with @SYMBOLS@.
[@c@] Clear memory.
[@h@] List commands.
[@q@] End session.
-}
module Main where
import FST.TransducerInterface
import FST.RRegTypes
import FST.Arguments
import FST.Info
import Text.Printf
import Control.Monad (liftM)
import Control.Monad.State
import Control.Monad.Error
import Control.Monad.IO.Class (liftIO)
import System.Environment (getArgs)
import System.Console.Haskeline
-- | Main entry point
main :: IO ()
main = do
args <- getArgs
case args of
[] -> do
welcome
runInputT defaultSettings (evalStateT loop emptyInfo)
as -> do
ret <- runErrorT (batchMode as)
case ret of
Left err -> putStrLn err
Right _ -> return ()
-- | Display welcome message
welcome :: IO ()
welcome = putStr $ unlines [
"***********************************************************",
"* Finite State Transducer Studio",
"* Written purely in Haskell.",
"* Version : 0.10",
"* Updated : 17 March 2013",
"* Author : Markus Forsberg",
"* With contributions by Baldur Blöndal & John J. Camilleri",
"***********************************************************",
"",
"Type 'h' for help."
]
-- | Run in batch mode with given arguments
batchMode :: [String] -> ErrorT String IO ()
batchMode cmdopt = do
(file, cmd) <- (ErrorT . return . parseBatch) cmdopt
-- Only accept .NET or .FST files
when (not (isNET file) && not (isFST file)) $
throwError "Input file must end with *.fst or *.net"
transducer <- if isFST file
then do str <- open file
fmap (flip compile []) $ ErrorT $ return $ parseProgram str
-- Load transducer directly from .NET files
else load file
let action = if isUpB cmd then upB else downB
inputFile = inputB cmd
outputFile = outputB cmd
case inputFile of
Just file -> do
str <- open file
case outputFile of
Just f -> saveToFile f str
Nothing -> throwError $ action transducer str
Nothing -> liftIO $ interact (action transducer)
-- | Apply up in batch mode
upB :: Transducer String -> String -> String
upB transducer str =
case applyUp transducer (words str) of
Just xs -> unlines (map unwords xs)
Nothing -> []
-- | Apply down in batch mode
downB :: Transducer String -> String -> String
downB transducer str =
case applyDown transducer (words str) of
Just xs -> unlines (map unwords xs)
Nothing -> []
-- | Error when there is no built transducer
noTransducer :: String
noTransducer = "No transducer has been loaded/built."
-- | Error when there is no regular expression
noExpression :: String
noExpression = "No regular expression has been typed/loaded into fstStudio."
-- | Error when there is no loaded input
noInput :: String
noInput = "No input has been loaded."
-- | Error when no output has been produced
noOutputs :: String
noOutputs = "No outputs has been produced."
-- | Adds a new transducer to the environment and returns it
mkTransducer :: MonadState Info m => Transducer String -> m (Transducer String)
mkTransducer newTransducer = do
modify (updateTransducer newTransducer)
return newTransducer
-- | Main interactive-shell loop
loop :: StateT Info (InputT IO) ()
loop = do
input <- lift $ getInputLine "> "
let command = fmap (parseInteractive . words) input
case command of
Nothing -> return ()
Just Quit -> lift $ outputStrLn "Session ended."
Just cmd -> do
-- Run a single command entered at the prompt
result <- runErrorT $ runCmd cmd
-- Print the resulting output or error message
lift (either outputStrLn outputStrLn result)
loop
-- | Called for each user command
runCmd :: InteractiveCommand -> ErrorT String (StateT Info (InputT IO)) String
runCmd BuildTransducer = do
info <- get
unless (expressionRead info) $ throwError noExpression
let newTransducer = compile (getExpression info) []
modify (updateTransducer newTransducer)
return $ printf "Built a deterministic, minimal transducer with %d states and %d transitions."
(numberOfStates newTransducer) (numberOfTransitions newTransducer)
runCmd BuildNTransducer = do
info <- get
unless (expressionRead info) $ throwError noExpression
newTransducer <- mkTransducer $ compileN (getExpression info) []
return $ printf "Built a possibly non-deterministic, non-minimal transducer with %d states and %d transitions."
(numberOfStates newTransducer) (numberOfTransitions newTransducer)
runCmd Minimize = do
info <- get
unless (transducerBuilt info) $ throwError noTransducer
newTransducer <- mkTransducer $ minimize $ getTransducer info
return $
printf "Minimized loaded/built transducer resulting in a transducer with %d states and %d transitions."
(numberOfStates newTransducer) (numberOfTransitions newTransducer)
runCmd Determinize = do
info <- get
unless (transducerBuilt info) $ throwError noTransducer
newTransducer <- mkTransducer $ determinize $ getTransducer info
return $
printf "Determinized loaded/built transducer resulting in a transducer with %d states and %d transitions."
(numberOfStates newTransducer) (numberOfTransitions newTransducer)
runCmd ViewTransducer = do
info <- get
if transducerBuilt info
then return $ showTransducer $ getTransducer info
else throwError $ noTransducer
runCmd (Load file)
| isFST file = do
res <- liftIO $ runErrorT $ open file
str <- ErrorT $ return res
reg <- ErrorT $ return $ parseProgram str
modify (updateExpression reg)
return (printf "Loaded a regular relation from %s." file)
| isNET file = do
res <- liftIO $ runErrorT $ load file
transd <- ErrorT $ return res
modify (updateTransducer transd)
return (printf "Loaded transducer from file %s." file)
| isDAT file = do
res <- liftIO $ runErrorT $ open file
str <- ErrorT $ return res
modify $ updateInput $ words str
return $ printf "Read input from file %s." file
| otherwise =
throwError $ "Unable to load from "++file++". The filename must end with *.fst, *.net or *.dat."
runCmd (LUnion file1 file2)
| isNET file1 && isNET file2 = do
res1 <- liftIO $ runErrorT $ load file1
res2 <- liftIO $ runErrorT $ load file2
t1 <- ErrorT $ return res1
t2 <- ErrorT $ return res2
modify $ updateTransducer (unionT t1 t2)
return "Loaded and unified two transducers."
| isNET file1 && isTHIS file2 = unionWith file1
| isTHIS file1 && isNET file2 = unionWith file2
| otherwise = return $ printf "Unable to union %s and %s." file1 file2 where
unionWith file = do
info <- get
res <- liftIO $ runErrorT $ load file
unless (transducerBuilt info) $ throwError "No interior transducer built."
r1 <- ErrorT $ return res
modify $ \info -> updateTransducer (unionT r1 (getTransducer info)) info
return "Loaded a transducer, and unified it with the interior transducer."
runCmd (LProduct file1 file2)
| isNET file1 && isNET file2 = do
res1 <- liftIO $ runErrorT $ load file1
res2 <- liftIO $ runErrorT $ load file2
t1 <- ErrorT (return res1)
t2 <- ErrorT (return res2)
modify (updateTransducer (productT t1 t2))
return "Loaded and concatenated two transducers."
| isNET file1 && isTHIS file2 = productWith file1
| isTHIS file1 && isNET file2 = productWith file2
| otherwise = return $ printf "Unable to concatenate %s and %s." file1 file2 where
productWith file = do
info <- get
res <- liftIO $ runErrorT $ load file
unless (transducerBuilt info) $ throwError "No interior transducer built."
t1 <- ErrorT $ return res
modify $ \info -> updateTransducer (productT t1 (getTransducer info)) info
return "Loaded a transducer, and concatenated it with the interior transducer."
runCmd (LStar file)
| isNET file = do
res <- liftIO $ runErrorT $ load file
t1 <- ErrorT (return res)
modify $ updateTransducer (starT t1)
return "Loaded a transducer, and applied Kleene's star."
| isTHIS file = do
info <- get
unless (transducerBuilt info) $ throwError "No interior transducer built."
modify $ updateTransducer (starT (getTransducer info))
return "Applied Kleene's star on interior transducer."
| otherwise = return $ printf "Unable to apply Kleene's star on %s." file
runCmd (LComposition file1 file2)
| isNET file1 && isNET file2 = do
res1 <- liftIO $ runErrorT $ load file1
res2 <- liftIO $ runErrorT $ load file2
t1 <- ErrorT (return res1)
t2 <- ErrorT (return res2)
modify $ updateTransducer (compositionT t1 t2)
return "Loaded and composed two transducers."
| isNET file1 && isTHIS file2 = composeWith file1
| isTHIS file1 && isNET file2 = composeWith file2
| otherwise = return $ printf "Unable to compose %s and %s." file1 file2 where
composeWith file = do
info <- get
res <- liftIO $ runErrorT $ load file
unless (transducerBuilt info) $ throwError "No interior transducer built."
t1 <- ErrorT (return res)
modify $ \info -> updateTransducer (compositionT t1 (getTransducer info)) info
return "Loaded a transducer, and composed it with the interior transducer."
runCmd (Save file) = do
info <- get
case () of
_ | isNET file -> do
res <- liftIO $ runErrorT $ save file $ getTransducer info
_ <- ErrorT (return res)
return $ printf "Saved transducer to file %s." file
| outputsRead info -> do
res <- liftIO $ runErrorT $ saveToFile file $ unlines $ getOutputs info
_ <- ErrorT (return res)
return $ printf "Saved outputs to file %s." file
| otherwise -> return noOutputs
runCmd (StdInReg f) =
case parseExp f of
Left err -> throwError err
Right reg -> modify (updateExpression reg) >> return "Read a regular relation."
runCmd ViewReg = do
info <- get
if expressionRead info
then return $ show (getExpression info)
else throwError noExpression
runCmd Quit = return "Session ended."
runCmd ClearMemory = modify (const emptyInfo) >> return ""
runCmd NoCommand = throwError "Invalid Command. Type 'h' for help."
runCmd Help = return help
runCmd ViewInput = do
info <- get
if inputRead info
then return $ unwords $ getInput info
else throwError noInput
runCmd ViewOutput = do
info <- get
if outputsRead info
then return $ unlines $ getOutputs info
else throwError noOutputs
runCmd ApplyUp = do
info <- get
case (transducerBuilt info, inputRead info) of
(True, True) ->
case applyUp (getTransducer info) (getInput info) of
Just res -> do
modify $ updateOutputs $ map unwords res
return "Input accepted. Type 'vo' to view outputs."
Nothing -> throwError "Input rejected."
(True, False) -> throwError noTransducer
_ -> throwError noInput
runCmd ApplyDown = do
info <- get
case (transducerBuilt info, inputRead info) of
(True, True) ->
case applyDown (getTransducer info) (getInput info) of
Just res -> do
modify (updateOutputs (map unwords res))
return "Input accepted. Type 'vo' to view outputs."
Nothing -> throwError "Input rejected."
(True, False) -> throwError noTransducer
_ -> throwError noInput
runCmd (ApplyU inp) = do
info <- get
unless (transducerBuilt info) $ throwError noTransducer
case applyUp (getTransducer info) inp of
Just res -> do
modify $ updateOutputs $ map unwords res
return "Input accepted. Type 'vo' to view outputs."
Nothing -> return "Input rejected."
runCmd (ApplyD inp) = do
info <- get
unless (transducerBuilt info) $ throwError noTransducer
case applyDown (getTransducer info) inp of
Just res -> do
modify $ updateOutputs $ map unwords res
return "Input accepted. Type 'vo' to view outputs."
Nothing -> return "Input rejected."
-- | Dislay list of shell commands for user
help :: String
help = unlines [
"List of Commands:",
"r <reg exp> : read a regular relation from standard input.",
"b : build a deterministic, minimal transducer.",
"bn : build a possibly non-deterministic, non-minimal transducer.",
"m : minimize loaded/built transducer.",
"det : determinize loaded/built transducer.",
"s <filename> : save to file.",
"l <filename> : load from file.",
"l a | b : load and union.",
"l a b : load and concatenate.",
"l a * : load and apply Kleene's star.",
"l a .o. b : load and compose.",
"vt : view loaded/built transducer.",
"vr : view typed/loaded regular relation.",
"vi : view loaded input.",
"vo : view produced output.",
"d : apply transducer down with loaded input.",
"u : apply transducer up with loaded input.",
"d <symbols> : apply transducer down with symbols.",
"u <symbols> : apply transducer up with symbols.",
"c : Clear memory.",
"h : display list of commands.",
"q : end session."
]