packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Frontend/Pass/MangleNames/DetectClashes.hs

module HsBindgen.Frontend.Pass.MangleNames.DetectClashes (
    detectClashes
  ) where

import Data.Either (partitionEithers)
import Data.Foldable qualified as Foldable
import Data.Map qualified as Map
import Data.Maybe (maybeToList)

import Clang.HighLevel.Types

import HsBindgen.Config.Prelims (FieldNamingStrategy (..))
import HsBindgen.Frontend.Pass.MangleNames.CreateNames
import HsBindgen.Frontend.Pass.MangleNames.Error
import HsBindgen.Frontend.Pass.MangleNames.IsPass
import HsBindgen.Frontend.Pass.MangleNames.Names
import HsBindgen.Frontend.Pass.TypecheckMacros.IsPass
import HsBindgen.Imports ()
import HsBindgen.IR.C qualified as C
import HsBindgen.Language.Haskell qualified as Hs

{-------------------------------------------------------------------------------
  Traversal 2: Detect clashes

  Build a single 'NameRegistry' from the 'NameMap' (top-level names and scoped
  names) and the 'CreateNames' AST (local names in annotations), then report all
  collisions in one sweep. Over-removal is explicitly accepted: a single
  declaration may participate in several collisions, and every participating
  owner is dropped.

  Ideally, we'd not purge all conflicts, but instead let selection resolve
  conflict groups. While this is possible, it is also a difficult problem: If we
  deselect a declaration in a confict group, all reverse transitive dependencies
  of the deselected declaration also become unusable. This _may affect other
  conflict groups_, and so is a recursive problem with a fix point.

-------------------------------------------------------------------------------}

detectClashes ::
     forall l.
     FieldNamingStrategy
  -> NameMap
  -> [C.Decl l CreateNames]
  -> (NameRegistry, [MangleNamesFailure])
detectClashes strategy nameMap decls =
    (registry, collisionFailures (collisions registry) ++ deriveFailures)
  where
    registry :: NameRegistry
    deriveFailures :: [MangleNamesFailure]
    (registry, deriveFailures) = Foldable.foldl' aux (emptyNameRegistry, []) decls
      where
        aux (nameRegistry, failures) decl = fmap (++failures) (registerDecl nameRegistry decl)

    registerDecl :: NameRegistry -> C.Decl l CreateNames -> (NameRegistry, [MangleNamesFailure])
    registerDecl reg decl = (,failures) $
        Foldable.foldl'
          (\r (role, scope, loc', sname) -> registerSomeName role scope owner loc' sname r)
          reg0
          successes
      where
        owner :: C.DeclId
        owner = decl.info.id

        loc :: SingleLoc C.DeclPath
        loc = decl.info.loc

        -- The declaration's own top-level name. Squashed declarations do not
        -- appear in 'decls', so they are never registered here.
        reg0 :: NameRegistry
        reg0 = case lookupSomeName owner nameMap of
          Just sname -> registerSomeName Declaration GlobalScope owner loc sname reg
          Nothing    -> reg

        (failures, successes) = partitionEithers $ derivedNames strategy nameMap owner loc decl.kind

-- | Looking up created names for scoped names can fail
type DerivedNamesResult = Either MangleNamesFailure (NameRole, Scope, SingleLoc C.DeclPath, Hs.SomeName)

-- | All derived (non-top-level) names of a declaration, with the role, scope,
-- and location under which they must be unique.
--
-- Most derived names are blamed on (and located at) the enclosing declaration.
-- Record field selectors are the exception: they carry the field's /own/
-- location, so that a within-record duplicate-field collision points at the
-- offending fields.
derivedNames :: forall l.
     FieldNamingStrategy
  -> NameMap
  -> C.DeclId
  -> SingleLoc C.DeclPath
  -> C.DeclKind l CreateNames
  -> [DerivedNamesResult]
derivedNames strategy nameMap owner loc = \case
    C.DeclStruct struct ->
         success (Constructor, GlobalScope, loc, Hs.demoteNs struct.ann.constr)
       : [ success (AuxType, GlobalScope, loc, Hs.demoteNs flamNames.aux)
         | C.Flam _ flamNames <- [struct.flam] ]
      ++ concatMap fieldNames struct.fields
      ++ foldMap regularFieldNames (C.flamStructField struct.flam)
    C.DeclUnion union ->
         newtypeNames union.ann
      ++ concatMap fieldNames union.fields
    C.DeclEnum enum ->
         newtypeNames enum.ann
      ++ concatMap enumConstantName enum.constants
    -- An untagged enum constant is a top-level declaration in its own right;
    -- its name is registered as a 'Declaration'. We must /not/ also register
    -- the inner enum-constant scoped name, or it would collide with itself.
    C.DeclUntaggedEnumConstant{}      -> []
    C.DeclTypedef typedef             -> typedefNames typedef.ann
    C.DeclMacro (MacroType macroType) -> newtypeNames macroType.ann
    C.DeclMacro MacroValue{}          -> []
    C.DeclFunction{}                  -> []
    C.DeclGlobal{}                    -> []
    C.DeclOpaque{}                    -> []
  where
    success :: (NameRole, Scope, SingleLoc C.DeclPath, Hs.SomeName) -> DerivedNamesResult
    success = Right

    failure :: C.DeclId -> C.ScopedName -> DerivedNamesResult
    failure declId scopedName =
        Left $ MangleNamesFailure owner loc $
          MangleNamesCollisionError $ DetectClashesScopedNameNotMangled declId scopedName

    -- Field selectors created under 'OmitFieldPrefixes' enable
    -- @DuplicateRecordFields@, so they need only be unique within a record.
    fieldScope :: Scope
    fieldScope = case strategy of
      AddFieldPrefixes  -> GlobalScope
      OmitFieldPrefixes -> DeclScope owner

    fieldNames :: C.Field CreateNames -> [DerivedNamesResult]
    fieldNames = C.elimField regularFieldNames implicitFieldNames

    regularFieldNames :: C.RegularField CreateNames -> [DerivedNamesResult]
    regularFieldNames field =
        case lookupScopedName owner field.info.name nameMap of
          Nothing     -> [ failure owner field.info.name ]
          Just hsName -> [ success (Field, fieldScope, field.info.loc, hsName) ]

    implicitFieldNames :: C.ImplicitField CreateNames -> [DerivedNamesResult]
    implicitFieldNames field =
        ( case lookupScopedName owner field.info.name nameMap of
            Nothing     -> [ failure owner field.info.name ]
            Just hsName -> [ success (Field, fieldScope, field.info.loc, hsName) ]
        ) ++ concatMap indirectFieldNames field.indirect

    indirectFieldNames :: C.IndirectField CreateNames -> [DerivedNamesResult]
    indirectFieldNames field =
        case lookupScopedName owner field.info.name nameMap of
          Nothing     -> [ failure owner field.info.name ]
          Just hsName -> [ success (Field, fieldScope, field.info.loc, hsName) ]

    enumConstantName :: C.EnumConstant CreateNames -> [DerivedNamesResult]
    enumConstantName constant =
        case lookupScopedName owner constant.info.name nameMap of
          Nothing     -> [ failure owner constant.info.name ]
          Just hsName -> [ success (Constructor, GlobalScope, constant.info.loc, hsName) ]

    -- The newtype "unwrap" field is recorded only under 'AddFieldPrefixes',
    -- where it is the globally-unique @unwrap<TypeName>@. Under
    -- 'OmitFieldPrefixes' it is simply @unwrap@, lives alone in its own record,
    -- and so can never collide.
    newtypeNames :: NewtypeNames -> [DerivedNamesResult]
    newtypeNames n =
        success (Constructor, GlobalScope, loc, Hs.demoteNs n.dataConstr)
      : [ success (Field, GlobalScope, loc, Hs.demoteNs n.field)
        | AddFieldPrefixes <- [strategy] ]

    typedefNames :: TypedefNames -> [DerivedNamesResult]
    typedefNames t =
         newtypeNames t.orig
      ++ concat [ success (AuxType, GlobalScope, loc, Hs.demoteNs auxName) : newtypeNames auxNames
                | (auxName, auxNames) <- maybeToList t.aux ]

-- | Turn registry collisions into per-declaration failures
--
-- Each 'Collision' produced by 'collisions' lives in a single 'Scope'. A
-- collision within a 'DeclScope' is a set of record field selectors of one
-- declaration that mangle to the same Haskell name (only possible under
-- 'OmitFieldPrefixes'); we report it as a more specific
-- 'DetectClashesDuplicateFieldName'. All other collisions are reported as
-- 'DetectClashesCollision', one failure per participating declaration.
collisionFailures :: [Collision] -> [MangleNamesFailure]
collisionFailures = concatMap getCollisionFailures
  where
    getCollisionFailures :: Collision -> [MangleNamesFailure]
    getCollisionFailures c = case c.origins of
        o:_ | DeclScope{} <- o.scope ->
          -- Within-record duplicate field selector. All origins share the same
          -- owner (the enclosing record), so this yields a single failure.
          [ MangleNamesFailure o.owner o.loc $ MangleNamesCollisionError $
              DetectClashesDuplicateFieldName c.name (map (.loc) c.origins)
          ]
        _otherwise ->
          [ MangleNamesFailure o.owner o.loc $ MangleNamesCollisionError $
              DetectClashesCollision c.name idsWithLocs
          | o <- origins
          ]
      where
        -- One failure per distinct declaration.
        origins :: [NameOrigin]
        origins = Map.elems $ Map.fromList $ map (\o -> (o.owner, o)) c.origins

        idsWithLocs :: [C.WithLocationInfo C.DeclId]
        idsWithLocs =
          [ C.WithLocationInfo (C.declIdLocationInfo o.owner [o.loc]) o.owner
          | o <- origins
          ]