packages feed

dawgdic-1.0.0: src/Data/DAWG/Internal/DictionaryBuilder.hs

{-|
Module: Data.DAWG.Internal.DictionaryBuilder
Description: Exports dictionary builder as well as its internal API.
Copyright: (c) Andrey Prokopenko, 2025
License: BSD-3-Clause
Stability: experimental
-}
module Data.DAWG.Internal.DictionaryBuilder where

import Control.Monad (forM_, when)
import Control.Monad.Primitive (PrimMonad, PrimState)
import Data.Bits
import Data.Char
import Data.Maybe (fromMaybe)
import Data.Primitive.MutVar
import GHC.Stack (HasCallStack)

import Data.Primitive.PrimArray.Combinators
import Data.DAWG.Internal.BaseType (BaseType, UCharType, SizeType, UUHT, UHHT, ObjectPool)
import Data.DAWG.Internal.DictionaryExtraUnit (DictionaryExtraUnit (..))
import Data.DAWG.Internal.DictionaryUnit (DictionaryUnit (..))
import Data.DAWG.Internal.Dictionary (Dictionary (..))
import Data.DAWG.Internal.DAWG (DAWG (..))

import qualified Data.Primitive.PrimArray.Utils as A
import qualified Data.Vector.Generic.Mutable as V
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Hashtables as HT

import qualified Data.DAWG.Internal.DAWG as Dawg
import qualified Data.DAWG.Internal.DictionaryExtraUnit as Extra
import qualified Data.DAWG.Internal.DictionaryUnit as DictUnit
import qualified Data.DAWG.Internal.LinkTable as LT

-- ** DAWG Dictionary Builder

-- | A mutable builder of 'Data.DAWG.Internal.Dictionary.Dictionary'.
newtype DictionaryBuilder m =
  DDBRef { getDDBRef :: MutVar (PrimState m) (DictionaryBuilder_ m) }

-- | Builder of Dictionary. Do not access directly. Use 'DictionaryBuilder' instead.
data DictionaryBuilder_ m = DictionaryBuilder
  { dawgDictionaryBuilderDawg :: DAWG -- ^ DAWG.
  , dawgDictionaryBuilderUnits :: ObjectPool (PrimState m) DictionaryUnit -- ^ Pool of dictionary units.
  , dawgDictionaryBuilderExtras :: UHHT m BaseType DictionaryExtraUnit -- ^ Table of extra blocks (of 256) which represents supportive circular linked list used.
  , dawgDictionaryBuilderLabels :: UUHT m SizeType UCharType
  , dawgDictionaryBuilderLinkTable :: LT.LinkTable m
  , dawgDictionaryBuilderRefs :: !(IntArray (PrimState m))
  }

unfixedIndex, numOfUnusedUnits :: Int

-- | Use it as index for 'dawgDictionaryBuilderRefs' to get the current unfixed index.
unfixedIndex = 0
{-# INLINE unfixedIndex #-}

-- | Use it as index for 'dawgDictionaryBuilderRefs' to get the number of unfixed units.
numOfUnusedUnits = 1
{-# INLINE numOfUnusedUnits #-}

-- | Upper mask.
upperMask :: BaseType
upperMask = complement (pred DictUnit.offsetMax)
{-# INLINE upperMask #-}

-- | Lower mask.
lowerMask :: BaseType
lowerMask = 0xFF
{-# INLINE lowerMask #-}

-- | Gets a current size of units.
numOfUnits :: DictionaryBuilder_ m -> BaseType
numOfUnits = fromIntegral . V.length . dawgDictionaryBuilderUnits
{-# INLINE numOfUnits #-}

-- | Gets a current size of blocks.
numOfBlocks :: PrimMonad m => DictionaryBuilder_ m -> m BaseType
numOfBlocks = fmap fromIntegral . HT.size . dawgDictionaryBuilderExtras
{-# INLINE numOfBlocks #-}

-- | Constant: @16@.
numOfUnfixedBlocks :: BaseType
numOfUnfixedBlocks = 16
{-# INLINE numOfUnfixedBlocks #-}

-- | Constant: @256@.
blockSize :: BaseType
blockSize = 256
{-# INLINE blockSize #-}

-- | Build dictionary from 'Data.DAWG.Internal.DAWG.DAWG'.
-- If build failed, it returns 'Nothing'.
build
  :: HasCallStack
  => PrimMonad m
  => DAWG -> m (Maybe (DictionaryBuilder m))
build !dawg = do
  !dref@DDBRef{..} <- new dawg
  !preDdb <- readMutVar getDDBRef
  let !ltsize = dawgNumOfMergingStates dawg + (dawgNumOfMergingStates dawg .>>. 1)
  LT.init (dawgDictionaryBuilderLinkTable preDdb) (fromIntegral ltsize)

  reserveUnit 0 dref
  -- after unit reservation most likely vectors are being resized
  !ddb <- readMutVar getDDBRef
  let units = dawgDictionaryBuilderUnits ddb

  modifyExtras ddb 0 $ Extra.setIsUsed
  !u0 <- units !~  0
  let (!isOffsetSet, !u1) = DictUnit.setOffset 1 u0
  if not isOffsetSet
    then pure Nothing
    else do
      let !u2 =  DictUnit.setLabel (fromIntegral $ ord '\0') u1
      units <~~ 0 $ u2

      buildResult <- if (Dawg.size dawg > 1)
        then do
          buildFromDawg Dawg.root 0 dref
        else pure True

      if not buildResult
        then pure Nothing
        else do
          fixAllBlocks dref
          pure $! Just dref

-- | Build a dictionary from 'Data.DAWG.Internal.DAWG.DAWG' and freezes its result.
-- Throws an error when build fails.
build' :: HasCallStack => PrimMonad m => DAWG -> m Dictionary
build' dawg = build dawg >>= \case
  Just dict -> freeze dict
  Nothing -> error "failed to build dictionary"
{-# INLINE build' #-}

-- | Generates 'Data.DAWG.Internal.Dictionary.Dictionary' out of 'DictionaryBuilder'.
-- Once this function is called, 'DictionaryBuilder' must not be used anymore.
freeze :: PrimMonad m => DictionaryBuilder m -> m Dictionary
freeze DDBRef{..} = do
  ddb <- readMutVar getDDBRef
  dictionaryUnits <- VG.unsafeFreeze $! dawgDictionaryBuilderUnits ddb
  let dictionarySize = fromIntegral $! VG.length dictionaryUnits
  pure Dictionary{..}
{-# INLINE freeze #-}

-- ** Helpers

-- | Initialises a new 'DictionaryBuilder' from DAWG.
new :: HasCallStack => PrimMonad m => DAWG -> m (DictionaryBuilder m)
new !dawgDictionaryBuilderDawg = do
   !dawgDictionaryBuilderUnits <- V.new 0
   !dawgDictionaryBuilderExtras <- HT.initialize 0
   !dawgDictionaryBuilderLabels <- HT.initialize 0
   !dawgDictionaryBuilderLinkTable <- HT.initialize 0
   !dawgDictionaryBuilderRefs <- A.replicate 2 0
   dawgDictionaryBuilderRefs <~ unfixedIndex $ 0
   let d = DictionaryBuilder{..}
   DDBRef <$> newMutVar d
{-# INLINE new #-}

-- | Recursively build dictionary by traversing DAWG
-- starting from dawg index and dictionary index.
buildFromDawg
  :: HasCallStack
  => PrimMonad m => BaseType -> BaseType -> DictionaryBuilder m -> m Bool
buildFromDawg dawgIx dictIx dref@DDBRef{..} = do
  ddb <- readMutVar getDDBRef
  let dawg = dawgDictionaryBuilderDawg ddb
  if Dawg.isLeaf dawgIx dawg
    then pure True
    else do
      let !dawgChildIx = Dawg.child dawgIx dawg
          whenMerging !ix action = do
            if not (Dawg.isMerging ix dawg)
            then pure Nothing
            else action

          withOffset !ix action = do
            !offset <- LT.find (dawgDictionaryBuilderLinkTable ddb) ix
            if offset /= 0
              then action ix offset
              else pure Nothing

          withRenewedOffset !ix !offset = do
            let !renewedOffset = offset .^. dictIx
            if 0 == (renewedOffset .&. upperMask)
              || 0 == (renewedOffset .&. lowerMask)
              then do
                when (Dawg.isLeaf ix dawg) do
                  dawgDictionaryBuilderUnits ddb !<~~ dictIx $! DictUnit.setHasLeaf
                !u <- dawgDictionaryBuilderUnits ddb !~ dictIx
                let (!_isSet, !nu) = DictUnit.setOffset renewedOffset u
                dawgDictionaryBuilderUnits ddb <~~ dictIx $ nu
                pure $! Just True
              else pure Nothing

      whenMerging dawgChildIx (withOffset dawgChildIx withRenewedOffset) >>= \case
        Just x -> pure x
        Nothing -> do
          offset <- arrangeChildNodes dawgIx dictIx dref
          if offset == 0
            then pure False
            else do
              when (Dawg.isMerging dawgChildIx dawg) do
                LT.insert (dawgDictionaryBuilderLinkTable ddb) dawgChildIx offset
              let go !ix
                    | ix == 0 = pure True
                    | otherwise = do
                        let !l = Dawg.label ix dawg
                            !dictChildIx = offset .^. fromIntegral @_ @BaseType l
                        !buildResult <- buildFromDawg ix dictChildIx dref
                        if not buildResult
                          then pure False
                          else do
                            let !nextIx = Dawg.sibling ix dawg
                            go nextIx

              go dawgChildIx

-- | Arrange child nodes for given dawg index and dictionary index.
arrangeChildNodes
  :: HasCallStack
  => PrimMonad m
  => BaseType -> BaseType -> DictionaryBuilder m -> m BaseType
arrangeChildNodes dawgIx dictIx dref@DDBRef{..} = do
  clearLabels dref
  ddb <- readMutVar getDDBRef

  labelSizeRef <- newMutVar (0 :: SizeType)

  let dawg = dawgDictionaryBuilderDawg ddb
      !dawgChildIx = Dawg.child dawgIx dawg

      collectChildLabels 0 = pure ()
      collectChildLabels !ix = do
        l <- readMutVar labelSizeRef
        HT.insert (dawgDictionaryBuilderLabels ddb) l (Dawg.label ix dawg)
        modifyMutVar' labelSizeRef succ
        let !childIx = Dawg.sibling ix dawg
        collectChildLabels childIx

  -- Arrange child nodes.
  collectChildLabels dawgChildIx

  -- Find a good offset.
  !offset <- findGoodOffset dictIx ddb
  !offsetIsSet <- do
    !u <- dawgDictionaryBuilderUnits ddb !~ dictIx
    let (res, nu) = DictUnit.setOffset (dictIx .^. offset) u
    dawgDictionaryBuilderUnits ddb <~~ dictIx $ nu
    pure res
  if not offsetIsSet
    then pure 0
    else do
      l <- readMutVar labelSizeRef
      let populateChildNodes !i dawgChildIx'
            | i < fromIntegral l = do
                ddb1 <- readMutVar getDDBRef
                label <- fromMaybe 0 <$> HT.lookup (dawgDictionaryBuilderLabels ddb1) i
                let !dictChildIx = offset .^. fromIntegral @_ @BaseType label
                reserveUnit dictChildIx dref
                ddb2 <- readMutVar getDDBRef

                if Dawg.isLeaf dawgChildIx' dawg
                  then do
                    dawgDictionaryBuilderUnits ddb2 !<~~ dictIx $ DictUnit.setHasLeaf
                    dawgDictionaryBuilderUnits ddb2 !<~~ dictChildIx $
                      DictUnit.setValue $! Dawg.value dawgChildIx' dawg
                   
                  else do
                    dawgDictionaryBuilderUnits ddb2 !<~~ dictChildIx $
                      DictUnit.setLabel label

                let !nextDawgChildIx = Dawg.sibling dawgChildIx' dawg
                populateChildNodes (succ i) nextDawgChildIx
            | otherwise = pure ()
      populateChildNodes 0 dawgChildIx
      ddb3 <- readMutVar getDDBRef
      modifyExtras ddb3 offset $! Extra.setIsUsed
      
      pure offset

-- | Find a good offset for given dictionary index.
findGoodOffset
  :: HasCallStack
  => PrimMonad m => BaseType -> DictionaryBuilder_ m -> m BaseType
findGoodOffset ix ddb = do
  !unfixedIndex' <- fromIntegral <$> dawgDictionaryBuilderRefs ddb ! unfixedIndex
  let !numOfUnits' = numOfUnits ddb
  if numOfUnits' <= unfixedIndex'
    then pure $ numOfUnits' .|. (ix .&. lowerMask)
    else do
      let scanUnusedUnits shouldStop !uix
            | shouldStop && uix == unfixedIndex' =
                pure (numOfUnits' .|. (ix .&. lowerMask))
            | otherwise = do
                l0 <- fromMaybe 0 <$> HT.lookup (dawgDictionaryBuilderLabels ddb) 0
                let !offset = uix .^. fromIntegral l0
                isGoodOffset ix offset ddb >>= \case
                  True -> pure offset
                  False -> do
                    !ex <- extras ddb (fromIntegral uix)
                    let !nuix = Extra.next ex
                    scanUnusedUnits True nuix
      scanUnusedUnits False unfixedIndex'
{-# INLINE findGoodOffset #-}

-- | Recursively checks whether given offset is good for dictionanry index.
isGoodOffset
  :: HasCallStack
  => PrimMonad m => BaseType -> BaseType -> DictionaryBuilder_ m -> m Bool
isGoodOffset ix offset ddb = do
  !extra' <- extras ddb offset
  if Extra.isUsed extra' then pure False else do
    let !relativeOffset = ix .^. offset
    if (relativeOffset .&. lowerMask /= 0) && (relativeOffset .&. upperMask /= 0)
      then pure False else do
        lsize <- HT.size (dawgDictionaryBuilderLabels ddb)
        let findCollision !i
              | i >= lsize = pure True
              | otherwise = do
                  l <- fromMaybe 0 <$> HT.lookup (dawgDictionaryBuilderLabels ddb) (fromIntegral i)
                  !ex' <- extras ddb (offset .^. fromIntegral @_ @BaseType l)
                  if Extra.isFixed ex'
                    then pure False
                    else findCollision (succ i)
        findCollision 1
{-# INLINE isGoodOffset #-}

-- | Reserve a new unit.
reserveUnit
  :: HasCallStack
  => PrimMonad m => BaseType -> DictionaryBuilder m -> m ()
reserveUnit ix dref = do
  do
    !ddb0 <- readMutVar (getDDBRef dref)
    when (numOfUnits ddb0 <= ix) do
      expandDictionary dref

  -- removes an unused unit from a circular linked list
  !ddb <- readMutVar (getDDBRef dref)
  !unfixedIndex' <- dawgDictionaryBuilderRefs ddb ! unfixedIndex

  when (ix == fromIntegral unfixedIndex') do
    ex' <- extras ddb ix
    let !nextUnfixedIx = Extra.next ex'
    dawgDictionaryBuilderRefs ddb <~ unfixedIndex $ fromIntegral nextUnfixedIx
    when (nextUnfixedIx == ix) do
      dawgDictionaryBuilderRefs ddb <~ unfixedIndex $ fromIntegral $ numOfUnits ddb

  !ex' <- extras ddb ix
  let !next' = Extra.next ex'
      !prev' = Extra.prev ex'

  modifyExtras ddb prev' $! Extra.setNext next'
  modifyExtras ddb next' $! Extra.setPrev prev'
  modifyExtras ddb ix $! Extra.setIsFixed
{-# INLINE reserveUnit #-}

-- | Expands dictionary by allocating a memory for new unit and block and aligning block elements.
expandDictionary :: HasCallStack => PrimMonad m => DictionaryBuilder m -> m ()
expandDictionary dref@DDBRef{..} = do
  (srcNumOfUnits, srcNumOfBlocks, destNumOfUnits, destNumOfBlocks) <- do
    !ddb <- readMutVar getDDBRef
    numOfBlocks' <- numOfBlocks ddb
    let !srcNumOfUnits = numOfUnits ddb
        !srcNumOfBlocks = numOfBlocks'

        !destNumOfUnits = srcNumOfUnits + blockSize
        !destNumOfBlocks = succ srcNumOfBlocks

  -- Fix old block
    when (numOfUnfixedBlocks < destNumOfBlocks) do
      fixBlock (srcNumOfBlocks - numOfUnfixedBlocks) dref

    -- dest - src
    !newUnits <- V.grow (dawgDictionaryBuilderUnits ddb)
      (fromIntegral blockSize)
    forM_ [srcNumOfUnits .. destNumOfUnits - 1] \ix -> do
      newUnits <~~ ix $ 0

    allocateExtras ddb destNumOfBlocks
    let extras' = dawgDictionaryBuilderExtras ddb
        !ddb' = ddb
          { dawgDictionaryBuilderUnits = newUnits
          , dawgDictionaryBuilderExtras = extras'
          }

    writeMutVar getDDBRef ddb'
    pure (srcNumOfUnits, srcNumOfBlocks, destNumOfUnits, destNumOfBlocks)

  !ddb1 <- readMutVar getDDBRef

  if numOfUnfixedBlocks < destNumOfBlocks
    then do
      numOfBlocks' <- numOfBlocks ddb1
      let !blockId = srcNumOfBlocks - numOfUnfixedBlocks
          !lastId = numOfBlocks' - 1

      swapBlocks ddb1 blockId lastId
      forM_ [srcNumOfUnits .. pred destNumOfUnits] \i -> do
        modifyExtras ddb1 i $! const Extra.empty

    else do
      numOfBlocks' <- numOfBlocks ddb1
      let !lastId = numOfBlocks' - 1
      clearBlock lastId ddb1

  -- create a circular linked list for a new block
  !ddb2 <- readMutVar getDDBRef

  let setNeighbourBlocks !i = do
        modifyExtras ddb2 (pred i) $ Extra.setNext i
        modifyExtras ddb2 i $ Extra.setPrev (pred i)

  forM_ [succ srcNumOfUnits .. pred destNumOfUnits] setNeighbourBlocks
  !unfixedIndex' <- dawgDictionaryBuilderRefs ddb2 ! unfixedIndex
  let !uix = fromIntegral unfixedIndex'
  modifyExtras ddb2  srcNumOfUnits $ Extra.setPrev (pred destNumOfUnits)
  modifyExtras ddb2 (pred destNumOfUnits) $ Extra.setNext srcNumOfUnits

  -- Merge 2 circular linked lists
  unfixedIndexBlock <- extras ddb2 uix
  modifyExtras ddb2 srcNumOfUnits $ Extra.setPrev (Extra.prev unfixedIndexBlock)
  modifyExtras ddb2 (pred destNumOfUnits) $ Extra.setNext uix

  modifyExtras ddb2 (Extra.prev unfixedIndexBlock) $ Extra.setNext srcNumOfUnits
  modifyExtras ddb2 uix $ Extra.setPrev (pred destNumOfUnits)
  writeMutVar getDDBRef ddb2

-- | Fixes all blocks. If there is more than 16 blocks, only unfixed blocks will be fixed.
fixAllBlocks
  :: HasCallStack
  => PrimMonad m
  => DictionaryBuilder m -> m ()
fixAllBlocks dref@DDBRef{..} = do
  ddb <- readMutVar getDDBRef
  numOfBlocks' <- numOfBlocks ddb
  let !begin = if numOfUnfixedBlocks < numOfBlocks'
        then numOfBlocks' - numOfUnfixedBlocks
        else 0
      !end = numOfBlocks'
  forM_ [begin .. pred end] \blockId -> do
    fixBlock blockId dref
{-# INLINE fixAllBlocks #-}

-- | Fix block by its id.
fixBlock
  :: HasCallStack
  => PrimMonad m => BaseType -> DictionaryBuilder m -> m ()
fixBlock blockId dref@DDBRef{..} = do
  ddb <- readMutVar getDDBRef
  let !begin = blockId * blockSize
      !end = begin + blockSize

      findUnusedOffsetForLabel !offset
        | offset /= end = do
            block <- extras ddb offset
            if not $ Extra.isUsed block
              then pure offset
              else findUnusedOffsetForLabel (succ offset)
        | otherwise = pure 0

  offset <- findUnusedOffsetForLabel begin

  -- Labels of unused units are modified
  let go !ix
        | ix /= end = do
            ddb1 <- readMutVar getDDBRef
            Extra.isFixed <$> extras ddb1 ix >>= \case
              True -> pure ()
              False -> do
                reserveUnit ix dref
                ddb2 <- readMutVar getDDBRef
                dawgDictionaryBuilderUnits ddb2 !<~~ ix $ DictUnit.setLabel $!
                  (fromIntegral @_ @UCharType $! ix .^. fromIntegral offset)
                numUnusedUnits' <-  dawgDictionaryBuilderRefs ddb2 ! numOfUnusedUnits
                dawgDictionaryBuilderRefs ddb2 <~ numOfUnusedUnits $ succ numUnusedUnits'
            go (succ ix)
        | otherwise = pure ()
  go begin

-- | Remove all labels.
clearLabels :: PrimMonad m => DictionaryBuilder m -> m ()
clearLabels DDBRef{..} = do
  ddb <- readMutVar getDDBRef
  lkeys <- HT.keys (dawgDictionaryBuilderLabels ddb)
  VG.forM_ lkeys \label -> HT.delete (dawgDictionaryBuilderLabels ddb) label
{-# INLINE clearLabels #-}

-- | Dump dictionary builder to stdout.
dump :: DictionaryBuilder IO -> IO ()
dump DDBRef{..} = do
  ddb <- readMutVar getDDBRef

  !bs <- fromIntegral <$> numOfBlocks ddb
  !ls <- HT.size $ dawgDictionaryBuilderLabels ddb
  let !us = V.length $ dawgDictionaryBuilderUnits ddb
      !ms = maximum [us, bs * fromIntegral blockSize, ls]
      labelToString x = concat [ show $ chr $ fromIntegral x, " (", show x, ")" ]

  putStrLn $ concat [ "i\tu(", show us, ")\t\tb(", show bs, ")\t\t\tl(", show ls, ")"]

  forM_ [0 .. ms - 1] \i -> do
    !u <- maybe "" show <$> (V.readMaybe (dawgDictionaryBuilderUnits ddb) i)
    !b <- do
        b' <- extras ddb (fromIntegral i)
        if b' == Extra.empty then pure "" else pure $ show b'
    !l <-  maybe "" labelToString <$> (HT.lookup (dawgDictionaryBuilderLabels ddb) $ fromIntegral i)

    when (any (/= mempty) [b, l] || u /= show DictUnit.empty) do
      putStrLn $ concat [ show i, "\t", u, "\t", b, "\t", l  ]

  uix <- dawgDictionaryBuilderRefs ddb ! unfixedIndex
  uns <- dawgDictionaryBuilderRefs ddb ! numOfUnusedUnits

  putStrLn $ concat [ "unfixed : ", show uix ]
  putStrLn $ concat [ "num_unused_states : ", show uns ]

-- ** Dictionary extra/blocks helpers

-- | Gets the entire block (hashtable) by its id. Throws an error if the block is missing.
lookupBlock
  :: (HasCallStack, PrimMonad m)
  => DictionaryBuilder_ m -> BaseType -> m (UUHT m BaseType DictionaryExtraUnit)
lookupBlock ddb ix =
  HT.lookup (dawgDictionaryBuilderExtras ddb) (ix `div` blockSize) >>= \case
    Nothing -> error "Missing block"
    Just block -> pure block
{-# INLINE lookupBlock #-}

-- | Inserts a block by its index into the hashtable.
insertBlock
  :: PrimMonad m
  => DictionaryBuilder_ m -> UUHT m BaseType DictionaryExtraUnit -> BaseType -> m ()
insertBlock ddb block ix =
  HT.insert (dawgDictionaryBuilderExtras ddb) (ix `div` blockSize) block
{-# INLINE insertBlock #-}

-- | Swap two blocks by their ids. Both blocks should be present.
-- If at least one of blocks is missing, error will be thrown.
swapBlocks
  :: (HasCallStack, PrimMonad m)
  => DictionaryBuilder_ m -> BaseType -> BaseType -> m ()
swapBlocks ddb b1 b2 = do
  let getBlock blockId =
        HT.lookup (dawgDictionaryBuilderExtras ddb) blockId >>= \case
          Nothing -> error "Missing block"
          Just block -> pure block

  block1 <- getBlock b1
  block2 <- getBlock b2

  HT.insert (dawgDictionaryBuilderExtras ddb) b1 block2
  HT.insert (dawgDictionaryBuilderExtras ddb) b2 block1
{-# INLINE swapBlocks #-}

-- | Replaces the content of the block by its id with empty units.
clearBlock
  :: HasCallStack => PrimMonad m => BaseType -> DictionaryBuilder_ m -> m ()
clearBlock !blockId ddb = do
  block <- HT.lookup (dawgDictionaryBuilderExtras ddb) blockId >>= \case
    Nothing -> error "Missing block"
    Just block -> pure block
  bsize <- HT.size block
  when (bsize > 0) do
    forM_ [0 .. pred bsize] \ix -> do
      HT.insert block (fromIntegral ix `mod` blockSize) Extra.empty
{-# INLINE clearBlock #-}

-- | Get block content by its id.
extras
  :: forall m. HasCallStack
  => PrimMonad m
  => DictionaryBuilder_ m -> BaseType -> m DictionaryExtraUnit
extras !ddb !ix = do
  !block <- lookupBlock ddb ix
  fromMaybe Extra.empty <$> HT.lookup block (ix `mod` blockSize)
{-# INLINE extras #-}

-- | Modifies block content by its id and modifier function.
modifyExtras
  :: HasCallStack
  => PrimMonad m
  => DictionaryBuilder_ m
  -> BaseType -> (DictionaryExtraUnit -> DictionaryExtraUnit) -> m ()
modifyExtras !ddb !ix modifier = do
  !block <- lookupBlock ddb ix
  let f Nothing = Just $! modifier Extra.empty
      f (Just !x) = Just $! modifier x
  HT.alter block f (ix `mod` blockSize)
{-# INLINE modifyExtras #-}

-- | Allocates new empty blocks by provided size, if it is greater than 'numOfBlocks'.
allocateExtras
  :: HasCallStack
  => PrimMonad m
  => DictionaryBuilder_ m -> BaseType -> m ()
allocateExtras !ddb destSize = do
  srcSize <- numOfBlocks ddb
  when (srcSize < destSize) do
    forM_ [srcSize .. pred destSize] \ix -> do
      block <- HT.initialize 0
      forM_ [0 .. pred blockSize] \bix -> do
        HT.insert block bix Extra.empty
      HT.insert (dawgDictionaryBuilderExtras ddb) ix block
{-# INLINE allocateExtras #-}