///|
/// Why a graph was refused.
///
/// Every function that can reject its input raises this and names the node or
/// edge it objected to, so a caller can point at its own input instead of
/// printing a message. A graph-level mistake is a value here, never a process
/// abort: the numbers came from the caller, and the caller can handle them.
///
/// ```mbt check
/// test {
///   let refused = @moongraph.Refused::Cycle(nodes=[0, 1, 0])
///   let line = match refused {
///     Cycle(nodes~) => "a cycle through " + nodes.length().to_string() + " nodes"
///     _ => "something else"
///   }
///   assert_eq(line, "a cycle through 3 nodes")
/// }
/// ```
#derive(Eq, @debug.Debug)
pub(all) suberror Refused {
  /// An edge with a negative weight reached an algorithm that requires
  /// non-negative ones, such as a shortest-path search that settles a node
  /// once. The payload is the edge number that carried it.
  NegativeWeight(edge~ : Int32)
  /// The graph has a cycle where an acyclic graph was required. The payload is
  /// one witness: node numbers, first and last equal.
  Cycle(nodes~ : Array[Int32])
  /// No route exists between the two nodes, or the supplies of a flow problem
  /// cannot be routed between them.
  Disconnected(from~ : Int32, to~ : Int32)
  /// The problem has no answer at the limits the caller set, such as a
  /// chromatic number above the colour limit.
  NoSolution
  /// The graph does not have the property the function requires. `kind` is the
  /// property, `what` the reason, so a caller can print one line without
  /// matching every case.
  NotA(kind~ : String, what~ : String)
  /// A node or edge number is outside the graph.
  OutOfRange(node~ : Int32)
  /// A text in a graph format could not be read. `at` is a byte offset from the
  /// start of the text; `line` and `column` count from 1. `got` is what was
  /// found and `want` is what was expected, both as short literal text.
  Malformed(
    at~ : Int32,
    line~ : Int32,
    column~ : Int32,
    got~ : String,
    want~ : String
  )
}

///|
pub extend Refused with Eq::{equal, not_equal}

///|
pub extend Refused with @debug.Debug::{to_repr}

///|
/// The refusal for a byte offset, with the line and column computed from the
/// text it points into. Parsers report positions in all three units because a
/// caller may be reading a file (line and column) or a byte stream (offset).
pub fn Refused::at_offset(
  text : StringView,
  at : Int32,
  got : String,
  want : String,
) -> Refused {
  let mut line = 1
  let mut column = 1
  let limit = if at < text.length() { at } else { text.length() }
  for i in 0..