pubgrub-0.1.0.0: src/PubGrub/Report.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Rendering a failed resolution as a human-readable proof, over any 'Domain'.
--
-- A terminal t'Incompatibility' carries the 'Cause' tree that produced it, which
-- is all this module needs: 'reportConflict' replays that tree as PubGrub's
-- argument for why no solution exists, one line per step it keeps, ending
-- \"So, because …, version solving failed\".
--
-- == What is fixed and what is the domain's
--
-- The wording is the domain's; the /shape/ of the argument is fixed: which
-- steps get a line, which are skipped, where a line number is minted and where
-- a blank line falls. The shape is here, written once. The words come from a
-- t'Formatter' the domain supplies (what a fact says, what a /hole/, a set of
-- values in which the provider had nothing, says, and what a set of terms
-- concludes), together with any rules of its own that say a step in fewer
-- lines than the standard cases would, and a pass over the tree before the walk
-- begins.
--
-- == How the proof is made
--
-- The walker visits each derived step and writes one line for it: a
-- connective, a __premise__, and what the step concludes. The premise is what
-- varies, and it is chosen by a __rule__ — the first of the line rules to fire.
-- A rule is a @do@ block in 'Maybe', written in the vocabulary of
-- "PubGrub.Derivation": each line reads a view of the tree or states a guard,
-- and the last says the premise and names the sub-derivation, if any, the line
-- continues (@Line@). Two facts named together in one premise are joined by a
-- second table, the clause rules, in the same language.
--
-- Two rules keep the proof short, and both are PubGrub's own. Neither is
-- cosmetic: a proof is only useful if it is read to the end, and the naive
-- replay of the derivation tree — one line per derived incompatibility, each
-- naming one external fact — is too long for that.
--
-- * __A chain of two external facts is one sentence__: a domain's clause rules.
-- Where a dependency's dependee is the depender of the next, the proof writes
-- \"@foo@ depends on @bar@ /which depends on/ @baz@\" rather than naming
-- @bar@ twice, and where two facts share a depender it writes \"depends on
-- /both/ X and Y\". What a dependency is, the core does not know, so these
-- rules belong to the domain's t'Formatter'.
--
-- * __Every other derived step is skipped__. Where a step's derived cause has
-- a single derived cause of its own, that intermediate /conclusion/ is left
-- out and both external facts are named in one line. Most derivation graphs
-- are linear chains, so this halves them without losing anything a reader
-- needs.
--
-- == Line numbers are for joins only
--
-- A line is numbered only if a later line refers back to it, and in a linear
-- chain none does. A number is given only at a step with two derived causes,
-- so a proof that never branches carries no numbers at all.
--
-- A sub-derivation that two steps share is explained twice rather than
-- referred to: an t'Incompatibility' has no identity to recognise it by.
module PubGrub.Report
( -- * The domain's words
Formatter (..)
, mkFormatter
, Line (..)
, leaf
, summaryWith
, listing
, anyOf
-- * The proof
, reportConflict
) where
import Data.List (unsnoc)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import PubGrub.Derivation (Rule, bothDerived, bothExternal,
chainStep, firstOf, isDerived)
import PubGrub.Incompatibility (Cause (..), Domain (..),
Incompatibility (..))
import PubGrub.Term (Term (..))
-- --------------------------------------------------------------------------
-- The domain's words
-- --------------------------------------------------------------------------
-- | What a domain says, and the rules of its own it adds to the standard ones.
--
-- Every field that says something answers with a fragment: no connective in
-- front of it and no full stop after it, because the walker puts its own
-- \"Because\" before a fragment and ends every line as a sentence. A fragment
-- must therefore never open with \"because\" or \"and\".
data Formatter v = Formatter
{ sayFact :: FactOf v -> Incompatibility v -> Text
-- ^ a leaf that states a fact, a seed's or the provider's; the
-- incompatibility it is the cause of comes with it, for a fact whose
-- sentence depends on which of its terms survived
, sayNoValues :: v -> SetOf v -> Text
-- ^ the solver's own leaf: nothing to offer in this set
, sayTerms :: Map (v) (Term (SetOf v)) -> Text
-- ^ what a set of terms concludes; 'summaryWith' is the standard sentences
, prepare :: Incompatibility v -> Incompatibility v
-- ^ a pass over the tree before the walk; 'id' for none
, lineRules :: [Rule (Incompatibility v) (Line v)]
-- ^ the domain's own ways of saying a step that is not a branch, tried
-- before the standard rules for a chain of steps; a rule must keep the
-- proof sound on its own, and can check that it does by replaying the
-- steps it covers with 'PubGrub.Incompatibility.priorCauseTerms'
, clauseRules :: [Rule (Incompatibility v, Incompatibility v) Text]
-- ^ the domain's ways of saying two external facts as one premise, tried
-- in order; the fallback names them side by side
}
-- | A formatter from the domain's three sentences, with no rules of its own:
-- what a fact says, what the solver's own finding that a variable has no
-- value in a set says, and how to name a variable with a set of its values,
-- from which 'summaryWith' makes the conclusions. The three other fields add
-- nothing ('prepare' is 'id', and there are no rules); set them by record
-- update, as in
-- @(mkFormatter says hole name){clauseRules = [rule]}@.
mkFormatter
:: (FactOf v -> Incompatibility v -> Text)
-> (v -> SetOf v -> Text)
-> (v -> SetOf v -> Text)
-> Formatter v
mkFormatter says hole name =
Formatter
{ sayFact = says
, sayNoValues = hole
, sayTerms = summaryWith name name
, prepare = id
, lineRules = []
, clauseRules = []
}
-- | What a rule proposes for one line of the proof.
data Line v
= Line
Text
-- ^ the premise, which goes after the connective
(Maybe (Incompatibility v))
-- ^ the sub-derivation the line continues, explained before it; a line
-- that continues nothing opens a line of derivation
-- | An external fact at a leaf of the proof, in the domain's words; a derived
-- node, which is not a leaf, reads as what its terms conclude.
leaf :: Formatter v -> Incompatibility v -> Text
leaf fmt c = case incompCause c of
Seed f -> sayFact fmt f c
Fact f -> sayFact fmt f c
NoValues v s -> sayNoValues fmt v s
Derived _ _ -> sayTerms fmt (incompTerms c)
-- | What an incompatibility says, in PubGrub's standard sentences, given how
-- the domain names a variable with its set: @named@ where the sentence calls
-- for the full form (\"every version of foo\") and @bare@ where it does not. A
-- negative term is never written as \"not …\": it says the variable /must/ be
-- in that set, and the sentence carries the negation — \"foo requires bar >=2\",
-- \"if foo and bar then baz <3\", \"either foo <2 or bar <3\". The empty
-- incompatibility is failure by name: \"version solving failed\".
summaryWith
:: (v -> SetOf v -> Text)
-> (v -> SetOf v -> Text)
-> Map (v) (Term (SetOf v))
-> Text
summaryWith named bare ts = case Map.toList ts of
[] -> "version solving failed"
[(p, Positive r)] -> bare p r <> " is forbidden"
[(p, Negative r)] -> bare p r <> " is required"
[(p1, Positive r1), (p2, Negative r2)] -> requires p1 r1 p2 r2
[(p2, Negative r2), (p1, Positive r1)] -> requires p1 r1 p2 r2
[(p1, Positive r1), (p2, Positive r2)] -> bare p1 r1 <> " is incompatible with " <> bare p2 r2
[(p1, Negative r1), (p2, Negative r2)] -> "either " <> bare p1 r1 <> " or " <> bare p2 r2
terms ->
let positives = [(p, r) | (p, Positive r) <- terms]
negatives = [bare p r | (p, Negative r) <- terms]
in case (positives, negatives) of
([(p, r)], _ : _) -> named p r <> " requires " <> anyOf negatives
(_ : _, _ : _) ->
"if " <> T.intercalate " and " (map (uncurry bare) positives)
<> " then " <> anyOf negatives
(_, []) -> "one of " <> anyOf (map (uncurry bare) positives) <> " must be false"
([], _) -> "one of " <> anyOf negatives <> " must be true"
where
-- A /derived/ two-term incompatibility. The spec says \"requires\" here and
-- keeps \"depends on\" for the external fact (@references\/pubgrub.md:1005@,
-- both in one line): a dependency is declared, a requirement is what the
-- solver worked out.
requires p1 r1 p2 r2 = named p1 r1 <> " requires " <> bare p2 r2
-- | \"a, b and c\"
listing :: [Text] -> Text
listing xs = case unsnoc xs of
Just (front@(_ : _), l) -> T.intercalate ", " front <> " and " <> l
_ -> T.intercalate ", " xs
-- | \"a or b or c\"
anyOf :: [Text] -> Text
anyOf = T.intercalate " or "
-- --------------------------------------------------------------------------
-- The proof
-- --------------------------------------------------------------------------
-- | Render an incompatibility and its derivation as a human-readable proof.
reportConflict :: Domain v => Formatter v -> Incompatibility v -> Text
reportConflict fmt root0
| isDerived root = render (reverse (outLines (derived fmt True root (Out [] 1))))
-- The terminal incompatibility is itself an external fact: nothing was
-- derived, so the fact is the whole explanation.
| otherwise = leaf fmt root
where
root = prepare fmt root0
-- Every line is a sentence and ends as one. A blank line between two
-- branches of a derivation (references/pubgrub.md:632) stays blank.
render ls = T.intercalate "\n" [line n t | (n, t) <- ls]
line _ "" = ""
line n t = prefix n <> t <> "."
prefix Nothing = ""
prefix (Just n) = T.pack (show n) <> ". "
-- --------------------------------------------------------------------------
-- The walker
-- --------------------------------------------------------------------------
-- | The proof under construction: its lines __newest first__, and the next free
-- line number.
data Out = Out
{ outLines :: [(Maybe Int, Text)]
, outNext :: !Int
}
emit :: Text -> Out -> Out
emit t out = out{outLines = (Nothing, t) : outLines out}
-- | Give the line just emitted a number, so a later line can cite it. Numbers
-- are minted here and nowhere else.
numberLast :: Out -> (Int, Out)
numberLast out = case outLines out of
((Just n, t) : rest) -> (n, out{outLines = (Just n, t) : rest})
((Nothing, t) : rest) ->
( outNext out
, out{outLines = (Just (outNext out), t) : rest, outNext = outNext out + 1}
)
-- Unreachable: every branch emits at least one line before this is called.
-- Answering with the unused number keeps the proof well-formed if it ever is.
[] -> (outNext out, out)
-- | Explain one derived incompatibility, ending with the line that concludes it.
-- @final@ marks the last line of the whole proof, which reads \"So, …\" and
-- concludes failure by name.
derived :: Domain v => Formatter v -> Bool -> Incompatibility v -> Out -> Out
derived fmt final inc out = case bothDerived inc of
-- Case 1 (references/pubgrub.md:595): both causes derived — the derivation
-- branches, and this is the only place a line number is wanted. Explain the
-- first branch, number the line it ends on, then explain the second and close
-- by referring back. The blank line between the two branches is the spec's
-- (:632): it marks that a new line of derivation starts. No rule chooses
-- this: it is the structure of the proof, not a way of saying a step.
Just (c1, c2) ->
let (n, out') = numberLast (derived fmt False c1 out)
in emit
(opening (Just c2) <> summary c1 <> " (" <> T.pack (show n) <> "), " <> says inc)
(derived fmt False c2 (emit "" out'))
Nothing -> case firstOf rules inc of
Just (Line premise below) ->
emit
(opening below <> premise <> ", " <> says inc)
(maybe out (\d -> derived fmt False d out) below)
-- Not reached: a derived step either branches, chains or joins two
-- facts, and the last two rules of the table fire on any such step.
Nothing -> emit (leaf fmt inc) out
where
-- The domain's rules go between the spec's case 3 and its case 2: a run
-- rule collapses what the ordinary skip would print two facts at a time,
-- and the spec's 2.iii fires on any chain step, so the table is total over
-- chain steps. Case 3 fires only on a step both of whose causes are
-- external, which no chain rule reads, so its place in the table is free.
rules = twoFacts fmt : lineRules fmt ++ [everyOtherStep fmt, oneFact fmt]
summary c = sayTerms fmt (incompTerms c)
-- "So," marks the conclusion (references/pubgrub.md:1027); "Because" opens a
-- line of derivation, which only a step with nothing below it does; "And
-- because" continues one.
opening below
| final = "So, because "
| Just _ <- below = "And because "
| otherwise = "Because "
says c
| final = "version solving failed"
| otherwise = summary c
-- --------------------------------------------------------------------------
-- The spec's cases
-- --------------------------------------------------------------------------
-- | Both causes are external. One line, and the only case that opens a line
-- of derivation rather than continuing one.
twoFacts :: Formatter v -> Rule (Incompatibility v) (Line v)
twoFacts fmt step = do
(c1, c2) <- bothExternal step
pure (Line (clause fmt c1 c2) Nothing)
-- | One cause is derived, from a derived cause and an external fact of its
-- own: the every-other-step skip. The derived cause's own conclusion is left
-- unsaid and the two external facts are named together here, which is where a
-- long linear chain loses half its lines.
everyOtherStep :: Formatter v -> Rule (Incompatibility v) (Line v)
everyOtherStep fmt step = do
(d, e) <- chainStep step
(dd, de) <- chainStep d
pure (Line (clause fmt de e) (Just dd))
-- | One cause is derived and one external, and the derived cause branches or
-- bottoms out, so it gets its own lines and this one closes with the external
-- fact alone.
oneFact :: Formatter v -> Rule (Incompatibility v) (Line v)
oneFact fmt step = do
(d, e) <- chainStep step
pure (Line (leaf fmt e) (Just d))
-- | Two external facts, as one clause: the first of the domain's clause rules
-- to fire, or the two facts side by side.
clause :: Formatter v -> Incompatibility v -> Incompatibility v -> Text
clause fmt c1 c2 = fromMaybe (leaf fmt c1 <> " and " <> leaf fmt c2) (firstOf (clauseRules fmt) (c1, c2))