///|
/// Strict numeric CSV: origin,age,value; no quoting, comments or missing cells.
/// Cell order is irrelevant, but duplicate coordinates are errors.
pub fn Triangle::from_csv(
  text : String,
  incremental~ : Bool = false,
) -> Triangle raise RunoffError {
  if text.length() > 1048576 {
    raise Parse("CSV input exceeds 1 Mi code units")
  }
  let lines = text.split("\n").to_array()
  if lines.is_empty() || lines[0].trim().to_owned() != "origin,age,value" {
    raise Parse("CSV header must be origin,age,value")
  }
  let cells : Array[(Int32, Int32, Double)] = []
  let mut order = 0
  for k in 1..= 8256 {
      raise Parse("CSV cell limit exceeded")
    }
    let columns = line.split(",").to_array()
    if columns.length() != 3 {
      raise Parse("CSV needs exactly three columns")
    }
    let coordinates : Array[Int32] = []
    for column in 0..<2 {
      let token = columns[column].trim().to_owned()
      if token.is_empty() {
        raise Parse("empty coordinate")
      }
      let mut value = 0
      for ch in token {
        if ch < '0' || ch > '9' {
          raise Parse("coordinate must be unsigned decimal integer")
        }
        value = value * 10 + (ch.to_int() - '0'.to_int())
        if value >= 128 {
          raise Parse("coordinate exceeds 127")
        }
      }
      coordinates.push(value)
    }
    let token = columns[2].trim().to_owned()
    let amount : Double = @json.from_json(parse_json_bounded(token)) catch {
      _ => raise Parse("value must be a finite JSON number")
    }
    nonnegative(amount, "CSV value must be nonnegative")
    let origin = coordinates[0]
    let age = coordinates[1]
    if origin + age + 1 > order {
      order = origin + age + 1
    }
    cells.push((origin, age, amount))
  }
  if order < 2 || order > 128 {
    raise Shape("CSV implied order must be 2..128")
  }
  let rows : Array[Array[Double]] = []
  let seen : Array[Array[Bool]] = []
  for i in 0.. String {
  let lines = ["origin,age,value"]
  let rows = if incremental { self.incremental() } else { self.cumulative() }
  for origin in 0..