packages feed

wireform-proto-0.2.0.0: src/Proto/CodeGen/Encode.hs

{- | Code generation for message encoding functions.

Generates specialized 'buildMessage' implementations using the archetype
encode functions from 'Proto.Internal.Encode.Archetype', with tag bytes pre-computed
at code generation time as compile-time constants.

* First pass: compute exact wire size via 'messageSize'
* Second pass: serialize via 'buildMessage' using pre-known sizes
* Skip default-valued fields (proto3 semantics)
* Packed encoding for repeated scalar fields
* Minimize allocations: no intermediate ByteString for submessages
-}
module Proto.CodeGen.Encode (
  genEncodeInstance,
  genSizeInstance,
) where

import Data.Text (Text)
import Data.Text qualified as T
import Prettyprinter
import Proto.CodeGen.Types (hsFieldName, hsTypeName)
import Proto.IDL.AST
import Proto.Internal.CodeGen.Combinators (txt)


wireVarint, wire64Bit, wireLengthDelimited, wire32Bit :: Int
wireVarint = 0
wire64Bit = 1
wireLengthDelimited = 2
wire32Bit = 5


computeTagByte :: Int -> Int -> Int
computeTagByte fieldNum wireType = fieldNum * 8 + wireType


-- | Generate a MessageEncode instance for a message.
genEncodeInstance :: MessageDef -> Doc ann
genEncodeInstance msg =
  let fields = extractFields (msgElements msg)
  in vsep
       [ txt "instance MessageEncode" <+> pretty (hsTypeName (msgName msg)) <+> txt "where"
       , indent 2 $
           vsep
             [ txt "buildMessage msg ="
             , indent 2 $ case fields of
                 [] -> txt "mempty"
                 _ -> vsep (fmap (uncurry genFieldBuild) (zip [0 ..] fields))
             ]
       ]
  where
    genFieldBuild :: Int -> FieldInfo -> Doc ann
    genFieldBuild idx fi =
      let op = if idx == 0 then mempty else txt "<> "
          accessor = "msg." <> hsFieldName (fiName fi)
          fieldNum = fiFieldNum fi
      in op <> genBuildExpr fieldNum accessor (fiLabel fi) (fiType fi)


genBuildExpr :: Int -> Text -> Maybe FieldLabel -> FieldType -> Doc ann
genBuildExpr fieldNum accessor lbl ft = case lbl of
  Just Repeated -> genRepeatedBuild fieldNum accessor ft
  Just Optional -> txt "(maybe mempty (\\v -> " <> genSingleBuild fieldNum "v" ft <> txt ") " <> pretty accessor <> txt ")"
  _ -> genProto3Build fieldNum accessor ft


genProto3Build :: Int -> Text -> FieldType -> Doc ann
genProto3Build fieldNum accessor ft =
  let cond = defaultCheck accessor ft
  in txt "(if " <> cond <> txt " then mempty else " <> genSingleBuild fieldNum accessor ft <> txt ")"


genSingleBuild :: Int -> Text -> FieldType -> Doc ann
genSingleBuild fieldNum accessor = \case
  FTScalar SDouble -> txt "archDouble " <> pretty (computeTagByte fieldNum wire64Bit) <+> pretty accessor
  FTScalar SFloat -> txt "archFloat " <> pretty (computeTagByte fieldNum wire32Bit) <+> pretty accessor
  FTScalar SInt32 -> txt "archVarint " <> pretty (computeTagByte fieldNum wireVarint) <+> txt "(fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SInt64 -> txt "archVarint " <> pretty (computeTagByte fieldNum wireVarint) <+> txt "(fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SUInt32 -> txt "archVarint " <> pretty (computeTagByte fieldNum wireVarint) <+> txt "(fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SUInt64 -> txt "archVarint " <> pretty (computeTagByte fieldNum wireVarint) <+> pretty accessor
  FTScalar SSInt32 -> txt "archSVarint32 " <> pretty (computeTagByte fieldNum wireVarint) <+> pretty accessor
  FTScalar SSInt64 -> txt "archSVarint64 " <> pretty (computeTagByte fieldNum wireVarint) <+> pretty accessor
  FTScalar SFixed32 -> txt "archFixed32 " <> pretty (computeTagByte fieldNum wire32Bit) <+> pretty accessor
  FTScalar SFixed64 -> txt "archFixed64 " <> pretty (computeTagByte fieldNum wire64Bit) <+> pretty accessor
  FTScalar SSFixed32 -> txt "archFixed32 " <> pretty (computeTagByte fieldNum wire32Bit) <+> txt "(fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SSFixed64 -> txt "archFixed64 " <> pretty (computeTagByte fieldNum wire64Bit) <+> txt "(fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SBool -> txt "archBool " <> pretty (computeTagByte fieldNum wireVarint) <+> pretty accessor
  FTScalar SString -> txt "archString " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> pretty accessor
  FTScalar SBytes -> txt "archBytes " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> pretty accessor
  FTNamed _ -> txt "(let sz = messageSize " <> pretty accessor <> txt " in archSubmessage " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> txt "sz (buildMessage " <> pretty accessor <> txt "))"


genRepeatedBuild :: Int -> Text -> FieldType -> Doc ann
genRepeatedBuild fieldNum accessor = \case
  FTScalar SString ->
    txt "V.foldl' (\\acc v -> acc <> archString " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> txt "v) mempty " <> pretty accessor
  FTScalar SBytes ->
    txt "V.foldl' (\\acc v -> acc <> archBytes " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> txt "v) mempty " <> pretty accessor
  FTScalar s ->
    txt "encode" <> pretty (packedFnName s) <+> pretty (T.pack (show fieldNum)) <+> pretty accessor
  FTNamed _ ->
    txt "V.foldl' (\\acc v -> let sz = messageSize v in acc <> archSubmessage " <> pretty (computeTagByte fieldNum wireLengthDelimited) <+> txt "sz (buildMessage v)) mempty " <> pretty accessor


defaultCheck :: Text -> FieldType -> Doc ann
defaultCheck accessor = \case
  FTScalar SBool -> pretty accessor <+> txt "== False"
  FTScalar SString -> pretty accessor <+> pretty ("== \"\"" :: Text)
  FTScalar SBytes -> txt "BS.null " <> pretty accessor
  FTScalar _ -> pretty accessor <+> txt "== 0"
  FTNamed _ -> pretty accessor <+> txt "== Nothing"


-- | Generate a MessageSize instance.
genSizeInstance :: MessageDef -> Doc ann
genSizeInstance msg =
  let fields = extractFields (msgElements msg)
  in vsep
       [ txt "instance MessageSize" <+> pretty (hsTypeName (msgName msg)) <+> txt "where"
       , indent 2 $
           vsep
             [ txt "messageSize msg ="
             , indent 2 $ case fields of
                 [] -> txt "0"
                 _ -> vsep (fmap (uncurry genFieldSize) (zip [0 ..] fields))
             ]
       ]
  where
    genFieldSize :: Int -> FieldInfo -> Doc ann
    genFieldSize idx fi =
      let op = if idx == 0 then mempty else txt "+ "
          accessor = "msg." <> hsFieldName (fiName fi)
          fieldNum = fiFieldNum fi
      in op <> genSizeExpr fieldNum accessor (fiLabel fi) (fiType fi)


genSizeExpr :: Int -> Text -> Maybe FieldLabel -> FieldType -> Doc ann
genSizeExpr fieldNum accessor lbl ft = case lbl of
  Just Repeated -> txt "(sizeRepeated " <> pretty (T.pack (show fieldNum)) <+> pretty accessor <> txt ")"
  Just Optional -> txt "(maybe 0 (\\v -> " <> genSingleSize "v" ft <> txt ") " <> pretty accessor <> txt ")"
  _ -> txt "(if " <> defaultCheck accessor ft <> txt " then 0 else " <> genSingleSize accessor ft <> txt ")"


genSingleSize :: Text -> FieldType -> Doc ann
genSingleSize accessor = \case
  FTScalar SDouble -> txt "archFixed64Size"
  FTScalar SFloat -> txt "archFixed32Size"
  FTScalar SFixed32 -> txt "archFixed32Size"
  FTScalar SFixed64 -> txt "archFixed64Size"
  FTScalar SSFixed32 -> txt "archFixed32Size"
  FTScalar SSFixed64 -> txt "archFixed64Size"
  FTScalar SBool -> txt "archBoolSize"
  FTScalar SInt32 -> txt "archVarintSize (fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SInt64 -> txt "archVarintSize (fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SUInt32 -> txt "archVarintSize (fromIntegral " <> pretty accessor <> txt ")"
  FTScalar SUInt64 -> txt "archVarintSize " <> pretty accessor
  FTScalar SSInt32 -> txt "(1 + varintSize (fromIntegral (zigZag32 " <> pretty accessor <> txt ")))"
  FTScalar SSInt64 -> txt "(1 + varintSize (zigZag64 " <> pretty accessor <> txt "))"
  FTScalar SString -> txt "archStringSize " <> pretty accessor
  FTScalar SBytes -> txt "archBytesSize " <> pretty accessor
  FTNamed _ -> txt "archSubmessageSize (messageSize " <> pretty accessor <> txt ")"


packedFnName :: ScalarType -> Text
packedFnName = \case
  SDouble -> "PackedDouble"
  SFloat -> "PackedFloat"
  SInt32 -> "PackedVarint"
  SInt64 -> "PackedVarint"
  SUInt32 -> "PackedVarint"
  SUInt64 -> "PackedVarint"
  SSInt32 -> "PackedSVarint32"
  SSInt64 -> "PackedSVarint64"
  SFixed32 -> "PackedFixed32"
  SFixed64 -> "PackedFixed64"
  SSFixed32 -> "PackedFixed32"
  SSFixed64 -> "PackedFixed64"
  SBool -> "PackedVarint"
  s -> error ("Cannot pack: " <> show s)


data FieldInfo = FieldInfo
  { fiName :: Text
  , fiFieldNum :: Int
  , fiLabel :: Maybe FieldLabel
  , fiType :: FieldType
  }


extractFields :: [MessageElement] -> [FieldInfo]
extractFields = concatMap go
  where
    go = \case
      MEField fd ->
        [ FieldInfo
            { fiName = fieldName fd
            , fiFieldNum = unFieldNumber (fieldNumber fd)
            , fiLabel = fieldLabel fd
            , fiType = fieldType fd
            }
        ]
      _ -> []