Skip to content

Commit c249a64

Browse files
committed
REPL: Now rework the way things execute in Command.hs.
1 parent 7de059b commit c249a64

File tree

2 files changed

+94
-125
lines changed

2 files changed

+94
-125
lines changed

saw-script/src/SAWScript/REPL/Command.hs

Lines changed: 93 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import Control.Monad (unless, void)
3636
import Control.Monad.IO.Class (liftIO)
3737
import Control.Monad.State (modify)
3838

39-
import Data.Char (isSpace,isPunctuation,isSymbol)
39+
import Data.Char (isSpace)
4040
import Data.Function (on)
4141
import Data.List (intersperse, nub)
4242
import qualified Data.Text as Text
@@ -61,6 +61,10 @@ import SAWCentral.TopLevel (TopLevelRW(..))
6161
import SAWCentral.AST (PrimitiveLifecycle(..), everythingAvailable)
6262

6363

64+
replFileName :: FilePath
65+
replFileName = "<stdin>"
66+
67+
6468
------------------------------------------------------------
6569
-- REPL commands
6670

@@ -72,7 +76,7 @@ failOn errs = case (reverse errs) of
7276
fail (Text.unpack lastMsg)
7377

7478
cdCmd :: FilePath -> REPL ()
75-
cdCmd f | null f = liftIO $ putStrLn $ "[error] :cd requires a path argument"
79+
cdCmd f | null f = liftIO $ putStrLn $ "Error: The :cd command requires a path argument"
7680
| otherwise = do
7781
exists <- liftIO $ doesDirectoryExist f
7882
if exists
@@ -136,7 +140,7 @@ quitCmd =
136140

137141
searchCmd :: Text -> REPL ()
138142
searchCmd str
139-
| Text.null str = liftIO $ putStrLn $ "[error] :search requires at least one argument"
143+
| Text.null str = liftIO $ putStrLn $ "Error: The :search command requires at least one argument"
140144
| otherwise = do
141145

142146
-- FUTURE: it would be nice to be able to use the words
@@ -275,7 +279,7 @@ tenvCmd = do
275279

276280
typeOfCmd :: Text -> REPL ()
277281
typeOfCmd str
278-
| Text.null str = liftIO $ putStrLn "[error] :type requires an argument"
282+
| Text.null str = liftIO $ putStrLn "Error: The :type command requires an argument"
279283
| otherwise = do
280284
rw <- getTopLevelRW
281285
let environ = rwEnviron rw
@@ -291,13 +295,6 @@ typeOfCmd str
291295
------------------------------------------------------------
292296
-- Command table
293297

294-
-- | Result of searching for a command.
295-
data SearchResult
296-
= Found (REPL ()) -- ^ Successfully parsed command
297-
| Ambiguous Text [Text] -- ^ Ambiguous name with list of
298-
-- possible matches
299-
| Unknown Text -- ^ An unknown command
300-
301298
-- | Command description.
302299
data CommandDescr = CommandDescr
303300
{ cName :: Text
@@ -325,7 +322,7 @@ instance Ord CommandDescr where
325322
-- from "one arg of this type".
326323
data CommandBody
327324
= ExprArg (Text -> REPL ())
328-
| TypeArg (Text -> REPL ())
325+
| TypeArgs (Text -> REPL ())
329326
| FilenameArg (FilePath -> REPL ())
330327
| ShellArg (Text -> REPL ())
331328
| NoArg (REPL ())
@@ -350,7 +347,7 @@ nbCommandList :: [CommandDescr]
350347
nbCommandList =
351348
[ CommandDescr ":env" [] (NoArg envCmd)
352349
"display the current sawscript environment"
353-
, CommandDescr ":search" [] (TypeArg searchCmd)
350+
, CommandDescr ":search" [] (TypeArgs searchCmd)
354351
"search the environment by type"
355352
, CommandDescr ":tenv" [] (NoArg tenvCmd)
356353
"display the current sawscript type environment"
@@ -382,68 +379,28 @@ genHelp cs = map cmdHelp cs
382379

383380

384381
------------------------------------------------------------
385-
-- Evaluation
386-
387-
-- | Run a command.
388-
runCommand :: SearchResult -> REPL ()
389-
runCommand sr = case sr of
390-
Found cmd ->
391-
exceptionProtect cmd
392-
393-
Unknown cmd ->
394-
liftIO $ TextIO.putStrLn $ "Unknown command: " <> cmd
395-
396-
Ambiguous cmd cmds -> liftIO $ do
397-
TextIO.putStrLn $ cmd <> " is ambiguous; it could mean one of:"
398-
TextIO.putStrLn $ "\t" <> Text.intercalate ", " cmds
399-
400-
401-
{- Evaluation is fairly straightforward; however, there are a few important
402-
caveats:
403-
404-
1. 'return' is type-polymorphic. This means that 'return "foo"' will produce
405-
a type-polymorphic function 'm -> m String', for any monad 'm'. It also
406-
means that if you type 'return "foo"' into a naively-written interpreter,
407-
you won't see 'foo'! The solution is to force each statement that comes
408-
into the REPL to have type 'TopLevel t' ('TopLevel' is the SAWScript
409-
version of 'IO'). This gets done as soon as the statement is parsed.
410-
411-
2. Handling binding variables to values is somewhat tricky. When you're
412-
allowed to bind variables in the REPL, you're basically working in the
413-
context of a partially evaluated module: all the results of all the old
414-
computations are in scope, and you're allowed to add new computations that
415-
depend on them. It's insufficient to merely hang onto the AST for the
416-
old computations, as that could cause them to be evaluated multiple times;
417-
it could also cause their type to be inferred differently several times,
418-
which is bad. Instead, we hang onto the inferred types of previous
419-
computations and use them to seed the name resolver and the typechecker;
420-
we also hang onto the results and use them to seed the interpreter. -}
421-
sawScriptCmd :: Text -> REPL ()
422-
sawScriptCmd str = do
382+
-- SAWScript execution
383+
384+
-- | Execute some SAWScript text.
385+
executeSAWScriptText :: Text -> REPL ()
386+
executeSAWScriptText str = do
423387
errs_or_stmt <- liftIO $ Loader.readStmtSemiUnchecked replFileName str
424388
case errs_or_stmt of
425389
Left errs -> failOn errs
426-
Right stmt ->
427-
do mpst <- getProofState
428-
case mpst of
390+
Right stmt -> do
391+
mbPst <- getProofState
392+
exceptionProtect $ case mbPst of
429393
Nothing -> void $ liftTopLevel (interpretTopStmt True stmt)
430394
Just _ -> void $ liftProofScript (interpretTopStmt True stmt)
431395

432-
replFileName :: FilePath
433-
replFileName = "<stdin>"
434-
435396

436397
------------------------------------------------------------
437-
-- Command parsing
398+
-- Command execution
438399

439400
-- | Strip leading space.
440401
sanitize :: Text -> Text
441402
sanitize = Text.dropWhile isSpace
442403

443-
-- | Strip trailing space.
444-
sanitizeEnd :: Text -> Text
445-
sanitizeEnd = Text.dropWhileEnd isSpace
446-
447404
-- | Find commands that begin with a given prefix.
448405
--
449406
-- If given a string that's both itself a command and a prefix of
@@ -468,68 +425,80 @@ searchExactCommandByPrefix prefix =
468425
[cmd] -> Just cmd
469426
_ -> Nothing
470427

471-
-- | Split at the first word boundary.
472-
splitCommand :: Text -> Maybe (Text, Text)
473-
splitCommand txt = do
474-
(c, more) <- Text.uncons (sanitize txt)
475-
case c of
476-
':'
477-
| (as,bs) <- Text.span (\x -> isPunctuation x || isSymbol x) more
478-
, not (Text.null as) -> Just (Text.cons ':' as, sanitize bs)
479-
480-
| (as,bs) <- Text.break isSpace more
481-
, not (Text.null as) -> Just (Text.cons ':' as, sanitize bs)
482-
483-
| otherwise -> Nothing
484-
485-
_ -> Just (Text.cons c more, "")
486-
487-
-- | Look up a string in a command list. If given a string that's both
488-
-- itself a command and a prefix of something else, choose that
489-
-- command; otherwise such commands are inaccessible. Also, deduplicate
490-
-- the list of results to avoid silliness with command aliases.
491-
findSomeCommand :: Text -> CommandMap -> [CommandDescr]
492-
findSomeCommand str commandTable = nub $ Trie.lookupWithExact str commandTable
493-
494-
-- | Look up a string in the command list.
495-
findCommand :: Text -> [CommandDescr]
496-
findCommand str = findSomeCommand str commands
497-
498-
-- | Parse a line as a command.
499-
parseCommand :: (Text -> [CommandDescr]) -> Text -> Maybe SearchResult
500-
parseCommand findCmd line = do
501-
(cmd,args) <- splitCommand line
502-
let args' = sanitizeEnd args
503-
case findCmd cmd of
504-
-- nothing matched; if it doesn't begin with a colon, eval it
505-
[] -> case Text.uncons cmd of
506-
Nothing -> Nothing
507-
Just (':', _) -> Just (Unknown cmd)
508-
Just _ -> Just (Found (sawScriptCmd line))
509-
510-
-- matched exactly one command; run it
511-
[c] -> case cBody c of
512-
ExprArg body -> Just (Found (body args'))
513-
TypeArg body -> Just (Found (body args'))
514-
FilenameArg body -> Just (Found (body =<< expandHome args'))
515-
ShellArg body -> Just (Found (body args'))
516-
NoArg body -> Just (Found body)
517-
518-
-- matched several things; complain
519-
cs -> Just (Ambiguous cmd (map cName cs))
520-
521-
where
522-
expandHome path =
523-
case Text.unpack path of
524-
'~' : c : more | isPathSeparator c -> do dir <- liftIO getHomeDirectory
525-
return (dir </> more)
526-
path' -> pure path'
527-
428+
-- | Do tilde-expansion on filenames.
429+
expandHome :: Text -> REPL FilePath
430+
expandHome path =
431+
case Text.uncons path of
432+
Nothing -> pure ""
433+
Just ('~', more) -> case Text.uncons more of
434+
Just (c, more') | isPathSeparator c -> do
435+
dir <- liftIO getHomeDirectory
436+
return (dir </> Text.unpack more')
437+
_ ->
438+
pure $ Text.unpack path
439+
Just _ ->
440+
pure $ Text.unpack path
441+
442+
-- | Execute a REPL :-command.
443+
executeReplCommand :: CommandDescr -> [Text] -> REPL ()
444+
executeReplCommand cmd args0 =
445+
let noargs action args = case args of
446+
[] -> action
447+
_ -> do
448+
let msg = "The command " <> cName cmd <> " takes no arguments"
449+
liftIO $ TextIO.putStrLn msg
450+
in
451+
let onearg action args = case args of
452+
[] -> action ""
453+
[arg] -> action arg
454+
_ -> do
455+
let msg = "The command " <> cName cmd <> " takes only one argument"
456+
liftIO $ TextIO.putStrLn msg
457+
in
458+
exceptionProtect $ case cBody cmd of
459+
ExprArg action ->
460+
onearg action args0
461+
TypeArgs action ->
462+
action (Text.intercalate " " args0)
463+
FilenameArg action -> do
464+
args' <- mapM expandHome args0
465+
onearg action args'
466+
ShellArg action ->
467+
onearg action args0
468+
NoArg action ->
469+
noargs action args0
470+
471+
-- | Execute REPL :-command text.
472+
executeReplCommandText :: Text -> REPL ()
473+
executeReplCommandText text =
474+
let textWords = filter (\w -> not $ Text.null w) $ Text.split isSpace text in
475+
case textWords of
476+
[] -> pure ()
477+
cmdName : args ->
478+
case searchCommandsByPrefix cmdName of
479+
[] ->
480+
-- Historically SAW accepts ":?cmd" without a space
481+
if Text.isPrefixOf ":?" cmdName then
482+
executeReplCommandText $ ":? " <> Text.drop 2 cmdName
483+
else
484+
liftIO $ TextIO.putStrLn $ "Unknown command: " <> cmdName
485+
[cmd] ->
486+
executeReplCommand cmd args
487+
cmds -> liftIO $ do
488+
let msg1 = cmdName <> " is ambiguous; it could mean one of:"
489+
msg2 = Text.intercalate ", " $ map cName cmds
490+
TextIO.putStrLn $ msg1
491+
TextIO.putStrLn $ "\t" <> msg2
492+
493+
-- | Execute some text typed at the REPL.
528494
executeText :: Text -> REPL ()
529-
executeText text =
530-
-- XXX why is findCommand passed to parseCommand...?
531-
case parseCommand findCommand text of
495+
executeText text = do
496+
-- skip whitespace
497+
let text' = sanitize text
498+
case Text.uncons text' of
532499
Nothing ->
533500
pure ()
534-
Just result -> do
535-
runCommand result
501+
Just (':', _) ->
502+
executeReplCommandText text'
503+
Just _ ->
504+
executeSAWScriptText text'

saw-script/src/SAWScript/REPL/Haskeline.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ completeReplCommand text cursor =
351351
-- strengthen the argument schema.
352352
case cBody cmd of
353353
ExprArg _ -> completeSAWScriptValue (last args) cursor
354-
TypeArg _ -> completeSAWScriptType (last args) cursor
354+
TypeArgs _ -> completeSAWScriptType (last args) cursor
355355
FilenameArg _ -> completeFilename cursor
356356
ShellArg _ -> completeFilename cursor
357357
NoArg _ -> return (cursorLeftRaw cursor, [])

0 commit comments

Comments
 (0)