yamlet-1.0.0.0: src/Yamlet/Value.hs
-- | The values of YAML documents: the content with resolved tags, without the
-- styles, comments and positions of the syntax tree.
--
-- A 'Value' has t'Yamlet.Decode.FromYaml' and t'Yamlet.Encode.ToYaml'
-- instances, e.g. to read a document whose structure a program does not
-- know:
--
-- >>> decodeText @Value "!point {x: 1, y: 2.5}\n"
-- Right (Tagged "!point" (Mapping [(String "x",Int 1),(String "y",Float (Finite 2.5))]))
--
-- An alias becomes a copy of the value that it refers to:
--
-- >>> decodeText @Value "base: &b [1, 2]\ncopy: *b\n"
-- Right (Mapping [(String "base",Sequence [Int 1,Int 2]),(String "copy",Sequence [Int 1,Int 2])])
--
-- A small input with many aliases can give a large value. To prevent this,
-- the decoder limits the aliases. Each node and each character of a scalar,
-- a tag or an anchor counts as one unit. The aliases can add 100000 units to
-- a document. For a document with more units, they can add as many units as
-- the document has.
-- The documents of a stream share the limit, as if they were one document.
-- A document beyond the limit is an error.
module Yamlet.Value
( -- * Values
Value (..)
, FloatValue (..)
, floatValueToRealFloat
, realFloatToFloatValue
, describe
-- * Tags
, valueTag
, nullTag
, boolTag
, intTag
, floatTag
, strTag
, seqTag
, mapTag
) where
import Control.DeepSeq
import Data.Scientific qualified as Sci
import Data.Text qualified as T
import GHC.Generics
import Yamlet.Internal.Utils
-- | The value of a node.
--
-- 'Eq' and 'Ord' compare the entries of mappings in order, so two mappings
-- with the same entries in a different order are not equal, unlike in YAML.
data Value
= Null
| Bool !Bool
| Int !Integer
| Float !FloatValue
| String !T.Text
| Sequence ![Value]
| -- | The entries of a mapping in the order of the input. The keys are
-- unique. The encoder does not check this for a mapping that a program
-- builds, and a mapping with two equal keys does not read back. Keys are
-- equal as in YAML, e.g. two mappings with the same entries in a
-- different order are equal keys.
Mapping ![(Value, Value)]
| -- | A value with a tag that is not the tag of the core schema for it,
-- e.g. @!point {x: 1}@. A scalar with a tag that the schema does not
-- know is a v'String' inside, e.g. @!secret abc@.
--
-- The encoder writes the tag. Some values read back with a change:
--
-- * A value with its own tag of the core schema reads back without
-- 'Tagged', e.g. @Tagged intTag (Int 1)@ as @Int 1@.
--
-- * A value that does not fit a tag of the core schema does not read
-- back, e.g. a v'String' with 'intTag'.
--
-- * A scalar other than a v'String' with a tag that the schema does not
-- know reads back as a v'String', e.g. @Tagged "!x" (Int 5)@ as
-- @Tagged "!x" (String "5")@.
--
-- * YAML has no syntax for the empty tag or a tag of one character, e.g.
-- @x@ or @!@. The encoder drops such a tag, e.g. @Tagged "" (Int 1)@
-- reads back as @Int 1@.
--
-- * A node has one tag, so the encoder writes only the outermost tag that
-- it does not drop, e.g. @Tagged "!a" (Tagged "!b" (Sequence []))@
-- reads back as @Tagged "!a" (Sequence [])@.
Tagged !T.Text !Value
deriving stock (Eq, Ord, Show, Generic)
-- The instances of the sum types are written by hand, because GHC does not
-- always remove the generic representation of a sum type. A strict field of
-- a type without lazy parts, e.g. a text, is already in normal form.
instance NFData Value where
rnf = \case
Null -> ()
Bool _ -> ()
Int _ -> ()
Float _ -> ()
String _ -> ()
Sequence xs -> rnf xs
Mapping kvs -> rnf kvs
Tagged _ v -> rnf v
-- | The value of a floating-point number. A finite value is exact, e.g. @0.1@
-- is exactly one tenth.
--
-- Arithmetic on a t'Data.Scientific.Scientific' with a huge exponent, e.g.
-- @1e1000000000@, can use all memory. Convert a value from an untrusted input
-- with 'floatValueToRealFloat' or with the bounded conversions of
-- "Data.Scientific".
data FloatValue
= -- | A finite value other than negative zero.
--
-- The encoder writes a value whose exponent in scientific notation is
-- beyond the range from -1000 to 1000, e.g. @1.0e+1001@, but the decoder
-- rejects it. The decoder never gives such a value, and a 'Double' is
-- always in the range.
Finite !Sci.Scientific
| -- | Negative zero, e.g. @-0.0@, which a t'Data.Scientific.Scientific'
-- cannot hold.
NegativeZero
| Infinity
| NegativeInfinity
| NaN
deriving stock (Eq, Ord, Show, Generic)
instance NFData FloatValue where
rnf = rwhnf
-- | The nearest value of a floating-point type, e.g. 'Double', infinite if
-- the value is out of its range. The decimal converts to the type directly,
-- so it is rounded once, e.g. a t'Float' does not go by way of a 'Double'.
--
-- >>> map (floatValueToRealFloat @Double) [Finite 0.1, Finite 1e400, NegativeZero]
-- [0.1,Infinity,-0.0]
floatValueToRealFloat :: RealFloat a => FloatValue -> a
floatValueToRealFloat = \case
Finite s -> Sci.toRealFloat s
NegativeZero -> -0
Infinity -> 1 / 0
NegativeInfinity -> -(1 / 0)
NaN -> 0 / 0
-- With INLINEABLE, GHC specializes the function at the type of a caller in
-- another module, also the conversion of "Data.Scientific" inside it, as a
-- probe with a newtype of Double showed. The specializations are for the
-- types of the instances of the library. Without them, the decode benchmarks
-- of the config and the JSON input allocate more.
{-# INLINEABLE floatValueToRealFloat #-}
{-# SPECIALIZE floatValueToRealFloat :: FloatValue -> Double #-}
{-# SPECIALIZE floatValueToRealFloat :: FloatValue -> Float #-}
-- | The value of a floating-point number, e.g. a 'Double'. A finite number
-- becomes the shortest decimal that reads back as the same number, e.g.
-- @0.1@.
--
-- >>> map (realFloatToFloatValue @Double) [0.1, -0, 1 / 0]
-- [Finite 0.1,NegativeZero,Infinity]
realFloatToFloatValue :: RealFloat a => a -> FloatValue
realFloatToFloatValue d
| isNaN d = NaN
| isInfinite d = if d > 0 then Infinity else NegativeInfinity
| isNegativeZero d = NegativeZero
| otherwise = Finite (Sci.fromFloatDigits d)
-- As for 'floatValueToRealFloat'. Without the specializations, the encode
-- benchmarks of the config and the JSON input are slower and allocate more.
{-# INLINEABLE realFloatToFloatValue #-}
{-# SPECIALIZE realFloatToFloatValue :: Double -> FloatValue #-}
{-# SPECIALIZE realFloatToFloatValue :: Float -> FloatValue #-}
-- | The kind of a value in plain words, for error messages, e.g. "a list".
-- The tag of 'Tagged' does not change it.
--
-- >>> describe (Tagged "!point" (Mapping []))
-- "a mapping"
describe :: Value -> String
describe = \case
Null -> "null"
Bool _ -> "a boolean"
Int _ -> "an integer"
Float _ -> "a floating-point number"
String _ -> "a string"
Sequence _ -> "a list"
Mapping _ -> "a mapping"
Tagged _ v -> describe v
-- | The tag of a value: the tag of 'Tagged', or else the tag of the core
-- schema, e.g. 'intTag' for an v'Int'.
--
-- >>> map valueTag [Int 1, Tagged "!point" (Mapping [])]
-- ["tag:yaml.org,2002:int","!point"]
valueTag :: Value -> T.Text
valueTag = \case
Null -> nullTag
Bool _ -> boolTag
Int _ -> intTag
Float _ -> floatTag
String _ -> strTag
Sequence _ -> seqTag
Mapping _ -> mapTag
Tagged tag _ -> tag
-- | @tag:yaml.org,2002:null@.
nullTag :: T.Text
nullTag = coreTagPrefix <> "null"
-- | @tag:yaml.org,2002:bool@.
boolTag :: T.Text
boolTag = coreTagPrefix <> "bool"
-- | @tag:yaml.org,2002:int@.
intTag :: T.Text
intTag = coreTagPrefix <> "int"
-- | @tag:yaml.org,2002:float@.
floatTag :: T.Text
floatTag = coreTagPrefix <> "float"
-- | @tag:yaml.org,2002:str@.
strTag :: T.Text
strTag = coreTagPrefix <> "str"
-- | @tag:yaml.org,2002:seq@.
seqTag :: T.Text
seqTag = coreTagPrefix <> "seq"
-- | @tag:yaml.org,2002:map@.
mapTag :: T.Text
mapTag = coreTagPrefix <> "map"
-- $setup
-- >>> import Yamlet