scrod-0.2026.2.21: source/library/Scrod/Convert/FromGhc/ExportOrdering.hs
-- | Reorder items according to the module's export list.
--
-- When an export list is present, items are reordered so that exported
-- items come first (in export-list order), followed by implicit items,
-- then unexported items. Export-list-only entries (section headings,
-- inline docs, re-exports with no matching declaration) become
-- synthetic items.
--
-- When no export list is present, items are returned unchanged.
module Scrod.Convert.FromGhc.ExportOrdering (reorderByExports) where
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Numeric.Natural as Natural
import qualified Scrod.Convert.FromGhc.Internal as Internal
import qualified Scrod.Core.Category as Category
import qualified Scrod.Core.Column as Column
import qualified Scrod.Core.Doc as Doc
import qualified Scrod.Core.Export as Export
import qualified Scrod.Core.ExportIdentifier as ExportIdentifier
import qualified Scrod.Core.ExportName as ExportName
import qualified Scrod.Core.ExportNameKind as ExportNameKind
import qualified Scrod.Core.Item as Item
import qualified Scrod.Core.ItemKey as ItemKey
import qualified Scrod.Core.ItemKind as ItemKind
import qualified Scrod.Core.ItemName as ItemName
import qualified Scrod.Core.Line as Line
import qualified Scrod.Core.Located as Located
import qualified Scrod.Core.Location as Location
import qualified Scrod.Core.Section as Section
import qualified Scrod.Core.Visibility as Visibility
import qualified Scrod.Core.Warning as Warning
-- | Reorder items according to the module's export list.
--
-- When @Nothing@, return items unchanged. When @Just exports@,
-- reorder items to match the export list and create synthetic items
-- for export-list-only entries (sections, docs, re-exports).
reorderByExports ::
Maybe [Export.Export] ->
[Located.Located Item.Item] ->
[Located.Located Item.Item]
reorderByExports mExports items = case mExports of
Nothing -> items
Just [] -> items
Just exports ->
let nameMap = topLevelNameMap items
nextKey = nextItemKey items
(exportedItems, usedKeys, _) = walkExports exports nameMap Set.empty nextKey
implicitItems = collectImplicit usedKeys items
usedKeys2 = foldr (Set.insert . Item.key . Located.value) usedKeys implicitItems
unexportedItems = collectUnexported usedKeys2 items
usedKeys3 = foldr (Set.insert . Item.key . Located.value) usedKeys2 unexportedItems
remainingItems = collectRemaining usedKeys3 items
usedKeys4 = foldr (Set.insert . Item.key . Located.value) usedKeys3 remainingItems
childItems =
filter
( \li ->
let v = Located.value li
in Maybe.isJust (Item.parentKey v)
&& not (Set.member (Item.key v) usedKeys4)
)
items
in exportedItems <> implicitItems <> unexportedItems <> remainingItems <> childItems
-- | Build a map from top-level item names to located items.
-- For items whose names contain type variables (indicated by a space),
-- both the full name and the base name (first word) are indexed.
-- Pattern synonyms are included even when they have a parent key
-- (e.g. from a COMPLETE pragma), since they are still independently
-- exportable.
topLevelNameMap :: [Located.Located Item.Item] -> Map.Map Text.Text (Located.Located Item.Item)
topLevelNameMap items =
Map.fromList
[ entry
| li <- items,
isTopLevelOrExportable (Located.value li),
Just n <- [Item.name (Located.value li)],
let full = ItemName.unwrap n,
entry <- (full, li) : [(base, li) | Just base <- [Internal.baseItemName full]]
]
where
isTopLevelOrExportable val =
Maybe.isNothing (Item.parentKey val)
|| Item.kind val == ItemKind.PatternSynonym
-- | Compute the next available item key (one past the maximum).
nextItemKey :: [Located.Located Item.Item] -> Natural.Natural
nextItemKey items = case items of
[] -> 0
_ -> 1 + maximum (fmap (ItemKey.unwrap . Item.key . Located.value) items)
-- | Walk the export list, emitting items in export order. The @used@
-- set tracks item keys already emitted to avoid duplicates. Returns
-- the emitted items, the final used set, and the next available
-- synthetic key.
walkExports ::
[Export.Export] ->
Map.Map Text.Text (Located.Located Item.Item) ->
Set.Set ItemKey.ItemKey ->
Natural.Natural ->
([Located.Located Item.Item], Set.Set ItemKey.ItemKey, Natural.Natural)
walkExports exports nameMap used nextKey = case exports of
[] -> ([], used, nextKey)
e : es -> case e of
Export.Identifier ident ->
let name = ExportName.name (ExportIdentifier.name ident)
in case Map.lookup name nameMap of
Just li
| not (Set.member (Item.key (Located.value li)) used) ->
let meta = exportMetadataItems ident nextKey
nextKey2 = nextKey + fromIntegral (length meta)
used2 = Set.insert (Item.key (Located.value li)) used
(rest, used3, nextKey3) = walkExports es nameMap used2 nextKey2
in (li : meta <> rest, used3, nextKey3)
Just _ ->
-- Duplicate export: emit only metadata, skip the item.
let meta = exportMetadataItems ident nextKey
nextKey2 = nextKey + fromIntegral (length meta)
(rest, used2, nextKey3) = walkExports es nameMap used nextKey2
in (meta <> rest, used2, nextKey3)
Nothing ->
let (unresolvedItem, nextKey2) = mkUnresolvedExport ident nextKey
meta = exportMetadataItems ident nextKey2
nextKey3 = nextKey2 + fromIntegral (length meta)
(rest, used2, nextKey4) = walkExports es nameMap used nextKey3
in (unresolvedItem : meta <> rest, used2, nextKey4)
Export.Group section ->
let (sectionItem, nextKey2) = mkSectionItem section nextKey
(rest, used2, nextKey3) = walkExports es nameMap used nextKey2
in (sectionItem : rest, used2, nextKey3)
Export.Doc doc ->
let (docItem, nextKey2) = mkDocItem doc nextKey
(rest, used2, nextKey3) = walkExports es nameMap used nextKey2
in (docItem : rest, used2, nextKey3)
Export.DocNamed name ->
let (docItem, nextKey2) = mkDocNamedItem name nextKey
(rest, used2, nextKey3) = walkExports es nameMap used nextKey2
in (docItem : rest, used2, nextKey3)
-- | Collect implicit items that haven't been used yet.
collectImplicit ::
Set.Set ItemKey.ItemKey ->
[Located.Located Item.Item] ->
[Located.Located Item.Item]
collectImplicit usedKeys =
filter
( \li ->
let item = Located.value li
in Maybe.isNothing (Item.parentKey item)
&& Item.visibility item == Visibility.Implicit
&& not (Set.member (Item.key item) usedKeys)
)
-- | Collect unexported top-level items that haven't been used.
collectUnexported ::
Set.Set ItemKey.ItemKey ->
[Located.Located Item.Item] ->
[Located.Located Item.Item]
collectUnexported usedKeys =
filter
( \li ->
let item = Located.value li
in Maybe.isNothing (Item.parentKey item)
&& Item.visibility item == Visibility.Unexported
&& not (Set.member (Item.key item) usedKeys)
)
-- | Catch-all for any top-level items not captured by the above
-- collectors. This cannot happen in practice today, but guards against
-- future drift between Visibility and ExportOrdering.
collectRemaining ::
Set.Set ItemKey.ItemKey ->
[Located.Located Item.Item] ->
[Located.Located Item.Item]
collectRemaining usedKeys =
filter
( \li ->
let item = Located.value li
in Maybe.isNothing (Item.parentKey item)
&& not (Set.member (Item.key item) usedKeys)
)
-- | Create synthetic items for export-level doc and/or warning metadata
-- on an identifier.
exportMetadataItems :: ExportIdentifier.ExportIdentifier -> Natural.Natural -> [Located.Located Item.Item]
exportMetadataItems ident startKey =
let warningItems = case ExportIdentifier.warning ident of
Nothing -> []
Just w ->
[ mkSyntheticItem
startKey
Nothing
(warningToDoc w)
Nothing
ItemKind.DocumentationChunk
]
warningCount :: Natural.Natural
warningCount = fromIntegral (length warningItems)
docItems = case ExportIdentifier.doc ident of
Nothing -> []
Just d ->
[ mkSyntheticItem
(startKey + warningCount)
Nothing
d
Nothing
ItemKind.DocumentationChunk
]
in warningItems <> docItems
-- | Convert a warning to a doc paragraph for inline display.
warningToDoc :: Warning.Warning -> Doc.Doc
warningToDoc w =
Doc.Paragraph
. Doc.Bold
. Doc.Append
$ [ Doc.String (Text.pack "Warning"),
Doc.String (Text.pack " ("),
Doc.String (Category.unwrap (Warning.category w)),
Doc.String (Text.pack "): "),
Doc.String (Warning.value w)
]
-- | Create an item for an unresolved export (no matching declaration).
mkUnresolvedExport ::
ExportIdentifier.ExportIdentifier ->
Natural.Natural ->
(Located.Located Item.Item, Natural.Natural)
mkUnresolvedExport ident nextKey =
let exportName = ExportIdentifier.name ident
name = ExportName.name exportName
namespaceSig = case ExportName.kind exportName of
Just ExportNameKind.Module -> Just (Text.pack "module")
Just ExportNameKind.Pattern -> Just (Text.pack "pattern")
Just ExportNameKind.Type -> Just (Text.pack "type")
Nothing -> Nothing
in ( mkSyntheticItem
nextKey
(Just (ItemName.MkItemName name))
Doc.Empty
namespaceSig
ItemKind.UnresolvedExport,
nextKey + 1
)
-- | Create a section heading item from an export group.
mkSectionItem ::
Section.Section ->
Natural.Natural ->
(Located.Located Item.Item, Natural.Natural)
mkSectionItem section nextKey =
let hdr = Section.header section
doc = Doc.Header hdr
in ( mkSyntheticItem nextKey Nothing doc Nothing ItemKind.DocumentationChunk,
nextKey + 1
)
-- | Create a documentation item from an inline export doc.
mkDocItem ::
Doc.Doc ->
Natural.Natural ->
(Located.Located Item.Item, Natural.Natural)
mkDocItem doc nextKey =
( mkSyntheticItem nextKey Nothing doc Nothing ItemKind.DocumentationChunk,
nextKey + 1
)
-- | Create a documentation item from an unresolved named doc reference.
mkDocNamedItem ::
Text.Text ->
Natural.Natural ->
(Located.Located Item.Item, Natural.Natural)
mkDocNamedItem name nextKey =
( mkSyntheticItem nextKey chunkName Doc.Empty Nothing ItemKind.DocumentationChunk,
nextKey + 1
)
where
chunkName = Just . ItemName.MkItemName $ Text.pack "$" <> name
-- | Create a synthetic item with a given key, not tied to any source
-- location.
mkSyntheticItem ::
Natural.Natural ->
Maybe ItemName.ItemName ->
Doc.Doc ->
Maybe Text.Text ->
ItemKind.ItemKind ->
Located.Located Item.Item
mkSyntheticItem key itemName doc sig kind =
Located.MkLocated
{ Located.location =
Location.MkLocation
{ Location.line = Line.MkLine 0,
Location.column = Column.MkColumn 0
},
Located.value =
Item.MkItem
{ Item.key = ItemKey.MkItemKey key,
Item.kind = kind,
Item.parentKey = Nothing,
Item.name = itemName,
Item.documentation = doc,
Item.since = Nothing,
Item.signature = sig,
Item.visibility = Visibility.Exported
}
}