///|
/// Counting result for a set of files of one language.
pub(all) struct Counts {
  mut files : Int32
  mut lines : Int32
  mut code : Int32
  mut comments : Int32
  mut blanks : Int32
  blobs : Map[String, Counts]
}

///|
pub fn Counts::add(self : Counts, other : Counts) -> Unit {
  self.files += other.files
  self.lines += other.lines
  self.code += other.code
  self.comments += other.comments
  self.blanks += other.blanks
  for kv in other.blobs {
    match self.blobs.get(kv.0) {
      Some(existing) => existing.add(kv.1)
      None => self.blobs.set(kv.0, kv.1.copy())
    }
  }
}

///|
/// Aggregation must not mutate a per-file report that shares this value.
pub fn Counts::copy(self : Counts) -> Counts {
  let blobs : Map[String, Counts] = Map([])
  for kv in self.blobs {
    blobs.set(kv.0, kv.1.copy())
  }
  {
    files: self.files,
    lines: self.lines,
    code: self.code,
    comments: self.comments,
    blanks: self.blanks,
    blobs,
  }
}

///|
pub fn Counts::new() -> Counts {
  { files: 0, lines: 0, code: 0, comments: 0, blanks: 0, blobs: Map([]), }
}

///|
priv enum Tok {
  LineTok // line comment starter (includes doc comments)
  BlockTok(String, String) // block comment (start, end)
  StrTok(Int32) // index into the merged string token table
}

///|
priv enum St {
  Normal
  InBlock(Int32, String, String) // depth, current start marker, end marker
  InStr(Int32) // index into merged string token table
}

///|
priv struct StrTokDef {
  start : String
  end : String
  escapable : Bool
}

///|
priv struct Matcher {
  line_toks : Array[String] // sorted by length desc
  block_toks : Array[(String, String)] // sorted by start length desc
  str_toks : Array[StrTokDef] // sorted by start length desc
}

///|
fn build_matcher(lang : @lang.LangDef) -> Matcher {
  let line_toks : Array[String] = []
  for t in lang.line_comments {
    line_toks.push(normalize_token(t))
  }
  // `important_syntax` marks language-specific constructs such as HTML
  //