///|
fn words(text : String) -> Array[String] {
  text.split(" ").filter(x => !x.is_empty()).map(x => x.to_owned()).collect()
}

///|
/// Parse Linux's space-indented, preorder /proc/locks output.
/// max_chars limits UTF-16 units before tokenization; limits must be positive.
pub fn parse(
  text : String,
  max_chars~ : Int32 = 4194304,
  max_records~ : Int32 = 10000,
  max_depth~ : Int32 = 64,
) -> Snapshot raise LockError {
  if max_chars <= 0 || max_records <= 0 || max_depth <= 0 {
    invalid(0, "limits must be positive")
  }
  if text.length() > max_chars {
    invalid(0, "input character budget exceeded")
  }
  let records : Array[Entry] = []
  let parents : Array[Int32?] = []
  let stack : Array[Int32] = []
  let roots : Map[Int64, Bool] = Map([])
  let mut line_no = 0
  for raw in text.split("\n") {
    line_no += 1
    let line = raw.trim_end(chars="\r").to_owned()
    if line.is_empty() {
      continue
    }
    if records.length() >= max_records {
      invalid(line_no, "record budget exceeded")
    }
    if line.contains("\t") {
      invalid(line_no, "tabs cannot encode kernel wait depth")
    }
    guard line.find(":") is Some(colon) else {
      invalid(line_no, "missing ordinal colon")
      continue
    }
    let ordinal = nonnegative(line[:colon].to_owned(), line_no)
    if ordinal == 0L {
      invalid(line_no, "ordinal must be positive")
    }
    let tail = line[colon + 1:].to_owned()
    let mut spaces = 0
    for c in tail {
      if c != ' ' {
        break
      }
      spaces += 1
    }
    if spaces == 0 {
      invalid(line_no, "missing space after ordinal")
    }
    let body = tail[spaces:].to_owned()
    let blocked = body.has_prefix("-> ")
    let depth = if blocked { spaces } else { 0 }
    if depth > max_depth {
      invalid(line_no, "wait depth budget exceeded")
    }
    let fields = words(if blocked { body[3:].to_owned() } else { body })
    guard fields is [kind_text, state, mode_text, pid, file, start, end] else {
      invalid(line_no, "expected seven lock fields after ordinal/arrow")
      continue
    }
    let kind = match kind_text {
      "POSIX" => Posix
      "FLOCK" => Flock
      "OFDLCK" => Ofd
      "LEASE" => Lease
      _ => Other(kind_text)
    }
    let mode = match mode_text {
      "READ" => Read
      "WRITE" => Write
      "UNLCK" => Unlock
      _ => Other(mode_text)
    }
    let owner = if pid == "-1" {
      OpenDescription
    } else {
      let n = unsigned(pid, 10UL, 2147483647UL, line_no).to_int()
      if n == 0 {
        NotVisible
      } else {
        Process(n)
      }
    }
    let parent = if blocked {
      if depth > stack.length() {
        invalid(line_no, "orphan waiter or skipped wait depth")
      }
      let index = stack[depth - 1]
      if records[index].ordinal != ordinal {
        invalid(line_no, "waiter ordinal differs from its tree")
      }
      Some(index)
    } else {
      if roots.contains(ordinal) {
        invalid(line_no, "duplicate root ordinal")
      }
      roots[ordinal] = true
      None
    }
    let entry : Entry = {
      line: line_no,
      ordinal,
      depth,
      kind,
      state,
      mode,
      owner,
      file: parse_file(file, line_no),
      range: range_at(start, end, line_no),
      parent_line: parent.map(i => records[i].line),
    }
    // The next sibling replaces this stack slot; ancestors remain intact.
    while stack.length() > depth {
      ignore(stack.pop())
    }
    stack.push(records.length())
    records.push(entry)
    parents.push(parent)
  }
  { records, parents, }
}