nova-nix-0.9.0.0: src/Nix/Eval/CStatus.hs
-- | Failure statuses at the C data-layer boundary.
--
-- Every C allocator reports failure through a sentinel or a status
-- instead of aborting: a NULL pointer, @UINT32_MAX@ from the bytecode
-- emitters, @NN_SYMBOL_INVALID@ from the symbol table, @-1@ from the
-- list constructors and the attribute set insert. Each covers two
-- causes a caller needs to tell apart: the sub-arena is outside its
-- init .. destroy window (nothing ran 'Nix.Eval.Arena.arenaInit', or
-- 'Nix.Eval.Arena.arenaDestroy' already ran), or it is live and could
-- not grow. Only the C side knows which, so the failure path asks it
-- once, through @nn_arena_live@, and raises a 'CStatusError' that names
-- the C entry point. The question is asked only after a failure, so
-- the hot path pays the sentinel comparison and nothing more.
--
-- The thunk state transitions report a third kind of refusal: a thunk
-- not in the state the transition needs. The boundary only asks for a
-- transition it has just made valid, so that refusal is a defect in this
-- library, raised as 'CInvariantViolated' rather than carried on.
module Nix.Eval.CStatus
( -- * Errors
CStatusError (..),
-- * Checks
checkedCPtr,
cStatusFailure,
cInvariantViolated,
-- * Arena state
arenaLive,
)
where
import Control.Exception (Exception (..), throwIO)
import Foreign.C.Types (CInt (..))
import Foreign.Ptr (Ptr, nullPtr)
-- ---------------------------------------------------------------------------
-- FFI import
-- ---------------------------------------------------------------------------
foreign import ccall unsafe "nn_arena_live"
c_nn_arena_live :: IO CInt
-- ---------------------------------------------------------------------------
-- Errors
-- ---------------------------------------------------------------------------
-- | Why a C data-layer call reported failure. Each constructor carries
-- the C entry point that refused.
data CStatusError
= -- | The call ran outside the 'Nix.Eval.Arena.arenaInit' ..
-- 'Nix.Eval.Arena.arenaDestroy' window. A setup diagnosis to act
-- on by adding the bracket and starting the process again, not an
-- error to catch and retry in place: GHC updates a thunk whose
-- evaluation raised with that exception, so any shared value whose
-- evaluation reached C before the window re-raises this inside a
-- later, live arena. The @null@, @true@ and @false@ thunks are C
-- statics rather than arena cells, so they are not among them.
ArenaNotInitialized !String
| -- | The arena is live and the allocation itself failed: exhaustion,
-- or a size the C side rejects. The second field is the detail.
CAllocationFailed !String !String
| -- | The C side refused a state transition the boundary had just made
-- valid, or reported a state no transition produces. A defect in
-- this library, never a resource failure; the second field says
-- which state was wrong.
CInvariantViolated !String !String
deriving (Eq, Show)
instance Exception CStatusError where
displayException (ArenaNotInitialized site) =
site
++ ": the C data layer is not initialized; evaluation must run between "
++ "Nix.Eval.Arena.arenaInit and Nix.Eval.Arena.arenaDestroy"
displayException (CAllocationFailed site detail) = site ++ ": " ++ detail
displayException (CInvariantViolated site detail) =
site ++ ": internal invariant violated: " ++ detail
-- ---------------------------------------------------------------------------
-- Checks
-- ---------------------------------------------------------------------------
-- | Reject a NULL from a C allocator before it can flow onward as a
-- pointer: the C side signals failure deliberately, and the next
-- dereference would be undefined behaviour in a release build.
checkedCPtr :: String -> Ptr a -> IO (Ptr a)
checkedCPtr site ptr
| ptr == nullPtr = cStatusFailure site "C allocation failed"
| otherwise = pure ptr
-- | Raise the 'CStatusError' for a sentinel @site@ just returned. Asked
-- right after the failing call and before anything else touches the
-- arena, so the answer describes the state that call saw.
cStatusFailure :: String -> String -> IO a
cStatusFailure site detail = do
live <- arenaLive
throwIO (if live then CAllocationFailed site detail else ArenaNotInitialized site)
-- | Raise 'CInvariantViolated' for a transition @site@ refused. Unlike
-- 'cStatusFailure' it does not ask whether the arena is live: the caller
-- holds a cell the arena handed out, so liveness is not the question.
cInvariantViolated :: String -> String -> IO a
cInvariantViolated site detail = throwIO (CInvariantViolated site detail)
-- ---------------------------------------------------------------------------
-- Arena state
-- ---------------------------------------------------------------------------
-- | Whether every C sub-arena is inside its init .. destroy window.
arenaLive :: IO Bool
arenaLive = (/= 0) <$> c_nn_arena_live