summaryrefslogtreecommitdiff
path: root/source/Felix/Provers.hs
diff options
context:
space:
mode:
Diffstat (limited to 'source/Felix/Provers.hs')
-rw-r--r--source/Felix/Provers.hs1860
1 files changed, 1860 insertions, 0 deletions
diff --git a/source/Felix/Provers.hs b/source/Felix/Provers.hs
new file mode 100644
index 0000000..16ef655
--- /dev/null
+++ b/source/Felix/Provers.hs
@@ -0,0 +1,1860 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Felix.Provers
+ ( Vampire
+ , vampire
+ , VampireTaskMode(..)
+ , VampireStatus(..)
+ , CanonicalAtpOutcome(..)
+ , CanonicalAtpRejection(..)
+ , VampireProtocolError(..)
+ , classifyVampireProtocol
+ , vampireStatusParser
+ , TimeLimit(..)
+ , MemoryLimit(..)
+ , defaultTimeLimit
+ , defaultMemoryLimit
+ , ProverAnswer
+ ( CounterSatisfiable
+ , ContradictoryAxioms
+ , Uncertain
+ , Error
+ )
+ , pattern Yes
+ , ProverStream(..)
+ , ProverOutputStream(..)
+ , ProverProcessError(..)
+ , VerificationDialect(..)
+ , PreparedVerificationRequest
+ , preparedVerificationDialect
+ , preparedVerificationText
+ , preparedVerificationBytes
+ , preparedVerificationByteCount
+ , preparedVerificationRequestId
+ , PreparedTypedProverTask
+ , prepareTypedProverTask
+ , preparedTypedProverLogicalProblem
+ , preparedTypedProverRequest
+ , AcceptedVampireRun
+ , acceptedVampireRequestId
+ , provedVampireRun
+ , runPreparedTypedProver
+ , runPreparedTypedProverWithObserver
+ , EffectiveJobs
+ , effectiveJobs
+ , effectiveJobsValue
+ , selectEffectiveJobs
+ , WorkPosition
+ , workPosition
+ , workPositionModuleOrdinal
+ , workPositionLocalRequestOrdinal
+ , VampireExecutor
+ , VampireExecutorFault
+ , VampireRequestOwner
+ , VampireHandle
+ , VampireTerminal(..)
+ , renderVampireTerminalDiagnostic
+ , VampireCompletion
+ , vampireCompletionRequestId
+ , vampireCompletionTerminal
+ , AtpDuration
+ , atpDurationNanoseconds
+ , atpDurationFromNanoseconds
+ , SlowAtpOutcome(..)
+ , SlowAtpTask(..)
+ , SlowAtpReport(..)
+ , slowAtpOmittedTaskCount
+ , slowAtpReportFromTasks
+ , withVampireExecutor
+ , withVampireExecutorUsingClock
+ , withVampireRequestOwner
+ , submitVampireRequest
+ , awaitVampireRequest
+ , awaitPreparedVampireRequest
+ , cancelVampireRequest
+ , runPreparedTypedProverWithExecutor
+ , vampireExecutorSlowAtpReport
+ ) where
+
+import Base
+import Felix.Checking.Authority qualified as Authority
+import Felix.Checking.Backend.Problem
+import Felix.Checking.Backend.Tptp
+
+import Control.Concurrent.STM
+ ( STM
+ , TBQueue
+ , TMVar
+ , TVar
+ , atomically
+ , check
+ , modifyTVar'
+ , newEmptyTMVarIO
+ , newTBQueueIO
+ , newTVarIO
+ , orElse
+ , readTMVar
+ , readTBQueue
+ , readTVar
+ , readTVarIO
+ , throwSTM
+ , tryPutTMVar
+ , tryReadTMVar
+ , writeTBQueue
+ , writeTVar
+ )
+import Control.Exception
+ ( AsyncException
+ , IOException
+ , SomeException
+ , displayException
+ , fromException
+ )
+import Control.Exception qualified as Exception
+import Control.Monad (replicateM, unless, when)
+import Data.ByteString qualified as ByteString
+import Data.IntMap.Strict qualified as IntMap
+import Data.IORef
+ ( IORef
+ , atomicModifyIORef'
+ , newIORef
+ , readIORef
+ , writeIORef
+ )
+import Data.Set qualified as Set
+import Data.Map.Strict qualified as Map
+import Data.Ord (Down(..))
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as TextEncoding
+import Data.Text.Encoding.Error qualified as TextEncodingError
+import Numeric.Natural (Natural)
+import Felix.Report.Location (Location)
+import System.Exit (ExitCode(..))
+import System.Posix.Signals (sigKILL, signalProcessGroup)
+import System.Posix.Types (ProcessGroupID)
+import System.Process
+ ( CreateProcess(..)
+ , ProcessHandle
+ , StdStream(CreatePipe)
+ , getPid
+ , proc
+ , waitForProcess
+ , withCreateProcess
+ )
+import System.Timeout qualified as Timeout
+import Text.Megaparsec
+import Text.Megaparsec.Char qualified as Char
+import UnliftIO.Async
+ ( async
+ , cancel
+ , concurrently
+ , waitCatch
+ , waitCatchSTM
+ )
+
+data Vampire = Vampire
+ { vampireExecutable :: FilePath
+ , vampireTimeLimit :: TimeLimit
+ , vampireMemoryLimit :: MemoryLimit
+ }
+ deriving (Show, Eq)
+
+newtype TimeLimit = Seconds Word64 deriving (Show, Eq, Num)
+newtype MemoryLimit = Megabytes Word64 deriving (Show, Eq)
+
+defaultTimeLimit :: TimeLimit
+defaultTimeLimit = Seconds 10
+
+defaultMemoryLimit :: MemoryLimit
+defaultMemoryLimit = Megabytes 5000
+
+vampire :: FilePath -> TimeLimit -> MemoryLimit -> Vampire
+vampire = Vampire
+
+vampireArguments :: Vampire -> [String]
+vampireArguments Vampire{..} =
+ [ "--input_syntax", "tptp"
+ , "--mode", "casc"
+ , "--time_limit", toSeconds vampireTimeLimit
+ , "--memory_limit", toMegabytes vampireMemoryLimit
+ , "--cores", "2"
+ ]
+
+-- | A strictly positive invocation-local bound for module pipelines and
+-- top-level Vampire invocations.
+newtype EffectiveJobs = EffectiveJobs Int
+ deriving (Show, Eq, Ord)
+
+effectiveJobs :: Int -> Maybe EffectiveJobs
+effectiveJobs amount
+ | amount > 0 = Just (EffectiveJobs amount)
+ | otherwise = Nothing
+
+effectiveJobsValue :: EffectiveJobs -> Int
+effectiveJobsValue (EffectiveJobs amount) = amount
+
+-- | Select the worker bound, with processor discovery injected for focused
+-- testing. Asynchronous cancellation is never mistaken for failed discovery.
+selectEffectiveJobs
+ :: Maybe EffectiveJobs
+ -> IO Int
+ -> IO EffectiveJobs
+selectEffectiveJobs override detectProcessors =
+ case override of
+ Just selected ->
+ pure selected
+ Nothing -> do
+ detectedResult <- Exception.try detectProcessors
+ case detectedResult of
+ Left detectionFailure
+ | Just asynchronous <-
+ (fromException detectionFailure
+ :: Maybe AsyncException) ->
+ Exception.throwIO asynchronous
+ | otherwise ->
+ fallback
+ Right detectedRaw ->
+ let detected = max 1 detectedRaw
+ -- Two portfolio workers plus one module pipeline
+ -- nominally consume about three logical CPUs.
+ automaticJobs = max 1 ((detected + 1) `div` 3)
+ in pure (EffectiveJobs automaticJobs)
+ where
+ fallback = pure (EffectiveJobs 1)
+
+-- | Stable runtime diagnostic position. It is deliberately separate from
+-- request, validation, cache, and mathematical identities.
+data WorkPosition = WorkPosition !Natural !Natural
+ deriving (Show, Eq, Ord)
+
+workPosition :: Natural -> Natural -> WorkPosition
+workPosition = WorkPosition
+
+workPositionModuleOrdinal :: WorkPosition -> Natural
+workPositionModuleOrdinal (WorkPosition moduleOrdinal _localOrdinal) =
+ moduleOrdinal
+
+workPositionLocalRequestOrdinal :: WorkPosition -> Natural
+workPositionLocalRequestOrdinal (WorkPosition _moduleOrdinal localOrdinal) =
+ localOrdinal
+
+toSeconds :: TimeLimit -> String
+toSeconds (Seconds secs) = show secs
+
+toMegabytes :: MemoryLimit -> String
+toMegabytes (Megabytes mbs) = show mbs
+
+data VampireTaskMode
+ = DirectTask
+ | IndirectTask
+ deriving (Show, Eq)
+
+data VampireStatus
+ = StatusTheorem
+ | StatusCounterSatisfiable
+ | StatusContradictoryAxioms
+ | StatusTimeout
+ | StatusResourceOut
+ | StatusGaveUp
+ | StatusUnknown
+ | UnsupportedStatus Text
+ deriving (Show, Eq)
+
+data CanonicalAtpOutcome
+ = Proved
+ | Counterexample
+ | ContradictoryInput
+ | Indeterminate
+ deriving (Show, Eq, Ord)
+
+-- | The closed non-accepting subset of canonical ATP outcomes. Protocol
+-- disagreement is represented separately from a semantic non-proof, while an
+-- accepting result is unrepresentable here.
+data CanonicalAtpRejection
+ = RejectedCounterexample
+ | RejectedContradictoryInput
+ | RejectedIndeterminate
+ deriving (Show, Eq)
+
+data VampireProtocolError
+ = UnsuccessfulVampireExit ExitCode
+ | UnsupportedVampireStatuses (Set Text)
+ | MalformedVampireStatusLines (Set Text)
+ | MissingVampireOutcome
+ | ConflictingTerminalOutcomes (Set CanonicalAtpOutcome)
+ deriving (Show, Eq)
+
+data CompleteCapturedStreams = CompleteCapturedStreams
+ { completedStdout :: !Text
+ , completedStderr :: !Text
+ }
+ deriving (Show, Eq)
+
+data CompletedTranscript = CompletedTranscript
+ { completedExitCode :: !ExitCode
+ , completedStreams :: !CompleteCapturedStreams
+ }
+ deriving (Show, Eq)
+
+data PartialCapturedStreams = PartialCapturedStreams
+ { partialStdout :: !ByteString.ByteString
+ , partialStderr :: !ByteString.ByteString
+ }
+ deriving (Eq)
+
+instance Show PartialCapturedStreams where
+ showsPrec precedence PartialCapturedStreams{..} =
+ showParen (precedence > applicationPrecedence)
+ ( showString "PartialCapturedStreams "
+ . shows
+ ( ByteString.length partialStdout
+ , ByteString.length partialStderr
+ )
+ )
+
+data BoundedBytes = BoundedBytes
+ { boundedBytesOriginalCount :: !Int
+ , boundedBytesRetained :: !ByteString.ByteString
+ , boundedBytesTruncated :: !Bool
+ }
+ deriving (Eq)
+
+instance Show BoundedBytes where
+ showsPrec precedence BoundedBytes{..} =
+ showParen (precedence > applicationPrecedence)
+ ( showString "BoundedBytes "
+ . shows
+ ( boundedBytesOriginalCount
+ , ByteString.length boundedBytesRetained
+ , boundedBytesTruncated
+ )
+ )
+
+data BoundedCapturedStreams = BoundedCapturedStreams
+ { boundedStdout :: !BoundedBytes
+ , boundedStderr :: !BoundedBytes
+ }
+ deriving (Show, Eq)
+
+data BoundedDiagnostic = BoundedDiagnostic
+ { boundedDiagnosticSummary :: !Text
+ , boundedDiagnosticStdout :: !BoundedBytes
+ , boundedDiagnosticStderr :: !BoundedBytes
+ }
+ deriving (Show, Eq)
+
+-- | Classify parsed Vampire statuses without depending on stream or line order.
+classifyVampireProtocol
+ :: VampireTaskMode
+ -> ExitCode
+ -> [VampireStatus]
+ -> Either VampireProtocolError CanonicalAtpOutcome
+classifyVampireProtocol mode exitCode statuses =
+ case exitCode of
+ ExitFailure _ ->
+ Left (UnsuccessfulVampireExit exitCode)
+ ExitSuccess
+ | not (Set.null unsupported) ->
+ Left (UnsupportedVampireStatuses unsupported)
+ | otherwise ->
+ classifyOutcomes
+ (Set.fromList
+ [ outcome
+ | status <- statuses
+ , Just outcome <- [vampireStatusOutcome mode status]
+ ])
+ where
+ unsupported =
+ Set.fromList
+ [ status
+ | UnsupportedStatus status <- statuses
+ ]
+
+classifyOutcomes
+ :: Set CanonicalAtpOutcome
+ -> Either VampireProtocolError CanonicalAtpOutcome
+classifyOutcomes outcomes =
+ case Set.toList terminalOutcomes of
+ [] ->
+ if Indeterminate `Set.member` outcomes
+ then Right Indeterminate
+ else Left MissingVampireOutcome
+ [outcome] ->
+ Right outcome
+ _ ->
+ Left (ConflictingTerminalOutcomes terminalOutcomes)
+ where
+ terminalOutcomes = Set.delete Indeterminate outcomes
+
+vampireStatusOutcome
+ :: VampireTaskMode
+ -> VampireStatus
+ -> Maybe CanonicalAtpOutcome
+vampireStatusOutcome mode = \case
+ StatusTheorem ->
+ Just Proved
+ StatusCounterSatisfiable ->
+ Just Counterexample
+ StatusContradictoryAxioms ->
+ Just case mode of
+ DirectTask -> ContradictoryInput
+ IndirectTask -> Proved
+ StatusTimeout ->
+ Just Indeterminate
+ StatusResourceOut ->
+ Just Indeterminate
+ StatusGaveUp ->
+ Just Indeterminate
+ StatusUnknown ->
+ Just Indeterminate
+ UnsupportedStatus{} ->
+ Nothing
+
+data VerificationDialect
+ = VerificationFof
+ | VerificationTh0
+ deriving (Show, Eq)
+
+data PreparedVerificationRequest = PreparedVerificationRequest
+ { preparedVerificationDialect :: !VerificationDialect
+ , preparedVerificationMode :: !VampireTaskMode
+ , preparedVerificationInput :: !ByteString.ByteString
+ , preparedVerificationIdentity :: !Authority.PreparedRequestId
+ }
+ deriving (Eq)
+
+-- | Decode the one canonical serialized request only at a textual boundary.
+-- Prepared requests deliberately do not retain an equivalent 'Text' copy.
+preparedVerificationText :: PreparedVerificationRequest -> Text
+preparedVerificationText request =
+ let encoded = preparedVerificationInput request
+ withoutFinalNewline =
+ case ByteString.unsnoc encoded of
+ Just (body, 10) -> body
+ _ -> encoded
+ in TextEncoding.decodeUtf8 withoutFinalNewline
+
+preparedVerificationByteCount
+ :: PreparedVerificationRequest
+ -> Int
+preparedVerificationByteCount =
+ ByteString.length . preparedVerificationInput
+
+preparedVerificationBytes
+ :: PreparedVerificationRequest
+ -> ByteString.ByteString
+preparedVerificationBytes =
+ preparedVerificationInput
+
+preparedVerificationRequestId
+ :: PreparedVerificationRequest
+ -> Authority.PreparedRequestId
+preparedVerificationRequestId =
+ preparedVerificationIdentity
+
+data PreparedTypedProverTask ref local origin global =
+ PreparedTypedProverTask
+ !(TypedProblem ref local origin global)
+ !PreparedVerificationRequest
+
+prepareTypedProverTask
+ :: (Ord local, Ord global)
+ => VampireTaskMode
+ -> TypedProblem ref local origin global
+ -> Either
+ (TypedTptpPreparationError local global)
+ (PreparedTypedProverTask ref local origin global)
+prepareTypedProverTask mode problem = do
+ prepared <-
+ prepareTypedTptpProblem problem
+ let dialect =
+ case preparedTypedTptpRoute prepared of
+ RouteFof -> VerificationFof
+ RouteTh0 -> VerificationTh0
+ bytes =
+ TextEncoding.encodeUtf8
+ (preparedTypedTptpTextNewline prepared)
+ identity =
+ Authority.preparedRequestId
+ (case dialect of
+ VerificationFof -> Authority.PreparedRequestFof
+ VerificationTh0 -> Authority.PreparedRequestTh0)
+ (case mode of
+ DirectTask -> Authority.PreparedRequestDirect
+ IndirectTask -> Authority.PreparedRequestIndirect)
+ bytes
+ preparedRequest =
+ PreparedVerificationRequest
+ { preparedVerificationDialect = dialect
+ , preparedVerificationMode = mode
+ , preparedVerificationInput = bytes
+ , preparedVerificationIdentity = identity
+ }
+ pure
+ (PreparedTypedProverTask
+ problem
+ preparedRequest)
+
+preparedTypedProverLogicalProblem
+ :: PreparedTypedProverTask ref local origin global
+ -> TypedProblem ref local origin global
+preparedTypedProverLogicalProblem
+ (PreparedTypedProverTask
+ problem
+ _request) =
+ problem
+
+preparedTypedProverRequest
+ :: PreparedTypedProverTask ref local origin global
+ -> PreparedVerificationRequest
+preparedTypedProverRequest
+ (PreparedTypedProverTask
+ _problem
+ request) =
+ request
+
+data AcceptedVampireRun = AcceptedVampireRun
+ { acceptedVampireRequestId :: !Authority.PreparedRequestId }
+ deriving (Eq)
+
+data ProverAnswer
+ = ProvedAnswer AcceptedVampireRun
+ | CounterSatisfiable Text
+ | ContradictoryAxioms Text
+ | Uncertain Text
+ | Error Text Text
+ deriving (Eq)
+
+pattern Yes :: ProverAnswer
+pattern Yes <- ProvedAnswer _
+
+{-# COMPLETE Yes, CounterSatisfiable, ContradictoryAxioms, Uncertain, Error #-}
+
+instance Show ProverAnswer where
+ showsPrec _ (ProvedAnswer _) =
+ showString "Yes"
+ showsPrec precedence (CounterSatisfiable task) =
+ showParen (precedence > applicationPrecedence)
+ (showString "CounterSatisfiable "
+ . showsPrec (applicationPrecedence + 1) task)
+ showsPrec precedence (ContradictoryAxioms task) =
+ showParen (precedence > applicationPrecedence)
+ (showString "ContradictoryAxioms "
+ . showsPrec (applicationPrecedence + 1) task)
+ showsPrec precedence (Uncertain task) =
+ showParen (precedence > applicationPrecedence)
+ (showString "Uncertain "
+ . showsPrec (applicationPrecedence + 1) task)
+ showsPrec precedence (Error errorLabel message) =
+ showParen (precedence > applicationPrecedence)
+ (showString "Error "
+ . showsPrec (applicationPrecedence + 1) errorLabel
+ . showChar ' '
+ . showsPrec (applicationPrecedence + 1) message)
+
+provedVampireRun :: ProverAnswer -> Maybe AcceptedVampireRun
+provedVampireRun = \case
+ ProvedAnswer accepted ->
+ Just accepted
+ CounterSatisfiable{} ->
+ Nothing
+ ContradictoryAxioms{} ->
+ Nothing
+ Uncertain{} ->
+ Nothing
+ Error{} ->
+ Nothing
+
+applicationPrecedence :: Int
+applicationPrecedence = 10
+
+data ProverStream
+ = ProverStdin
+ | ProverStdout
+ | ProverStderr
+ deriving (Show, Eq)
+
+data ProverOutputStream
+ = ProverOutputStdout
+ | ProverOutputStderr
+ deriving (Show, Eq)
+
+data ProverProcessError
+ = ProverLaunchFailed !FilePath !Text
+ | ProverCommunicationFailed !FilePath !ProverStream !Text
+ | ProverOutputMalformedUtf8 !FilePath !ProverOutputStream !Text
+ | ProverTimedOut
+ !FilePath
+ !TimeLimit
+ !BoundedCapturedStreams
+ | ProverOutputLimitExceeded
+ !FilePath
+ !ProverOutputStream
+ !BoundedCapturedStreams
+ | ProverTerminatedBySignal
+ !FilePath
+ !Int
+ !BoundedCapturedStreams
+ | ProverLifecycleFailed !FilePath !Text
+ deriving (Show, Eq)
+
+data SupervisorAbort
+ = SupervisorCommunicationFailed !ProverStream !Text
+ | SupervisorOutputLimitExceeded !ProverOutputStream
+ | SupervisorLifecycleFailed !Text
+ deriving (Show)
+
+instance Exception.Exception SupervisorAbort
+
+data CaptureBuffer = CaptureBuffer
+ { captureByteCount :: !Int
+ , captureChunksReversed :: ![ByteString.ByteString]
+ }
+
+vampireOutputByteLimit :: Int
+vampireOutputByteLimit =
+ 16 * 1024 * 1024
+
+vampireWallGraceSeconds :: Integer
+vampireWallGraceSeconds =
+ 2
+
+captureChunkSize :: Int
+captureChunkSize =
+ 32 * 1024
+
+runPreparedTypedProver
+ :: Vampire
+ -> PreparedTypedProverTask ref local origin global
+ -> IO (Either ProverProcessError ProverAnswer)
+runPreparedTypedProver
+ vampireCommand =
+ runPreparedTypedProverWithObserver
+ (\_request -> pure ())
+ vampireCommand
+
+runPreparedTypedProverWithObserver
+ :: (PreparedVerificationRequest -> IO ())
+ -> Vampire
+ -> PreparedTypedProverTask ref local origin global
+ -> IO (Either ProverProcessError ProverAnswer)
+runPreparedTypedProverWithObserver
+ observer
+ vampireCommand
+ (PreparedTypedProverTask
+ _problem
+ preparedRequest) = do
+ terminal <- runPreparedVerificationTerminal
+ observer
+ vampireCommand
+ preparedRequest
+ completionProverResult
+ preparedRequest
+ (VampireCompletion
+ (preparedVerificationRequestId preparedRequest)
+ terminal)
+
+-- | Invocation-local bounded owner of Vampire subprocesses. The finite queue
+-- carries only immutable prepared bytes plus runtime diagnostic position;
+-- checker builders and typed tasks never cross this boundary.
+data VampireExecutor = VampireExecutor
+ { executorQueue :: !(TBQueue ExecutorJob)
+ , executorState :: !(TVar VampireExecutorState)
+ , executorNextHandle :: !(TVar Int)
+ , executorCommand :: !Vampire
+ , executorRequestObserver
+ :: !(WorkPosition -> PreparedVerificationRequest -> IO ())
+ , executorMonotonicClock :: !(IO Word64)
+ , executorSlowAtpAccumulator :: !(TVar SlowAtpAccumulator)
+ }
+
+data ExecutorJob = ExecutorJob
+ { executorJobPosition :: !WorkPosition
+ , executorJobLocation :: !Location
+ , executorJobRequest :: !PreparedVerificationRequest
+ , executorJobCancelled :: !(TVar Bool)
+ , executorJobStarted :: !(TVar Bool)
+ , executorJobCompletion
+ :: !(TMVar VampireCompletion)
+ }
+
+data VampireExecutorState
+ = ExecutorRunning
+ | ExecutorClosing
+ | ExecutorFaulted !VampireExecutorFault
+
+-- | A global executor fault is distinct from one request's declared process
+-- or protocol outcome. Its bounded message carries no process transcript.
+newtype VampireExecutorFault = VampireExecutorFault Text
+ deriving (Show)
+
+instance Exception.Exception VampireExecutorFault
+
+data VampireRequestOwner = VampireRequestOwner
+ { requestOwnerExecutor :: !VampireExecutor
+ , requestOwnerState :: !(TVar VampireRequestOwnerState)
+ }
+
+data VampireRequestOwnerState
+ = RequestOwnerOpen !(IntMap VampireHandle)
+ | RequestOwnerClosed
+
+-- | An opaque, owner-scoped completion handle. It deliberately retains no
+-- prepared request, command, transcript, builder, or parser state.
+data VampireHandle = VampireHandle
+ { vampireHandleOrdinal :: !Int
+ , vampireHandleOwner :: !VampireRequestOwner
+ , vampireHandleRequestId :: !Authority.PreparedRequestId
+ , vampireHandleCancelled :: !(TVar Bool)
+ , vampireHandleStarted :: !(TVar Bool)
+ , vampireHandleCompletion :: !(TMVar VampireCompletion)
+ }
+
+data VampireTerminal
+ = VampireAccepted
+ | VampireRejected !CanonicalAtpRejection !BoundedDiagnostic
+ | VampireProtocolFailed !BoundedDiagnostic
+ | VampireProcessFailed !ProverProcessError
+ | VampireCancelled
+ deriving (Show, Eq)
+
+data VampireCompletion = VampireCompletion
+ { vampireCompletionRequestId :: !Authority.PreparedRequestId
+ , vampireCompletionTerminal :: !VampireTerminal
+ }
+ deriving (Show, Eq)
+
+-- | Monotonic executor wall time in nanoseconds.
+newtype AtpDuration = AtpDuration Word64
+ deriving (Show, Eq, Ord)
+
+atpDurationNanoseconds :: AtpDuration -> Word64
+atpDurationNanoseconds (AtpDuration nanoseconds) = nanoseconds
+
+atpDurationFromNanoseconds :: Word64 -> AtpDuration
+atpDurationFromNanoseconds = AtpDuration
+
+data SlowAtpOutcome
+ = SlowAtpAccepted
+ | SlowAtpRejected !CanonicalAtpRejection
+ | SlowAtpProtocolFailed
+ | SlowAtpProcessFailed
+ deriving (Show, Eq)
+
+data SlowAtpTask = SlowAtpTask
+ { slowAtpDuration :: !AtpDuration
+ , slowAtpOutcome :: !SlowAtpOutcome
+ , slowAtpPosition :: !WorkPosition
+ , slowAtpLocation :: !Location
+ , slowAtpRequestId :: !Authority.PreparedRequestId
+ }
+ deriving (Show, Eq)
+
+data SlowAtpReport = SlowAtpReport
+ { slowAtpQualifyingTaskCount :: !Int
+ , slowAtpTasks :: ![SlowAtpTask]
+ }
+ deriving (Show, Eq)
+
+slowAtpOmittedTaskCount :: SlowAtpReport -> Int
+slowAtpOmittedTaskCount report =
+ slowAtpQualifyingTaskCount report - length (slowAtpTasks report)
+
+data SlowAtpRank = SlowAtpRank
+ !AtpDuration
+ !(Down WorkPosition)
+ deriving (Eq, Ord)
+
+data SlowAtpAccumulator = SlowAtpAccumulator
+ { slowAtpAccumulatorQualifyingTaskCount :: !Int
+ , slowAtpAccumulatorRetained :: !(Map.Map SlowAtpRank SlowAtpTask)
+ }
+
+slowAtpThreshold :: AtpDuration
+slowAtpThreshold = AtpDuration 5000000000
+
+slowAtpRetainedLimit :: Int
+slowAtpRetainedLimit = 12
+
+emptySlowAtpAccumulator :: SlowAtpAccumulator
+emptySlowAtpAccumulator = SlowAtpAccumulator 0 Map.empty
+
+recordSlowAtpTask
+ :: SlowAtpTask
+ -> SlowAtpAccumulator
+ -> SlowAtpAccumulator
+recordSlowAtpTask task accumulator
+ | slowAtpDuration task < slowAtpThreshold = accumulator
+ | otherwise =
+ accumulator
+ { slowAtpAccumulatorQualifyingTaskCount =
+ slowAtpAccumulatorQualifyingTaskCount accumulator + 1
+ , slowAtpAccumulatorRetained = bounded
+ }
+ where
+ rank = SlowAtpRank
+ (slowAtpDuration task)
+ (Down (slowAtpPosition task))
+ inserted = Map.insert rank task (slowAtpAccumulatorRetained accumulator)
+ bounded
+ | Map.size inserted > slowAtpRetainedLimit = Map.deleteMin inserted
+ | otherwise = inserted
+
+finalizeSlowAtpReport :: SlowAtpAccumulator -> SlowAtpReport
+finalizeSlowAtpReport accumulator =
+ SlowAtpReport
+ { slowAtpQualifyingTaskCount =
+ slowAtpAccumulatorQualifyingTaskCount accumulator
+ , slowAtpTasks =
+ snd <$> Map.toDescList (slowAtpAccumulatorRetained accumulator)
+ }
+
+-- | Pure report construction used by policy tests and the executor.
+slowAtpReportFromTasks :: [SlowAtpTask] -> SlowAtpReport
+slowAtpReportFromTasks =
+ finalizeSlowAtpReport
+ . foldl' (flip recordSlowAtpTask) emptySlowAtpAccumulator
+
+renderVampireTerminalDiagnostic :: VampireTerminal -> Maybe Text
+renderVampireTerminalDiagnostic = \case
+ VampireAccepted -> Nothing
+ VampireRejected _rejection diagnostic ->
+ Just (renderBoundedDiagnostic diagnostic)
+ VampireProtocolFailed diagnostic ->
+ Just (renderBoundedDiagnostic diagnostic)
+ VampireProcessFailed processFailure ->
+ Just (Text.pack (show processFailure))
+ VampireCancelled -> Nothing
+
+data VampireExecutorClosed = VampireExecutorClosed
+ deriving (Show)
+
+instance Exception.Exception VampireExecutorClosed
+
+data VampireRequestOwnerClosed = VampireRequestOwnerClosed
+ deriving (Show)
+
+instance Exception.Exception VampireRequestOwnerClosed
+
+data RequestObserverFailure = RequestObserverFailure SomeException
+
+instance Show RequestObserverFailure where
+ show (RequestObserverFailure observerError) =
+ "request observer failed: " <> displayException observerError
+
+instance Exception.Exception RequestObserverFailure
+
+-- | Bracket exactly the selected number of workers. Cancelling the bracket
+-- cancels each worker; a worker owning a subprocess in turn terminates and
+-- reaps that process group through the ordinary supervisor boundary.
+withVampireExecutor
+ :: EffectiveJobs
+ -> Vampire
+ -> (WorkPosition -> PreparedVerificationRequest -> IO ())
+ -> (VampireExecutor -> IO value)
+ -> IO value
+withVampireExecutor selected command observer action =
+ withVampireExecutorUsingClock
+ getMonotonicTimeNSec
+ selected
+ command
+ observer
+ action
+
+-- | Executor constructor with an injected monotonic clock for focused timing
+-- policy tests. Production callers use 'withVampireExecutor'.
+withVampireExecutorUsingClock
+ :: IO Word64
+ -> EffectiveJobs
+ -> Vampire
+ -> (WorkPosition -> PreparedVerificationRequest -> IO ())
+ -> (VampireExecutor -> IO value)
+ -> IO value
+withVampireExecutorUsingClock clock selected command observer action =
+ Exception.bracket acquire release (action . fst)
+ where
+ workerCount = effectiveJobsValue selected
+
+ acquire = do
+ queue <- newTBQueueIO
+ (fromIntegral workerCount * 2)
+ state <- newTVarIO ExecutorRunning
+ nextHandle <- newTVarIO 0
+ slowTasks <- newTVarIO emptySlowAtpAccumulator
+ let executor =
+ VampireExecutor
+ { executorQueue = queue
+ , executorState = state
+ , executorNextHandle = nextHandle
+ , executorCommand = command
+ , executorRequestObserver = observer
+ , executorMonotonicClock = clock
+ , executorSlowAtpAccumulator = slowTasks
+ }
+ workers <- replicateM workerCount
+ (async (superviseExecutorWorker executor))
+ pure (executor, workers)
+
+ release (executor, workers) = do
+ atomically do
+ state <- readTVar (executorState executor)
+ case state of
+ ExecutorRunning ->
+ writeTVar (executorState executor) ExecutorClosing
+ ExecutorClosing ->
+ pure ()
+ ExecutorFaulted{} ->
+ pure ()
+ traverse_ cancel workers
+ traverse_ waitCatch workers
+
+-- | Bracket the complete set of handles submitted by one module pipeline.
+-- Normal early return, failure, and asynchronous cancellation all cancel and
+-- reap the remaining owned work before the scope is left.
+withVampireRequestOwner
+ :: VampireExecutor
+ -> (VampireRequestOwner -> IO value)
+ -> IO value
+withVampireRequestOwner executor =
+ Exception.bracket acquire release
+ where
+ acquire =
+ VampireRequestOwner executor
+ <$> newTVarIO (RequestOwnerOpen IntMap.empty)
+
+ release owner = Exception.mask_ do
+ handles <- atomically do
+ state <- readTVar (requestOwnerState owner)
+ case state of
+ RequestOwnerClosed ->
+ pure []
+ RequestOwnerOpen owned -> do
+ writeTVar (requestOwnerState owner) RequestOwnerClosed
+ pure (IntMap.elems owned)
+ traverse_ signalVampireCancellation handles
+ traverse_ awaitVampireCancellation handles
+ traverse_ deregisterVampireHandle handles
+
+runPreparedTypedProverWithExecutor
+ :: VampireRequestOwner
+ -> WorkPosition
+ -> Location
+ -> PreparedTypedProverTask ref local origin global
+ -> IO (Either ProverProcessError ProverAnswer)
+runPreparedTypedProverWithExecutor
+ owner
+ position
+ location
+ (PreparedTypedProverTask
+ _problem
+ preparedRequest) = do
+ handle <- submitVampireRequest owner position location preparedRequest
+ awaitPreparedVampireRequest preparedRequest handle
+ `Exception.onException` cancelVampireRequest handle
+
+vampireExecutorSlowAtpReport :: VampireExecutor -> IO SlowAtpReport
+vampireExecutorSlowAtpReport executor =
+ finalizeSlowAtpReport
+ <$> readTVarIO (executorSlowAtpAccumulator executor)
+
+submitVampireRequest
+ :: VampireRequestOwner
+ -> WorkPosition
+ -> Location
+ -> PreparedVerificationRequest
+ -> IO VampireHandle
+submitVampireRequest owner position location request = do
+ -- Force the compact queue payload before masked ownership acquisition.
+ _ <- Exception.evaluate (preparedVerificationByteCount request)
+ _ <- Exception.evaluate (preparedVerificationRequestId request)
+ Exception.mask_ do
+ cancelled <- newTVarIO False
+ started <- newTVarIO False
+ completion <- newEmptyTMVarIO
+ let executor = requestOwnerExecutor owner
+ handle <- atomically do
+ executorStatus <- readTVar (executorState executor)
+ case executorStatus of
+ ExecutorClosing -> throwSTM VampireExecutorClosed
+ ExecutorFaulted fault -> throwSTM fault
+ ExecutorRunning -> pure ()
+ ownerStatus <- readTVar (requestOwnerState owner)
+ owned <- case ownerStatus of
+ RequestOwnerClosed -> throwSTM VampireRequestOwnerClosed
+ RequestOwnerOpen current -> pure current
+ ordinal <- readTVar (executorNextHandle executor)
+ writeTVar (executorNextHandle executor) (ordinal + 1)
+ let acquired =
+ VampireHandle
+ { vampireHandleOrdinal = ordinal
+ , vampireHandleOwner = owner
+ , vampireHandleRequestId =
+ preparedVerificationRequestId request
+ , vampireHandleCancelled = cancelled
+ , vampireHandleStarted = started
+ , vampireHandleCompletion = completion
+ }
+ job =
+ ExecutorJob
+ { executorJobPosition = position
+ , executorJobLocation = location
+ , executorJobRequest = request
+ , executorJobCancelled = cancelled
+ , executorJobStarted = started
+ , executorJobCompletion = completion
+ }
+ writeTBQueue (executorQueue executor) job
+ writeTVar
+ (requestOwnerState owner)
+ (RequestOwnerOpen (IntMap.insert ordinal acquired owned))
+ pure acquired
+ pure handle
+
+awaitVampireRequest :: VampireHandle -> IO VampireCompletion
+awaitVampireRequest handle = Exception.mask \restore -> do
+ let executor = requestOwnerExecutor (vampireHandleOwner handle)
+ completion <- restore (atomically do
+ executorStatus <- readTVar (executorState executor)
+ case executorStatus of
+ ExecutorFaulted fault -> throwSTM fault
+ ExecutorRunning -> readTMVar (vampireHandleCompletion handle)
+ ExecutorClosing ->
+ (readTMVar (vampireHandleCompletion handle))
+ `orElse` throwSTM VampireExecutorClosed)
+ deregisterVampireHandle handle
+ pure completion
+
+-- | Await one already submitted request and recover the ordinary typed
+-- prover result. The supplied immutable request is checked against every
+-- completion before any caller may interpret its terminal outcome.
+awaitPreparedVampireRequest
+ :: PreparedVerificationRequest
+ -> VampireHandle
+ -> IO (Either ProverProcessError ProverAnswer)
+awaitPreparedVampireRequest request handle =
+ awaitVampireRequest handle >>= completionProverResult request
+
+cancelVampireRequest :: VampireHandle -> IO ()
+cancelVampireRequest handle = Exception.mask_ do
+ signalVampireCancellation handle
+ awaitVampireCancellation handle
+ deregisterVampireHandle handle
+
+signalVampireCancellation :: VampireHandle -> IO ()
+signalVampireCancellation handle = do
+ let completion =
+ VampireCompletion
+ (vampireHandleRequestId handle)
+ VampireCancelled
+ _ <- Exception.evaluate completion
+ atomically do
+ writeTVar (vampireHandleCancelled handle) True
+ started <- readTVar (vampireHandleStarted handle)
+ unless started
+ (void
+ (publishVampireTerminalSTM
+ (vampireHandleCompletion handle)
+ completion))
+
+awaitVampireCancellation :: VampireHandle -> IO ()
+awaitVampireCancellation handle =
+ -- A running worker acknowledges ordinary cancellation only after
+ -- terminating and reaping its owned process group. After a global fault,
+ -- the worker lifecycle flag is the acknowledgement because the terminal
+ -- cell deliberately remains subordinate to that fault.
+ atomically do
+ executorStatus <- readTVar
+ (executorState (requestOwnerExecutor (vampireHandleOwner handle)))
+ case executorStatus of
+ ExecutorFaulted{} -> do
+ stillRunning <- readTVar (vampireHandleStarted handle)
+ check (not stillRunning)
+ _ -> void (readTMVar (vampireHandleCompletion handle))
+
+deregisterVampireHandle :: VampireHandle -> IO ()
+deregisterVampireHandle handle = atomically do
+ let owner = vampireHandleOwner handle
+ state <- readTVar (requestOwnerState owner)
+ case state of
+ RequestOwnerClosed -> pure ()
+ RequestOwnerOpen owned ->
+ writeTVar
+ (requestOwnerState owner)
+ (RequestOwnerOpen
+ (IntMap.delete (vampireHandleOrdinal handle) owned))
+
+superviseExecutorWorker :: VampireExecutor -> IO ()
+superviseExecutorWorker executor = do
+ result <- Exception.try (executorWorker executor)
+ case result of
+ Right () -> pure ()
+ Left workerFailure -> do
+ state <- readTVarIO (executorState executor)
+ case state of
+ ExecutorRunning ->
+ recordExecutorFault executor workerFailure
+ ExecutorClosing ->
+ pure ()
+ ExecutorFaulted{} ->
+ pure ()
+
+recordExecutorFault :: VampireExecutor -> SomeException -> IO ()
+recordExecutorFault executor workerFailure = atomically do
+ state <- readTVar (executorState executor)
+ case state of
+ ExecutorRunning ->
+ writeTVar
+ (executorState executor)
+ (ExecutorFaulted
+ (VampireExecutorFault
+ (boundedText
+ (Text.pack (displayException workerFailure)))))
+ ExecutorClosing -> pure ()
+ ExecutorFaulted{} -> pure ()
+
+executorWorker :: VampireExecutor -> IO ()
+executorWorker executor = do
+ next <- atomically do
+ state <- readTVar (executorState executor)
+ case state of
+ ExecutorRunning -> Just <$> readTBQueue (executorQueue executor)
+ ExecutorClosing -> pure Nothing
+ ExecutorFaulted{} -> pure Nothing
+ case next of
+ Nothing -> pure ()
+ Just job -> do
+ shouldRun <- atomically do
+ completed <- tryReadTMVar (executorJobCompletion job)
+ case completed of
+ Just _ -> pure False
+ Nothing -> do
+ writeTVar (executorJobStarted job) True
+ pure True
+ when shouldRun
+ (executeJob executor job
+ `Exception.finally`
+ atomically (writeTVar (executorJobStarted job) False))
+ executorWorker executor
+
+data JobWait value
+ = JobFinished !(Either SomeException value)
+ | JobCancelled
+ | JobExecutorStopped
+
+executeJob :: VampireExecutor -> ExecutorJob -> IO ()
+executeJob executor job =
+ Exception.mask \restore -> do
+ running <- async
+ (restore
+ (timedExecutorRun executor job))
+ waited <- restore
+ (atomically
+ (do
+ state <- readTVar (executorState executor)
+ case state of
+ ExecutorRunning ->
+ (JobFinished <$> waitCatchSTM running)
+ `orElse`
+ (do
+ cancelled <- readTVar
+ (executorJobCancelled job)
+ check cancelled
+ pure JobCancelled)
+ ExecutorClosing -> pure JobExecutorStopped
+ ExecutorFaulted{} -> pure JobExecutorStopped))
+ `Exception.onException`
+ (cancel running >> void (waitCatch running))
+ case waited of
+ JobFinished result -> case result of
+ Left workerFailure -> Exception.throwIO workerFailure
+ Right terminal ->
+ publishVampireTerminal
+ (preparedVerificationRequestId
+ (executorJobRequest job))
+ (executorJobCompletion job)
+ terminal
+ JobCancelled -> do
+ cancel running
+ void (waitCatch running)
+ publishVampireTerminal
+ (preparedVerificationRequestId
+ (executorJobRequest job))
+ (executorJobCompletion job)
+ VampireCancelled
+ JobExecutorStopped -> do
+ cancel running
+ void (waitCatch running)
+
+timedExecutorRun
+ :: VampireExecutor
+ -> ExecutorJob
+ -> IO VampireTerminal
+timedExecutorRun executor job = Exception.mask \restore -> do
+ started <- executorMonotonicClock executor
+ terminal <- restore
+ (runPreparedVerificationTerminal
+ (executorRequestObserver executor (executorJobPosition job))
+ (executorCommand executor)
+ (executorJobRequest job))
+ finished <- executorMonotonicClock executor
+ let task = SlowAtpTask
+ { slowAtpDuration = AtpDuration (finished - started)
+ , slowAtpOutcome = slowAtpOutcomeFromTerminal terminal
+ , slowAtpPosition = executorJobPosition job
+ , slowAtpLocation = executorJobLocation job
+ , slowAtpRequestId =
+ preparedVerificationRequestId (executorJobRequest job)
+ }
+ atomically
+ (modifyTVar'
+ (executorSlowAtpAccumulator executor)
+ (recordSlowAtpTask task))
+ pure terminal
+
+slowAtpOutcomeFromTerminal :: VampireTerminal -> SlowAtpOutcome
+slowAtpOutcomeFromTerminal = \case
+ VampireAccepted -> SlowAtpAccepted
+ VampireRejected rejection _diagnostic -> SlowAtpRejected rejection
+ VampireProtocolFailed{} -> SlowAtpProtocolFailed
+ VampireProcessFailed{} -> SlowAtpProcessFailed
+ VampireCancelled ->
+ impossible "cancelled Vampire task was recorded as completed"
+
+publishVampireTerminal
+ :: Authority.PreparedRequestId
+ -> TMVar VampireCompletion
+ -> VampireTerminal
+ -> IO ()
+publishVampireTerminal requestId completionCell terminal = do
+ let completion = VampireCompletion requestId terminal
+ _ <- Exception.evaluate completion
+ void (atomically (publishVampireTerminalSTM completionCell completion))
+
+publishVampireTerminalSTM
+ :: TMVar VampireCompletion
+ -> VampireCompletion
+ -> STM Bool
+publishVampireTerminalSTM = tryPutTMVar
+
+runPreparedVerificationTerminal
+ :: (PreparedVerificationRequest -> IO ())
+ -> Vampire
+ -> PreparedVerificationRequest
+ -> IO VampireTerminal
+runPreparedVerificationTerminal observer command request = do
+ transcriptResult <-
+ runPreparedVampireProcessWithObserver observer command request
+ pure case transcriptResult of
+ Left processFailure ->
+ VampireProcessFailed processFailure
+ Right transcript ->
+ case classifyVampireCompleted
+ (preparedVerificationMode request)
+ transcript of
+ Left protocolFailure ->
+ VampireProtocolFailed
+ (boundedTranscriptDiagnostic
+ (Text.pack (show protocolFailure))
+ transcript)
+ Right Proved ->
+ VampireAccepted
+ Right Counterexample ->
+ VampireRejected
+ RejectedCounterexample
+ (boundedTranscriptDiagnostic
+ "Vampire found a countermodel"
+ transcript)
+ Right ContradictoryInput ->
+ VampireRejected
+ RejectedContradictoryInput
+ (boundedTranscriptDiagnostic
+ "Vampire reported contradictory input"
+ transcript)
+ Right Indeterminate ->
+ VampireRejected
+ RejectedIndeterminate
+ (boundedTranscriptDiagnostic
+ "Vampire did not establish the obligation"
+ transcript)
+
+completionProverResult
+ :: PreparedVerificationRequest
+ -> VampireCompletion
+ -> IO (Either ProverProcessError ProverAnswer)
+completionProverResult request completion = do
+ unless (actualId == expectedId)
+ (Exception.throwIO
+ (VampireExecutorFault
+ "Vampire completion has the wrong request id"))
+ case vampireCompletionTerminal completion of
+ VampireAccepted ->
+ pure
+ (Right
+ (ProvedAnswer
+ (AcceptedVampireRun actualId)))
+ VampireRejected rejection _diagnostic ->
+ pure
+ (Right
+ (case rejection of
+ RejectedCounterexample ->
+ CounterSatisfiable task
+ RejectedContradictoryInput ->
+ ContradictoryAxioms task
+ RejectedIndeterminate ->
+ Uncertain task))
+ VampireProtocolFailed diagnostic ->
+ pure
+ (Right
+ (Error
+ "typed obligation"
+ (renderBoundedDiagnostic diagnostic)))
+ VampireProcessFailed processFailure ->
+ pure (Left processFailure)
+ VampireCancelled ->
+ Exception.throwIO
+ (VampireExecutorFault
+ "active admission encountered a cancelled Vampire request")
+ where
+ expectedId = preparedVerificationRequestId request
+ actualId = vampireCompletionRequestId completion
+ task = preparedVerificationText request
+
+runPreparedVampireProcessWithObserver
+ :: (PreparedVerificationRequest -> IO ())
+ -> Vampire
+ -> PreparedVerificationRequest
+ -> IO (Either ProverProcessError CompletedTranscript)
+runPreparedVampireProcessWithObserver
+ observer
+ vampireCommand@Vampire{vampireExecutable = executable}
+ preparedRequest = do
+ callbackStarted <- newIORef False
+ processResult <- Exception.try
+ (withCreateProcess
+ (proc executable (vampireArguments vampireCommand))
+ { std_in = CreatePipe
+ , std_out = CreatePipe
+ , std_err = CreatePipe
+ , create_group = True
+ }
+ \mIn mOut mErr processHandle ->
+ Exception.mask \restore -> do
+ writeIORef callbackStarted True
+ case (mIn, mOut, mErr) of
+ (Just inputHandle, Just outputHandle, Just errorHandle) -> do
+ maybePid <- getPid processHandle
+ processGroup <- case maybePid of
+ Just pid ->
+ pure (fromIntegral pid)
+ Nothing ->
+ impossible
+ "Vampire supervisor: missing process-group id"
+ restore
+ (do
+ runRequestObserver
+ observer
+ preparedRequest
+ superviseVampireProcess
+ executable
+ (vampireTimeLimit vampireCommand)
+ preparedRequest
+ inputHandle
+ outputHandle
+ errorHandle
+ processGroup
+ processHandle)
+ `Exception.onException`
+ forceTerminateProcessGroup
+ processGroup
+ processHandle
+ _ ->
+ impossible
+ "Vampire supervisor: expected CreatePipe handles")
+ :: IO
+ (Either
+ SomeException
+ (Either ProverProcessError CompletedTranscript))
+ case processResult of
+ Right result ->
+ pure result
+ Left err
+ | Just (RequestObserverFailure observerFailure) <-
+ fromException err ->
+ Exception.throwIO observerFailure
+ | Just asynchronous <-
+ (fromException err :: Maybe AsyncException) ->
+ Exception.throwIO asynchronous
+ | Just ioFailure <-
+ (fromException err :: Maybe IOException) -> do
+ started <- readIORef callbackStarted
+ pure
+ (Left
+ (if started
+ then
+ ProverLifecycleFailed
+ executable
+ (boundedText
+ (exceptionText ioFailure))
+ else
+ ProverLaunchFailed
+ executable
+ (boundedText
+ (exceptionText ioFailure))))
+ | otherwise ->
+ Exception.throwIO err
+
+runRequestObserver
+ :: (PreparedVerificationRequest -> IO ())
+ -> PreparedVerificationRequest
+ -> IO ()
+runRequestObserver observer request =
+ observer request `Exception.catch` \observerError ->
+ case fromException observerError :: Maybe AsyncException of
+ Just asynchronous ->
+ Exception.throwIO asynchronous
+ Nothing ->
+ Exception.throwIO
+ (RequestObserverFailure observerError)
+
+superviseVampireProcess
+ :: FilePath
+ -> TimeLimit
+ -> PreparedVerificationRequest
+ -> Handle
+ -> Handle
+ -> Handle
+ -> ProcessGroupID
+ -> ProcessHandle
+ -> IO (Either ProverProcessError CompletedTranscript)
+superviseVampireProcess
+ executable
+ timeLimit
+ preparedRequest
+ inputHandle
+ outputHandle
+ errorHandle
+ processGroup
+ processHandle = do
+ stdoutCapture <- newIORef emptyCaptureBuffer
+ stderrCapture <- newIORef emptyCaptureBuffer
+ let terminateOwnedProcess =
+ forceTerminateProcessGroup
+ processGroup
+ processHandle
+ communicate =
+ communicateWithVampire
+ preparedRequest
+ inputHandle
+ outputHandle
+ errorHandle
+ stdoutCapture
+ stderrCapture
+ processHandle
+ supervised <-
+ Timeout.timeout
+ (wallTimeoutMicroseconds timeLimit)
+ (Exception.try communicate
+ :: IO (Either SupervisorAbort ExitCode))
+ case supervised of
+ Nothing -> do
+ terminateOwnedProcess
+ partial <-
+ capturedStreams
+ stdoutCapture
+ stderrCapture
+ pure
+ (Left
+ (ProverTimedOut
+ executable
+ timeLimit
+ (boundedCapturedStreams partial)))
+ Just (Left abort) -> do
+ terminateOwnedProcess
+ partial <-
+ capturedStreams
+ stdoutCapture
+ stderrCapture
+ pure
+ (Left
+ (supervisorAbortError
+ executable
+ partial
+ abort))
+ Just (Right exitCode) -> do
+ terminateRemainingProcessGroup processGroup
+ partial <-
+ capturedStreams
+ stdoutCapture
+ stderrCapture
+ case exitCode of
+ ExitFailure signalCode
+ | signalCode < 0 ->
+ pure
+ (Left
+ (ProverTerminatedBySignal
+ executable
+ (negate signalCode)
+ (boundedCapturedStreams partial)))
+ _ ->
+ pure do
+ stdout <- decodeOutput
+ executable
+ ProverOutputStdout
+ (partialStdout partial)
+ stderr <- decodeOutput
+ executable
+ ProverOutputStderr
+ (partialStderr partial)
+ Right
+ CompletedTranscript
+ { completedExitCode = exitCode
+ , completedStreams =
+ CompleteCapturedStreams
+ { completedStdout = stdout
+ , completedStderr = stderr
+ }
+ }
+
+communicateWithVampire
+ :: PreparedVerificationRequest
+ -> Handle
+ -> Handle
+ -> Handle
+ -> IORef CaptureBuffer
+ -> IORef CaptureBuffer
+ -> ProcessHandle
+ -> IO ExitCode
+communicateWithVampire
+ preparedRequest
+ inputHandle
+ outputHandle
+ errorHandle
+ stdoutCapture
+ stderrCapture
+ processHandle = do
+ _ <-
+ concurrently
+ (concurrently
+ (captureCommunication ProverStdin
+ (ByteString.hPut
+ inputHandle
+ (preparedVerificationInput preparedRequest)
+ `Exception.finally` hClose inputHandle))
+ (captureOutput
+ ProverOutputStdout
+ outputHandle
+ stdoutCapture))
+ (captureOutput
+ ProverOutputStderr
+ errorHandle
+ stderrCapture)
+ captureLifecycle (waitForProcess processHandle)
+
+captureCommunication
+ :: ProverStream
+ -> IO a
+ -> IO a
+captureCommunication stream action =
+ action `Exception.catch` \(err :: IOException) ->
+ throwIO
+ (SupervisorCommunicationFailed
+ stream
+ (exceptionText err))
+
+captureOutput
+ :: ProverOutputStream
+ -> Handle
+ -> IORef CaptureBuffer
+ -> IO ()
+captureOutput stream handle capture =
+ captureCommunication (outputStream stream) go
+ where
+ go = do
+ bytes <- ByteString.hGetSome handle captureChunkSize
+ if ByteString.null bytes
+ then pure ()
+ else do
+ exceeded <- appendCapturedChunk capture bytes
+ if exceeded
+ then
+ throwIO
+ (SupervisorOutputLimitExceeded stream)
+ else go
+
+outputStream :: ProverOutputStream -> ProverStream
+outputStream = \case
+ ProverOutputStdout ->
+ ProverStdout
+ ProverOutputStderr ->
+ ProverStderr
+
+captureLifecycle :: IO a -> IO a
+captureLifecycle action =
+ action `Exception.catch` \(err :: IOException) ->
+ throwIO (SupervisorLifecycleFailed (exceptionText err))
+
+emptyCaptureBuffer :: CaptureBuffer
+emptyCaptureBuffer =
+ CaptureBuffer
+ { captureByteCount = 0
+ , captureChunksReversed = []
+ }
+
+appendCapturedChunk
+ :: IORef CaptureBuffer
+ -> ByteString.ByteString
+ -> IO Bool
+appendCapturedChunk capture bytes =
+ atomicModifyIORef' capture \buffer@CaptureBuffer{..} ->
+ let remaining =
+ vampireOutputByteLimit - captureByteCount
+ retained =
+ ByteString.take remaining bytes
+ retainedLength =
+ ByteString.length retained
+ buffer' =
+ buffer
+ { captureByteCount =
+ captureByteCount + retainedLength
+ , captureChunksReversed =
+ if ByteString.null retained
+ then captureChunksReversed
+ else retained : captureChunksReversed
+ }
+ in (buffer', ByteString.length bytes > remaining)
+
+capturedStreams
+ :: IORef CaptureBuffer
+ -> IORef CaptureBuffer
+ -> IO PartialCapturedStreams
+capturedStreams stdoutCapture stderrCapture =
+ PartialCapturedStreams
+ <$> capturedBytes stdoutCapture
+ <*> capturedBytes stderrCapture
+
+capturedBytes :: IORef CaptureBuffer -> IO ByteString.ByteString
+capturedBytes capture =
+ ByteString.concat
+ . reverse
+ . captureChunksReversed
+ <$> readIORef capture
+
+boundedTranscriptBytes :: Int
+boundedTranscriptBytes = 32 * 1024
+
+boundedTranscriptHalf :: Int
+boundedTranscriptHalf = boundedTranscriptBytes `div` 2
+
+boundedBytes :: ByteString.ByteString -> BoundedBytes
+boundedBytes bytes =
+ let original = ByteString.length bytes
+ in if original <= boundedTranscriptBytes
+ then
+ BoundedBytes
+ { boundedBytesOriginalCount = original
+ , boundedBytesRetained = bytes
+ , boundedBytesTruncated = False
+ }
+ else
+ BoundedBytes
+ { boundedBytesOriginalCount = original
+ , boundedBytesRetained =
+ ByteString.take boundedTranscriptHalf bytes
+ <> ByteString.drop
+ (original - boundedTranscriptHalf)
+ bytes
+ , boundedBytesTruncated = True
+ }
+
+boundedText :: Text -> Text
+boundedText =
+ decodeBoundedBytes . boundedBytes . TextEncoding.encodeUtf8
+
+decodeBoundedBytes :: BoundedBytes -> Text
+decodeBoundedBytes =
+ TextEncoding.decodeUtf8With TextEncodingError.lenientDecode
+ . boundedBytesRetained
+
+boundedCapturedStreams
+ :: PartialCapturedStreams
+ -> BoundedCapturedStreams
+boundedCapturedStreams PartialCapturedStreams{..} =
+ BoundedCapturedStreams
+ { boundedStdout = boundedBytes partialStdout
+ , boundedStderr = boundedBytes partialStderr
+ }
+
+boundedTranscriptDiagnostic
+ :: Text
+ -> CompletedTranscript
+ -> BoundedDiagnostic
+boundedTranscriptDiagnostic summary CompletedTranscript{completedStreams = streams} =
+ BoundedDiagnostic
+ { boundedDiagnosticSummary = boundedText summary
+ , boundedDiagnosticStdout =
+ boundedBytes
+ (TextEncoding.encodeUtf8 (completedStdout streams))
+ , boundedDiagnosticStderr =
+ boundedBytes
+ (TextEncoding.encodeUtf8 (completedStderr streams))
+ }
+
+renderBoundedDiagnostic :: BoundedDiagnostic -> Text
+renderBoundedDiagnostic BoundedDiagnostic{..} =
+ Text.unlines
+ [ boundedDiagnosticSummary
+ , renderBoundedStream "stdout" boundedDiagnosticStdout
+ , renderBoundedStream "stderr" boundedDiagnosticStderr
+ ]
+
+renderBoundedStream :: Text -> BoundedBytes -> Text
+renderBoundedStream streamLabel bytes =
+ streamLabel
+ <> (if boundedBytesTruncated bytes
+ then
+ " (retained first and last 16 KiB of "
+ <> Text.pack (show (boundedBytesOriginalCount bytes))
+ <> " bytes)"
+ else "")
+ <> ":\n"
+ <> decodeBoundedBytes bytes
+
+supervisorAbortError
+ :: FilePath
+ -> PartialCapturedStreams
+ -> SupervisorAbort
+ -> ProverProcessError
+supervisorAbortError executable partial = \case
+ SupervisorCommunicationFailed stream message ->
+ ProverCommunicationFailed executable stream (boundedText message)
+ SupervisorOutputLimitExceeded stream ->
+ ProverOutputLimitExceeded
+ executable
+ stream
+ (boundedCapturedStreams partial)
+ SupervisorLifecycleFailed message ->
+ ProverLifecycleFailed executable (boundedText message)
+
+wallTimeoutMicroseconds :: TimeLimit -> Int
+wallTimeoutMicroseconds (Seconds seconds) =
+ fromInteger
+ (min
+ (toInteger (maxBound :: Int))
+ ((toInteger seconds + vampireWallGraceSeconds)
+ * 1000000))
+
+forceTerminateProcessGroup
+ :: ProcessGroupID
+ -> ProcessHandle
+ -> IO ()
+forceTerminateProcessGroup processGroup processHandle =
+ Exception.uninterruptibleMask_ do
+ terminateRemainingProcessGroup processGroup
+ _ <- waitForProcess processHandle
+ pure ()
+
+terminateRemainingProcessGroup :: ProcessGroupID -> IO ()
+terminateRemainingProcessGroup processGroup =
+ ignoreMissingProcess
+ (signalProcessGroup sigKILL processGroup)
+
+ignoreMissingProcess :: IO () -> IO ()
+ignoreMissingProcess action =
+ action `Exception.catch` \(err :: IOException) ->
+ if isDoesNotExistError err
+ then pure ()
+ else throwIO err
+
+decodeOutput
+ :: FilePath
+ -> ProverOutputStream
+ -> ByteString.ByteString
+ -> Either ProverProcessError Text
+decodeOutput executable stream bytes =
+ case TextEncoding.decodeUtf8' bytes of
+ Left err ->
+ Left
+ (ProverOutputMalformedUtf8
+ executable
+ stream
+ (boundedText (Text.pack (show err))))
+ Right output ->
+ Right output
+
+exceptionText :: IOException -> Text
+exceptionText =
+ Text.pack . displayException
+
+classifyVampireCompleted
+ :: VampireTaskMode
+ -> CompletedTranscript
+ -> Either VampireProtocolError CanonicalAtpOutcome
+classifyVampireCompleted mode CompletedTranscript{..} =
+ case completedExitCode of
+ ExitFailure _ ->
+ classifyVampireProtocol mode completedExitCode []
+ ExitSuccess -> do
+ statuses <- statusesFromCompleteStreams completedStreams
+ classifyVampireProtocol mode completedExitCode statuses
+
+statusesFromCompleteStreams
+ :: CompleteCapturedStreams
+ -> Either VampireProtocolError [VampireStatus]
+statusesFromCompleteStreams CompleteCapturedStreams{..}
+ | Set.null malformed =
+ Right statuses
+ | otherwise =
+ Left (MalformedVampireStatusLines malformed)
+ where
+ (malformed, statuses) =
+ foldMap classifyLine
+ (Text.lines completedStdout <> Text.lines completedStderr)
+
+ classifyLine line =
+ case parseMaybe vampireStatusParser line of
+ Just status ->
+ (mempty, [status])
+ Nothing
+ | "SZS status" `Text.isInfixOf` line ->
+ (Set.singleton line, [])
+ | otherwise ->
+ mempty
+
+vampireStatusFromText :: Text -> VampireStatus
+vampireStatusFromText = \case
+ "Theorem" -> StatusTheorem
+ "CounterSatisfiable" -> StatusCounterSatisfiable
+ "ContradictoryAxioms" -> StatusContradictoryAxioms
+ "Timeout" -> StatusTimeout
+ "ResourceOut" -> StatusResourceOut
+ "GaveUp" -> StatusGaveUp
+ "Unknown" -> StatusUnknown
+ status -> UnsupportedStatus status
+
+-- | Parse a Vampire SZS status line.
+--
+-- Recognizes both standard lines like:
+-- % SZS status Timeout for 123
+-- and lines prefixed by worker ids (seen with portfolio output), e.g.:
+-- % (2581105)SZS status Timeout for
+vampireStatusParser :: Parsec Void Text VampireStatus
+vampireStatusParser = do
+ _ <- Char.char '%'
+ Char.hspace
+ optional do
+ _ <- Char.char '('
+ _ <- some Char.digitChar
+ _ <- Char.char ')'
+ Char.hspace
+ _ <- chunk "SZS"
+ Char.hspace1
+ _ <- chunk "status"
+ Char.hspace1
+ status <-
+ takeWhile1P
+ (Just "SZS status")
+ (\character -> character /= ' ' && character /= '\t')
+ _ <- takeRest
+ pure (vampireStatusFromText status)