packages feed

keiki-0.2.0.0: src/Keiki/Render/Inspector.hs

-- | A Markdown edge-detail renderer for 'SymTransducer', a sibling to
-- the Mermaid topology renderer in "Keiki.Render.Mermaid". Where
-- 'Keiki.Render.Mermaid.toMermaid' shows the /shape/ of a workflow (one
-- line per edge), this renderer lays out every edge in /full/: its
-- source and target states, its 0-based edge index, the input
-- (command) constructor, the output (event) constructor(s), the guard
-- predicate (structural and/or domain-readable), the register slots it
-- writes, and — optionally — each output field's term.
--
-- The output is a deterministic 'Data.Text.Text' Markdown document with
-- edges grouped under a level-3 heading per source state. It is pure:
-- no IO, no SMT solver.
module Keiki.Render.Inspector
  ( EdgeInspectorOptions (..),
    defaultEdgeInspectorOptions,
    renderEdgeInspector,
  )
where

import Data.Text (Text)
import Data.Text qualified as T
import Keiki.Core
  ( Edge (..),
    HsPred (..),
    OutFields (..),
    OutTerm (..),
    SymTransducer (..),
    Update (..),
    WireCtor (..),
  )
import Keiki.Internal.Slots (indexNName)
import Keiki.Render.Mermaid (edgeInputName)
import Keiki.Render.Pretty (prettyPred, prettyTerm)

-- | Which detail fields each edge block carries. Every field is opt-in
-- via a 'Bool'. 'defaultEdgeInspectorOptions' turns on everything that
-- needs no domain-readable rendering; the two pretty options
-- ('includePrettyGuard', 'includeOutputFields') reuse
-- "Keiki.Render.Pretty" and default to 'False'.
data EdgeInspectorOptions = EdgeInspectorOptions
  { -- | Show @edge index: N@ (the 0-based position in @edgesOut t s@).
    includeEdgeIndex :: Bool,
    -- | Show the structural guard summary, e.g. @PAnd PInCtor PEq@.
    includeStructuralGuard :: Bool,
    -- | Show the domain-readable guard from
    --     'Keiki.Render.Pretty.prettyPred', e.g.
    --     @(ConfirmAccount && ConfirmAccount.confirmCode == confirmCode)@.
    includePrettyGuard :: Bool,
    -- | Show the register slots the edge writes.
    includeWrittenSlots :: Bool,
    -- | Show each output field's term, positionally (field 0, field 1,
    --     …), via 'Keiki.Render.Pretty.prettyTerm'. 'WireCtor' carries no
    --     field names, so fields are labelled by position only.
    includeOutputFields :: Bool
  }

-- | The default: everything on except the two pretty options that reuse
-- "Keiki.Render.Pretty".
defaultEdgeInspectorOptions :: EdgeInspectorOptions
defaultEdgeInspectorOptions =
  EdgeInspectorOptions
    { includeEdgeIndex = True,
      includeStructuralGuard = True,
      includePrettyGuard = False,
      includeWrittenSlots = True,
      includeOutputFields = False
    }

-- | Render a 'SymTransducer' to a Markdown edge-detail document. Edges
-- are grouped under a @### \<state\>@ heading per source state, in
-- @[minBound .. maxBound]@ order; states with no outgoing edges produce
-- no section. The state type's @Bounded@/@Enum@ enumerate the states and
-- @Show@ names them, mirroring 'Keiki.Render.Mermaid.toMermaid'.
renderEdgeInspector ::
  (Bounded s, Enum s, Show s) =>
  EdgeInspectorOptions ->
  SymTransducer (HsPred rs ci) rs s ci co ->
  Text
renderEdgeInspector opts t =
  let states = [minBound .. maxBound]
      section s = case edgesOut t s of
        [] -> Nothing
        edges -> Just (renderState opts s edges)
   in T.intercalate
        (T.pack "\n\n")
        ( T.pack "# Edge inspector"
            : [blk | Just blk <- map section states]
        )

-- | One @### \<state\>@ section: the heading, a blank line, then one
-- block per outgoing edge (in @edgesOut@ order, with its 0-based index).
-- Edge blocks are separated by a single newline (no blank line between
-- them).
renderState ::
  (Show s) =>
  EdgeInspectorOptions ->
  s ->
  [Edge (HsPred rs ci) rs ci co s] ->
  Text
renderState opts s edges =
  T.intercalate
    (T.pack "\n")
    ( (T.pack "### " <> T.pack (show s))
        : T.pack ""
        : [renderEdge opts s i e | (i, e) <- zip [0 ..] edges]
    )

-- | One edge block: a header bullet naming the transition, then one
-- indented detail bullet per enabled, non-empty field. @update@ is bound
-- by the 'Edge' pattern (never via the @update@ selector) so the
-- existential write-set does not escape.
renderEdge ::
  (Show s) =>
  EdgeInspectorOptions ->
  s ->
  Int ->
  Edge (HsPred rs ci) rs ci co s ->
  Text
renderEdge opts s i e@Edge {guard = g, update = u, target = tgt} =
  T.intercalate (T.pack "\n") (header : details)
  where
    header =
      T.pack "- **"
        <> T.pack (show s)
        <> T.pack " -> "
        <> T.pack (show tgt)
        <> T.pack "**"

    details =
      concat
        [ bullet
            (includeEdgeIndex opts)
            (T.pack "edge index: " <> T.pack (show i)),
          [detail (T.pack "input: " <> maybe (T.pack "?") id (edgeInputName e))],
          [detail (T.pack "output: " <> maybe (T.pack "\x03B5") id (outputName e))],
          outputFieldsBullets,
          bullet
            (includeStructuralGuard opts)
            (T.pack "guard (structural): " <> guardSummary g),
          bullet
            (includePrettyGuard opts)
            (T.pack "guard (pretty): " <> prettyPred g),
          writtenBullets
        ]

    -- An edge's written-slot listing; omitted when the edge writes
    -- nothing (an empty list would be noise).
    writtenBullets
      | not (includeWrittenSlots opts) = []
      | otherwise = case writtenSlots u of
          [] -> []
          ws -> [detail (T.pack "written slots: " <> T.intercalate (T.pack "; ") ws)]

    -- Each output's fields, positionally. Only outputs that have at
    -- least one field contribute; the bullet is omitted when no output
    -- carries a field.
    outputFieldsBullets
      | not (includeOutputFields opts) = []
      | otherwise = case [grp | Just grp <- map outputGroup (output e)] of
          [] -> []
          grps -> [detail (T.pack "output fields: " <> T.intercalate (T.pack "; ") grps)]

    detail x = T.pack "  - " <> x
    bullet cond x = if cond then [detail x] else []

-- | One output's field listing: @\<wcName\>[field 0: t0; field 1: t1]@,
-- or 'Nothing' when the output constructor has no fields.
outputGroup :: OutTerm rs ci co -> Maybe Text
outputGroup (OPack _ wc fs) = case zip [0 :: Int ..] (prettyOutFields fs) of
  [] -> Nothing
  fields ->
    Just
      ( T.pack (wcName wc)
          <> T.pack "["
          <> T.intercalate
            (T.pack "; ")
            [ T.pack "field " <> T.pack (show k) <> T.pack ": " <> ft
            | (k, ft) <- fields
            ]
          <> T.pack "]"
      )

-- | Pretty-print each field 'Term' of an 'OutFields' HList, in order,
-- reusing 'Keiki.Render.Pretty.prettyTerm'.
prettyOutFields :: OutFields rs ci ifs fs -> [Text]
prettyOutFields OFNil = []
prettyOutFields (OFCons t rest) = prettyTerm t : prettyOutFields rest

-- | The output constructor name(s) for an edge, joined with @"; "@ for
-- Markdown (the Mermaid renderer uses @\<br/\>@ for three or more, which
-- is a diagram-only line break). 'Nothing' for an ε-edge (empty output).
outputName :: Edge (HsPred rs ci) rs ci co s -> Maybe Text
outputName Edge {output = outs} = case outs of
  [] -> Nothing
  many -> Just (T.intercalate (T.pack "; ") (map wcN many))
  where
    wcN :: OutTerm rs ci co -> Text
    wcN (OPack _ wc _) = T.pack (wcName wc)

-- | A structural, total summary of a guard predicate: its constructor
-- tags in left-to-right (prefix) order, with 'PCmp' carrying its
-- direction. Replicated from "Keiki.Render.Mermaid" (where it is not
-- exported); kept byte-identical to that original.
guardSummary :: HsPred rs ci -> Text
guardSummary = T.intercalate (T.pack " ") . go
  where
    go :: HsPred rs ci -> [Text]
    go PTop = [T.pack "PTop"]
    go PBot = [T.pack "PBot"]
    go (PAnd a b) = T.pack "PAnd" : go a ++ go b
    go (POr a b) = T.pack "POr" : go a ++ go b
    go (PNot p) = T.pack "PNot" : go p
    go (PEq _ _) = [T.pack "PEq"]
    go (PInCtor _) = [T.pack "PInCtor"]
    go PLeftArm = [T.pack "PLeftArm"]
    go PRightArm = [T.pack "PRightArm"]
    go (PCmp c _ _) = [T.pack "PCmp " <> T.pack (show c)]

-- | Recover the names of the slots an edge's 'Update' writes.
-- Replicated from "Keiki.Render.Mermaid" (not exported there); kept
-- byte-identical. 'USet's @KnownSymbol s@ (from the pattern match) lets
-- 'indexNName' read the slot name off the index.
writtenSlots :: Update rs w ci -> [Text]
writtenSlots UKeep = []
writtenSlots (USet ix _) = [T.pack (indexNName ix)]
writtenSlots (UCombine a b) = writtenSlots a ++ writtenSlots b