///|
pub fn render_duplicate_functions_json(
  report : DuplicateFunctionsReport,
) -> String {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-duplicate-function-bodies"))
  root.set("schema_version", int_json(1))
  root.set("file_size", int_json(report.file_size))
  root.set("sha256", Json::string(report.sha256))
  root.set("comparison_scope", Json::string(report.comparison_scope))
  let s = report.summary
  let summary : Map[String, Json] = Map([])
  summary.set("local_function_count", int_json(s.local_function_count))
  summary.set("duplicate_group_count", int_json(s.duplicate_group_count))
  summary.set("duplicate_member_count", int_json(s.duplicate_member_count))
  summary.set("singleton_function_count", int_json(s.singleton_function_count))
  summary.set("total_body_bytes", int_json(s.total_body_bytes))
  summary.set("duplicate_body_bytes", int_json(s.duplicate_body_bytes))
  summary.set("singleton_body_bytes", int_json(s.singleton_body_bytes))
  summary.set("total_prefix_bytes", int_json(s.total_prefix_bytes))
  summary.set("total_encoded_bytes", int_json(s.total_encoded_bytes))
  root.set("summary", Json::object(summary))
  root.set(
    "groups",
    Json::array(
      report.groups.map(fn(group) {
        let row : Map[String, Json] = Map([])
        row.set("sha256", Json::string(group.sha256))
        row.set("body_size", int_json(group.body_size))
        row.set("member_count", int_json(group.member_count))
        row.set("member_body_bytes", int_json(group.member_body_bytes))
        row.set("member_prefix_bytes", int_json(group.member_prefix_bytes))
        row.set("member_total_bytes", int_json(group.member_total_bytes))
        row.set(
          "members",
          Json::array(
            group.members.map(fn(definition) {
              let item : Map[String, Json] = Map([])
              item.set("index", int_json(definition.index))
              item.set("name", Json::string(definition.name))
              item.set("raw_name", path_optional_string(definition.raw_name))
              item.set("type_index", int_json(definition.type_index))
              item.set("offset", int_json(definition.offset))
              item.set("body_offset", int_json(definition.body_offset))
              item.set("body_size", int_json(definition.body_size))
              item.set("prefix_size", int_json(definition.prefix_size))
              item.set("total_size", int_json(definition.total_size))
              Json::object(item)
            }),
          ),
        )
        Json::object(row)
      }),
    ),
  )
  Json::object(root).stringify(indent=2) + "\n"
}

///|
pub fn render_duplicate_functions(
  report : DuplicateFunctionsReport,
  top : Int,
) -> String {
  let out = StringBuilder()
  out.write_string(
    "IDENTICAL FUNCTION BODIES\n\n" + report.comparison_scope + "\n",
  )
  out.write_string(
    "Input: \{report.file_size} bytes; SHA-256 \{report.sha256}\n",
  )
  let s = report.summary
  out.write_string("\nLOCAL FUNCTION BYTE LEDGER\n")
  out.write_string(
    "Local definitions: \{s.local_function_count} = duplicate-group members \{s.duplicate_member_count} + singletons \{s.singleton_function_count}\n",
  )
  out.write_string("Duplicate groups: \{s.duplicate_group_count}\n")
  out.write_string(
    "Body bytes: \{s.total_body_bytes} B = duplicate-group members \{s.duplicate_body_bytes} B + singletons \{s.singleton_body_bytes} B\n",
  )
  out.write_string(
    "Encoded bytes: \{s.total_encoded_bytes} B = body \{s.total_body_bytes} B + length prefixes \{s.total_prefix_bytes} B\n",
  )
  let shown = top.max(0).min(report.groups.length())
  out.write_string(
    "\nShowing \{shown} of \{report.groups.length()} duplicate groups; Top limits text only.\n",
  )
  if report.groups.is_empty() {
    out.write_string("No identical function bodies found.\n")
  }
  for i in 0..