|
1
|
1
|
{-# LANGUAGE GADTs, RecordWildCards, MagicHash, ScopedTypeVariables, CPP,
|
|
2
|
|
- UnboxedTuples, LambdaCase, UnliftedFFITypes #-}
|
|
|
2
|
+ UnboxedTuples, LambdaCase, UnliftedFFITypes, NamedFieldPuns #-}
|
|
3
|
3
|
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
|
|
4
|
4
|
{-# LANGUAGE BangPatterns #-}
|
|
5
|
5
|
|
| ... |
... |
@@ -32,6 +32,7 @@ import GHCi.BreakArray |
|
32
|
32
|
import GHCi.StaticPtrTable
|
|
33
|
33
|
|
|
34
|
34
|
import qualified Data.Map as Map
|
|
|
35
|
+import qualified Data.Set as Set
|
|
35
|
36
|
import Control.Concurrent
|
|
36
|
37
|
import Control.DeepSeq
|
|
37
|
38
|
import Control.Exception
|
| ... |
... |
@@ -211,7 +212,9 @@ resumeSeq hvref = clearEvalStatus <$> do |
|
211
|
212
|
mask_ $ do
|
|
212
|
213
|
putMVar resumeBreakMVar () -- this awakens the stopped thread...
|
|
213
|
214
|
redirectInterrupts resumeThreadId $
|
|
214
|
|
- readThreadEvalStatus resumeThreadId
|
|
|
215
|
+ withShadowThread resumeThreadId $
|
|
|
216
|
+ atomically $
|
|
|
217
|
+ readThreadEvalStatus resumeThreadId
|
|
215
|
218
|
|
|
216
|
219
|
evalOptsSeq :: EvalOpts
|
|
217
|
220
|
evalOptsSeq = EvalOpts
|
| ... |
... |
@@ -244,8 +247,9 @@ sandboxIO opts io = do |
|
244
|
247
|
labelThread tid "GHCi sandbox"
|
|
245
|
248
|
unsafeUnmask runIt >>= writeThreadEvalStatus tid
|
|
246
|
249
|
|
|
247
|
|
- redirectInterrupts tid $ unsafeUnmask $
|
|
248
|
|
- readThreadEvalStatus tid
|
|
|
250
|
+ redirectInterrupts tid $ unsafeUnmask $ atomically $
|
|
|
251
|
+ (EvalPaused <$> readAnyThreadEvalBreak)
|
|
|
252
|
+ `orElse` readThreadEvalStatus tid
|
|
249
|
253
|
else
|
|
250
|
254
|
-- GLUT on OS X needs to run on the main thread. If you
|
|
251
|
255
|
-- try to use it from another thread then you just get a
|
| ... |
... |
@@ -379,7 +383,8 @@ resumeStmt opts rtid = do |
|
379
|
383
|
mask_ $ do
|
|
380
|
384
|
putMVar resumeBreakMVar () -- this awakens the stopped thread...
|
|
381
|
385
|
redirectInterrupts resumeThreadId $
|
|
382
|
|
- readThreadEvalStatus resumeThreadId
|
|
|
386
|
+ withShadowThread resumeThreadId $
|
|
|
387
|
+ atomically $ readThreadEvalStatus resumeThreadId
|
|
383
|
388
|
|
|
384
|
389
|
-- when abandoning a computation we have to
|
|
385
|
390
|
-- (a) kill the thread with an async exception, so that the
|
| ... |
... |
@@ -399,7 +404,8 @@ abandonStmt hvref = do |
|
399
|
404
|
ResumeContext{..} <- getThreadResumeContext tid
|
|
400
|
405
|
killThread tid
|
|
401
|
406
|
putMVar resumeBreakMVar ()
|
|
402
|
|
- _ <- readThreadEvalStatus tid
|
|
|
407
|
+ _ <- withShadowThread tid $ atomically $
|
|
|
408
|
+ readThreadEvalStatus tid
|
|
403
|
409
|
return ()
|
|
404
|
410
|
|
|
405
|
411
|
-- Malloc and copy the bytes. We don't have any way to monitor the
|
| ... |
... |
@@ -505,42 +511,114 @@ newtype ResumeContext = ResumeContext |
|
505
|
511
|
-- The debugger can unblock that thread by signaling its MVar.
|
|
506
|
512
|
}
|
|
507
|
513
|
|
|
508
|
|
--- brilliant STM
|
|
509
|
|
--- * only retries when the map is written again
|
|
510
|
|
--- * essentially blocks until the result for this thread is there
|
|
511
|
|
-readThreadEvalStatus :: ThreadId -> IO (EvalStatus [HValueRef])
|
|
|
514
|
+-- | To evaluate a statement in the interpreter, a thread is forked by
|
|
|
515
|
+-- `sandboxIO` to run the statement. To synchronize, the statement result will
|
|
|
516
|
+-- be written by the thread to the global thread dbg status
|
|
|
517
|
+-- (`writeThreadEvalStatus`) and `sandboxIO` will block waiting for either any
|
|
|
518
|
+-- breakpoint to be hit ('readAnyThreadEvalBreak') /or/ for the thread
|
|
|
519
|
+-- specifically to write its final result. To wait for that thread
|
|
|
520
|
+-- specifically, use this function.
|
|
|
521
|
+--
|
|
|
522
|
+-- Succeeds when the given thread writes to the global debugger map an eval result.
|
|
|
523
|
+readThreadEvalStatus :: ThreadId -> STM (EvalStatus [HValueRef])
|
|
512
|
524
|
readThreadEvalStatus tid = do
|
|
513
|
|
- atomically $ do
|
|
514
|
|
- dbgmap <- readTVar globalDbgStatusVar
|
|
515
|
|
- case Map.lookup tid dbgmap of
|
|
516
|
|
- Just r -> do
|
|
517
|
|
- writeTVar globalDbgStatusVar $!
|
|
518
|
|
- Map.delete tid dbgmap -- clear it
|
|
519
|
|
- pure r
|
|
520
|
|
- Nothing -> retry -- until someone writes the result for this thread
|
|
|
525
|
+ GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
|
526
|
+ case Map.lookup tid threadEvalStatuses of
|
|
|
527
|
+ Just r -> do
|
|
|
528
|
+ writeTVar globalDbgStatusVar
|
|
|
529
|
+ GlobalDbgStatus
|
|
|
530
|
+ { threadEvalStatuses = Map.delete tid threadEvalStatuses -- clear it
|
|
|
531
|
+ , shadowedThreads
|
|
|
532
|
+ }
|
|
|
533
|
+ pure r
|
|
|
534
|
+ Nothing -> retry -- until someone writes the result for this thread
|
|
|
535
|
+
|
|
|
536
|
+-- | All threads running in the interpreter will be debugger-enabled, meaning
|
|
|
537
|
+-- if they hit an enabled breakpoint, the 'globalBreakAction' will be
|
|
|
538
|
+-- executed and write a 'EvalBreak' value to the global thread dbg status map.
|
|
|
539
|
+-- The debugger will be waiting for threads to break using `readAnyThreadEvalBreak`.
|
|
|
540
|
+--
|
|
|
541
|
+-- Succeeds when any thread hits a breakpoint.
|
|
|
542
|
+readAnyThreadEvalBreak :: STM EvalBreak
|
|
|
543
|
+readAnyThreadEvalBreak = do
|
|
|
544
|
+ dbgs <- readTVar globalDbgStatusVar
|
|
|
545
|
+ -- try reading all mvars and retry if none yield a result
|
|
|
546
|
+ foldr (\x next -> readStatus dbgs x `orElse` next) retry (Map.toList (threadEvalStatuses dbgs))
|
|
|
547
|
+ where
|
|
|
548
|
+ readStatus GlobalDbgStatus{..} (tid, _) -- don't look at dbg-shadowed threads
|
|
|
549
|
+ | Set.member tid shadowedThreads
|
|
|
550
|
+ = retry
|
|
|
551
|
+
|
|
|
552
|
+ readStatus GlobalDbgStatus{..} (tid, status)
|
|
|
553
|
+ = do
|
|
|
554
|
+ case status of
|
|
|
555
|
+ EvalComplete{} -> retry -- successes can be read with `readThreadEvalStatus`
|
|
|
556
|
+ EvalPaused b -> do
|
|
|
557
|
+ writeTVar globalDbgStatusVar
|
|
|
558
|
+ GlobalDbgStatus
|
|
|
559
|
+ { threadEvalStatuses = Map.delete tid threadEvalStatuses
|
|
|
560
|
+ , shadowedThreads
|
|
|
561
|
+ }
|
|
|
562
|
+ pure b
|
|
521
|
563
|
|
|
522
|
564
|
writeThreadEvalStatus :: ThreadId -> EvalStatus [HValueRef] -> IO ()
|
|
523
|
|
-writeThreadEvalStatus tid new = do
|
|
|
565
|
+writeThreadEvalStatus tid new =
|
|
524
|
566
|
atomically $ do
|
|
525
|
|
- dbgmap <- readTVar globalDbgStatusVar
|
|
526
|
|
- writeTVar globalDbgStatusVar $!
|
|
527
|
|
- Map.insertWith (\_ _ -> error "writeThreadEvalStatus: should be impossible") tid new dbgmap
|
|
528
|
|
-
|
|
529
|
|
--- | A global synchronization variable which a thread writes an 'EvalBreak' to
|
|
530
|
|
--- when it hits a breakpoint. This variable is shared across all threads, and
|
|
531
|
|
--- the debugger can read the threads which hit breakpoints by reading from this
|
|
532
|
|
--- variable. It will also be written with 'EvalSuccess' when a `sandboxIO`
|
|
533
|
|
--- action finishes (TODO: wait what? that is a bit weird. so what if we run two
|
|
534
|
|
--- evalStmt? they will both race to write a result? but then how do we tell
|
|
535
|
|
--- which result we got from where? this won't do in general, but let's see if
|
|
536
|
|
--- it can solve some problems first)
|
|
|
567
|
+ GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
|
568
|
+ writeTVar globalDbgStatusVar $
|
|
|
569
|
+ GlobalDbgStatus
|
|
|
570
|
+ { threadEvalStatuses =
|
|
|
571
|
+ Map.insertWith (\_ _ -> error "writeThreadEvalStatus: should be impossible")
|
|
|
572
|
+ tid new threadEvalStatuses
|
|
|
573
|
+ , shadowedThreads }
|
|
|
574
|
+
|
|
|
575
|
+-- | Make sure that anyone watching for threads hitting breakpoints with
|
|
|
576
|
+-- 'readAnyThreadEvalBreak' ignore the given thread if it ever hits a breakpoint.
|
|
537
|
577
|
--
|
|
538
|
|
--- IDEA: maybe `sandboxIO` and friends receive as an argument the var on which
|
|
539
|
|
--- to expect the result specifically? And distinguish that from the global break var?
|
|
540
|
|
--- Then we split the EvalSuccess and EvalBreak into two datatypes.
|
|
|
578
|
+-- This forces this thread's result to be received solely through
|
|
|
579
|
+-- 'readThreadEvalStatus' for that ThreadId, which is typically desired when an
|
|
|
580
|
+-- expression to run should not be seen at all by the debugger (e.g. when
|
|
|
581
|
+-- evaluating an expr to :force a variable)
|
|
|
582
|
+withShadowThread :: ThreadId -> IO a -> IO a
|
|
|
583
|
+withShadowThread tid = bracket_ setShadowThread unsetShadowThread
|
|
|
584
|
+ where
|
|
|
585
|
+ setShadowThread = atomically $ do
|
|
|
586
|
+ GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
|
587
|
+ writeTVar globalDbgStatusVar
|
|
|
588
|
+ GlobalDbgStatus
|
|
|
589
|
+ { threadEvalStatuses
|
|
|
590
|
+ , shadowedThreads = Set.insert tid shadowedThreads
|
|
|
591
|
+ }
|
|
|
592
|
+ unsetShadowThread = atomically $ do
|
|
|
593
|
+ GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
|
594
|
+ writeTVar globalDbgStatusVar
|
|
|
595
|
+ GlobalDbgStatus
|
|
|
596
|
+ { threadEvalStatuses
|
|
|
597
|
+ , shadowedThreads = Set.delete tid shadowedThreads
|
|
|
598
|
+ }
|
|
|
599
|
+
|
|
|
600
|
+-- | When we launch a new thread using the debugger, we map ... TODO ... all
|
|
|
601
|
+-- threads launched by that main stmt may further spawn threads, and any of
|
|
|
602
|
+-- them might hit a breakpoint (break array toggled, or per-thread step in
|
|
|
603
|
+-- might be used, or pause button, etc...).
|
|
541
|
604
|
--
|
|
542
|
|
--- Instead, let's just assume we may wait for a single thread's result and this
|
|
543
|
|
--- kind of falls out nicely. See my impl. notes.
|
|
544
|
|
-globalDbgStatusVar :: TVar (Map.Map ThreadId (EvalStatus [HValueRef]))
|
|
545
|
|
-globalDbgStatusVar = unsafePerformIO $ newTVarIO Map.empty
|
|
|
605
|
+-- When we launch that main thread we also write its result to this map. So
|
|
|
606
|
+-- this map will only contain the results of threads we explicitly care to
|
|
|
607
|
+-- observe the result of. Not all threads will write their results here,
|
|
|
608
|
+-- certainly not ones spawned unknowningly by the main stmt.
|
|
|
609
|
+-- ...
|
|
|
610
|
+-- todo words
|
|
|
611
|
+globalDbgStatusVar :: TVar GlobalDbgStatus
|
|
|
612
|
+globalDbgStatusVar = unsafePerformIO $ newTVarIO $ GlobalDbgStatus Map.empty Set.empty
|
|
546
|
613
|
{-# NOINLINE globalDbgStatusVar #-}
|
|
|
614
|
+
|
|
|
615
|
+data GlobalDbgStatus = GlobalDbgStatus
|
|
|
616
|
+ { threadEvalStatuses :: !(Map.Map ThreadId (EvalStatus [HValueRef]))
|
|
|
617
|
+ -- ^ Whenever a thread hits a breakpoint it inserts in this map its 'EvalBreak'.
|
|
|
618
|
+ -- We can additionally observe the result of a thread by calling
|
|
|
619
|
+ -- `writeThreadEvalStatus` at the end with its result, e.g. in 'sandboxIO'
|
|
|
620
|
+ , shadowedThreads :: !(Set.Set ThreadId)
|
|
|
621
|
+ -- ^ Threads in this map are meant to be ignored by 'readAnyThreadEvalBreak'.
|
|
|
622
|
+ -- That is, even if one of the threads in this set hits a breakpoint, we
|
|
|
623
|
+ -- can only read its 'EvalBreak' with @'readThreadEvalStatus' tid@ directly.
|
|
|
624
|
+ } |