packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Frontend/Analysis/Typedefs.hs

-- | Analyse typedefs
--
-- Intended for qualified import.
--
-- > import HsBindgen.Frontend.Analysis.Typedefs (TypedefAnalysis)
-- > import HsBindgen.Frontend.Analysis.Typedefs qualified as TypedefAnalysis
module HsBindgen.Frontend.Analysis.Typedefs (
    TypedefAnalysis(..)
  , Conclusion(..)
  , Squash(..)
  , fromDecls
  ) where

import Data.Map.Strict qualified as Map

import Clang.HighLevel.Types

import HsBindgen.Errors
import HsBindgen.Frontend.Analysis
import HsBindgen.Frontend.Analysis.DeclUseGraph (DeclUseGraph)
import HsBindgen.Frontend.Analysis.DeclUseGraph qualified as DeclUseGraph
import HsBindgen.Frontend.Pass.ResolveBindingSpecs.IsPass (ResolveBindingSpecs)
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C

{-------------------------------------------------------------------------------
  Definition
-------------------------------------------------------------------------------}

-- | Typedef analysis
--
-- See 'Conclusion' for detailed discussion.
data TypedefAnalysis = TypedefAnalysis{
      map :: Map C.DeclId Conclusion
    }
  deriving stock (Show)

instance Semigroup TypedefAnalysis where
  a <> b = TypedefAnalysis{
        map = Map.unionWithKey unexpectedOverlap a.map b.map
      }
    where
      unexpectedOverlap :: C.DeclId -> Conclusion -> Conclusion -> Conclusion
      unexpectedOverlap declId conclusion1 conclusion2 =
          panicPure $ concat [
              "Unexpected overlap for "
            , show declId
            , ": "
            , show (conclusion1, conclusion2)
            ]

instance Monoid TypedefAnalysis where
  mempty = TypedefAnalysis{
        map = Map.empty
      }

data Squash = SquashTypedef {
      typedefLoc :: SingleLoc C.DeclPath
    , targetId   :: C.DeclId
    }
  deriving stock (Eq, Show, Generic)

-- | What should we do with a particular declaration?
--
-- NOTE: The /absence/ of a conclusion in the t'TypedefAnalysis' indicates that
-- we should leave the declaration as-is.
data Conclusion =
    -- | Squash typedef
    --
    -- Squashing means removing the declaration of the typedef from the list of
    -- declarations altogether, so that we do not generate a Haskell datatype
    -- for it. We squash typedefs if
    --
    -- 1. We have a typedef around a declaration of the same name
    --
    --    > typedef struct foo { .. } foo;
    --
    --    This helps avoid name clashes in Haskell.
    --
    -- 2. The typedef is the /only/ reference to the underlying decl:
    --
    --    > typedef struct foo { .. } foo_t;
    --
    --    In this case @foo_t@ is just a syntactic convenience for @struct foo@,
    --    rather than indicating some kind of semantic difference. Since the
    --    @struct@ prefix is /anyway/ not present in Haskell, that syntactic
    --    convenience is irrelevant there and we squash this typedef also.
    --
    -- The name mangler must be instructed to use the name of the typedef for
    -- the struct; this after all is how C code will most likely refer to this
    -- struct. /If/ the only two cases for squashing are the ones listed above,
    -- this would automatically take care of use sites: assigning the name of
    -- the typedef to the struct (in Haskell) would make no difference if they
    -- have the same name, and would only affect the typedef itself if the names
    -- are different, because in that case that is the /only/ use of the struct.
    -- However, if in the future we make squashing configurable, and for example
    -- make it possible to squash @foo_t@ in
    --
    -- > typedef struct foo { .. } foo_t;
    -- > void f(struct foo x);
    --
    -- then use sites would also need to be handled explicitly; see also
    -- <https://github.com/well-typed/hs-bindgen/issues/1356>.
    Squash Squash

    -- | Instruct the name mangler to add a suffix to the Haskell type
    --   corresponding to this C type
    --
    -- C distinguishes between the \"tagged\" namespace and the \"ordinary\"
    -- namespace, so that there is no name clash in
    --
    -- > typedef struct foo { .. } * foo;
    --
    -- In Haskell these live in the /same/ namespace, and so we cannot assign
    -- the same name to both of these types. We also cannot /squash/ the typedef
    -- in this case, because @struct foo@ and (the typedef) @foo@ are
    -- /different/ types. We must therefore instruct the name mangler to use
    -- a different name for one of these two types; we add a suffix.
  | AddSuffix Text

    -- | Instruct the name mangler to use the name of another C declaration
    --
    -- Useful in conjunction with 'HsBindgen.Frontend.Analysis.Typedefs.Squash'. That is, the C typedef is squashed,
    -- and the squash target is assigned the name of the typedef with
    -- 'UseNameOf'.
  | UseNameOf C.DeclId
  deriving stock (Show)

conclude :: C.DeclId -> Conclusion -> TypedefAnalysis
conclude declId conclusion = TypedefAnalysis $ Map.singleton declId conclusion

{-------------------------------------------------------------------------------
  Analysis proper
-------------------------------------------------------------------------------}

fromDecls :: DeclUseGraph -> [C.Decl l ResolveBindingSpecs] -> TypedefAnalysis
fromDecls declUseGraph = mconcat . map aux
  where
     aux :: C.Decl l ResolveBindingSpecs -> TypedefAnalysis
     aux decl =
         case decl.kind of
           C.DeclTypedef typedef ->
             analyseTypedef declUseGraph decl.info typedef
           _otherwise ->
             mempty

analyseTypedef ::
      DeclUseGraph
  -> C.DeclInfo ResolveBindingSpecs
  -> C.Typedef ResolveBindingSpecs
  -> TypedefAnalysis
analyseTypedef declUseGraph typedefInfo typedef =
    case directPayload of
      -- The typedef is a direct alias of a tagged type: a candidate for
      -- squashing.
      Just payload
        | shouldSquash payload
        -> mconcat [
               conclude typedefInfo.id $ Squash $ SquashTypedef {
                   typedefLoc = typedefInfo.loc
                 , targetId   = payload.id
                 }
             , conclude payload.id $ UseNameOf typedefInfo.id
             ]

      -- Otherwise, suffix every tagged type that is referenced /within/ the
      -- typedef's own type and that would mangle to the same Haskell name as the
      -- typedef itself.
      --
      -- This is a purely /local/ decision: it depends only on this typedef's
      -- name and its own type structure, never on which other declarations
      -- happen to be in scope. We can only resolve a name clash here when the
      -- clashing tagged type is syntactically referenced by the typedef; clashes
      -- with unrelated declarations of the same name are detected (and reported)
      -- by the collision check in "HsBindgen.Frontend.Pass.MangleNames".
      _otherwise ->
        foldMap suffixClashingPayload clashingPayloads
  where
    typedefName :: Text
    typedefName = typedefInfo.id.name.text

    -- All tagged types referenced anywhere within the typedef's type.
    payloads :: [TaggedPayload]
    payloads = taggedPayloads typedef.typ

    -- The tagged type the typedef is a direct alias of (the head of the type,
    -- reached without any indirection), if any.
    directPayload :: Maybe TaggedPayload
    directPayload = case [ p | p <- payloads, p.isDirect ] of
        p : _      -> Just p
        _otherwise -> Nothing

    -- Tagged types referenced /through indirection/ within the typedef's type
    -- whose name clashes with the typedef itself.
    --
    -- A single typedef can reference the same tagged type more than once (e.g. a
    -- function-pointer typedef with several arguments of that type), so we
    -- deduplicate by 'C.DeclId': each clashing tag must yield exactly one
    -- 'AddSuffix' conclusion, otherwise the 'TypedefAnalysis' '<>' would panic on
    -- the duplicate key.
    clashingPayloads :: [TaggedPayload]
    clashingPayloads = Map.elems $ Map.fromList [
          (p.id, p)
        | p <- payloads
        , not p.isDirect
        , p.id.name.text == typedefName
        ]

    shouldSquash :: TaggedPayload -> Bool
    shouldSquash payload = or [
          payload.id.name.text == typedefName
        , length useSites == 1
        ]
      where
        useSites :: [(C.DeclId, Dependency)]
        useSites = DeclUseGraph.getUseSitesNoSelfReferences declUseGraph payload.id

    suffixClashingPayload :: TaggedPayload -> TypedefAnalysis
    suffixClashingPayload payload =
        conclude payload.id $ AddSuffix $ tagSuffix payload.id.name.kind

-- | The (deterministic) suffix used to disambiguate a tagged type from a
-- same-named typedef.
--
-- We suffix the /tagged/ type (and not the typedef) because C code refers to a
-- typedef by its bare name, but to a tagged type via its @struct@\/@union@\/
-- @enum@ keyword; the suffix mirrors that keyword.
tagSuffix :: C.NameKind -> Text
tagSuffix = \case
    C.NameKindTagged C.TagKindStruct -> "_struct"
    C.NameKindTagged C.TagKindUnion  -> "_union"
    C.NameKindTagged C.TagKindEnum   -> "_enum"
    -- Only tagged types are ever suffixed (see 'taggedPayloads'), so this is
    -- unreachable; we panic rather than invent a plausible-but-wrong suffix.
    kind -> panicPure $ "tagSuffix: unexpected non-tagged name kind " ++ show kind

{-------------------------------------------------------------------------------
  Internal auxiliary: typedefs around "tagged" decls (structs, unions, enums)

  We only need to worry about typedefs around tagged declarations, because only
  then there is a chance of name clashes (in Haskell we do not distinguish
  between the tagged and ordinary namespaces).
-------------------------------------------------------------------------------}

data TaggedPayload = TaggedPayload{
      isDirect :: Bool
    , id       :: C.DeclId
    }

-- | All tagged declarations (struct, union, enum) referenced within a type.
--
-- The 'isDirect' flag is 'True' only for a tagged type that the typedef is a
-- direct alias of, i.e. one reached without going through any indirection
-- (pointer, array, function, block). A qualifier (such as @const@) is
-- transparent: it changes neither the Haskell representation nor the mangled
-- name, so a tagged type underneath it stays direct.
--
-- Any layer of indirection means the tagged type and the typedef are
-- /different/ types that nonetheless mangle to the same Haskell name, so the
-- tagged type must be given a suffix. This covers
--
-- > typedef struct {..} * foo;
-- > typedef struct {..} foo[10];
-- > typedef struct foo (*foo)(struct bar arg);
-- > typedef const struct foo * foo;
--
-- See <https://github.com/well-typed/hs-bindgen/issues/1445>. Note that we
-- recurse into /function/ types (both the result and the argument types), so a
-- tagged type referenced there is detected as well.
taggedPayloads :: C.Type ResolveBindingSpecs -> [TaggedPayload]
taggedPayloads = go True
  where
    go :: Bool -> C.Type ResolveBindingSpecs -> [TaggedPayload]
    go direct = \case
        C.TypeRef declId         -> typeRef direct declId
        C.TypeEnum ref           -> typeRef direct ref.name

        -- A qualifier (e.g. @const@) is transparent: it changes neither the
        -- Haskell representation nor the mangled name, so the tagged type
        -- underneath is reached with the /same/ directness.
        C.TypeQual _qual ty      -> go direct ty

        -- Indirection: the tagged type underneath is a /different/ type that
        -- merely mangles to the same name, so it is no longer direct.
        C.TypePointers _n ty     -> go False ty
        C.TypeConstArray _n ty   -> go False ty
        C.TypeIncompleteArray ty -> go False ty
        C.TypeBlock ty           -> go False ty
        C.TypeFun args res       ->
          concatMap (go False . (.typ)) args ++ go False res

        -- No tagged type to find.
        C.TypePrim{}             -> []
        C.TypeComplex{}          -> []
        C.TypeVoid               -> []

        -- A macro type and another typedef each have their own name; the tagged
        -- type they may wrap is not /syntactically/ referenced by this typedef,
        -- so resolving such a clash is out of scope for this local analysis.
        C.TypeMacro{}            -> []
        C.TypeTypedef{}          -> []
        C.TypeExtBinding{}       -> []

    typeRef :: Bool -> C.DeclId -> [TaggedPayload]
    typeRef isDirect declId =
        case C.checkIsTagged declId.name.kind of
          Nothing -> []
          Just _  -> [TaggedPayload{isDirect = isDirect, id = declId}]