packages feed

polytropos-cbor-0.1.0.2: src/Polytropos/CBOR.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}

{-|
Module      : Polytropos.CBOR
Description : High-performance binary CBOR serialization for Polytropos records and variants
Copyright   : (c) 2026 The Polytropos Authors
License     : EUPL-1.2 OR Apache-2.0
Maintainer  : nadia.yvette.chambers@ik.me
Stability   : experimental
Portability : non-portable (GHC extensions)

High-performance binary CBOR serialization of heterogeneous and homogeneous
@polytropos@ open records and variants via @cborg@ \/ @serialise@, without intermediate
tree boxing.

= Architectural Overview & Rationale

== Binary Wire Format

* A record encodes as a standard CBOR array: one length header (the row's arity)
  followed by the flat payload field encodings in row order.
* A variant encodes as a CBOR array consisting of the constructor's 'Word' tag followed
  by that constructor's payload field encodings.

== Positional & Allocation-Free

Because field names exist only at the type level in row polymorphism, the binary
encoder is purely /positional/: no string keys are written to the wire. Encoding
and decoding are executed as single unrolled passes directly over the flat
@Rep@ payload array with no intermediate allocations.

== Truly Heterogeneous Row Support

Unlike satellites restricted to homogeneous types, @polytropos-cbor@ provides genuine
heterogeneous serialization across open rows via 'AllSerialise': every field requires
'Serialise' at /its own concrete type/, so rows mixing 'String', 'Int', 'Word', custom
types, or nested rows serialize and deserialize directly.

= When to Use This Module

* When network bandwidth, binary storage size, and serialization speed are critical.
* For inter-service RPC, binary logs, and compact caching.

= Worked Example

> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE TypeApplications #-}
> {-# OPTIONS_GHC -fplugin=Polytropos.Plugin #-}
>
> import Codec.Serialise (serialise, deserialise)
> import Polytropos
> import Polytropos.CBOR
>
> -- A genuinely heterogeneous record:
> type Packet = Record '["id" ':= Int, "payload" ':= String, "flags" ':= Word]
>
> pkt :: Packet
> pkt = REC { id = 101, payload = "sensor-ok", flags = 0 }
>
> -- Serializes to compact positional CBOR binary:
> bytes = serialise pkt
>
> -- Deserializes back to Packet:
> pkt' :: Packet
> pkt' = deserialise bytes
-}
module Polytropos.CBOR
  ( -- * Heterogeneous Row Codecs
    AllSerialise (..)
  , FieldCount (..)
  , AllCtorsSerialise (..)

    -- * Homogeneity Evidence and Introspection Helpers
    -- $homogeneity
  , AllFieldsAre
  , AllCtorsHom (..)
  , RowField
  , SumField
  ) where

import Data.Functor.Identity (Identity (..))
import Data.Kind             (Constraint, Type)
import Data.Proxy            (Proxy (..))
import Data.Void             (Void)
import GHC.Exts              (Any, toList)
import GHC.TypeLits          (KnownSymbol, symbolVal)
import Codec.CBOR.Decoding   (Decoder, decodeListLen, decodeWord)
import Codec.CBOR.Encoding   (Encoding, encodeListLen, encodeWord)
import Codec.Serialise       (Serialise (..))
import Polytropos            (Record, Variant)
import Polytropos.Core       (Pair (..), Row, SumRow, RowRep (..), noInlineUnsafeCo,
                              productRep, injectRep, repTag, repVals)

{-------------------------------------------------------------------------------
  Heterogeneous row constraint walks
-------------------------------------------------------------------------------}

-- | Computes the arity (field count) of a row at compile time.
class FieldCount (r :: Row) where
  fieldCount :: Proxy r -> Int

instance FieldCount '[] where
  fieldCount _ = 0

instance FieldCount r => FieldCount ((n ':= a) ': r) where
  fieldCount _ = 1 + fieldCount (Proxy @r)

-- | Evidence that every field of the row @r@ has a 'Serialise' instance
-- at that field's own type. Serializes the erased payload slots in row order.
class AllSerialise (r :: Row) where
  encRecordCBOR :: Proxy r -> [Identity Any] -> Encoding
  decRecordCBOR :: forall s. Proxy r -> Decoder s [Identity Any]

instance AllSerialise '[] where
  encRecordCBOR _ _ = mempty
  decRecordCBOR _ = pure []

instance (Serialise a, AllSerialise r) => AllSerialise ((n ':= a) ': r) where
  encRecordCBOR _ (Identity x : xs) =
    encode (noInlineUnsafeCo x :: a) <> encRecordCBOR (Proxy @r) xs
  encRecordCBOR _ [] = error "polytropos-cbor: payload arity mismatch"

  decRecordCBOR _ = do
    x <- decode
    xs <- decRecordCBOR (Proxy @r)
    pure (Identity (noInlineUnsafeCo (x :: a) :: Any) : xs)

-- | Evidence that every constructor of the sum row @s@ has a heterogeneous
-- field row satisfying 'AllSerialise', paired with its constructor tag index.
class AllCtorsSerialise (s :: SumRow) where
  encVariantCBOR :: Proxy s -> Word -> Word -> [Identity Any] -> Encoding
  decVariantCBOR :: forall t. Proxy s -> Word -> Int -> Decoder t [Identity Any]

instance AllCtorsSerialise '[] where
  encVariantCBOR _ _ _ _ = error "polytropos-cbor: empty variant has no constructors"
  decVariantCBOR _ _ _ = fail "polytropos-cbor: unknown constructor tag"

instance (FieldCount fs, AllSerialise fs, AllCtorsSerialise s)
      => AllCtorsSerialise ((c ':= fs) ': s) where
  encVariantCBOR _ 0 origTag slots =
    encodeListLen (fromIntegral (1 + fieldCount (Proxy @fs)))
      <> encodeWord origTag
      <> encRecordCBOR (Proxy @fs) slots
  encVariantCBOR _ tag origTag slots =
    encVariantCBOR (Proxy @s) (tag - 1) origTag slots

  decVariantCBOR _ 0 len
    | len /= 1 + fieldCount (Proxy @fs) = fail "polytropos-cbor: variant field count mismatch"
    | otherwise = decRecordCBOR (Proxy @fs)
  decVariantCBOR _ tag len =
    decVariantCBOR (Proxy @s) (tag - 1) len

{-------------------------------------------------------------------------------
  Record codecs
-------------------------------------------------------------------------------}

instance (FieldCount r, AllSerialise r) => Serialise (Record r) where
  encode (RowRep rep) =
    encodeListLen (fromIntegral (fieldCount (Proxy @r)))
      <> encRecordCBOR (Proxy @r) (toList (repVals rep))

  decode = do
    n <- decodeListLen
    if n /= fieldCount (Proxy @r)
      then fail "polytropos-cbor: record arity mismatch"
      else do
        slots <- decRecordCBOR (Proxy @r)
        pure (RowRep (productRep slots))

{-------------------------------------------------------------------------------
  Variant codecs
-------------------------------------------------------------------------------}

instance AllCtorsSerialise s => Serialise (Variant s) where
  encode (RowRep rep) =
    let tag = repTag rep
    in encVariantCBOR (Proxy @s) tag tag (toList (repVals rep))

  decode = do
    n <- decodeListLen
    if n < 1
      then fail "polytropos-cbor: variant encoding missing constructor tag"
      else do
        tag <- decodeWord
        slots <- decVariantCBOR (Proxy @s) tag n
        pure (RowRep (injectRep tag slots))

{-------------------------------------------------------------------------------
  Homogeneity evidence and row introspection (Reflection helpers)
-------------------------------------------------------------------------------}

-- $homogeneity
--
-- === Homogeneity Evidence and Introspection Helpers
--
-- In addition to the primary heterogeneous codecs ('AllSerialise', 'AllCtorsSerialise'),
-- this module provides closed type families and reflection classes for
-- inspecting row metadata and enforcing uniform payload types.

-- | Evidence that every field of the row @r@ has type @a@.
type family AllFieldsAre (a :: Type) (r :: Row) :: Constraint where
  AllFieldsAre _  '[]                = ()
  AllFieldsAre a ((n ':= a) ': rest) = AllFieldsAre a rest

-- | The field type of a homogeneous row: the type of every field, 'Void'
-- for the empty row.
type family RowField (r :: Row) :: Type where
  RowField '[]                 = Void
  RowField ((n ':= a) ': rest) = a

-- | The field type shared by every constructor of a sum row, 'Void' for the
-- empty sum.
type family SumField (s :: SumRow) :: Type where
  SumField '[]                = Void
  SumField ((c ':= fs) ': s)  = RowField fs

-- | Every constructor of the sum row @s@ has a homogeneous field row of type
-- @a@, reified as constructor names paired with their arities.
class AllCtorsHom (a :: Type) (s :: SumRow) where
  ctorArities :: Proxy a -> Proxy s -> [(String, Int)]

instance AllCtorsHom a '[] where
  ctorArities _ _ = []

instance (KnownSymbol c, AllFieldsAre a fs, AllCtorsHom a s)
      => AllCtorsHom a ((c ':= fs) ': s) where
  ctorArities pa _ =
    (symbolVal (Proxy @c), length (arityList (Proxy @fs)))
      : ctorArities pa (Proxy @s)
    where
      arityList :: Proxy fs -> [a]
      arityList _ = []