///|
/// Versioned, canonical ASCII snapshot: MM1:n:r0,r1,...\n (no final newline).
/// Rank rows are in bitmask order and revalidated on decoding, not trusted.
pub fn Matroid::encode(self : Matroid) -> String raise MatroidError {
  let ranks = self.rank_snapshot()
  let mut out = "MM1:\{self.size}:"
  for i = 0; i < ranks.length(); i = i + 1 {
    if i > 0 {
      out = out + ","
    }
    out = out + "\{ranks[i]}"
  }
  out
}

///|
fn parse_snapshot_number(text : String) -> Int32 raise MatroidError {
  let chars = text.to_array()
  if chars.length() == 0 || chars.length() > 2 {
    raise InvalidInput("snapshot integer width")
  }
  if chars.length() > 1 && chars[0] == '0' {
    raise InvalidInput("snapshot leading zero")
  }
  let mut value = 0
  for c in chars {
    let digit = match c {
      '0' => 0
      '1' => 1
      '2' => 2
      '3' => 3
      '4' => 4
      '5' => 5
      '6' => 6
      '7' => 7
      '8' => 8
      '9' => 9
      _ => raise InvalidInput("snapshot nondecimal integer")
    }
    value = value * 10 + digit
  }
  value
}

///|
/// Strict decode: 16KiB cap, canonical decimals, exact row count, all rank axioms.
pub fn decode_snapshot(text : String) -> Matroid raise MatroidError {
  if text.length() > 16384 {
    raise InvalidInput("snapshot exceeds 16KiB")
  }
  let parts = text.split(":").to_array()
  if parts.length() != 3 || parts[0].to_owned() != "MM1" {
    raise InvalidInput("snapshot header")
  }
  let n = parse_snapshot_number(parts[1].to_owned())
  if n > 12 {
    raise InvalidInput("snapshot supports at most 12 elements")
  }
  let rows = parts[2].to_owned().split(",").to_array()
  if rows.length() != 1 << n {
    raise InvalidInput("snapshot row count")
  }
  let ranks = []
  for row in rows {
    ranks.push(parse_snapshot_number(row.to_owned()))
  }
  Matroid::from_ranks(n, ranks)
}