packages feed

dawgdic-0.1.0: src/Data/DAWG/Internal/GuideBuilder.hs

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

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

import Data.Primitive.PrimArray.Combinators
import Data.DAWG.Internal.BaseType
import Data.DAWG.Internal.DAWG
import Data.DAWG.Internal.Dictionary
import Data.DAWG.Internal.Guide (Guide (..))
import Data.DAWG.Internal.GuideUnit (GuideUnit (..))

import qualified Data.Vector.Generic.Mutable as V
import qualified Data.Vector.Unboxed as UV

import qualified Data.DAWG.Internal.DAWG as Dawg
import qualified Data.DAWG.Internal.Dictionary as Dict
import qualified Data.DAWG.Internal.GuideUnit as GuideUnit

#ifdef trace
import Data.Char
import Data.DAWG.Trace
#endif

-- ** Guide Builder

-- | A mutable builder of 'Data.DAWG.Internal.Guide.Guide'.
newtype GuideBuilder m = GRef { getGRef :: MutVar (PrimState m) (GuideBuilder_ m) }

-- | Builder of Guide. Do not acess directly. Use 'GuideBuilder' instead.
data GuideBuilder_ m = GuideBuilder
  { guideBuilderDawg :: DAWG -- ^ DAWG.
  , guideBuilderDictionary :: Dictionary -- ^ Dictionary.
  , guideBuilderUnits :: ObjectPool (PrimState m) GuideUnit -- ^ Pool of guide units.
  , guideBuilderIsFixedTable :: ObjectPool (PrimState m) UCharType -- ^ Pool of characters.
  }

-- | Build 'Data.DAWG.Internal.Guide.Guide'
-- from 'Data.DAWG.Internal.DAWG.DAWG' and 'Data.DAWG.Internal.Dictionary.Dictionary'.
-- Returns 'False' if build failed.
build :: HasCallStack => PrimMonad m => DAWG -> Dictionary -> m (Maybe Guide)
build dawg dict = do
  gref@GRef{..} <- new dawg dict
  resizeUnitsAndFlags gref

  gb <- readMutVar getGRef
  if dictionarySize (guideBuilderDictionary gb) == 1
    then freeze gb >>= pure . Just
    else do
      buildFromIndexes Dawg.root Dict.root gb >>= \case
        False -> pure Nothing
        True -> freeze gb >>= pure . Just
{-# INLINE build #-}

-- | Same as 'build' but throws an error if build fails.
build' :: HasCallStack => PrimMonad m => DAWG -> Dictionary -> m Guide
build' dawg dict = build dawg dict >>= \case
  Just guide -> pure guide
  Nothing -> error "failed to build guide"
{-# INLINE build' #-}

-- | Generates 'Data.DAWG.Internal.Guide.Guide' out of 'GuideBuilder'.
-- Once this function is called, 'GuideBuilder' must not be used anymore.
freeze :: PrimMonad m => GuideBuilder_ m -> m Guide
freeze gb = do
  funits <- UV.freeze $ guideBuilderUnits gb  
  let !guideUnits = (UV.fromList . UV.toList) funits
  let !guideSize = fromIntegral $ UV.length guideUnits
  pure Guide{..}
{-# INLINE freeze #-}

-- ** Helpers

-- | Initialises a new 'GuideBuilder' from DAWG and Dictionary.
new :: PrimMonad m => DAWG -> Dictionary -> m (GuideBuilder m)
new guideBuilderDawg guideBuilderDictionary = do
  guideBuilderUnits <- V.new 0
  guideBuilderIsFixedTable <- V.new 0
  let g = GuideBuilder{..}
  GRef <$> newMutVar g
{-# INLINE new #-}

-- | Resize both units and flags based on a dictionary size. If guide size is equal or greater than dictionary size, it will leave guide builder unchanged.
resizeUnitsAndFlags :: PrimMonad m => GuideBuilder m -> m ()
resizeUnitsAndFlags GRef{..} = do
  gb <- readMutVar getGRef
  let dictSize = fromIntegral $ dictionarySize $ guideBuilderDictionary gb
      unitsSize = V.length $ guideBuilderUnits gb
      flagsSize = V.length $ guideBuilderIsFixedTable gb
  newUnits <- V.grow (guideBuilderUnits gb)
    $ if unitsSize < dictSize then dictSize - unitsSize else 0
  newFlags <- V.grow (guideBuilderIsFixedTable gb)
    $ if flagsSize < dictSize then dictSize - flagsSize else 0
  let !ngb = gb { guideBuilderUnits = newUnits, guideBuilderIsFixedTable = newFlags }
  writeMutVar getGRef ngb
{-# INLINE resizeUnitsAndFlags #-}

-- | Build guide recursively from dawg index and dictionary index.
-- Returns 'Nothing' if fails.
buildFromIndexes :: PrimMonad m => BaseType -> BaseType -> GuideBuilder_ m -> m Bool
buildFromIndexes !dawgIx !dictIx !gb = do
#ifdef trace
  ifd <- isFixed dictIx gb
  traceIO $ concat
    ["buildGuide dawgIx ", show dawgIx
    , " dawgChildIx ", show $ Dawg.child dawgIx (guideBuilderDawg gb)
    , " dictIx ", show dictIx, " is_fixed_dix ", show ifd
    ]
#endif
  isFixed dictIx gb >>= \case
    True -> pure True
    False -> do
      setIsFixed dictIx gb

      -- finds the first non-terminal child.
      let !dawg = guideBuilderDawg gb
          !dict = guideBuilderDictionary gb
          !dawgChildIx = Dawg.child dawgIx dawg
      dawgChildIxRef <- newMutVar dawgChildIx
#ifdef trace
      traceIO $ concat
        [ "-buildGuide dawgChildIx ", show dawgChildIx
        , " l ", show $ chr $ fromIntegral $ Dawg.label dawgChildIx dawg
        , " (", show $ Dawg.label dawgChildIx dawg, ")"
        ]
#endif
      when (Dawg.label dawgChildIx dawg == 0) do
        writeMutVar dawgChildIxRef $! Dawg.sibling dawgChildIx dawg

      !dawgChildIx' <- readMutVar dawgChildIxRef
      if dawgChildIx' == 0
        then pure True
        else do
          guideBuilderUnits gb !<~~ dictIx
            $! GuideUnit.setChild $! Dawg.label dawgChildIx' dawg

          let go !ix !dictIx'
                | ix == 0 = pure True
                | otherwise = do
                    let !childLabel = Dawg.label ix dawg
#ifdef trace
                    traceIO $ concat
                      ["-go dawgChildIx ", show ix, " dictChildIx ", show dictIx'
                      , " childLabel ", show childLabel
                      ]
#endif
                    case Dict.followChar (fromIntegral childLabel) dictIx' dict of
                      Nothing -> pure False
                      Just dictChildIx -> do
#ifdef trace
                        traceIO $ concat
                          [ "-go follow dawgChildIx ", show ix
                          , " dictChildIx ", show dictChildIx
                          ]
#endif
                        -- outer recursion loop
                        buildFromIndexes ix dictChildIx gb >>= \case
                          False -> pure False
                          True -> do
                            let !dawgSiblingIx = Dawg.sibling ix dawg
                                !siblingLabel = Dawg.label dawgSiblingIx dawg
                            when (dawgSiblingIx /= 0) do
                              guideBuilderUnits gb !<~~ dictChildIx
                                $! GuideUnit.setSibling siblingLabel

                            go dawgSiblingIx dictIx'

          go dawgChildIx' dictIx
{-# INLINE buildFromIndexes #-}

-- | Sets dictionary unit as fixed by index.
setIsFixed :: PrimMonad m => BaseType -> GuideBuilder_ m -> m ()
setIsFixed !ix gb = do
  let setIsFixed' !v = v .|. (1 .<<. (fromIntegral ix `mod` 8))
  guideBuilderIsFixedTable gb !<~~ (fromIntegral ix `div` 8) $! setIsFixed'
{-# INLINE setIsFixed #-}

-- | Checks whether given dictionary index is fixed or not.
isFixed :: PrimMonad m => BaseType -> GuideBuilder_ m -> m Bool
isFixed !ix gb = do
  v <- guideBuilderIsFixedTable gb !~ (fromIntegral ix `div` 8)
  let x = v .&. (1 .<<. (fromIntegral ix `mod` 8))
  pure $ x /= 0
{-# INLINE isFixed #-}