packages feed

tadka-2.0.0.0: interop/ghc/Tadka/Interop/GHC.hs

{-# LANGUAGE OverloadedStrings #-}

-- | One-directional adapter: GHC 'SrcSpan' → tadka 'Span'.
--
-- A plain function against Phase 1/2 types; no core module depends on this.
-- Targets GHC 9.6.7 through 9.14.1 (see the @ghc@ bound in @tadka.cabal@).
-- Only 'GHC.Types.SrcLoc''s 'SrcSpan'/'RealSrcSpan' accessors are used, which
-- have been stable public GHC API across that whole range — no CPP needed,
-- but this should be reconfirmed by an actual per-version CI build rather
-- than assumed from this note.
--
-- No compatibility guarantee.
module Tadka.Interop.GHC
  ( SrcSpanConvError (..)
  , spanFromSrcSpan
  , offsetFromLineCol
  ) where

import           Data.Text        (Text)
import qualified Data.Text        as T
import           GHC.Types.SrcLoc (SrcSpan (..), srcSpanEndCol, srcSpanEndLine,
                                   srcSpanStartCol, srcSpanStartLine)

import           Tadka            (Span, mkSpan)

-- | Why a 'SrcSpan' could not be converted.
data SrcSpanConvError
  = UnhelpfulSrcSpan          -- ^ the span was an @UnhelpfulSpan@ (no real location)
  | LineColOutOfBounds Int Int -- ^ a 1-based (line, column) not present in the source
  | NegativeSpanLength        -- ^ end preceded start
  deriving (Eq, Show)

-- | Total, safe indexing into a list: 'Nothing' out of bounds, never a partial
-- crash. Kept local (mirrors 'Tadka.Internal.Width.atMay') so no caller in this
-- module ever reaches for the partial @(!!)@ directly.
atMayList :: [a] -> Int -> Maybe a
atMayList xs i
  | i < 0     = Nothing
  | otherwise = case drop i xs of
      (x : _) -> Just x
      []      -> Nothing

-- | Convert a 1-based (line, column) into a 0-based character offset within the
-- given source, or 'Nothing' if the position is not in bounds. Column may point
-- one past the end of a line (the end-of-line position GHC uses). Total: the
-- line lookup is tied directly to the 'Maybe' via 'atMayList', so an
-- out-of-range line can never reach the column check below it.
offsetFromLineCol :: Text -> Int -> Int -> Maybe Int
offsetFromLineCol src line col = do
  here <- atMayList ls (line - 1)
  if col < 1 || col > T.length here + 1
    then Nothing
    else Just (before + (col - 1))
  where
    ls     = T.splitOn "\n" src
    before = sum (map ((+ 1) . T.length) (take (line - 1) ls))  -- +1 per newline

-- | Convert a GHC 'SrcSpan' to a tadka 'Span', given the source text (needed to
-- turn GHC's 1-based line/column into a character offset and length).
--
-- The four-way case split below is exhaustive over which of the two
-- endpoints resolved: both, only the start, only the end, or neither. Each
-- arm names the specific endpoint at fault rather than defaulting to the
-- start, so 'LineColOutOfBounds' always describes the position that was
-- actually out of bounds.
spanFromSrcSpan :: Text -> SrcSpan -> Either SrcSpanConvError Span
spanFromSrcSpan _   (UnhelpfulSpan _) = Left UnhelpfulSrcSpan
spanFromSrcSpan src (RealSrcSpan rss _) =
  case (offsetFromLineCol src (srcSpanStartLine rss) (srcSpanStartCol rss),
        offsetFromLineCol src (srcSpanEndLine rss)   (srcSpanEndCol rss)) of
    (Nothing, _) -> Left (LineColOutOfBounds (srcSpanStartLine rss) (srcSpanStartCol rss))
    (_, Nothing) -> Left (LineColOutOfBounds (srcSpanEndLine rss)   (srcSpanEndCol rss))
    (Just s, Just e)
      | e >= s    -> mkSpanNonNegative s (e - s)
      | otherwise -> Left NegativeSpanLength

-- | 'mkSpan', specialised to a call site where both arguments are already
-- non-negative by construction (`max 0` is a no-op on them): 'mkSpan' can
-- only fail on a negative offset or a negative length, so with both clamped
-- here — not merely reasoned to be non-negative three functions away in
-- 'offsetFromLineCol' — its 'Left' case is unreachable by local inspection,
-- not by trusting a distant invariant. 'mkSpan' is still the one used, so a
-- future tightening of its validation is not silently bypassed here.
mkSpanNonNegative :: Int -> Int -> Either SrcSpanConvError Span
mkSpanNonNegative s len = either (const (Left NegativeSpanLength)) Right (mkSpan (max 0 s) (max 0 len))