packages feed

aihc-parser-1.0.0.2: src/Aihc/Parser/Lex/Numbers.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

module Aihc.Parser.Lex.Numbers
  ( lexFloat,
    lexHexFloat,
    lexInt,
    lexIntBase,
    withOptionalMagicHashSuffix,
  )
where

import Aihc.Parser.Lex.Types
import Aihc.Parser.Syntax (Extension (ExtendedLiterals, MagicHash, NumericUnderscores), FloatType (..), NumericType (..))
import Data.Char (digitToInt, isAsciiLower, isAsciiUpper, isDigit, isHexDigit, isOctDigit)
import Data.Maybe (fromMaybe)
import Data.Ratio ((%))
import Data.Text (Text, pattern (:<))
import Data.Text qualified as T
import Numeric (readHex, readInt, readOct)
import Text.Read (readMaybe)

lexIntBase :: LexerEnv -> LexerState -> Maybe (LexToken, LexerState)
lexIntBase env st =
  case lexerInput st of
    '0' :< (base :< rest)
      | base `elem` ("xXoObB" :: String) ->
          let allowUnderscores = hasExt NumericUnderscores env
              isDigitChar
                | base `elem` ("xX" :: String) = isHexDigit
                | base `elem` ("oO" :: String) = isOctDigit
                | otherwise = (`elem` ("01" :: String))
              (digitsRaw, _) = takeDigitsWithUnderscores allowUnderscores isDigitChar rest
           in if T.null digitsRaw
                then Nothing
                else
                  let raw = "0" <> T.singleton base <> digitsRaw
                      n
                        | base `elem` ("xX" :: String) = readHexLiteral raw
                        | base `elem` ("oO" :: String) = readOctLiteral raw
                        | otherwise = readBinLiteral raw
                      (tokTxt, tokKind, st') =
                        lexIntSuffix env st raw n
                   in Just (mkToken st st' tokTxt tokKind, st')
    _ -> Nothing

lexHexFloat :: LexerEnv -> LexerState -> Maybe (LexToken, LexerState)
lexHexFloat env st =
  case lexerInput st of
    '0' :< (x :< rest1)
      | x `elem` ("xX" :: String),
        let (intDigits, rest2) = T.span isHexDigit rest1,
        not (T.null intDigits) -> do
          let (mFracDigits, rest3) =
                case rest2 of
                  '.' :< more ->
                    let (frac, rest') = T.span isHexDigit more
                     in if T.null frac
                          then (Nothing, rest2)
                          else (Just frac, rest')
                  _ -> (Nothing, rest2)
          case (mFracDigits, takeHexExponent rest3) of
            (Nothing, Nothing) -> Nothing
            (mFrac, mExpo) ->
              let dotAndFrac = maybe "" ("." <>) mFrac
                  expo = fromMaybe "" mExpo
                  fracDigits = fromMaybe "" mFrac
                  raw = "0" <> T.singleton x <> intDigits <> dotAndFrac <> expo
                  value = parseHexFloatLiteral (T.unpack intDigits) (T.unpack fracDigits) (T.unpack expo)
                  (tokTxt, tokKind, st') =
                    lexFloatSuffix env st raw value
               in Just (mkToken st st' tokTxt tokKind, st')
    _ -> Nothing

lexFloat :: LexerEnv -> LexerState -> Maybe (LexToken, LexerState)
lexFloat env st =
  let allowUnderscores = hasExt NumericUnderscores env
      input = lexerInput st
      startsWithUnderscore =
        case input of
          '_' :< _ -> True
          _ -> False
      (lhsRaw, rest) = takeDigitsWithUnderscores allowUnderscores isDigit input
   in if T.null lhsRaw || startsWithUnderscore
        then Nothing
        else case rest of
          '.' :< dotRest@(d :< _)
            | isDigit d ->
                let (rhsRaw, rest') = takeDigitsWithUnderscores allowUnderscores isDigit dotRest
                    (expo, _) = takeExponent allowUnderscores rest'
                    raw = lhsRaw <> "." <> rhsRaw <> expo
                    normalized = T.filter (/= '_') raw
                    value = parseDecimalFloatLiteral normalized
                    (tokTxt, tokKind, st') =
                      lexFloatSuffix env st raw value
                 in Just (mkToken st st' tokTxt tokKind, st')
          _ ->
            case takeExponent allowUnderscores rest of
              (expo, _)
                | T.null expo -> Nothing
                | otherwise ->
                    let raw = lhsRaw <> expo
                        normalized = T.filter (/= '_') raw
                        value = parseDecimalFloatLiteral normalized
                        (tokTxt, tokKind, st') =
                          lexFloatSuffix env st raw value
                     in Just (mkToken st st' tokTxt tokKind, st')

lexInt :: LexerEnv -> LexerState -> Maybe (LexToken, LexerState)
lexInt env st =
  let allowUnderscores = hasExt NumericUnderscores env
      input = lexerInput st
      startsWithUnderscore =
        case input of
          '_' :< _ -> True
          _ -> False
      (digitsRaw, _) = takeDigitsWithUnderscores allowUnderscores isDigit input
   in if T.null digitsRaw || startsWithUnderscore
        then Nothing
        else
          let digits = T.filter (/= '_') digitsRaw
              n = read (T.unpack digits) :: Integer
              (tokTxt, tokKind, st') =
                lexIntSuffix env st digitsRaw n
           in Just (mkToken st st' tokTxt tokKind, st')

-- | Parse optional MagicHash suffix (# or ##) and return the hash text and new state.
magicHashSuffix :: LexerEnv -> LexerState -> Maybe (Text, LexerState)
magicHashSuffix env st =
  case lexerInput st of
    '#' :< rest
      | hasExt MagicHash env ->
          case rest of
            '#' :< _ -> Just ("##", advanceN 2 st)
            _ -> Just ("#", advanceN 1 st)
    _ -> Nothing

-- | Parse optional MagicHash and ExtendedLiterals suffix for integers.
-- Handles: (nothing), #, ##, #Int8, #Int16, #Int32, #Int64, #Int, #Word8, #Word16, #Word32, #Word64, #Word
lexIntSuffix :: LexerEnv -> LexerState -> Text -> Integer -> (Text, LexTokenKind, LexerState)
lexIntSuffix env st raw n =
  let st' = advanceChars raw st
      input = lexerInput st'
      withMagicHash =
        case magicHashSuffix env st' of
          Just ("##", st'') -> (raw <> "##", TkInteger n TWordHash, st'')
          Just ("#", st'') -> (raw <> "#", TkInteger n TIntHash, st'')
          _ -> (raw, TkInteger n TInteger, st')
   in case input of
        '#' :< rest
          | hasExt ExtendedLiterals env ->
              case parseExtendedIntSuffix rest of
                Just (typeName, suffixLen) ->
                  let fullRaw = raw <> T.take (1 + suffixLen) input
                      st'' = advanceN (1 + suffixLen) st'
                   in (fullRaw, TkInteger n typeName, st'')
                Nothing -> withMagicHash
          | otherwise -> withMagicHash
        _ -> (raw, TkInteger n TInteger, st')

-- | Parse optional MagicHash and ExtendedLiterals suffix for floats.
-- Handles: (nothing), #, ##
lexFloatSuffix :: LexerEnv -> LexerState -> Text -> Rational -> (Text, LexTokenKind, LexerState)
lexFloatSuffix env st raw value =
  let st' = advanceChars raw st
   in case magicHashSuffix env st' of
        Just ("##", st'') -> (raw <> "##", TkFloat value TDoubleHash, st'')
        Just ("#", st'') -> (raw <> "#", TkFloat value TFloatHash, st'')
        _ -> (raw, TkFloat value TFractional, st')

-- | Parse an ExtendedLiterals type suffix after the initial '#'.
-- Returns (NumericType, length of type name without the '#').
-- e.g. "Word8" -> (TWord8Hash, 5), "Int" -> (TIntHash, 3)
parseExtendedIntSuffix :: Text -> Maybe (NumericType, Int)
parseExtendedIntSuffix input =
  case matchType input of
    Just ("Int8", len) -> Just (TInt8Hash, len)
    Just ("Int16", len) -> Just (TInt16Hash, len)
    Just ("Int32", len) -> Just (TInt32Hash, len)
    Just ("Int64", len) -> Just (TInt64Hash, len)
    Just ("Int", len) -> Just (TIntHash, len)
    Just ("Word8", len) -> Just (TWord8Hash, len)
    Just ("Word16", len) -> Just (TWord16Hash, len)
    Just ("Word32", len) -> Just (TWord32Hash, len)
    Just ("Word64", len) -> Just (TWord64Hash, len)
    Just ("Word", len) -> Just (TWordHash, len)
    _ -> Nothing

-- | Try to match one of the known type names at the start of the text.
-- Returns (matched name, length) if the match is followed by a non-identifier char
-- or end of input.
matchType :: Text -> Maybe (Text, Int)
matchType input =
  let candidates = ["Int8", "Int16", "Int32", "Int64", "Int", "Word8", "Word16", "Word32", "Word64", "Word"]
   in firstMatch candidates input

firstMatch :: [Text] -> Text -> Maybe (Text, Int)
firstMatch [] _ = Nothing
firstMatch (name : rest) input =
  let len = T.length name
   in case T.stripPrefix name input of
        Just remaining
          | T.null remaining || not (isIdentChar (T.head remaining)) ->
              Just (name, len)
        _ -> firstMatch rest input

isIdentChar :: Char -> Bool
isIdentChar c = isDigit c || isAsciiUpper c || c == '_' || isAsciiLower c

-- Scan ([_]*[digit])* and return a zero-copy split.
takeDigitsWithUnderscores :: Bool -> (Char -> Bool) -> Text -> (Text, Text)
takeDigitsWithUnderscores False isDigitChar = T.span isDigitChar
takeDigitsWithUnderscores True isDigitChar = \chars ->
  let (consumed, rest) = T.span (\c -> isDigitChar c || c == '_') chars
   in if T.null consumed || T.last consumed /= '_'
        then (consumed, rest)
        else
          let trimmed = T.dropWhileEnd (== '_') consumed
           in (trimmed, T.drop (T.length trimmed) chars)

takeExponent :: Bool -> Text -> (Text, Text)
takeExponent allowUnderscores chars =
  case chars of
    '_' :< _
      | allowUnderscores ->
          let (_allUnderscores, rest') = T.span (== '_') chars
           in case rest' of
                marker :< rest2
                  | marker `elem` ("eE" :: String) ->
                      let (_signPart, rest3) =
                            case rest2 of
                              sign :< more | sign `elem` ("+-" :: String) -> (T.singleton sign, more)
                              _ -> ("", rest2)
                          digitsStartWithUnderscore =
                            case rest3 of
                              '_' :< _ -> True
                              _ -> False
                          (digits, rest4) = takeDigitsWithUnderscores allowUnderscores isDigit rest3
                       in if T.null digits || digitsStartWithUnderscore
                            then ("", chars)
                            else
                              let consumed = T.take (T.length chars - T.length rest4) chars
                               in (consumed, rest4)
                _ -> ("", chars)
    marker :< rest
      | marker `elem` ("eE" :: String) ->
          let (_signPart, rest1) =
                case rest of
                  sign :< more | sign `elem` ("+-" :: String) -> (T.singleton sign, more)
                  _ -> ("", rest)
              digitsStartWithUnderscore =
                case rest1 of
                  '_' :< _ -> True
                  _ -> False
              (digits, rest2) = takeDigitsWithUnderscores allowUnderscores isDigit rest1
           in if T.null digits || digitsStartWithUnderscore then ("", chars) else let consumed = T.take (T.length chars - T.length rest2) chars in (consumed, rest2)
    _ -> ("", chars)

takeHexExponent :: Text -> Maybe Text
takeHexExponent chars =
  case chars of
    marker :< rest
      | marker `elem` ("pP" :: String) ->
          let (_signPart, rest1) =
                case rest of
                  sign :< more | sign `elem` ("+-" :: String) -> (T.singleton sign, more)
                  _ -> ("", rest)
              (digits, _) = T.span isDigit rest1
           in if T.null digits then Nothing else Just (T.take (T.length chars - T.length rest1 + T.length digits) chars)
    _ -> Nothing

readBaseLiteral :: String -> (String -> [(Integer, String)]) -> Text -> Integer
readBaseLiteral label parser txt =
  case parser (T.unpack (T.filter (/= '_') (T.drop 2 txt))) of
    [(n, "")] -> n
    _ -> error ("invalid " ++ label ++ " literal: " ++ T.unpack txt)

readHexLiteral :: Text -> Integer
readHexLiteral = readBaseLiteral "hex" readHex

readOctLiteral :: Text -> Integer
readOctLiteral = readBaseLiteral "octal" readOct

readBinLiteral :: Text -> Integer
readBinLiteral = readBaseLiteral "binary" (readInt 2 (`elem` ("01" :: String)) digitToInt)

parseDecimalFloatLiteral :: Text -> Rational
parseDecimalFloatLiteral txt =
  case parseSignedDecimalRational txt of
    Just value -> value
    Nothing -> error ("invalid decimal float literal: " ++ T.unpack txt)

parseSignedDecimalRational :: Text -> Maybe Rational
parseSignedDecimalRational txt = do
  let (sign, unsigned) =
        case T.uncons txt of
          Just ('-', rest) -> (-1, rest)
          Just ('+', rest) -> (1, rest)
          _ -> (1, txt)
      (mantissaTxt, exponentTxt0) = T.break (`elem` ['e', 'E']) unsigned
      exponentTxt = T.drop 1 exponentTxt0
  mantissa <- parseDecimalMantissa mantissaTxt
  exponentN <-
    if T.null exponentTxt0
      then Just 0
      else parseSignedInteger exponentTxt
  pure (applyDecimalExponent (fromInteger sign * mantissa) exponentN)

parseDecimalMantissa :: Text -> Maybe Rational
parseDecimalMantissa txt = do
  let (whole, frac0) = T.break (== '.') txt
      frac = T.drop 1 frac0
  if T.null whole && T.null frac
    then Nothing
    else do
      wholeDigits <- parseDecimalDigits whole
      fracDigits <- parseDecimalDigits frac
      let fracScale = 10 ^ T.length frac
      pure ((wholeDigits * fracScale + fracDigits) % fracScale)

parseDecimalDigits :: Text -> Maybe Integer
parseDecimalDigits digits
  | T.null digits = Just 0
  | T.all isDigit digits = readMaybe (T.unpack digits)
  | otherwise = Nothing

parseSignedInteger :: Text -> Maybe Integer
parseSignedInteger digits =
  case T.uncons digits of
    Just ('+', rest) -> parseDecimalDigits rest
    Just ('-', rest) -> negate <$> parseDecimalDigits rest
    _ -> parseDecimalDigits digits

applyDecimalExponent :: Rational -> Integer -> Rational
applyDecimalExponent value exponentN
  | exponentN >= 0 = value * fromInteger (10 ^ exponentN)
  | otherwise = value / fromInteger (10 ^ negate exponentN)

parseHexFloatLiteral :: String -> String -> String -> Rational
parseHexFloatLiteral intDigits fracDigits expo =
  (parseHexDigits intDigits + parseHexFraction fracDigits) * (2 ^^ exponentValue expo)

parseHexDigits :: String -> Rational
parseHexDigits = foldl (\acc d -> acc * 16 + fromIntegral (digitToInt d)) 0

parseHexFraction :: String -> Rational
parseHexFraction ds =
  sum [fromIntegral (digitToInt d) % (16 ^ i) | (d, i) <- zip ds [1 :: Integer ..]]

exponentValue :: String -> Int
exponentValue expo =
  case expo of
    _ : '-' : ds | not (null ds) -> negate (fromMaybe 0 (readMaybe ds))
    _ : '+' : ds | not (null ds) -> fromMaybe 0 (readMaybe ds)
    _ : ds | not (null ds) -> fromMaybe 0 (readMaybe ds)
    _ -> 0

withOptionalMagicHashSuffix ::
  Int ->
  LexerEnv ->
  LexerState ->
  Text ->
  LexTokenKind ->
  (Text -> LexTokenKind) ->
  (Text, LexTokenKind, LexerState)
withOptionalMagicHashSuffix maxHashes env st raw plainKind hashKind =
  let st' = advanceChars raw st
      hashCount =
        if hasExt MagicHash env
          then min maxHashes (T.length (T.takeWhile (== '#') (lexerInput st')))
          else 0
   in case hashCount of
        0 -> (raw, plainKind, st')
        _ ->
          let hashes = T.replicate hashCount "#"
              rawHash = raw <> hashes
           in (rawHash, hashKind rawHash, advanceChars hashes st')