packages feed

graft-0.1.0.0: src/Graft/Green/Internal.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MagicHash #-}

-- | Representation of green trees, shared by the library's modules and not
-- exposed by the package. Constructing values directly can break the cached
-- lengths and offsets that the rest of the library relies on; "Graft.Green"
-- re-exports the safe interface.
module Graft.Green.Internal
  ( -- * Representation
    TextLength (..)
  , textLengthOf
  , GreenNode (..)
  , GreenToken (..)
  , GreenElement (..)
    -- * Construction
  , greenNode
  , greenNodeReversed
  , greenNodeFromArray
  , greenToken
    -- * Children
  , elementTextLength
  , childCount
  , childAt
  , childStart
  , firstChildEndingAtOrAfter
    -- * Folding
  , foldM
    -- * Child edits
  , ChildEditError (..)
  , spliceBoundsError
  , checkedLength
  , setChild
  , setChildren
  , patchChildren
  , spliceChildrenUnchecked
  ) where

import Control.DeepSeq (NFData (..))
import Control.Exception (Exception)
import Control.Monad.ST (ST, runST)
import Data.Functor.Identity (Identity (..))
import Data.Foldable (toList)
import Data.Hashable (Hashable (..))
import Data.Primitive.PrimArray
import Data.Primitive.SmallArray
import Data.Text (Text)
import qualified Data.Text.Foreign as Text
import GHC.Exts (isTrue#, reallyUnsafePtrEquality#)

-- | A count of UTF-8 bytes, used for lengths and for offsets from the start
-- of a tree. It is neither a character count (as 'Data.Text.length' returns)
-- nor a UTF-16 code-unit count (as the Language Server Protocol uses), and
-- the newtype keeps those from being mixed up by accident. Numeric literals
-- and arithmetic work as for 'Int'; use the constructor or 'fromIntegral' to
-- convert deliberately.
--
-- Shared subtrees make a logical length exceed the physical text size.
-- Constructing or editing a node whose length would exceed
-- @maxBound :: Int@ fails with an error instead of wrapping around.
newtype TextLength = TextLength Int
  deriving newtype (Eq, Ord, Show, Read, Bounded, Enum, Num, Real, Integral, NFData, Hashable)

-- | The UTF-8 byte length of a text, in constant time.
textLengthOf :: Text -> TextLength
textLengthOf = TextLength . Text.lengthWord8

-- | An interior node: a kind and its children in source order. Construct one
-- with 'Graft.Green.node'. A node caches its total length and the offset of each
-- child, so reading a length or a child takes constant time.
data GreenNode kind = GreenNode
  !kind
  !TextLength
  !(SmallArray (GreenElement kind))
  !(PrimArray Int)

-- | A leaf: a kind and the exact text of a source fragment, which may be
-- empty. Construct one with 'Graft.Green.token'. Text is stored as given, with no
-- normalization.
data GreenToken kind = GreenToken !kind !TextLength {-# UNPACK #-} !Text
  deriving (Eq)

-- | A node or a token; either can be a child of a node.
-- Tokens are unpacked into the element, which saves a box per token (about
-- 15% of a typical tree's heap). Nodes stay boxed so that cursors can share
-- the parent node instead of re-allocating it on every descent.
data GreenElement kind
  = Node !(GreenNode kind)
  | Token {-# UNPACK #-} !(GreenToken kind)
  deriving (Eq, Show)

-- | Structural equality. Physically identical nodes compare equal without
-- traversal, which assumes that equality on @kind@ is reflexive. Otherwise a
-- subtree shared in several places is compared at each of them, so comparing
-- two separately built trees takes time proportional to their size written
-- out in full.
instance Eq kind => Eq (GreenNode kind) where
  left@(GreenNode leftKind leftLength leftChildren _)
    == right@(GreenNode rightKind rightLength rightChildren _) =
      isTrue# (reallyUnsafePtrEquality# left right)
        || (leftLength == rightLength && leftKind == rightKind && leftChildren == rightChildren)

-- | Visits a subtree shared in several places at each of them.
instance NFData kind => NFData (GreenNode kind) where
  rnf node = rnf (Node node)

instance NFData kind => NFData (GreenToken kind) where
  rnf (GreenToken kind _ text) = rnf kind `seq` rnf text

-- | Forces every kind and every token's text with one walk of the tree.
instance NFData kind => NFData (GreenElement kind) where
  rnf tree = runIdentity (foldM (\() element -> Identity (force element)) () tree)
    where
      force (Node (GreenNode kind _ _ _)) = rnf kind
      force (Token token) = rnf token

-- | Hashes the kind and number of children of every node, and every token,
-- so hashing takes time proportional to the size of the tree written out in
-- full, with a shared subtree hashed at each place it appears; hashes are not
-- cached.
instance Hashable kind => Hashable (GreenNode kind) where
  hashWithSalt salt node = hashWithSalt salt (Node node)

instance Hashable kind => Hashable (GreenToken kind) where
  hashWithSalt salt (GreenToken kind _ text) = salt `hashWithSalt` kind `hashWithSalt` text

-- | One preorder fold over the tree. Each node contributes its kind and its
-- number of children, and a preorder sequence with the number of children of
-- each node determines the tree, so the nesting is part of the hash. Lengths
-- are left out: the tokens' text already determines them.
instance Hashable kind => Hashable (GreenElement kind) where
  hashWithSalt salt tree = runIdentity (foldM (\current element -> Identity (mix current element)) salt tree)
    where
      mix current (Node (GreenNode kind _ children _)) =
        current `hashWithSalt` (0 :: Int) `hashWithSalt` kind `hashWithSalt` sizeofSmallArray children
      mix current (Token token) = current `hashWithSalt` (1 :: Int) `hashWithSalt` token

-- | Maps every kind in the tree, reusing the cached lengths and offsets. A
-- subtree shared in several places is mapped at each of them, and the
-- results are not shared. Kinds are stored strictly, so @fmap f@ evaluates
-- each result of @f@, and the functor laws hold for functions that don't
-- return bottom.
instance Functor GreenNode where
  fmap f (GreenNode kind len children starts) =
    GreenNode (f kind) len (mapSmallArray' (fmap f) children) starts

instance Functor GreenToken where
  fmap f (GreenToken kind len text) = GreenToken (f kind) len text

instance Functor GreenElement where
  fmap f (Node node) = Node (fmap f node)
  fmap f (Token token) = Token (fmap f token)

-- | Shows the smart-constructor call that rebuilds the node.
instance Show kind => Show (GreenNode kind) where
  showsPrec precedence (GreenNode kind _ children _) = showParen (precedence > 10) $
    showString "Green.node " . showsPrec 11 kind . showChar ' ' . shows (toList children)

-- | Shows the smart-constructor call that rebuilds the token.
instance Show kind => Show (GreenToken kind) where
  showsPrec precedence (GreenToken kind _ text) = showParen (precedence > 10) $
    showString "Green.token " . showsPrec 11 kind . showChar ' ' . showsPrec 11 text

-- | A node with a kind and its children in source order, which may be none.
-- Takes time proportional to the number of children; the children are
-- shared, not copied or traversed.
greenNode :: kind -> [GreenElement kind] -> GreenNode kind
greenNode kind children = runST $ do
  let count = length children
  elements <- newSmallArray count missingChild
  starts <- newPrimArray count
  let fill !_ !offset [] = pure offset
      fill !index !offset (!child : rest) = do
        writeSmallArray elements index child
        writePrimArray starts index (bytes offset)
        fill (index + 1) (addLength offset (elementTextLength child)) rest
  total <- fill 0 0 children
  freeze kind total elements starts

-- | Construct a node from the first @count@ elements of a list in reverse
-- source order, as a builder accumulates them, and return the rest of the
-- list. The list must have at least @count@ elements. The list is walked once.
greenNodeReversed :: kind -> Int -> [GreenElement kind] -> (GreenNode kind, [GreenElement kind])
greenNodeReversed kind count reversed = runST $ do
  elements <- newSmallArray count missingChild
  let place !index rest
        | index < 0 = pure rest
        | otherwise = case rest of
            [] -> error "Graft.Green.Internal.greenNodeReversed: too few elements"
            !child : older -> writeSmallArray elements index child >> place (index - 1) older
  remaining <- place (count - 1) reversed
  starts <- newPrimArray count
  let measure !index !offset
        | index == count = pure offset
        | otherwise = do
            child <- readSmallArray elements index
            writePrimArray starts index (bytes offset)
            measure (index + 1) (addLength offset (elementTextLength child))
  total <- measure 0 0
  node <- freeze kind total elements starts
  pure (node, remaining)

-- | A strict, monadic left fold over a tree's elements in preorder: each
-- element, then its children in source order. The accumulator is evaluated
-- to weak head normal form before each step. The loop runs over the child
-- arrays by index and folds the last child of each node by a tail call, so a
-- chain of single-child nodes runs in constant stack.
foldM :: Monad m => (b -> GreenElement kind -> m b) -> b -> GreenElement kind -> m b
foldM step = element
  where
    element !acc current = case current of
      Token _ -> step acc current
      Node (GreenNode _ _ elements _) ->
        step acc current >>= \ !entered -> children entered elements 0
    children !acc elements !index
      | index >= count = pure acc
      | index == count - 1 = element acc (indexSmallArray elements index)
      | otherwise =
          element acc (indexSmallArray elements index) >>= \ !next ->
            children next elements (index + 1)
      where
        count = sizeofSmallArray elements
{-# INLINE foldM #-}

-- | Construct a node from an array of children, measuring each child once.
greenNodeFromArray :: kind -> SmallArray (GreenElement kind) -> GreenNode kind
greenNodeFromArray kind children = runST $ do
  let count = sizeofSmallArray children
  starts <- newPrimArray count
  let measure !index !offset
        | index == count = pure offset
        | otherwise = do
            writePrimArray starts index (bytes offset)
            measure (index + 1) (addLength offset (elementTextLength (indexSmallArray children index)))
  total <- measure 0 0
  GreenNode kind total children <$> unsafeFreezePrimArray starts

freeze
  :: kind -> TextLength -> SmallMutableArray s (GreenElement kind) -> MutablePrimArray s Int
  -> ST s (GreenNode kind)
freeze kind total elements starts =
  GreenNode kind total <$> unsafeFreezeSmallArray elements <*> unsafeFreezePrimArray starts

bytes :: TextLength -> Int
bytes (TextLength count) = count

missingChild :: a
missingChild = error "Graft.Green.Internal: uninitialised child"

-- Lengths are sums of non-negative lengths, so a sum that comes out negative
-- has overflowed. Shared subtrees can reach that without the text existing,
-- and a wrapped length would make 'Graft.Green.render' size its buffer
-- wrongly, so overflow is an error.
checkedLength :: TextLength -> TextLength
checkedLength len
  | len < 0 = error "Graft.Green: a tree's length exceeds maxBound :: Int"
  | otherwise = len

addLength :: TextLength -> TextLength -> TextLength
addLength offset len = checkedLength (offset + len)

-- | Construct a token. 'Text' stores UTF-8 internally, so reading the byte
-- length takes constant time and copies nothing.
greenToken :: kind -> Text -> GreenToken kind
greenToken kind text = GreenToken kind (textLengthOf text) text

-- | Read the cached UTF-8 byte length of either element.
elementTextLength :: GreenElement kind -> TextLength
elementTextLength (Node (GreenNode _ len _ _)) = len
elementTextLength (Token (GreenToken _ len _)) = len

-- | Number of immediate children.
childCount :: GreenNode kind -> Int
childCount (GreenNode _ _ children _) = sizeofSmallArray children

-- | The child at an index, which must lie in @[0, childCount node)@.
childAt :: GreenNode kind -> Int -> GreenElement kind
childAt (GreenNode _ _ children _) = indexSmallArray children

-- | Start of the child at an index relative to the node. The child count is
-- accepted and returns the node's length.
childStart :: GreenNode kind -> Int -> TextLength
childStart (GreenNode _ len _ starts) index
  | index >= sizeofPrimArray starts = len
  | otherwise = TextLength (indexPrimArray starts index)

-- | The smallest index whose child ends at or after a relative offset, or the
-- child count if there is none. Binary search over the cached child offsets.
firstChildEndingAtOrAfter :: GreenNode kind -> TextLength -> Int
firstChildEndingAtOrAfter node offset = go 0 (childCount node)
  where
    go low high
      | low >= high = low
      | childStart node (middle + 1) >= offset = go low middle
      | otherwise = go (middle + 1) high
      where
        middle = (low + high) `div` 2

-- | Why a child edit failed. 'CannotEditTokenChildren' means the target is a
-- token; the others report a bad child position (or both positions of a
-- reversed range).
data ChildEditError
  = CannotEditTokenChildren
  | NegativeChildBoundary !Int
  | ReversedChildRange !Int !Int
  | ChildBoundaryOutOfBounds !Int
  deriving (Eq, Show)

instance Exception ChildEditError

instance NFData ChildEditError where
  rnf err = err `seq` ()

-- | Why @[start, end)@ is not a range of a node's children, if it isn't.
-- Errors are checked in this order: a negative start or end, then
-- @end < start@, then a start or end past the last child.
spliceBoundsError :: Int -> Int -> GreenNode kind -> Maybe ChildEditError
spliceBoundsError start end node
  | start < 0 = Just (NegativeChildBoundary start)
  | end < 0 = Just (NegativeChildBoundary end)
  | end < start = Just (ReversedChildRange start end)
  | start > count = Just (ChildBoundaryOutOfBounds start)
  | end > count = Just (ChildBoundaryOutOfBounds end)
  | otherwise = Nothing
  where
    count = childCount node

-- | Replace the child at a valid index. The length is adjusted by the
-- difference between the old and new child; offsets are reused when the
-- lengths agree and shifted otherwise.
setChild :: Int -> GreenElement kind -> GreenNode kind -> GreenNode kind
setChild index !replacement node@(GreenNode kind len children starts) =
  GreenNode kind (checkedLength (len + delta)) updated shifted
  where
    delta = elementTextLength replacement - elementTextLength (childAt node index)
    count = sizeofSmallArray children
    updated = runSmallArray $ do
      copy <- thawSmallArray children 0 count
      writeSmallArray copy index replacement
      pure copy
    shifted
      | delta == 0 = starts
      | otherwise = generatePrimArray count $ \position ->
          let start = indexPrimArray starts position
          in if position <= index then start else start + bytes delta

-- | Replace the children at valid indices, given in increasing order, with
-- one copy of the child array. Offsets before the first replacement are
-- copied, and the rest are recomputed.
setChildren :: [(Int, GreenElement kind)] -> GreenNode kind -> GreenNode kind
setChildren [] node = node
setChildren [(index, replacement)] node = setChild index replacement node
setChildren replacements@((first, _) : _) node@(GreenNode kind _ children starts) = runST $ do
  let count = sizeofSmallArray children
  elements <- thawSmallArray children 0 count
  mapM_ (\(index, !replacement) -> writeSmallArray elements index replacement) replacements
  offsets <- newPrimArray count
  copyPrimArray offsets 0 starts 0 first
  let measure !index !offset
        | index == count = pure offset
        | otherwise = do
            child <- readSmallArray elements index
            writePrimArray offsets index (bytes offset)
            measure (index + 1) (addLength offset (elementTextLength child))
  total <- measure first (childStart node first)
  freeze kind total elements offsets

-- | Replace several ranges of children at once, with one copy of the child
-- array: each @(start, end, elements)@ puts the elements in place of the
-- children in @[start, end)@, and an empty range inserts them at its
-- boundary. The ranges must be sorted, lie within @[0, childCount node]@
-- and not overlap. The untouched runs of children are copied as blocks and
-- keep their offsets, shifted by the change in length before them; only
-- the new elements are measured. When every new element has the length of
-- the child it replaces, the offsets are reused.
patchChildren :: [(Int, Int, [GreenElement kind])] -> GreenNode kind -> GreenNode kind
patchChildren [] node = node
patchChildren patches@((first, _, _) : _) node@(GreenNode kind len children starts)
  | sameLayout = runST $ do
      elements <- newSmallArray count missingChild
      copySmallArray elements 0 children 0 first
      let fill !source ((start, end, new) : rest) = do
            copySmallArray elements source children source (start - source)
            mapM_ (\(index, !child) -> writeSmallArray elements index child) (zip [start ..] new)
            fill end rest
          fill !source [] = copySmallArray elements source children source (count - source)
      fill first patches
      GreenNode kind len <$> unsafeFreezeSmallArray elements <*> pure starts
  | otherwise = patchLayout patches node
  where
    count = sizeofSmallArray children
    sameLayout = all unchanged patches
    -- Every new element must have the length of the child it replaces: an
    -- equal total over the range is not enough, as the offsets inside it
    -- would move.
    unchanged (start, end, new) =
      length new == end - start
        && and (zipWith sameLength [start ..] new)
    sameLength index child =
      elementTextLength child == childStart node (index + 1) - childStart node index

-- The general case of 'patchChildren': a new offset array, with the
-- untouched runs shifted and the new elements measured.
patchLayout :: [(Int, Int, [GreenElement kind])] -> GreenNode kind -> GreenNode kind
patchLayout [] node = node
patchLayout patches@((first, _, _) : _) node@(GreenNode kind len children starts) = runST $ do
  let count = sizeofSmallArray children
      newCount = count + sum [length new - (end - start) | (start, end, new) <- patches]
  elements <- newSmallArray newCount missingChild
  offsets <- newPrimArray newCount
  copySmallArray elements 0 children 0 first
  copyPrimArray offsets 0 starts 0 first
  let -- Copy an untouched run and its offsets, shifted by the delta so far.
      keep !target !source !run !delta = do
        copySmallArray elements target children source run
        let shift !step
              | step == run = pure ()
              | otherwise = do
                  writePrimArray offsets (target + step) (indexPrimArray starts (source + step) + delta)
                  shift (step + 1)
        shift 0
      -- Write the new elements of a patch and measure them.
      place !target !offset [] = pure (target, offset)
      place !target !offset (!child : more) = do
        writeSmallArray elements target child
        writePrimArray offsets target (bytes offset)
        place (target + 1) (addLength offset (elementTextLength child)) more
      go !target !source !delta ((start, end, new) : rest) = do
        let run = start - source
        keep target source run delta
        let from = childStart node start + TextLength delta
        (target', to) <- place (target + run) from new
        go target' end (delta + bytes to - bytes from - bytes (childStart node end - childStart node start)) rest
      go !target !source !delta [] = do
        keep target source (count - source) delta
        pure delta
  delta <- go first first 0 patches
  freeze kind (checkedLength (len + TextLength delta)) elements offsets

-- | Replace the children in @[start, end)@, which must satisfy
-- @0 <= start <= end <= childCount node@. Offsets before the range are
-- copied, and offsets after it are shifted by the change in length.
spliceChildrenUnchecked :: Int -> Int -> [GreenElement kind] -> GreenNode kind -> GreenNode kind
spliceChildrenUnchecked start end replacements node@(GreenNode kind len children starts) = runST $ do
  let count = sizeofSmallArray children
      inserted = length replacements
      newCount = count - (end - start) + inserted
      suffix = count - end
  elements <- newSmallArray newCount missingChild
  offsets <- newPrimArray newCount
  copySmallArray elements 0 children 0 start
  copyPrimArray offsets 0 starts 0 start
  let place !_ !offset [] = pure offset
      place !index !offset (!child : rest) = do
        writeSmallArray elements index child
        writePrimArray offsets index (bytes offset)
        place (index + 1) (addLength offset (elementTextLength child)) rest
  afterInserted <- place start (childStart node start) replacements
  let delta = afterInserted - childStart node end
      shift !position
        | position == suffix = pure ()
        | otherwise = do
            writePrimArray offsets (start + inserted + position)
              (indexPrimArray starts (end + position) + bytes delta)
            shift (position + 1)
  copySmallArray elements (start + inserted) children end suffix
  shift 0
  freeze kind (checkedLength (len + delta)) elements offsets