packages feed

aihc-cpp-1.0.0.0: src/Aihc/Cpp/Cursor.hs

{-# LANGUAGE BangPatterns #-}

-- |
-- Module      : Aihc.Cpp.Cursor
-- Description : Zero-copy cursor over ByteString for efficient scanning
-- License     : Unlicense
--
-- A lightweight cursor abstraction over a strict 'ByteString'. The cursor
-- tracks a position into a shared buffer, enabling O(1) peeking and
-- zero-copy slicing. All CPP-significant bytes are ASCII (0x00-0x7F),
-- so byte-level operations are safe; non-ASCII bytes (>= 0x80) can be
-- bulk-copied without decoding.
module Aihc.Cpp.Cursor
  ( Cursor (..),
    fromByteString,
    fromText,
    toText,
    null,
    peekByte,
    peekByte2,
    advance,
    advance2,
    sliceText,
    sliceSince,
    skipWhile,
    skipToInteresting,
    bufLength,

    -- * Line-oriented operations
    findNewline,
    skipNewline,
    lineSlice,
    startsWithByte,
    peekByteAt,
    atEnd,
  )
where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Data.Word (Word8)
import Prelude hiding (null)

-- | A position-based cursor into a 'ByteString' buffer.
-- The buffer is shared across all cursors derived from the same input,
-- enabling zero-copy slicing.
data Cursor = Cursor
  { curBuf :: {-# UNPACK #-} !ByteString,
    curPos :: {-# UNPACK #-} !Int
  }
  deriving (Show)

-- | Create a cursor at the start of a 'ByteString'.
fromByteString :: ByteString -> Cursor
fromByteString bs = Cursor bs 0

-- | Create a cursor from 'Text' by encoding to UTF-8.
fromText :: Text -> Cursor
fromText = fromByteString . TE.encodeUtf8

-- | Decode the remaining bytes from the cursor position as UTF-8 'Text'.
toText :: Cursor -> Text
toText (Cursor buf pos) = TE.decodeUtf8 (BS.drop pos buf)

-- | Is the cursor at the end of input?
null :: Cursor -> Bool
null (Cursor buf pos) = pos >= BS.length buf
{-# INLINE null #-}

-- | Check whether the cursor is at the end of input (synonym for 'null').
atEnd :: Cursor -> Bool
atEnd = null
{-# INLINE atEnd #-}

-- | Peek at the current byte without advancing. Returns 'Nothing' at end
-- of input.
peekByte :: Cursor -> Maybe Word8
peekByte (Cursor buf pos)
  | pos >= BS.length buf = Nothing
  | otherwise = Just (BS.index buf pos)
{-# INLINE peekByte #-}

-- | Peek at the current and next byte without advancing. Returns 'Nothing'
-- if fewer than 2 bytes remain.
peekByte2 :: Cursor -> Maybe (Word8, Word8)
peekByte2 (Cursor buf pos)
  | pos + 1 >= BS.length buf = Nothing
  | otherwise = Just (BS.index buf pos, BS.index buf (pos + 1))
{-# INLINE peekByte2 #-}

-- | Peek at a byte at an absolute position in the buffer.
peekByteAt :: Int -> Cursor -> Maybe Word8
peekByteAt absPos (Cursor buf _)
  | absPos < 0 || absPos >= BS.length buf = Nothing
  | otherwise = Just (BS.index buf absPos)
{-# INLINE peekByteAt #-}

-- | Advance the cursor by one byte.
advance :: Cursor -> Cursor
advance (Cursor buf pos) = Cursor buf (pos + 1)
{-# INLINE advance #-}

-- | Advance the cursor by two bytes.
advance2 :: Cursor -> Cursor
advance2 (Cursor buf pos) = Cursor buf (pos + 2)
{-# INLINE advance2 #-}

-- | Extract a zero-copy 'Text' slice from position @start@ to position
-- @end@ (exclusive) in the cursor's buffer.
sliceText :: Int -> Int -> Cursor -> Text
sliceText start end (Cursor buf _) =
  TE.decodeUtf8 (BS.take (end - start) (BS.drop start buf))
{-# INLINE sliceText #-}

-- | Extract a zero-copy 'Text' slice from the given start position to
-- the cursor's current position.
sliceSince :: Int -> Cursor -> Text
sliceSince start cur = sliceText start (curPos cur) cur
{-# INLINE sliceSince #-}

-- | Advance the cursor while the predicate holds for the current byte.
skipWhile :: (Word8 -> Bool) -> Cursor -> Cursor
skipWhile p = go
  where
    go !cur = case peekByte cur of
      Just b | p b -> go (advance cur)
      _ -> cur
{-# INLINE skipWhile #-}

-- | Total length of the underlying buffer.
bufLength :: Cursor -> Int
bufLength (Cursor buf _) = BS.length buf
{-# INLINE bufLength #-}

-- | Advance the cursor past bytes that cannot start any CPP-significant
-- two-character sequence. Stops at: @\"@ (0x22), @\'@ (0x27), @*@ (0x2A),
-- @-@ (0x2D), @/@ (0x2F), @\\@ (0x5C), @{@ (0x7B), @}@ (0x7D),
-- or end of input. This allows bulk-copying runs of plain text
-- (identifiers, whitespace, operators, non-ASCII UTF-8) without
-- per-byte dispatch.
skipToInteresting :: Cursor -> Cursor
skipToInteresting = go
  where
    go !cur = case peekByte cur of
      Nothing -> cur
      Just b
        | isInteresting b -> cur
        | otherwise -> go (advance cur)

    isInteresting :: Word8 -> Bool
    isInteresting b =
      b == 0x22 -- '"'
        || b == 0x27 -- '\''
        || b == 0x2A -- '*'
        || b == 0x2D -- '-'
        || b == 0x2F -- '/'
        || b == 0x5C -- '\\'
        || b == 0x7B -- '{'
        || b == 0x7D -- '}'
    {-# INLINE isInteresting #-}
{-# INLINE skipToInteresting #-}

-- | Find the next newline byte (0x0A) or EOF. Returns a cursor
-- positioned at the newline (or at EOF). The bytes from the
-- original position to the returned position form the line content
-- (without the newline).
findNewline :: Cursor -> Cursor
findNewline = go
  where
    go !cur = case peekByte cur of
      Nothing -> cur
      Just 0x0A -> cur -- '\n'
      Just _ -> go (advance cur)
{-# INLINE findNewline #-}

-- | Advance past a newline byte if the cursor is currently on one.
-- Returns 'Nothing' at EOF, 'Just cursor' after the newline otherwise.
skipNewline :: Cursor -> Maybe Cursor
skipNewline cur = case peekByte cur of
  Just 0x0A -> Just (advance cur) -- '\n'
  _ -> Nothing
{-# INLINE skipNewline #-}

-- | Create a cursor that views only the bytes from @curPos cur@ to @end@
-- (exclusive) in the same buffer. This is a logical "line slice" — a
-- sub-cursor bounded at @end@.
--
-- Implementation: creates a new cursor over a sub-ByteString. The
-- sub-ByteString shares the underlying memory (zero-copy via 'BS.take'
-- and 'BS.drop').
lineSlice :: Int -> Cursor -> Cursor
lineSlice end (Cursor buf start) =
  Cursor (BS.take (end - start) (BS.drop start buf)) 0
{-# INLINE lineSlice #-}

-- | Check if the byte at the cursor's current position matches the
-- given byte.
startsWithByte :: Word8 -> Cursor -> Bool
startsWithByte b cur = peekByte cur == Just b
{-# INLINE startsWithByte #-}