@@ -36,7 +36,7 @@ import Control.Monad (unless, void)
3636import Control.Monad.IO.Class (liftIO )
3737import Control.Monad.State (modify )
3838
39- import Data.Char (isSpace , isPunctuation , isSymbol )
39+ import Data.Char (isSpace )
4040import Data.Function (on )
4141import Data.List (intersperse , nub )
4242import qualified Data.Text as Text
@@ -61,6 +61,10 @@ import SAWCentral.TopLevel (TopLevelRW(..))
6161import 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
7478cdCmd :: 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
137141searchCmd :: Text -> REPL ()
138142searchCmd 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
276280typeOfCmd :: Text -> REPL ()
277281typeOfCmd 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.
302299data CommandDescr = CommandDescr
303300 { cName :: Text
@@ -325,7 +322,7 @@ instance Ord CommandDescr where
325322-- from "one arg of this type".
326323data 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]
350347nbCommandList =
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.
440401sanitize :: Text -> Text
441402sanitize = 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.
528494executeText :: 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'
0 commit comments