packages feed

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

{- | Code generation for message decoding functions.

Generates specialized 'messageDecoder' implementations that:
* Use a tight decode loop with accumulators for each field
* Dispatch on field number with a case expression
* Handle unknown fields by skipping efficiently
* Support both packed and unpacked repeated fields
* Support proto3 merge semantics for submessages

Field-order scheduling (inspired by hyperpb):
After decoding field N, the generated code predicts that the next
field on the wire will be field N+1 (declaration order). This is
almost always correct since every protobuf encoder emits fields in
order. On a correct prediction, we skip the full case dispatch and
go straight to the right decoder — a single comparison instead of
a multi-way branch.
-}
module Proto.CodeGen.Decode (
  genDecodeInstance,
) 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)


{- | Generate a MessageDecode instance for a message.

For messages with 2+ fields, generates per-field entry points that
predict the next field in declaration order (field-order scheduling).
For 0-1 field messages, uses the simpler flat dispatch.
-}
genDecodeInstance :: MessageDef -> Doc ann
genDecodeInstance msg =
  let fields = extractFields (msgElements msg)
      allAccs = fmap fieldAccum fields
  in case fields of
       [] -> genDecodeInstanceSimple msg fields allAccs
       [_] -> genDecodeInstanceSimple msg fields allAccs
       _ -> genDecodeInstanceScheduled msg fields allAccs


-- | Simple flat dispatch (for 0-1 fields).
genDecodeInstanceSimple :: MessageDef -> [FieldInfo] -> [Text] -> Doc ann
genDecodeInstanceSimple msg fields allAccs =
  vsep
    [ txt "instance MessageDecode" <+> pretty (hsTypeName (msgName msg)) <+> txt "where"
    , indent 2 $
        vsep
          [ txt "messageDecoder ="
              <+> txt "loop"
              <+> hsep (fmap (pretty . fieldDefault) fields)
          , indent 2 $ txt "where"
          , indent 4 $
              vsep
                [ txt "loop" <+> hsep (fmap (pretty . fieldAccum) fields) <+> txt "= do"
                , indent 2 $
                    vsep
                      [ txt "mTag <- getTagOrU"
                      , txt "case mTag of"
                      , indent 2 $
                          vsep
                            [ txt "UNothing -> pure" <+> genRecordCon msg fields
                            , txt "UJust (Tag fn wt) -> case fn of"
                            , indent 2 $ vsep (fmap (genFieldCaseSimple allAccs "loop") fields <> [genDefaultCase allAccs "loop"])
                            ]
                      ]
                ]
          ]
    ]


{- | Field-order scheduled dispatch (for 2+ fields).

Generates:
  loop_dispatch (the full case dispatch, used as fallback)
  loop_after_0, loop_after_1, ... (per-field entry points)

Each loop_after_i first checks if fn == field[i+1].fieldNum;
on match, decodes and calls loop_after_{i+1}; on mismatch,
falls through to loop_dispatch.
-}
genDecodeInstanceScheduled :: MessageDef -> [FieldInfo] -> [Text] -> Doc ann
genDecodeInstanceScheduled msg fields allAccs =
  let nFields = length fields
  in vsep
       [ txt "instance MessageDecode" <+> pretty (hsTypeName (msgName msg)) <+> txt "where"
       , indent 2 $
           vsep
             [ txt "messageDecoder ="
                 <+> txt "loop_dispatch"
                 <+> hsep (fmap (pretty . fieldDefault) fields)
             , indent 2 $ txt "where"
             , indent 4 $
                 vsep $
                   -- The full dispatch loop (fallback)
                   [genDispatchLoop msg fields allAccs nFields]
                     <>
                     -- Per-field entry points with next-field prediction
                     fmap (genAfterLoop msg fields allAccs nFields) [0 .. nFields - 1]
             ]
       ]


-- | Generate the full dispatch loop (fallback for mispredictions).
genDispatchLoop :: MessageDef -> [FieldInfo] -> [Text] -> Int -> Doc ann
genDispatchLoop msg fields allAccs nFields =
  vsep
    [ txt "loop_dispatch" <+> hsep (fmap (pretty . fieldAccum) fields) <+> txt "= do"
    , indent 2 $
        vsep
          [ txt "mTag <- getTagOrU"
          , txt "case mTag of"
          , indent 2 $
              vsep
                [ txt "UNothing -> pure" <+> genRecordCon msg fields
                , txt "UJust (Tag fn wt) -> case fn of"
                , indent 2 $
                    vsep
                      ( fmap (genFieldCaseScheduled allAccs nFields) fields
                          <> [genDefaultCase allAccs "loop_dispatch"]
                      )
                ]
          ]
    ]


-- | Generate a per-field "after" loop that predicts the next field.
genAfterLoop :: MessageDef -> [FieldInfo] -> [Text] -> Int -> Int -> Doc ann
genAfterLoop msg fields allAccs nFields currentIdx =
  let nextIdx = (currentIdx + 1) `mod` nFields
      nextField = fields !! nextIdx
      nextFn = T.pack (show (fiFieldNum nextField))
      afterName = afterLoopName nextIdx nFields
  in vsep
       [ txt (afterLoopNameForCurrent currentIdx) <+> hsep (fmap (pretty . fieldAccum) fields) <+> txt "= do"
       , indent 2 $
           vsep
             [ txt "mTag <- getTagOrU"
             , txt "case mTag of"
             , indent 2 $
                 vsep
                   [ txt "UNothing -> pure" <+> genRecordCon msg fields
                   , txt "UJust (Tag fn wt)"
                   , indent 2 $
                       vsep
                         [ txt "| fn ==" <+> pretty nextFn <+> txt "-> do"
                         , indent 4 $ genFieldDecodeScheduled allAccs nextField afterName nFields
                         , txt "| otherwise -> case fn of"
                         , indent 4 $
                             vsep
                               ( fmap (genFieldCaseScheduled allAccs nFields) fields
                                   <> [genDefaultCase allAccs "loop_dispatch"]
                               )
                         ]
                   ]
             ]
       ]


afterLoopNameForCurrent :: Int -> Text
afterLoopNameForCurrent idx = "loop_after_" <> T.pack (show idx)


afterLoopName :: Int -> Int -> Text
afterLoopName idx _nFields = "loop_after_" <> T.pack (show idx)


genFieldCaseSimple :: [Text] -> Text -> FieldInfo -> Doc ann
genFieldCaseSimple allAccs loopName fi =
  let fn = T.pack (show (fiFieldNum fi))
  in pretty fn
       <+> txt "-> do"
       <> line
       <> indent 2 (genFieldDecodeSimple allAccs fi loopName)


genFieldDecodeSimple :: [Text] -> FieldInfo -> Text -> Doc ann
genFieldDecodeSimple allAccs fi loopName =
  let idx = fiIndex fi
      newAccs = case fiLabel fi of
        Just Repeated -> replaceAt idx ("(" <> fieldAccum fi <> " <> V.singleton v)") allAccs
        _ -> replaceAt idx "v" allAccs
  in vsep
       [ txt "v <- " <> pretty (decoderExpr (fiType fi))
       , pretty loopName <+> hsep (fmap pretty newAccs)
       ]


genFieldCaseScheduled :: [Text] -> Int -> FieldInfo -> Doc ann
genFieldCaseScheduled allAccs nFields fi =
  let fn = T.pack (show (fiFieldNum fi))
      after = afterLoopName (fiIndex fi) nFields
  in pretty fn
       <+> txt "-> do"
       <> line
       <> indent 2 (genFieldDecodeScheduled allAccs fi after nFields)


genFieldDecodeScheduled :: [Text] -> FieldInfo -> Text -> Int -> Doc ann
genFieldDecodeScheduled allAccs fi loopName _nFields =
  let idx = fiIndex fi
      newAccs = case fiLabel fi of
        Just Repeated -> replaceAt idx ("(" <> fieldAccum fi <> " <> V.singleton v)") allAccs
        _ -> replaceAt idx "v" allAccs
  in vsep
       [ txt "v <- " <> pretty (decoderExpr (fiType fi))
       , pretty loopName <+> hsep (fmap pretty newAccs)
       ]


genDefaultCase :: [Text] -> Text -> Doc ann
genDefaultCase allAccs loopName =
  txt "_ -> skipField wt >> " <> pretty loopName <+> hsep (fmap pretty allAccs)


genRecordCon :: MessageDef -> [FieldInfo] -> Doc ann
genRecordCon msg fields = case fields of
  [] -> pretty (hsTypeName (msgName msg)) <+> txt "{ }"
  _ ->
    parens $
      pretty (hsTypeName (msgName msg))
        <+> braces (hsep (punctuate comma (fmap genAssign fields)))
  where
    genAssign fi =
      pretty (hsFieldName (fiName fi)) <+> txt "=" <+> pretty (fieldAccum fi)


fieldAccum :: FieldInfo -> Text
fieldAccum fi = "acc_" <> T.pack (show (fiIndex fi))


fieldDefault :: FieldInfo -> Text
fieldDefault fi = case fiLabel fi of
  Just Repeated -> case fiType fi of
    FTScalar s | isUnboxable s -> "VU.empty"
    _ -> "V.empty"
  Just Optional -> "Nothing"
  _ -> case fiType fi of
    FTScalar SBool -> "False"
    FTScalar SString -> "\"\""
    FTScalar SBytes -> "\"\""
    FTScalar _ -> "0"
    FTNamed _ -> "Nothing"


isUnboxable :: ScalarType -> Bool
isUnboxable = \case
  SString -> False
  SBytes -> False
  _ -> True


decoderExpr :: FieldType -> Text
decoderExpr = \case
  FTScalar SDouble -> "decodeFieldDouble"
  FTScalar SFloat -> "decodeFieldFloat"
  FTScalar SInt32 -> "fromIntegral <$> decodeFieldVarint"
  FTScalar SInt64 -> "fromIntegral <$> decodeFieldVarint"
  FTScalar SUInt32 -> "fromIntegral <$> decodeFieldVarint"
  FTScalar SUInt64 -> "decodeFieldVarint"
  FTScalar SSInt32 -> "decodeFieldSVarint32"
  FTScalar SSInt64 -> "decodeFieldSVarint64"
  FTScalar SFixed32 -> "decodeFieldFixed32"
  FTScalar SFixed64 -> "decodeFieldFixed64"
  FTScalar SSFixed32 -> "fromIntegral <$> decodeFieldFixed32"
  FTScalar SSFixed64 -> "fromIntegral <$> decodeFieldFixed64"
  FTScalar SBool -> "decodeFieldBool"
  FTScalar SString -> "decodeFieldString"
  FTScalar SBytes -> "decodeFieldBytes"
  FTNamed _ -> "decodeFieldMessage"


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


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


replaceAt :: Int -> a -> [a] -> [a]
replaceAt _ _ [] = []
replaceAt 0 x (_ : ys) = x : ys
replaceAt n x (y : ys) = y : replaceAt (n - 1) x ys