packages feed

nova-nix-0.9.0.0: src/Nix/Eval/CList.hs

-- | C-backed contiguous list of thunk pointers via FFI.
--
-- Wraps @cbits/nn_list.c@ - a flat array of @nn_thunk_t*@ pointers.
-- Replaces Haskell's @[Thunk]@ (linked-list cons cells, ~48 bytes per
-- element) with a contiguous C array (8 bytes per element).
--
-- Lists are immutable after construction: allocate with 'clistNew',
-- fill with 'clistSet', then read with 'clistGet'/'clistCount'.
-- All memory is freed in bulk via 'clistFreeAll' at evaluation end.
--
-- 'nullPtr' is the empty list, so the C constructors report failure
-- through a status of their own rather than a NULL, and every one is
-- checked here: a list the C side cannot allocate raises
-- 'Nix.Eval.CStatus.CStatusError', never reads back as empty.
module Nix.Eval.CList
  ( -- * Opaque handle
    NnList,
    CListPtr,

    -- * CList newtype
    CList (..),
    emptyCList,
    clistFromThunks,
    clistThunks,
    clistLen,
    clistIndex,
    clistDrop,

    -- * Lifecycle
    clistNew,
    clistFreeAll,

    -- * Access
    clistCount,
    clistGet,
    clistSet,

    -- * Conversion
    thunkListToCList,
    clistToThunkList,
  )
where

import Control.Monad (unless)
import Data.Word (Word32)
import Foreign.C.Types (CInt (..))
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (peek)
import Nix.Eval.CStatus (cStatusFailure)
import Nix.Eval.CThunk (CThunkPtr)
import System.IO.Unsafe (unsafePerformIO)

-- | Phantom type for C-side @nn_list_t@.
data NnList

-- | Pointer to a C-allocated list.
type CListPtr = Ptr NnList

-- ---------------------------------------------------------------------------
-- FFI imports (all unsafe - no callbacks, fast data access)
-- ---------------------------------------------------------------------------

foreign import ccall unsafe "nn_list_new"
  c_nn_list_new :: Ptr CListPtr -> Word32 -> IO CInt

foreign import ccall unsafe "nn_list_free_all"
  c_nn_list_free_all :: IO ()

foreign import ccall unsafe "nn_list_count"
  c_nn_list_count :: CListPtr -> IO Word32

foreign import ccall unsafe "nn_list_get"
  c_nn_list_get :: CListPtr -> Word32 -> IO CThunkPtr

foreign import ccall unsafe "nn_list_set"
  c_nn_list_set :: CListPtr -> Word32 -> CThunkPtr -> IO ()

foreign import ccall unsafe "nn_list_drop"
  c_nn_list_drop :: Ptr CListPtr -> CListPtr -> Word32 -> IO CInt

-- ---------------------------------------------------------------------------
-- Lifecycle
-- ---------------------------------------------------------------------------

-- | Allocate a new list with space for @count@ thunk pointers, or
-- 'nullPtr' (the empty list) when count is 0.  Raises
-- 'Nix.Eval.CStatus.CStatusError' when the C side cannot allocate it.
clistNew :: Word32 -> IO CListPtr
clistNew count = alloca $ \out -> do
  checkedListStatus "nn_list_new" =<< c_nn_list_new out count
  peek out

-- | Raise the 'Nix.Eval.CStatus.CStatusError' for a list constructor's
-- failure status.
checkedListStatus :: String -> CInt -> IO ()
checkedListStatus site status =
  unless (status == 0) (cStatusFailure site "list allocation failed")

-- | Free all tracked list headers (arena-style cleanup).
-- Items arrays are freed by the env page allocator.
clistFreeAll :: IO ()
clistFreeAll = c_nn_list_free_all

-- ---------------------------------------------------------------------------
-- Access
-- ---------------------------------------------------------------------------

-- | Number of elements in the list.
clistCount :: CListPtr -> IO Word32
clistCount = c_nn_list_count

-- | Get the thunk pointer at the given index.
clistGet :: CListPtr -> Word32 -> IO CThunkPtr
clistGet = c_nn_list_get

-- | Set the thunk pointer at the given index (for construction).
clistSet :: CListPtr -> Word32 -> CThunkPtr -> IO ()
clistSet = c_nn_list_set

-- ---------------------------------------------------------------------------
-- Conversion helpers
-- ---------------------------------------------------------------------------

-- | Convert a Haskell list of 'CThunkPtr' to a C list.
-- Allocates a new nn_list_t and fills it with the thunk pointers.
-- Returns 'nullPtr' for empty lists.  A list longer than the C count
-- can hold is refused rather than wrapped to a shorter allocation that
-- the fill would then run past.
thunkListToCList :: [CThunkPtr] -> IO CListPtr
thunkListToCList [] = pure nullPtr
thunkListToCList ptrs
  | len > fromIntegral (maxBound :: Word32) =
      cStatusFailure "nn_list_new" ("a list of " ++ show len ++ " elements is past the uint32_t count a C list holds")
  | otherwise = do
      clist <- clistNew (fromIntegral len)
      fillList clist 0 ptrs
      pure clist
  where
    len = length ptrs
    fillList _ _ [] = pure ()
    fillList cl !i (p : ps) = do
      clistSet cl i p
      fillList cl (i + 1) ps

-- | Convert a C list back to a Haskell list of 'CThunkPtr'.
-- Returns @[]@ for 'nullPtr'.
clistToThunkList :: CListPtr -> IO [CThunkPtr]
clistToThunkList cl
  | cl == nullPtr = pure []
  | otherwise = do
      n <- clistCount cl
      mapM (clistGet cl) [0 .. n - 1]

-- ---------------------------------------------------------------------------
-- CList newtype (wraps CListPtr for use in NixValue ADT)
-- ---------------------------------------------------------------------------

-- | Opaque wrapper around a C-backed list.  Used as the payload of
-- @VList@ in the 'NixValue' ADT.  The underlying @nn_list_t*@ is
-- arena-allocated and valid until evaluation end.
newtype CList = CList {unCList :: CListPtr}

-- | Pointer equality - two CLists are the same if they point to the
-- same C struct.  Deep equality is handled by the evaluator.
instance Eq CList where
  CList a == CList b = a == b

instance Show CList where
  show (CList p)
    | p == nullPtr = "<list 0>"
    | otherwise =
        let n = unsafePerformIO (clistCount p)
         in "<list " ++ show n ++ ">"

-- | The empty CList (null pointer, zero elements).
emptyCList :: CList
emptyCList = CList nullPtr

-- | Convert a Haskell list of 'CThunkPtr' to a 'CList'.
-- Uses 'unsafePerformIO' - safe because allocation is idempotent
-- per call (no shared mutable state beyond the tracking array).
{-# NOINLINE clistFromThunks #-}
clistFromThunks :: [CThunkPtr] -> CList
clistFromThunks ptrs = CList (unsafePerformIO (thunkListToCList ptrs))

-- | Convert a 'CList' back to a Haskell list of 'CThunkPtr'.
clistThunks :: CList -> [CThunkPtr]
clistThunks (CList p) = unsafePerformIO (clistToThunkList p)

-- | Number of elements in the list.
clistLen :: CList -> Int
clistLen (CList p)
  | p == nullPtr = 0
  | otherwise = fromIntegral (unsafePerformIO (clistCount p))

-- | The element at an index, or 'Nothing' outside the list, read from the
-- C array without materializing the list.  Pure because a list is never
-- written after construction.
clistIndex :: CList -> Int -> Maybe CThunkPtr
clistIndex cl@(CList p) i
  | i < 0 || i >= clistLen cl = Nothing
  | otherwise = Just $! unsafePerformIO (clistGet p (fromIntegral i))

-- | The list without its first @n@ elements, as 'drop' is on lists.  The
-- result is a new header over this list's C array (@nn_list_drop@), so it
-- costs the same whatever the length.  Allocating it under
-- 'unsafePerformIO' is safe because the array is immutable: a header
-- built twice or shared between calls describes the same elements.
clistDrop :: Int -> CList -> CList
clistDrop n cl@(CList p)
  | n <= 0 = cl
  | n >= clistLen cl = emptyCList
  | otherwise = CList (unsafePerformIO (alloca dropInto))
  where
    dropInto out = do
      checkedListStatus "nn_list_drop" =<< c_nn_list_drop out p (fromIntegral n)
      peek out