packages feed

canontra-0.1.0.0: src/Canontra/Canonical/FastScan.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}

{- |
Module      : Canontra.Canonical.FastScan
Description : SWAR (SIMD Within A Register) ASCII and line-ending fast scanner.

This module provides sub-microsecond hardware-speed scanning of byte buffers using
64-bit machine words (SWAR) to identify pure ASCII streams with Unix line endings.
For the vast majority (>99%) of source files, this allows bypassing Unicode NFC
string unpacking, line ending normalization copies, and text decoding validations.
-}
module Canontra.Canonical.FastScan
  ( ScanResult (..)
  , scanAsciiAndLineEndings
  , fastCanonicalizeBS
  , fastCanonicalizeText
  , isPureAsciiUnix
  ) where

import Data.Bits ((.&.), complement, xor)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Word (Word64, Word8)
import Foreign.Ptr (Ptr, castPtr, plusPtr)
import Foreign.Storable (peek)
import System.IO.Unsafe (unsafePerformIO)

import Canontra.Canonical.Unicode (canonicalizeText, normalizeLineEndings)

-- | Classification result of SWAR byte scanning.
data ScanResult
  = PureAsciiUnix      -- ^ 100% pure ASCII with standard Unix '\n' line endings (Zero transformation needed).
  | ContainsCRLF       -- ^ ASCII or UTF-8 but contains '\r' (Requires line ending normalization).
  | RequiresUnicodeNFC -- ^ Contains non-ASCII bytes >= 0x80 (Requires Unicode NFC normalization).
  deriving stock (Eq, Show, Enum, Bounded)

-- | Scans a ByteString 8 bytes per CPU cycle using 64-bit SWAR bit-twiddling.
{-# INLINE scanAsciiAndLineEndings #-}
scanAsciiAndLineEndings :: BS.ByteString -> ScanResult
scanAsciiAndLineEndings bs
  | BS.null bs = PureAsciiUnix
  | otherwise = unsafePerformIO $ BSU.unsafeUseAsCStringLen bs $ \(cPtr, len) -> do
      let !p = castPtr cPtr :: Ptr Word8
          !numWords = len `quot` 8
          !remBytes = len `rem` 8
      scanWords p numWords remBytes False
  where
    scanWords :: Ptr Word8 -> Int -> Int -> Bool -> IO ScanResult
    scanWords !p 0 !remCount !hasCR = scanRemaining p remCount hasCR
    scanWords !p !n !remCount !hasCR = do
      !w <- peek (castPtr p :: Ptr Word64)
      -- Check if any byte has high bit set (>= 0x80)
      if (w .&. 0x8080808080808080) /= 0
        then pure RequiresUnicodeNFC
        else do
          -- Check for '\r' (0x0D): SWAR zero-byte detection on (w ^ 0x0D0D0D0D0D0D0D0D)
          let !crXor = w `xor` 0x0D0D0D0D0D0D0D0D
              !hasCRWord = ((crXor - 0x0101010101010101) .&. complement crXor .&. 0x8080808080808080) /= 0
          scanWords (p `plusPtr` 8) (n - 1) remCount (hasCR || hasCRWord)

    scanRemaining :: Ptr Word8 -> Int -> Bool -> IO ScanResult
    scanRemaining _ 0 !hasCR
      | hasCR     = pure ContainsCRLF
      | otherwise = pure PureAsciiUnix
    scanRemaining !p !remCount !hasCR = do
      !b <- peek p
      if b >= 0x80
        then pure RequiresUnicodeNFC
        else scanRemaining (p `plusPtr` 1) (remCount - 1) (hasCR || b == 0x0D)

-- | Fast canonicalization of a raw ByteString directly into canonical Text.
-- For pure ASCII files with Unix line endings, this skips all line ending replacements
-- and NFC precomposition passes entirely.
{-# INLINE fastCanonicalizeBS #-}
fastCanonicalizeBS :: BS.ByteString -> Text
fastCanonicalizeBS !bs = case scanAsciiAndLineEndings bs of
  PureAsciiUnix      -> TE.decodeUtf8 bs
  ContainsCRLF       -> normalizeLineEndings (TE.decodeUtf8Lenient bs)
  RequiresUnicodeNFC -> canonicalizeText (TE.decodeUtf8Lenient bs)

-- | Fast canonicalization of an in-memory Text value.
-- If the text does not contain '\r' or any combining diacritical marks (>= U+0300),
-- it is returned immediately with zero heap allocation.
{-# INLINE fastCanonicalizeText #-}
fastCanonicalizeText :: Text -> Text
fastCanonicalizeText !t
  | not (T.any (\c -> c == '\r' || c >= '\x0300') t) = t
  | otherwise = canonicalizeText t

-- | Returns 'True' if the byte buffer is pure ASCII with Unix line endings.
{-# INLINE isPureAsciiUnix #-}
isPureAsciiUnix :: BS.ByteString -> Bool
isPureAsciiUnix bs = scanAsciiAndLineEndings bs == PureAsciiUnix