packages feed

tadka-2.0.0.0: src/Tadka/Internal/Related.hs

--
-- No compatibility guarantee.
module Tadka.Internal.Related
  ( TerminationReason (..)
  , RelatedTree (..)
  , defaultRelatedDepth
  , walkRelated
  , walkCauses
  , flattenRelated
  ) where

import           Data.Set                (Set)
import qualified Data.Set                as Set
import           Numeric.Natural         (Natural)

import           Tadka.Internal.Diagnostic
                   (Diagnostic (diagnosticCause, diagnosticId, related), SomeDiagnostic (..))
import           Tadka.Internal.Types    (DiagnosticId)

-- | Why a node's @related@ list is represented the way it is.
data TerminationReason
  = NotTerminated
    -- ^ fully expanded (its children — themselves possibly truncated — are present).
  | DepthTruncated
    -- ^ the node has related diagnostics, but the depth budget was exhausted;
    -- children omitted. A handler renders "(N more related diagnostics
    -- omitted)", computing N as @length (related d)@.
  | CycleOmitted
    -- ^ this node's 'diagnosticId' was already on the current path; it is not
    -- descended into again. A handler renders "(cycle omitted)".
  deriving (Eq, Show, Enum, Bounded)

-- | The result of a walk: a diagnostic, its expanded children (empty when
-- truncated), and why. This is the renderer-agnostic shape all handlers consume.
data RelatedTree = RelatedTree
  { relatedDiag        :: SomeDiagnostic
  , relatedChildren    :: [RelatedTree]
  , relatedTermination :: TerminationReason
  }


defaultRelatedDepth :: Natural
defaultRelatedDepth = 8

-- | Walk a diagnostic and its @related@ chain into a 'RelatedTree', detecting
-- cycles by 'diagnosticId' and bounding depth by the given budget. Total for
-- any input — even a structurally infinite one — because the budget is finite.
walkRelated :: Natural -> SomeDiagnostic -> RelatedTree
walkRelated limit = go limit Set.empty
  where
    go :: Natural -> Set DiagnosticId -> SomeDiagnostic -> RelatedTree
    go depth visited sd@(SomeDiagnostic e) =
      case diagnosticId e of
        Just i | i `Set.member` visited ->
          RelatedTree sd [] CycleOmitted
        mId ->
          let visited' = maybe visited (`Set.insert` visited) mId
              kids     = related e
          in if null kids
               then RelatedTree sd [] NotTerminated
               else if depth == 0
                      then RelatedTree sd [] DepthTruncated
                      else RelatedTree sd (map (go (depth - 1) visited') kids) NotTerminated

-- | Pre-order flatten to @(diagnostic, reason)@ pairs. Convenient for handlers
-- that render a flat list and for tests.
flattenRelated :: RelatedTree -> [(SomeDiagnostic, TerminationReason)]
flattenRelated (RelatedTree d kids term) =
  (d, term) : concatMap flattenRelated kids


-- | Follow 'diagnosticCause' from a root into a linear chain, up to a depth
-- budget and with cycle detection by 'diagnosticId' (a cause whose id was
-- already seen ends the chain). Returns the causes in order, excluding the
-- root. Total: the budget bounds depth and the visited set bounds cycles.
walkCauses :: Natural -> SomeDiagnostic -> [SomeDiagnostic]
walkCauses limit root@(SomeDiagnostic e0) = go limit (seed e0) root
  where
    seed e = maybe Set.empty (`Set.insert` Set.empty) (diagnosticId e)
    go :: Natural -> Set DiagnosticId -> SomeDiagnostic -> [SomeDiagnostic]
    go 0 _ _ = []
    go depth visited (SomeDiagnostic e) =
      case diagnosticCause e of
        Nothing -> []
        Just c@(SomeDiagnostic ce) ->
          case diagnosticId ce of
            Just i | i `Set.member` visited -> []
            mId -> c : go (depth - 1) (insMaybe mId visited) c
    insMaybe mId v = maybe v (`Set.insert` v) mId