packages feed

nova-nix-0.10.0.0: src/Nix/Eval/SourcePos.hs

-- | Source positions as the evaluator keeps them: a t'PosIdx' stored beside
-- an attribute, read back as a file, line and column on demand.
--
-- A position is recorded when the construct it belongs to is compiled, as
-- three words in the bytecode data buffer: the interned path of the file it
-- is in ('noFile' for text with no file behind it), its line and its column.
-- Its index is one past the record's offset, so 0 is free to mean no
-- position, the bias upstream's @PosIdx@ carries for the same reason
-- (@noPos@ in pos-idx.hh, @PosTable::add@ in pos-table.hh, at 2.24.9).
-- The record lives as long as the bytecode that refers to it, and is
-- never written again.
module Nix.Eval.SourcePos
  ( -- * Indices
    PosIdx (..),
    noPos,

    -- * Recording
    PosRecorder,
    newPosRecorder,
    recordPos,

    -- * Reading back
    RecordedPos (..),
    lookupPos,
  )
where

import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Text (Text)
import Data.Word (Word32)
import Nix.Eval.CBytecode (cbcData, cbcEmitData)
import Nix.Eval.Symbol (Symbol (..), symbolIntern, symbolText)
import Nix.Expr.Types (SourceOrigin (..), SrcPos (..))
import System.IO.Unsafe (unsafePerformIO)

-- | Where something was defined: 'noPos', or the index of a recorded
-- position.
newtype PosIdx = PosIdx {unPosIdx :: Word32}
  deriving (Eq, Show)

-- | Defined nowhere in source: a builtin's attribute, or one a builtin
-- made.
noPos :: PosIdx
noPos = PosIdx 0

-- | The file word of a position in text with no file.  No symbol is 0
-- (@NN_SYMBOL_INVALID@), so it cannot be mistaken for a path.
noFile :: Word32
noFile = 0

-- | Records the positions of one compilation.  It remembers the last
-- origin it recorded and that origin's file word, because the positions
-- of one parse share their origin, and interning the path again for each
-- one would copy and hash it as many times.
newtype PosRecorder = PosRecorder (IORef (Maybe SeenOrigin))

-- | An origin and its file word.
data SeenOrigin = SeenOrigin !SourceOrigin !Word32

-- | A recorder that has seen no origin yet.
newPosRecorder :: IO PosRecorder
newPosRecorder = PosRecorder <$> newIORef Nothing

-- | Record a position for the evaluator to read back.  A line or column
-- past 32 bits records 'noPos', as upstream's position table cannot
-- address one either.
recordPos :: PosRecorder -> SrcPos -> IO PosIdx
recordPos (PosRecorder seen) (SrcPos origin line column) = case (toWord32 line, toWord32 column) of
  (Just lineWord, Just columnWord) -> do
    file <- fileWord
    offset <- cbcEmitData file
    _ <- cbcEmitData lineWord
    _ <- cbcEmitData columnWord
    pure (PosIdx (offset + 1))
  _ -> pure noPos
  where
    fileWord = do
      remembered <- readIORef seen
      case remembered of
        Just (SeenOrigin known word) | known == origin -> pure word
        _ -> do
          word <- internedFile origin
          writeIORef seen (Just (SeenOrigin origin word))
          pure word
    internedFile (SourceFile path) = unSymbol <$> symbolIntern path
    internedFile (SourceText _) = pure noFile
    toWord32 n
      | n >= 0 && toInteger n <= toInteger (maxBound :: Word32) = Just (fromIntegral n)
      | otherwise = Nothing

-- | A recorded position, read back.
data RecordedPos
  = -- | A line and column in the file at this path.
    PosInFile !Text !Int !Int
  | -- | A line and column in text with no file behind it.
    PosInText !Int !Int
  deriving (Eq, Show)

-- | Read a position back, or 'Nothing' for 'noPos'.  'unsafePerformIO' is
-- safe here: the record was written once, when its index was made, and
-- no later write touches it, so the read answers the same every time.
lookupPos :: PosIdx -> Maybe RecordedPos
lookupPos (PosIdx 0) = Nothing
lookupPos (PosIdx idx) = Just $ unsafePerformIO $ do
  let offset = idx - 1
  file <- cbcData offset
  line <- fromIntegral <$> cbcData (offset + 1)
  column <- fromIntegral <$> cbcData (offset + 2)
  pure $
    if file == noFile
      then PosInText line column
      else PosInFile (symbolText (Symbol file)) line column