packages feed

hhlo-0.2.0.0: src/HHLO/IR/Pretty.hs

{-# LANGUAGE OverloadedStrings #-}

module HHLO.IR.Pretty
    ( Pretty(..)
    , render
    , renderLazy
    ) where

import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Text.Lazy.Builder (Builder, fromText, toLazyText)
import HHLO.IR.AST
import HHLO.Core.Types (DType(..), dtypeToText)

class Pretty a where
    pretty :: a -> Builder

render :: Pretty a => a -> Text
render = TL.toStrict . renderLazy

renderLazy :: Pretty a => a -> TL.Text
renderLazy = toLazyText . pretty

instance Pretty Module where
    pretty (Module funcs) =
        "module {\n"
        <> mconcat (map ((<> "\n") . indentFunc . pretty) funcs)
        <> "}"
      where
        indentFunc b =
            let ls = TL.splitOn (TL.pack "\n") (toLazyText b)
                indented = map (\l -> if TL.null l then l else TL.pack "  " <> l) ls
            in fromText (TL.toStrict (TL.intercalate (TL.pack "\n") indented))

instance Pretty Function where
    pretty (Function name args results returnVids ops) =
        fromText "func.func @" <> fromText name <> "("
        <> mconcat (intersperse (fromText ", ") (map pretty args))
        <> ") -> " <> prettyResults results <> " {\n"
        <> mconcat (map ((<> "\n") . ("    " <>)) (map pretty ops))
        <> returnLine returnVids results
        <> "}"

prettyResults :: [TensorType] -> Builder
prettyResults []  = "()"
prettyResults [r] = pretty r
prettyResults rs  = "(" <> mconcat (intersperse (fromText ", ") (map pretty rs)) <> ")"

prettyTypesList :: [TensorType] -> Builder
prettyTypesList []  = ""
prettyTypesList rs  = mconcat (intersperse (fromText ", ") (map pretty rs))

returnLine :: [ValueId] -> [TensorType] -> Builder
returnLine []     results = "    return : " <> prettyTypesList results <> "\n"
returnLine vids   results =
    let refs = mconcat (intersperse (fromText ", ") (map valueRefBuilder vids))
    in "    return " <> refs <> " : " <> prettyTypesList results <> "\n"

instance Pretty FuncArg where
    pretty (FuncArg name t) =
        fromText "%" <> fromText name <> ": " <> pretty t

instance Pretty Operation where
    pretty (Operation "stablehlo.reduce" operands operandTypes attrs regions results resultTypes)
        | null regions =
            -- Special format for reduce with 'applies' shorthand (no region):
            --   %n = stablehlo.reduce(%input init: %init) applies stablehlo.add
            --        across dimensions = [0] : (input_type, init_type) -> result_type
            prettyResultVids results <> " = stablehlo.reduce("
            <> valueRefBuilder (operands !! 0) <> " init: " <> valueRefBuilder (operands !! 1) <> ")"
            <> prettyReduceAttrs attrs
            <> " : " <> prettyResultType operandTypes resultTypes
        | otherwise =
            -- Generic form when a region is present:
            --   %n = "stablehlo.reduce"(%input, %init) ({
            --     ^bb0(%argN: type, %argM: type):
            --       %p = stablehlo.add %argN, %argM : (type, type) -> type
            --       "stablehlo.return"(%p) : (type) -> ()
            --   }) {dimensions = array<i64: ...>} : (types) -> type
            prettyResultVids results <> " = \"stablehlo.reduce\"("
            <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
            <> " ("
            <> mconcat (map prettyRegion regions)
            <> ") "
            <> prettyAttrs (filter (not . isAppliesAttr) attrs)
            <> " : " <> prettyResultType operandTypes resultTypes
      where
        isAppliesAttr (AttrString "applies" _) = True
        isAppliesAttr _ = False
    pretty (Operation "stablehlo.convolution" operands operandTypes attrs regions results resultTypes) =
        -- Custom format for convolution:
        --   %r = stablehlo.convolution(%lhs, %rhs)
        --        dim_numbers = ..., window = {...}
        --        {batch_group_count = 1 : i64, ...}
        --        : (lhs_type, rhs_type) -> result_type
        prettyResultVids results <> " = stablehlo.convolution("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> prettyConvAttrs attrs
        <> " : " <> prettyResultType operandTypes resultTypes
        <> mconcat (map prettyRegion regions)
    pretty (Operation "stablehlo.dot_general" operands operandTypes attrs regions results resultTypes) =
        -- Custom format for dot_general:
        --   %r = stablehlo.dot_general %lhs, %rhs,
        --        batching_dims = [0] x [0],
        --        contracting_dims = [2] x [1]
        --        : (lhs_type, rhs_type) -> result_type
        prettyResultVids results <> " = stablehlo.dot_general "
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ","
        <> prettyDotGeneralAttrs attrs
        <> " : " <> prettyResultType operandTypes resultTypes
        <> mconcat (map prettyRegion regions)
    pretty (Operation "stablehlo.batch_norm_inference" operands operandTypes attrs regions results resultTypes) =
        -- Custom format: %r = stablehlo.batch_norm_inference %x, %scale, %offset, %mean, %variance
        --   <{epsilon = 1.0E-5 : f32, feature_index = 1 : i64}> : type
        prettyResultVids results <> " = stablehlo.batch_norm_inference "
        <> mconcat (intersperse (", ") (map valueRefBuilder operands))
        <> prettyBNAttrs attrs
        <> " : " <> prettyResultType operandTypes resultTypes
        <> mconcat (map prettyRegion regions)
    pretty (Operation "stablehlo.gather" operands operandTypes attrs regions results resultTypes) =
        -- Generic form (no custom assembly in this parser version).
        prettyResultVids results <> " = \"stablehlo.gather\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.compare" operands operandTypes attrs regions results resultTypes) =
        -- Generic form for maximum parser compatibility.
        prettyResultVids results <> " = \"stablehlo.compare\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.slice" operands operandTypes attrs regions results resultTypes) =
        -- Generic form to maximise parser compatibility.
        prettyResultVids results <> " = \"stablehlo.slice\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.pad" operands operandTypes attrs regions results resultTypes) =
        -- Generic form to maximise parser compatibility.
        prettyResultVids results <> " = \"stablehlo.pad\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.dynamic_slice" operands operandTypes attrs regions results resultTypes) =
        -- Generic form to maximise parser compatibility.
        prettyResultVids results <> " = \"stablehlo.dynamic_slice\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.transpose" operands operandTypes attrs regions results resultTypes) =
        -- Generic form with array<i64: ...> for permutation (PJRT v1.16.0 compat).
        let attrs' = map fixPermAttr attrs
        in prettyResultVids results <> " = \"stablehlo.transpose\"("
           <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
           <> (if null attrs' then mempty else " " <> prettyAttrs attrs')
           <> " : " <> prettyResultType operandTypes resultTypes
      where
        fixPermAttr (AttrIntList "permutation" vals) =
            AttrRaw $ "permutation = array<i64: " <> T.intercalate ", " (map (T.pack . show) vals) <> ">"
        fixPermAttr a = a
    pretty (Operation "stablehlo.concatenate" operands operandTypes attrs regions results resultTypes) =
        -- Generic form (custom form syntax varies across parser versions).
        prettyResultVids results <> " = \"stablehlo.concatenate\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.iota" _ _ attrs _ results resultTypes) =
        -- Generic form (no operands).
        prettyResultVids results <> " = \"stablehlo.iota\"()"
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : () -> " <> prettyResults resultTypes
    pretty (Operation "stablehlo.sort" operands operandTypes attrs regions results resultTypes) =
        -- Generic form (has regions; fallback would already use generic, but explicit is clearer).
        prettyResultVids results <> " = \"stablehlo.sort\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.return" operands operandTypes _ regions _ _) =
        -- Generic form for the region terminator.
        "\"stablehlo.return\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands))
        <> ")"
        <> (if null regions then mempty else mconcat (map prettyRegion regions))
        <> " : "
        <> (if null operandTypes
            then "()"
            else "(" <> mconcat (intersperse (", ") (map pretty operandTypes)) <> ")")
        <> " -> ()"
    pretty (Operation "stablehlo.rng" operands operandTypes attrs regions results resultTypes) =
        -- Generic form for parser compatibility (no custom hook in some PJRT builds).
        prettyResultVids results <> " = \"stablehlo.rng\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation "stablehlo.rng_bit_generator" operands operandTypes attrs regions results resultTypes) =
        -- Generic form for parser compatibility.
        prettyResultVids results <> " = \"stablehlo.rng_bit_generator\"("
        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
        <> (if null attrs then mempty else " " <> prettyAttrs attrs)
        <> " : " <> prettyResultType operandTypes resultTypes
    pretty (Operation name operands operandTypes attrs regions results resultTypes) =
        if null regions
        then
            -- Existing custom-ish form for ops without regions.
            prettyResultVids results <> " = " <> fromText name
            <> (if null operands then mempty else " " <> mconcat (intersperse (fromText ", ") (map valueRefBuilder operands)))
            <> prettyAttrsForOp name attrs
            <> " : " <> prettyResultType operandTypes resultTypes
        else
            -- Generic assembly form for ops with regions (maximises parser compatibility).
            prettyResultVids results <> " = \"" <> fromText name <> "\""
            <> "(" <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"
            <> " (" <> mconcat (intersperse (", ") (map prettyRegion regions)) <> ")"
            <> (if null attrs then mempty else " " <> prettyAttrs attrs)
            <> " : " <> prettyResultType operandTypes resultTypes

-- | Pretty-print operation attributes.
-- For 'stablehlo.constant' with a single 'AttrDenseElements' we print the
-- dense value directly (no braces).
-- For 'stablehlo.broadcast_in_dim' with a single 'AttrIntList' we use the
-- custom trailing format @, dims = [...]@.
-- For all other cases we use the standard MLIR attribute-dictionary syntax.
prettyAttrsForOp :: Text -> [Attribute] -> Builder
prettyAttrsForOp _ [] = mempty
prettyAttrsForOp _ [AttrDenseElements shp dt vals] =
    " dense<" <> denseElements shp dt vals <> ">"
prettyAttrsForOp "stablehlo.broadcast_in_dim" [AttrIntList _name vals] =
    ", dims = [" <> fromText (T.intercalate ", " (map (T.pack . show) vals)) <> "]"
prettyAttrsForOp _ attrs = " " <> prettyAttrs attrs

-- | Pretty-print attributes for 'stablehlo.reduce'.
-- If an 'applies' attribute is present we use the shorthand form;
-- otherwise we only emit the dimensions (the region carries the op).
prettyReduceAttrs :: [Attribute] -> Builder
prettyReduceAttrs attrs =
    let dims = lookupAttrIntList "dimensions" attrs
        op   = lookupAttrString "applies" attrs
        dimsText = " across dimensions = [" <> fromText (T.intercalate ", " (map (T.pack . show) dims)) <> "]"
    in if T.null op
       then dimsText
       else " applies " <> fromText op <> dimsText

lookupAttrIntList :: Text -> [Attribute] -> [Int64]
lookupAttrIntList name = foldr f []
  where
    f (AttrIntList n xs) acc | n == name = xs ++ acc
    f _ acc = acc

lookupAttrString :: Text -> [Attribute] -> Text
lookupAttrString name = foldr f ""
  where
    f (AttrString n s) acc | n == name = s <> acc
    f _ acc = acc

-- | Pretty-print attributes for 'stablehlo.convolution'.
-- Extracts 'dim_numbers' and 'window' from the custom string attributes,
-- then renders the remaining attrs in the standard dictionary.
prettyConvAttrs :: [Attribute] -> Builder
prettyConvAttrs attrs =
    let dimNums   = lookupAttrString "dim_numbers" attrs
        window    = lookupAttrString "window" attrs
        rest      = filter (not . isCustomConvAttr) attrs
        custom    = (if T.null dimNums then mempty else " dim_numbers = " <> fromText dimNums <> ",")
                 <> (if T.null window  then mempty else " window = " <> fromText window)
        dict      = if null rest then mempty else " " <> prettyAttrs rest
    in custom <> dict
  where
    isCustomConvAttr (AttrString "dim_numbers" _) = True
    isCustomConvAttr (AttrString "window" _)      = True
    isCustomConvAttr _                            = False

-- | Pretty-print attributes for 'stablehlo.dot_general'.
-- Extracts 'batching_dims' and 'contracting_dims' from the custom string attributes.
prettyDotGeneralAttrs :: [Attribute] -> Builder
prettyDotGeneralAttrs attrs =
    let batch       = lookupAttrString "batching_dims" attrs
        contract    = lookupAttrString "contracting_dims" attrs
        rest        = filter (not . isCustomDotAttr) attrs
        custom      = (if T.null batch    then mempty else "\n    batching_dims = " <> fromText batch <> ",")
                   <> (if T.null contract then mempty else "\n    contracting_dims = " <> fromText contract)
        dict        = if null rest then mempty else "\n    " <> prettyAttrs rest
    in custom <> dict
  where
    isCustomDotAttr (AttrString "batching_dims" _)    = True
    isCustomDotAttr (AttrString "contracting_dims" _) = True
    isCustomDotAttr _                                 = False

-- | Pretty-print attributes for 'stablehlo.batch_norm_inference'.
-- Uses the generic op format with <{...}> around the attributes.
prettyBNAttrs :: [Attribute] -> Builder
prettyBNAttrs attrs = " <{" <> mconcat (intersperse (", ") (map prettyAttr attrs)) <> ">}"

-- | Build the nested list syntax for a 'dense<...>' attribute.
-- Scalar: @0.0@, 1-D: @[1, 2]@, 2-D: @[[1, 2], [3, 4]]@, etc.
denseElements :: [Integer] -> DType -> [Double] -> Builder
denseElements []     dt [v] = fromText (T.pack (formatDenseVal dt v))
denseElements []     _  _   = error "denseElements: scalar mismatch"
denseElements [n]    dt xs  =
    "[" <> fromText (T.intercalate ", " (map (T.pack . formatDenseVal dt) (take (fromIntegral n) xs))) <> "]"
denseElements (n:ns) dt xs  =
    let chunkSize = product ns
        chunks    = chunksOf (fromIntegral chunkSize) (take (fromIntegral (n * chunkSize)) xs)
    in "[" <> mconcat (intersperse (", ") (map (denseElements ns dt) chunks)) <> "]"

formatDenseVal :: DType -> Double -> String
formatDenseVal I64 v = show (round v :: Integer)
formatDenseVal I32 v = show (round v :: Integer)
formatDenseVal I16 v = show (round v :: Integer)
formatDenseVal I8  v = show (round v :: Integer)
formatDenseVal UI64 v = show (round v :: Integer)
formatDenseVal UI32 v = show (round v :: Integer)
formatDenseVal UI16 v = show (round v :: Integer)
formatDenseVal UI8  v = show (round v :: Integer)
formatDenseVal Bool v = if v /= 0.0 then "true" else "false"
formatDenseVal _    v = show v

chunksOf :: Int -> [a] -> [[a]]
chunksOf _ [] = []
chunksOf k xs = let (h, t) = splitAt k xs in h : chunksOf k t

-- | Pretty-print one or more result value IDs.
-- In standard MLIR, results are comma-separated without parens:
--   %0 = op(...)          -- one result
--   %0, %1 = op(...)      -- two results
prettyResultVids :: [ValueId] -> Builder
prettyResultVids []     = ""
prettyResultVids vs     =
    mconcat (intersperse (fromText ", ") (map valueRefBuilder vs))

-- | When an operation has no operands we print just the result type.
-- When it has operands we print (operandTypes) -> resultType.
-- For multi-result ops: (operandTypes) -> (resultType1, resultType2, ...)
prettyResultType :: [TensorType] -> [TensorType] -> Builder
prettyResultType [] [rt] = pretty rt
prettyResultType [] rts  = prettyResults rts
prettyResultType ots [rt] =
    "(" <> mconcat (intersperse (fromText ", ") (map pretty ots)) <> ") -> " <> pretty rt
prettyResultType ots rts =
    "(" <> mconcat (intersperse (fromText ", ") (map pretty ots)) <> ") -> " <> prettyResults rts

instance Pretty TensorType where
    pretty (TensorType [] dtype) =
        "tensor<" <> fromText (dtypeToText dtype) <> ">"
    pretty (TensorType shape dtype) =
        "tensor<" <> fromText dims <> "x" <> fromText (dtypeToText dtype) <> ">"
      where
        dims = T.intercalate "x" (map (T.pack . show) shape)

valueRefBuilder :: ValueId -> Builder
valueRefBuilder v = fromText (valueRef v)

prettyAttrs :: [Attribute] -> Builder
prettyAttrs attrs = "{" <> mconcat (intersperse (", ") (map prettyAttr attrs)) <> "}"

prettyAttr :: Attribute -> Builder
prettyAttr (AttrInt name val) =
    fromText name <> " = " <> fromText (T.pack (show val)) <> " : i64"
prettyAttr (AttrFloat name val) =
    fromText name <> " = " <> fromText (T.pack (show val)) <> " : f32"
prettyAttr (AttrBool name True) =
    fromText name <> " = true"
prettyAttr (AttrBool name False) =
    fromText name <> " = false"
prettyAttr (AttrString name s) =
    fromText name <> " = \"" <> fromText s <> "\""
prettyAttr (AttrIntList name vals) =
    fromText name <> " = [" <> fromText (T.intercalate ", " (map (T.pack . show) vals)) <> "]"
prettyAttr (AttrDenseElements shape dtype vals) =
    "value = dense<" <> denseElements shape dtype vals <> "> : " <> pretty (TensorType shape dtype)
prettyAttr (AttrDict pairs) =
    mconcat (intersperse (", ") (map prettyDictPair pairs))
  where
    prettyDictPair (k, v) = fromText k <> " = " <> prettyAttr v
prettyAttr (AttrRaw t) =
    fromText t

prettyRegion :: Region -> Builder
prettyRegion (Region blocks) =
    "{\n"
    <> mconcat (zipWith prettyBlock [0..] blocks)
    <> "  }"

prettyBlock :: Int -> Block -> Builder
prettyBlock idx (Block args ops) =
    "  ^bb" <> fromText (T.pack (show idx))
    <> (if null args then mempty else "(" <> mconcat (intersperse (", ") (map pretty args)) <> ")")
    <> ":\n"
    <> mconcat (map (\op -> "    " <> pretty op <> "\n") ops)

intersperse :: Builder -> [Builder] -> [Builder]
intersperse _   []     = []
intersperse _   [x]    = [x]
intersperse sep (x:xs) = x : map (sep <>) xs