packages feed

yamlet-1.0.0.0: src/Yamlet/Internal/Compose.hs

{-# OPTIONS_HADDOCK not-home #-}

-- | The checks of a syntax tree before the decoder reads it, and the values
-- of its nodes.
--
-- This module is intended for internal use only, and may change without warning
-- in subsequent releases.
module Yamlet.Internal.Compose
  ( prepareWithin
  , aliasLimit
  , representPrepared
  , Failure
  , noMergeKeys
  ) where

import Control.Monad
import Data.Foldable
import Data.IntMap.Strict qualified as IM
import Data.List qualified as L
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as M
import Data.Text qualified as T

import Yamlet.Internal.Schema
import Yamlet.Internal.Syntax qualified as S
import Yamlet.Internal.Utils
import Yamlet.Internal.View
import Yamlet.Value

-- | Check that the tags of a node are valid and that the keys of every
-- mapping are unique, and replace each alias with the node that it refers
-- to. The result has no aliases. A node without aliases comes back
-- unchanged.
--
-- The arguments are the limit of the visits that the aliases can add, see
-- 'aliasLimit', and the visits that the aliases of the documents before
-- added. The result also gives the visits that the aliases added with this
-- document.
prepareWithin :: Int -> Int -> S.Node -> Either Failure (S.Node, Int)
prepareWithin limit added root
  | needsNumbering root = (expandAliases root,) <$> numberWithin limit added root
  | otherwise = (root, added) <$ check root
  where
    -- The node has an alias or a collection key inside it.
    needsNumbering :: S.Node -> Bool
    needsNumbering n = case n.content of
      S.ScalarContent {} -> False
      S.SequenceContent _ xs -> any needsNumbering xs
      S.MappingContent _ kvs ->
        any (\(k, v) -> isCollection k || needsNumbering k || needsNumbering v) kvs
      S.AliasContent {} -> True
      where
        isCollection :: S.Node -> Bool
        isCollection k = case k.content of
          S.SequenceContent {} -> True
          S.MappingContent {} -> True
          _ -> False

-- | The limit of the visits of a traversal that the aliases of the documents
-- can add together: as many visits as the documents have, or a fixed minimum
-- for small documents. A node is one visit and each character of its scalar,
-- tag and anchor is one more, because the decoder copies the texts of each
-- alias. Without a limit, the visits of a small input can be exponential in
-- its size. The documents of a stream share the limit, so that many small
-- documents cannot add the minimum each.
aliasLimit :: [S.Node] -> Int
aliasLimit roots = max minExpansion (sum (map syntaxSize roots))

-- | The visits of a traversal of a node without aliases.
syntaxSize :: S.Node -> Int
syntaxSize n =
  ownVisits n + case n.content of
    S.SequenceContent _ xs -> sum (map syntaxSize xs)
    S.MappingContent _ kvs -> sum [syntaxSize k + syntaxSize v | (k, v) <- kvs]
    _ -> 0

-- | The visits of a node without the nodes inside it.
ownVisits :: S.Node -> Int
ownVisits n = 1 + anchorChars + tagChars + scalarChars
  where
    anchorChars :: Int
    anchorChars = maybe 0 T.length n.props.anchor

    tagChars :: Int
    tagChars = case n.props.tag of
      S.Tag t -> T.length t
      _ -> 0

    scalarChars :: Int
    scalarChars = case n.content of
      S.ScalarContent _ t -> T.length t
      _ -> 0

-- | The checks of 'prepareWithin' for a node with aliases or collection keys,
-- which compare by the numbers of their values, and the visits that the
-- aliases added. The limit is evaluated only at an alias, so that the size
-- of a document without aliases is not computed.
numberWithin :: Int -> Int -> S.Node -> Either Failure Int
numberWithin limit added root = do
  (_, st) <- go (Numbering M.empty M.empty 0 added) root
  Right st.added
  where
    -- Each value comes with its number.
    go :: Numbering -> S.Node -> Either Failure ((Value, Int), Numbering)
    go st sn =
      let off = sn.offset; props = sn.props
      in case sn.content of
           S.AliasContent name -> case M.lookup name st.anchors of
             Just (Just (v, i, visits))
               | st.added + visits > limit ->
                   Left . failure off $
                     "the aliases add more than "
                       ++ show limit
                       ++ " nodes and characters"
               | otherwise ->
                   Right
                     ( (v, i)
                     , st {visits = st.visits + visits, added = st.added + visits}
                     )
             Just Nothing ->
               Left . failure off $
                 "the alias *" ++ T.unpack name ++ " refers to a node that contains it"
             Nothing ->
               Left . failure off $ "undefined alias *" ++ T.unpack name
           S.ScalarContent style t -> do
             v <- scalar off props style t
             let visits = ownVisits sn
             Right $ number props v (ScalarShape v) visits visits (open props st)
           S.SequenceContent _ xs -> do
             tag <- collectionTag off props seqTag
             (vs, st') <- goList (open props st) xs
             let v = withTag tag (Sequence (map fst vs))
             let own = ownVisits sn
             Right $
               number
                 props
                 v
                 (SequenceShape tag (map snd vs))
                 own
                 (st'.visits - st.visits + own)
                 st'
           S.MappingContent _ kvs -> do
             tag <- collectionTag off props mapTag
             (entries, st') <- goPairs (open props st) kvs
             checkUniqueNumbers entries
             let v = withTag tag (Mapping [(k, x) | (_, (k, _), (x, _)) <- entries])
                 shape =
                   MappingShape tag (L.sort [(i, j) | (_, (_, i), (_, j)) <- entries])
                 own = ownVisits sn
             Right $ number props v shape own (st'.visits - st.visits + own) st'

    -- The values are in reverse until the end, so that the stack does not
    -- grow with the number of items.
    goList :: Numbering -> [S.Node] -> Either Failure ([(Value, Int)], Numbering)
    goList = loop []
      where
        loop
          :: [(Value, Int)]
          -> Numbering
          -> [S.Node]
          -> Either Failure ([(Value, Int)], Numbering)
        loop acc st = \case
          [] -> Right (reverse acc, st)
          x : xs -> case go st x of
            Left err -> Left err
            Right (v, st') -> loop (v : acc) st' xs

    -- The entries come with the nodes of their keys, in reverse as in
    -- 'goList'.
    goPairs
      :: Numbering
      -> [(S.Node, S.Node)]
      -> Either Failure ([(S.Node, (Value, Int), (Value, Int))], Numbering)
    goPairs = loop []
      where
        loop
          :: [(S.Node, (Value, Int), (Value, Int))]
          -> Numbering
          -> [(S.Node, S.Node)]
          -> Either Failure ([(S.Node, (Value, Int), (Value, Int))], Numbering)
        loop acc st = \case
          [] -> Right (reverse acc, st)
          (k, v) : kvs -> case go st k of
            Left err -> Left err
            Right (kv, st') -> case go st' v of
              Left err -> Left err
              Right (vv, st'') -> loop ((k, kv, vv) : acc) st'' kvs

    open :: S.Props -> Numbering -> Numbering
    open props st = case props.anchor of
      Just a -> st {anchors = M.insert a Nothing st.anchors}
      Nothing -> st

    -- Give the value the number of its shape, and define its anchor. The
    -- own visits are those of the node alone, and the visits are those of the
    -- node and of everything inside it. The copy at an alias has no anchor,
    -- so its visits leave out the anchor. A node inside with the same anchor
    -- comes later in the document, so its definition stays.
    number
      :: S.Props -> Value -> Shape -> Int -> Int -> Numbering -> ((Value, Int), Numbering)
    number props v shape own visits st =
      ((v, i), st {anchors = anchors', shapes = shapes', visits = st.visits + own})
      where
        i :: Int
        shapes' :: M.Map Shape Int
        (i, shapes') = case M.lookup shape st.shapes of
          Just j -> (j, st.shapes)
          Nothing -> let j = M.size st.shapes in (j, M.insert shape j st.shapes)

        anchors' :: M.Map T.Text (Maybe (Value, Int, Int))
        anchors' = case props.anchor of
          Just a
            | Just Nothing <- M.lookup a st.anchors ->
                M.insert a (Just (v, i, visits - T.length a)) st.anchors
          _ -> st.anchors

    -- Unlike in 'duplicate', comparing all pairs is not faster for few keys.
    checkUniqueNumbers :: [(S.Node, (Value, Int), (Value, Int))] -> Either Failure ()
    checkUniqueNumbers = loop IM.empty
      where
        loop
          :: IM.IntMap (S.Node, Value)
          -> [(S.Node, (Value, Int), (Value, Int))]
          -> Either Failure ()
        loop seen = \case
          [] -> Right ()
          (kn, (k, i), _) : rest -> case IM.lookup i seen of
            Just first -> Left $ duplicateKey (kn, k) first
            Nothing -> loop (IM.insert i (kn, k) seen) rest

-- | The value of a node that passed 'prepareWithin', so it has no aliases and its
-- keys are unique already.
representPrepared :: S.Node -> Either Failure Value
representPrepared = go
  where
    go :: S.Node -> Either Failure Value
    go sn =
      let off = sn.offset; props = sn.props
      in case sn.content of
           S.ScalarContent style t -> scalar off props style t
           S.SequenceContent _ xs -> do
             tag <- collectionTag off props seqTag
             vs <- mapEither go xs
             Right $ withTag tag (Sequence vs)
           S.MappingContent _ kvs -> do
             tag <- collectionTag off props mapTag
             entries <- mapEither (\(k, v) -> (,) <$> go k <*> go v) kvs
             Right $ withTag tag (Mapping entries)
           S.AliasContent _ -> Left $ failure off "unexpected alias"

-- | 'mapM' for 'Either', with a stack that does not grow with the length of
-- the list.
mapEither :: forall a e b. (a -> Either e b) -> [a] -> Either e [b]
mapEither f = go []
  where
    go :: [b] -> [a] -> Either e [b]
    go acc = \case
      [] -> Right (reverse acc)
      x : xs -> case f x of
        Left err -> Left err
        Right y -> go (y : acc) xs

-- | The offset of the node that caused an error and the message, and the
-- notes that go after it, e.g. the first key of a duplicate key.
type Failure = NE.NonEmpty (S.Offset, String)

failure :: S.Offset -> String -> Failure
failure off msg = (off, msg) NE.:| []

-- | The checks of 'numberWithin' for a node without aliases and collection
-- keys.
-- Only the keys get values, for the comparison.
check :: S.Node -> Either Failure ()
check sn =
  let off = sn.offset; props = sn.props
  in case sn.content of
       S.ScalarContent style t
         -- Only a number can fail without a tag.
         | S.NoTag <- props.tag
         , style /= S.Plain || not (maybeNumber t) ->
             Right ()
         | otherwise -> void (scalar off props style t)
       S.SequenceContent _ xs -> collectionTag off props seqTag *> traverse_ check xs
       S.MappingContent _ kvs -> do
         _ <- collectionTag off props mapTag
         keys <- mapEither (\(k, v) -> key k <* check v) kvs
         checkUniqueKeys keys
       S.AliasContent _ -> Left $ failure off "unexpected alias"
  where
    maybeNumber :: T.Text -> Bool
    maybeNumber t = maybe False (startsNumber . fst) (T.uncons t)

    key :: S.Node -> Either Failure (S.Node, Value)
    key k = case k.content of
      S.ScalarContent style t -> (k,) <$> scalar k.offset k.props style t
      _ -> Left $ failure k.offset "unexpected collection key"

    -- The keys come with their nodes.
    checkUniqueKeys :: [(S.Node, Value)] -> Either Failure ()
    checkUniqueKeys keys = case duplicate of
      Just (k, first) -> Left $ duplicateKey k first
      Nothing -> Right ()
      where
        -- The first scalar key that is equal to an earlier one, and the
        -- earlier one. A document with a collection key gets numbers for its
        -- keys instead.
        duplicate :: Maybe ((S.Node, Value), (S.Node, Value))
        duplicate = case drop maxPairwise keys of
          _ : _ -> viaMap M.empty keys
          [] -> pairwise [] keys

        -- Comparing all pairs is faster for 16 keys or fewer, by a
        -- measurement.
        maxPairwise :: Int
        maxPairwise = 16

        viaMap
          :: M.Map Value S.Node
          -> [(S.Node, Value)]
          -> Maybe ((S.Node, Value), (S.Node, Value))
        viaMap seen = \case
          [] -> Nothing
          k@(n, v) : ks -> case M.lookup v seen of
            Just first -> Just (k, (first, v))
            Nothing -> viaMap (M.insert v n seen) ks

        pairwise
          :: [(S.Node, Value)]
          -> [(S.Node, Value)]
          -> Maybe ((S.Node, Value), (S.Node, Value))
        pairwise seen = \case
          [] -> Nothing
          k@(_, v) : ks -> case L.find ((== v) . snd) seen of
            Just first -> Just (k, first)
            Nothing -> pairwise (k : seen) ks

-- | Replace each alias with a copy of the node that it refers to. The copy
-- has the offsets and the comments of the alias, and no anchor. The nodes
-- inside the copy have the offsets of the alias too, so that an error inside
-- the copy names the place and the path where the document uses the value,
-- and not those of the anchor. They have no comments, because the comments
-- are at the anchor already. The node must pass 'numberWithin', so every
-- alias refers to an earlier anchor.
expandAliases :: S.Node -> S.Node
expandAliases = fst . go M.empty
  where
    go
      :: M.Map T.Text (S.Tag, S.Content)
      -> S.Node
      -> (S.Node, M.Map T.Text (S.Tag, S.Content))
    go anchors sn = case sn.content of
      S.AliasContent name -> case M.lookup name anchors of
        Just (tag, content) ->
          ( S.Node
              { S.offset = sn.offset
              , S.endOffset = sn.endOffset
              , S.props = S.Props Nothing tag
              , S.comments = sn.comments
              , S.content = copyAt sn content
              }
          , anchors
          )
        Nothing -> (sn, anchors)
      S.ScalarContent {} -> define sn anchors
      S.SequenceContent style xs ->
        let (xs', anchors') = goList (open anchors) xs
        in close (withContent (S.SequenceContent style xs')) anchors'
      S.MappingContent style kvs ->
        let (kvs', anchors') = goPairs (open anchors) kvs
        in close (withContent (S.MappingContent style kvs')) anchors'
      where
        withContent :: S.Content -> S.Node
        withContent c =
          S.Node
            { S.offset = sn.offset
            , S.endOffset = sn.endOffset
            , S.props = sn.props
            , S.comments = sn.comments
            , S.content = c
            }

        -- No alias inside refers to the anchor, so its old definition can
        -- go. A node inside with the same anchor comes later in the
        -- document, so its definition stays.
        open :: M.Map T.Text (S.Tag, S.Content) -> M.Map T.Text (S.Tag, S.Content)
        open = maybe id M.delete sn.props.anchor

        close
          :: S.Node
          -> M.Map T.Text (S.Tag, S.Content)
          -> (S.Node, M.Map T.Text (S.Tag, S.Content))
        close n anchors' = case sn.props.anchor of
          Just a | M.member a anchors' -> (n, anchors')
          _ -> define n anchors'

    define
      :: S.Node
      -> M.Map T.Text (S.Tag, S.Content)
      -> (S.Node, M.Map T.Text (S.Tag, S.Content))
    define sn anchors = case sn.props.anchor of
      Just a -> (sn, M.insert a (sn.props.tag, sn.content) anchors)
      Nothing -> (sn, anchors)

    -- The content at the alias.
    copyAt :: S.Node -> S.Content -> S.Content
    copyAt alias = \case
      S.SequenceContent style xs -> S.SequenceContent style (map node xs)
      S.MappingContent style kvs ->
        S.MappingContent style [(node k, node v) | (k, v) <- kvs]
      c -> c
      where
        node :: S.Node -> S.Node
        node n =
          S.Node
            { S.offset = alias.offset
            , S.endOffset = alias.endOffset
            , S.props = n.props
            , S.comments = S.noComments
            , S.content = copyAt alias n.content
            }

    goList
      :: M.Map T.Text (S.Tag, S.Content)
      -> [S.Node]
      -> ([S.Node], M.Map T.Text (S.Tag, S.Content))
    goList anchors = \case
      [] -> ([], anchors)
      x : xs ->
        let (x', anchors') = go anchors x
            (xs', anchors'') = goList anchors' xs
        in (x' : xs', anchors'')

    goPairs
      :: M.Map T.Text (S.Tag, S.Content)
      -> [(S.Node, S.Node)]
      -> ([(S.Node, S.Node)], M.Map T.Text (S.Tag, S.Content))
    goPairs anchors = \case
      [] -> ([], anchors)
      (k, v) : kvs ->
        let (k', anchors') = go anchors k
            (v', anchors'') = go anchors' v
            (kvs', anchors''') = goPairs anchors'' kvs
        in ((k', v') : kvs', anchors''')

-- | The value with the tag, in 'Tagged' if the tag is not the one of the core
-- schema for the value.
withTag :: T.Text -> Value -> Value
withTag tag v
  | tag == valueTag v = v
  | otherwise = Tagged tag v

scalar :: S.Offset -> S.Props -> S.ScalarStyle -> T.Text -> Either Failure Value
scalar off props style t = case props.tag of
  S.NoTag
    | style == S.Plain -> case resolvePlainExact t of
        Right v -> Right v
        -- The check comes before the decoder, which knows if the value is
        -- a string. A node that a program built has no input to quote.
        Left _
          | off == S.noOffset -> Left $ failure off exponentOutOfRange
          | otherwise ->
              Left . failure off $
                exponentOutOfRange
                  ++ ", quote the value if it is a string, e.g. '"
                  ++ T.unpack t
                  ++ "'"
    | otherwise -> Right (String t)
  S.NonSpecificTag -> Right (String t)
  S.Tag tag
    | tag == seqTag || tag == mapTag ->
        Left . failure off $
          "the tag !!"
            ++ T.unpack (T.drop (T.length coreTagPrefix) tag)
            ++ " cannot be used on a scalar"
    | otherwise -> case resolveTaggedExact tag t of
        Just (Right v) -> Right (withTag tag v)
        Just (Left _) -> Left $ failure off exponentOutOfRange
        Nothing ->
          Left . failure off $
            "invalid value for the tag !!"
              ++ T.unpack (T.drop (T.length coreTagPrefix) tag)
              ++ if tag == boolTag && isYaml11Bool t
                then ", " ++ showText t ++ " is a boolean only in YAML 1.1"
                else ""

collectionTag :: S.Offset -> S.Props -> T.Text -> Either Failure T.Text
collectionTag off props def = case props.tag of
  S.NoTag -> Right def
  S.NonSpecificTag -> Right def
  S.Tag tag
    | tag == def || not (isCoreTag tag) -> Right tag
    | otherwise ->
        Left . failure off $
          "the tag !!"
            ++ T.unpack (T.drop (T.length coreTagPrefix) tag)
            ++ " cannot be used on a "
            ++ (if def == seqTag then "sequence" else "mapping")
  where
    isCoreTag :: T.Text -> Bool
    isCoreTag tag =
      tag `elem` [nullTag, boolTag, intTag, floatTag, strTag, seqTag, mapTag]

-- | The error at a key, with a note at the first key that is equal to it.
duplicateKey :: (S.Node, Value) -> (S.Node, Value) -> Failure
duplicateKey (kn, k) (firstNode, _) =
  (kn.offset, message) NE.:| [(firstNode.offset, note)]
  where
    message :: String
    message = case (k, inputText kn, inputText firstNode) of
      (String "<<", _, _) -> "duplicate key \"<<\"" ++ noMergeKeys
      (_, Just t, Just f)
        | t /= f -> "duplicate key " ++ t ++ ", the same value as the first key"
      (_, Just t, _) -> "duplicate key " ++ t
      (_, Nothing, _) -> "duplicate key"

    note :: String
    note = "the first key" ++ maybe "" (' ' :) (inputText firstNode)

-- | The hint after the message of an error at a key @<<@. YAML 1.1 used it to
-- merge mappings, and some tools still do, but in YAML 1.2 it is a string.
noMergeKeys :: String
noMergeKeys = ", merge keys are not supported"

-- | The state of the composition of a document with aliases or collection
-- keys. Equal values get the same number, so that keys compare in constant
-- time, even if they are large collections or come from aliases that expand
-- to huge values.
data Numbering = Numbering
  { anchors :: !(M.Map T.Text (Maybe (Value, Int, Int)))
  -- ^ An anchor maps to its value, the number of the value and the visits of
  -- a traversal of the node. It maps to Nothing while its node is composed.
  , shapes :: !(M.Map Shape Int)
  , visits :: !Int
  -- ^ The visits of a traversal of the nodes so far.
  , added :: !Int
  -- ^ The visits that the aliases added so far, with those of the documents
  -- before in the stream.
  }

-- | A value with the numbers of its items or entries in place of them. The
-- entries of a mapping are sorted, so their order does not matter. A scalar
-- is small, so it stays a value.
data Shape
  = ScalarShape !Value
  | SequenceShape !T.Text ![Int]
  | MappingShape !T.Text ![(Int, Int)]
  deriving stock (Eq, Ord)