///|
// Traverse values directly: native Go manifests read visible fields, so an
// object with no visible fields does not force assertions or hidden values.
fn Vm::manifest_json_ex(
  self : Vm,
  value : Value,
  style : (String, String, String),
  current_indent : String,
  depth : Int,
  limit : Int,
  loc : Location,
  path? : Array[String] = [],
) -> String raise {
  self.tick(loc)
  if depth >= self.limits.depth {
    raise problem(Limit, loc, "manifest depth limit exceeded")
  }
  let (indent, newline, key_separator) = style
  let output = StringBuilder()
  let mut size = 0
  let write = fn(s : String) raise {
    let bytes = @utf8.encode(s).length()
    if bytes > limit - size {
      raise problem(Limit, loc, "rendered JSON string exceeds output limit")
    }
    size += bytes
    output.write_string(s)
  }
  match value {
    Nil => write("null")
    Bool(b) => write(if b { "true" } else { "false" })
    Num(n) => write(manifest_number(n))
    Str(s) => write(manifest_json_quote(s))
    Closure(_, _, _) | Builtin(_) | Native(_) =>
      raise problem(
        Runtime,
        loc,
        "tried to manifest function at [" + path.join(" ") + "]",
      )
    ArrayValue(items) => {
      write("[")
      write(newline)
      let next_indent = current_indent + indent
      for i, item in items {
        if i > 0 {
          write(",")
          write(newline)
        }
        write(next_indent)
        write(
          self.manifest_json_ex(
            self.force(item, loc),
            style,
            next_indent,
            depth + 1,
            limit - size,
            loc,
            path=path + [i.to_string()],
          ),
        )
      }
      write(newline)
      write(current_indent)
      write("]")
    }
    ObjectValue(id, top) => {
      write("{")
      write(newline)
      let next_indent = current_indent + indent
      for i, key in self.keys(id, top, false) {
        if i > 0 {
          write(",")
          write(newline)
        }
        write(next_indent)
        write(manifest_json_quote(key))
        write(key_separator)
        write(
          self.manifest_json_ex(
            self.field(id, top, key, loc),
            style,
            next_indent,
            depth + 1,
            limit - size,
            loc,
            path=path + [key],
          ),
        )
      }
      write(newline)
      write(current_indent)
      write("}")
    }
  }
  output.to_string()
}

///|
// encoding/json with EscapeHTML(false) still escapes JavaScript line separators.
fn manifest_json_quote(s : String) -> String {
  Json::string(s)
  .stringify()
  .replace(old="\u2028", new="\\u2028")
  .replace(old="\u2029", new="\\u2029")
}