///|
fn function_instruction_range_json(range : FunctionInstructionRange) -> Json {
  let item : Map[String, Json] = Map([])
  item.set("file_offset", int_json(range.file_offset))
  item.set("size", int_json(range.size))
  Json::object(item)
}

///|
fn function_instructions_json_value(
  report : FunctionInstructionsReport,
) -> Json {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-function-instructions"))
  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("index", int_json(report.index))
  root.set("name", Json::string(report.name))
  root.set("raw_name", path_optional_string(report.raw_name))
  root.set("entry", function_instruction_range_json(report.entry))
  root.set("prefix", function_instruction_range_json(report.prefix))
  root.set("body", function_instruction_range_json(report.body))
  root.set(
    "locals_encoding",
    function_instruction_range_json(report.locals_encoding),
  )
  root.set("instruction_bytes", int_json(report.instruction_bytes))
  root.set("instruction_count", int_json(report.instruction_count))
  root.set("scope", Json::string(report.scope))
  root.set(
    "groups",
    Json::array(
      report.groups.map(fn(group) {
        let row : Map[String, Json] = Map([])
        row.set("mnemonic", Json::string(group.mnemonic))
        row.set("count", int_json(group.count))
        row.set("encoded_bytes", int_json(group.encoded_bytes))
        Json::object(row)
      }),
    ),
  )
  root.set(
    "instructions",
    Json::array(
      report.instructions.map(fn(instruction) {
        let row : Map[String, Json] = Map([])
        row.set("ordinal", int_json(instruction.ordinal))
        row.set("mnemonic", Json::string(instruction.mnemonic))
        row.set("file_offset", int_json(instruction.file_offset))
        row.set(
          "body_relative_offset",
          int_json(instruction.body_relative_offset),
        )
        row.set("encoded_size", int_json(instruction.encoded_size))
        Json::object(row)
      }),
    ),
  )
  Json::object(root)
}

///|
pub fn render_function_instructions_json(
  report : FunctionInstructionsReport,
) -> String {
  function_instructions_json_value(report).stringify(indent=2) + "\n"
}

///|
pub fn render_function_instructions(
  report : FunctionInstructionsReport,
  top : Int,
) -> String {
  let out = StringBuilder()
  out.write_string("FUNCTION INSTRUCTION ENCODING\n\n")
  out.write_string(
    "Input: \{report.file_size} bytes; SHA-256 \{report.sha256}\n",
  )
  out.write_string(
    "Function \{report.index}: \{report.name}; raw name: \{report.raw_name.unwrap_or("(unavailable)")}\n",
  )
  for
    (label, range) in [
      ("Entry", report.entry),
      ("Length prefix", report.prefix),
      ("Body", report.body),
      ("Locals encoding", report.locals_encoding),
    ] {
    out.write_string(
      "\{label}: file offset \{range.file_offset}, size \{range.size} B\n",
    )
  }
  out.write_string(
    "\nBYTE LEDGER\nEncoded bytes: \{report.entry.size} B = prefix \{report.prefix.size} B + body \{report.body.size} B\n",
  )
  out.write_string(
    "Body bytes: \{report.body.size} B = locals encoding \{report.locals_encoding.size} B + instructions \{report.instruction_bytes} B\n",
  )
  out.write_string(
    "Instructions: \{report.instruction_count}; grouped bytes: \{report.instruction_bytes} B\n",
  )
  let groups = top.max(0).min(report.groups.length())
  out.write_string(
    "\nOPCODE GROUPS: showing \{groups} of \{report.groups.length()} (bytes descending, count descending, mnemonic ascending)\nMNEMONIC  COUNT  ENCODED B\n",
  )
  for i in 0..