dawgdic-0.1.0: src/Data/DAWG/Internal/Completer.hs
{-|
Module: Data.DAWG.Internal.Completer
Description: Exports completer as well as its internal API.
Copyright: (c) Andrey Prokopenko, 2025
License: BSD-3-Clause
Stability: experimental
-}
{-# LANGUAGE CPP #-}
module Data.DAWG.Internal.Completer where
import Control.Monad.ST (runST)
import Data.Char
import Data.Vector.Unboxed (Vector)
import GHC.Stack (HasCallStack)
import Data.DAWG.Internal.BaseType
import Data.DAWG.Internal.Dictionary (Dictionary (..))
import Data.DAWG.Internal.Guide (Guide (..))
import Data.DAWG.Internal.Stack
import qualified Data.Vector.Unboxed as Vector
import qualified Data.Vector.Unboxed.Mutable as VM
import qualified Data.DAWG.Internal.Dictionary as Dict
import qualified Data.DAWG.Internal.Guide as Guide
#ifdef trace
import System.IO.Unsafe
import Data.DAWG.Trace
#endif
-- ** Completer
-- | Use 'Completer' to perform completion requests by given word prefixes. It accumulates data during traversing dictionary via associated guide. Resulted completion could be accessed via 'keyToString' helper.
data Completer = Completer
{ completerDictionary :: !Dictionary
, completerGuide :: !Guide
, completerKey :: !(Vector UCharType)
, completerIndexStack :: !Stack_
, completerLastIndex :: !BaseType
}
-- | Starts completion process for 'Completer' with a 'Dictionary' index and word prefix. For basic usage pass @0@ (dictionary 'Data.DAWG.Internal.Dictionary.root' index) as index.
-- For more complex scenarios different 'Dictionary' indexes could be used here too.
start :: HasCallStack => BaseType -> String -> Dictionary -> Guide -> Completer
start !ix !prefix !dict !guide =
#ifdef trace
unsafePerformIO do
traceIO $ concat [ "-completer:start ix ", show ix, " prefix ", prefix, " l ", show $ length prefix ]
pure $!
#endif
let !gsize = guideSize guide
prefix' = Vector.map (fromIntegral @_ @UCharType . ord) $ Vector.fromList prefix
!nc = Completer
{ completerDictionary = dict
, completerGuide = guide
, completerKey = runST do
let l = Vector.length prefix'
v <- Vector.unsafeThaw prefix'
v' <- VM.grow v 1
VM.unsafeWrite v' l 0
Vector.unsafeFreeze v'
, completerIndexStack = if gsize /= 0 then Elem ix EndOfStack else EndOfStack
, completerLastIndex = 0
}
in nc
{-# INLINE start #-}
-- | Retrieves next completion.
-- If present, 'Completer' will be returned. 'Nothing', otherwise.
next :: HasCallStack => Completer -> Maybe Completer
next !c =
let withNonEmptyStack comp action = case completerIndexStack comp of
EndOfStack -> Nothing
Elem !ix _rest -> action ix comp
{-# INLINE withNonEmptyStack #-}
withNonRootLastIndex !ix !comp action =
case completerLastIndex comp /= Dict.root of
True -> action ix comp
False -> findTerminal ix comp
{-# INLINE withNonRootLastIndex #-}
withChildLabel !ix comp =
#ifdef trace
unsafePerformIO do
let !childLabel = fromIntegral . Guide.child ix $! completerGuide comp
putStrLn $ concat
[ "next ix ", show ix, " lastIx ", show $ completerLastIndex comp
, " childLabel ", show childLabel]
pure $!
#else
let !childLabel = fromIntegral . Guide.child ix $! completerGuide comp in
#endif
if childLabel /= 0
then followTerminal childLabel ix comp
else go ix comp
{-# INLINE withChildLabel #-}
go :: HasCallStack => BaseType -> Completer -> Maybe Completer
go !ix' !c' =
let !siblingLabel = fromIntegral . Guide.sibling ix' $! completerGuide c'
!ksize = Vector.length (completerKey c')
!nkey = if ksize > 1
then runST do
v <- Vector.unsafeThaw . Vector.init . Vector.init $! completerKey c'
v' <- VM.grow v 1
VM.unsafeWrite v' (ksize - 2) 0
Vector.unsafeFreeze v'
else completerKey c'
-- drop last element
!nstack = case completerIndexStack c' of
EndOfStack -> EndOfStack
Elem !_ix !rest -> rest
!nc = c' { completerKey = nkey, completerIndexStack = nstack }
in case nstack of
EndOfStack -> Nothing
Elem !pix !_rest -> if siblingLabel /= 0
then followTerminal siblingLabel pix nc
else go pix nc
followTerminal !label' !ix' !c' =
case follow label' ix' c' of
Nothing -> Nothing
Just (!nextIx, !nextC) -> findTerminal nextIx nextC
{-# INLINE followTerminal #-}
nextByIx !ix comp =
#ifdef trace
unsafePerformIO do
traceIO $ concat
[ "next ix ", show ix, " lastIx ", show $ completerLastIndex comp]
pure $!
#endif
withNonRootLastIndex ix comp withChildLabel
{-# INLINE nextByIx #-}
in withNonEmptyStack c nextByIx
{-# INLINE next #-}
-- | Retrieves a completion result from 'Completer' as 'String'.
keyToString :: HasCallStack => Completer -> String
keyToString = fmap (chr . fromIntegral) . safeInit . Vector.toList . completerKey
where
safeInit [] = []
safeInit xs = init xs
{-# INLINE safeInit #-}
{-# INLINE keyToString #-}
-- | Retrieves a value associated
-- with the last visited index by 'Completer' from the 'Dictionary'.
value :: HasCallStack => Completer -> ValueType
value c = Dict.value (completerLastIndex c) (completerDictionary c)
{-# INLINE value #-}
-- | Retrieve all completion results by given @prefix@
-- from 'Dictionary' via associated 'Guide'. Consider following lexicon:
--
-- @
-- a
-- an
-- and
-- appear
-- apple
-- bin
-- can
-- cat
-- @
--
-- Once dictionary and guide are ready, call 'completeKeys':
--
-- >>> completeKeys "a" dict guide
-- ["a", "an", "and", "appear", "apple"]
--
completeKeys :: String -> Dictionary -> Guide -> [String]
completeKeys prefix dict guide =
let !l = fromIntegral $ length prefix
goDict acc !dictIx =
case Dict.followPrefixLength prefix l dictIx dict of
Nothing -> acc
Just !nextDictIx ->
let !nc = start nextDictIx "" dict guide
!nacc = goNext acc nc
in goDict nacc nextDictIx
goNext acc !comp = case next comp of
Nothing -> acc
Just !nc ->
let !nextWord = concat [prefix, keyToString nc]
!nacc = nextWord : acc
in goNext nacc nc
in goDict [] Dict.root
{-# INLINE completeKeys #-}
-- | Apply completer to entire lexicon, starting with a function
-- that will handle dictionary traversal following the first character of the word.
completeLexicon
:: forall a. (Char -> Completer -> a)
-> Dictionary
-> Guide
-> [a]
completeLexicon completerSelector dict guide =
let goDict dict' guide' !firstChar =
let prefix = chr $ fromIntegral firstChar
in case Dict.followChar firstChar 0 dict' of
Nothing -> []
Just !nextDictIx ->
let !nc = start nextDictIx "" dict' guide'
in goNext [] prefix nc
goNext acc prefix comp = case next comp of
Nothing -> acc
Just !nc ->
let !next' = completerSelector prefix nc
!nacc = next' : acc
in goNext nacc prefix nc
-- FIXME: optimise it even further
in concatMap (goDict dict guide) [(1 :: CharType) .. 127]
{-# INLINE completeLexicon #-}
-- | Traverses the entire DAWG, returns only words in no particular order.
keys :: Dictionary -> Guide -> [String]
keys dict guide =
let selectWord prefix comp = prefix : keyToString comp
in completeLexicon selectWord dict guide
-- | Traverses the entire DAWG, returns only values associated with words.
-- Considering the unboxed nature of dictionary units, if there is no value associated with
-- a word, it returns @0@.
values :: Dictionary -> Guide -> [ValueType]
values dict guide =
let selectValue _prefix = value
in completeLexicon selectValue dict guide
-- | Traverses the entire DAWG, returns list of pairs
-- where key is a word and value is 32-bit integer.
-- In absence of value, @0@ will be returned.
toList :: Dictionary -> Guide -> [(String, ValueType)]
toList dict guide =
let selectPair prefix comp =
let !k = prefix : keyToString comp
!v = value comp
in (k, v)
in completeLexicon selectPair dict guide
-- ** Helpers
-- | Helper function that is being used in 'next'.
-- Follows a given char within the 'Dictionary'
-- and performs necessary transformations to 'Completer'.
follow :: HasCallStack => CharType -> BaseType -> Completer -> Maybe (BaseType, Completer)
follow !label !ix !c =
case Dict.followChar label ix (completerDictionary c) of
Nothing -> Nothing
Just !nextIx ->
let !ksize = Vector.length (completerKey c)
!oldStack = completerIndexStack c
!nkey = appendKeyLabel label ksize (completerKey c)
!nc = c { completerKey = nkey, completerIndexStack = Elem nextIx oldStack }
in Just (nextIx, nc)
{-# INLINE follow #-}
-- | Helper function to identify the terminal occurence.
-- Returns 'Completer' if there was an occurence. 'Nothing', otherwise.
findTerminal :: HasCallStack => BaseType -> Completer -> Maybe Completer
findTerminal !ix !c
| Dict.hasValue ix (completerDictionary c) =
let !nc = c { completerLastIndex = ix }
in Just nc
| otherwise =
let !label = fromIntegral $! Guide.child ix $! completerGuide c
in case Dict.followChar label ix (completerDictionary c) of
Nothing -> Nothing
Just !nextIx ->
let !ksize = Vector.length (completerKey c)
!oldStack = completerIndexStack c
!nkey = appendKeyLabel label ksize (completerKey c)
!nc = c { completerKey = nkey
, completerIndexStack = Elem nextIx oldStack
}
in findTerminal nextIx nc
appendKeyLabel :: CharType -> Int -> Vector UCharType -> Vector UCharType
appendKeyLabel !label !ksize !key = runST do
v <- Vector.unsafeThaw key
let (!delta, !deltaIx) = if ksize >= 1 then (1, ksize - 1) else (2, ksize)
v' <- VM.grow v delta
VM.unsafeWrite v' deltaIx $ fromIntegral label
VM.unsafeWrite v' (deltaIx + 1) 0
Vector.unsafeFreeze v'
{-# INLINE appendKeyLabel #-}
-- | Dump completer state to stdout.
dump :: HasCallStack => String -> Completer -> IO ()
dump prefix Completer{..} = do
let msg = unlines $ fmap ((prefix <> " ") <>)
[ "ksize " <> (show $ Vector.length $ completerKey) <> " " <> show completerKey
, concat
["issize ", (show $ size $ completerIndexStack), " ", show completerIndexStack]
, "lastIx " <> show completerLastIndex
]
putStrLn msg