// A JSON rendering of the AST, for snapshot tests.
//
// The differential harness cannot check the SHAPE of the tree until the
// printer lands: accept/reject says nothing about whether the right nodes were
// built. These snapshots stand in until then, and remain useful afterwards as
// the readable form of an intentional AST change.
//
// Spans are rendered through @basic.show_loc, so a snapshot can be taken with
// them hidden (readable, low-churn) or shown (when a span is what is under
// test).

///|
fn tagged(kind : String, children : Map[String, Json]) -> Json {
  { "kind": kind, "children": children }
}

///|
fn[T] arr(xs : Array[T], f : (T) -> Json) -> Json {
  let out = []
  for x in xs {
    out.push(f(x))
  }
  Json::array(out)
}

///|
fn[T] opt(x : T?, f : (T) -> Json) -> Json {
  match x {
    Some(v) => f(v)
    None => Json::null()
  }
}

///|
pub fn Ident::to_json(self : Ident) -> Json {
  Json::string(self.name)
}

///|
pub fn valtype_json(t : ValType) -> Json {
  match t {
    I32 => "i32"
    I64 => "i64"
    F32 => "f32"
    F64 => "f64"
    V128 => "v128"
    Ref(r) =>
      return {
        "ref": heaptype_json(r.typ),
        "nullable": Json::boolean(r.nullable),
      }
  }
}

///|
pub fn heaptype_json(h : HeapType) -> Json {
  match h.keyword() {
    Some(k) => Json::string(k)
    None =>
      match h {
        Type(t) => { "type": t.to_json() }
        Exact(t) => { "exact": t.to_json() }
        _ => Json::string("?")
      }
  }
}

///|
pub fn FuncType::to_json(self : FuncType) -> Json {
  {
    "params": arr(self.params, p => {
      "name": opt(p.desc.0, i => i.to_json()),
      "type": valtype_json(p.desc.1),
    }),
    "results": arr(self.results, t => valtype_json(t)),
  }
}

///|
/// The instruction tree.
///
/// Deliberately shallow in places -- a node's own kind and its children, not
/// every field -- because these snapshots exist to catch a wrong SHAPE, and a
/// full dump would churn on every unrelated change.
pub fn[Info] Instr::to_json(self : Instr[Info]) -> Json {
  self.desc.to_json()
}

///|
fn[Info] body_json(b : Body[Info]) -> Json {
  arr(b.desc, i => i.to_json())
}

///|
pub fn[Info] InstrDesc::to_json(self : InstrDesc[Info]) -> Json {
  match self {
    Unreachable => Json::string("unreachable")
    Nop => Json::string("nop")
    Hole => Json::string("_")
    Null => Json::string("null")
    Get(x) => tagged("get", { "name": x.to_json() })
    Path(a, b) => tagged("path", { "ns": a.to_json(), "member": b.to_json() })
    Int(s) => tagged("int", { "raw": Json::string(s) })
    Float(s) => tagged("float", { "raw": Json::string(s) })
    Char(c) => tagged("char", { "value": Json::string(c.to_string()) })
    Str(t, s) =>
      tagged("string", {
        "prefix": opt(t, i => i.to_json()),
        "value": bytes_json(s),
      })
    Block(label~, block~, ..) =>
      tagged("block", {
        "label": opt(label, l => l.to_json()),
        "body": body_json(block),
      })
    Loop(label~, block~, ..) =>
      tagged("loop", {
        "label": opt(label, l => l.to_json()),
        "body": body_json(block),
      })
    While(label~, cond~, step~, block~) =>
      tagged("while", {
        "label": opt(label, l => l.to_json()),
        "cond": cond.to_json(),
        "step": opt(step, s => s.to_json()),
        "body": body_json(block),
      })
    If(label~, cond~, if_block~, else_block~, ..) =>
      tagged("if", {
        "label": opt(label, l => l.to_json()),
        "cond": cond.to_json(),
        "then": body_json(if_block),
        "else": opt(else_block, b => body_json(b)),
      })
    Let(binds, init) =>
      tagged("let", {
        "bindings": arr(binds, b => {
          "name": opt(b.0, i => i.to_json()),
          "type": opt(b.1, t => valtype_json(t)),
        }),
        "init": opt(init, i => i.to_json()),
      })
    Set(x, op, v) =>
      tagged("set", {
        "name": x.to_json(),
        "op": opt(op, o => Json::string(binop_name(o.desc))),
        "value": v.to_json(),
      })
    Tee(x, v) => tagged("tee", { "name": x.to_json(), "value": v.to_json() })
    Call(f, args) =>
      tagged("call", {
        "callee": f.to_json(),
        "args": arr(args, a => a.to_json()),
      })
    TailCall(f, args) =>
      tagged("become", {
        "callee": f.to_json(),
        "args": arr(args, a => a.to_json()),
      })
    BinOpI(op, l, r) =>
      tagged("binop", {
        "op": Json::string(binop_name(op.desc)),
        "lhs": l.to_json(),
        "rhs": r.to_json(),
      })
    UnOpI(op, v) =>
      tagged("unop", {
        "op": Json::string(
          match op.desc {
            Neg => "-"
            Pos => "+"
            Not => "!"
          },
        ),
        "operand": v.to_json(),
      })
    Return(v) => tagged("return", { "value": opt(v, i => i.to_json()) })
    Br(l, v) =>
      tagged("br", { "label": l.to_json(), "value": opt(v, i => i.to_json()) })
    BrIf(l, v) => tagged("br_if", { "label": l.to_json(), "cond": v.to_json() })
    Sequence(xs) => tagged("sequence", { "items": arr(xs, i => i.to_json()) })
    Select(c, a, b) =>
      tagged("select", {
        "cond": c.to_json(),
        "then": a.to_json(),
        "else": b.to_json(),
      })
    StructGet(e, f) =>
      tagged("struct.get", { "object": e.to_json(), "field": f.to_json() })
    Struct(t, fields) =>
      tagged("struct", {
        "type": opt(t, i => i.to_json()),
        "fields": arr(fields, f => {
          "name": f.0.to_json(),
          // None is the punning shorthand `{x}`; kept distinguishable.
          "value": opt(f.1, v => v.to_json()),
        }),
      })
    Cast(e, _) => tagged("cast", { "operand": e.to_json() })
    Test(e, _) => tagged("test", { "operand": e.to_json() })
    NonNull(e) => tagged("non_null", { "operand": e.to_json() })
    // Everything else renders as its constructor name alone: these snapshots
    // are for shape, and spelling out all 67 arms would churn without adding
    // signal.
    _ => Json::string("...")
  }
}

///|
fn binop_name(op : BinOp) -> String {
  match op {
    Add => "+"
    Sub => "-"
    Mul => "*"
    Div(None) => "/"
    Div(Some(Signed)) => "/s"
    Div(Some(Unsigned)) => "/u"
    Rem(Signed) => "%s"
    Rem(Unsigned) => "%u"
    And => "&"
    Or => "|"
    Xor => "^"
    Shl => "<<"
    Shr(Signed) => ">>s"
    Shr(Unsigned) => ">>u"
    Eq => "=="
    Ne => "!="
    Lt(None) => "<"
    Lt(Some(Signed)) => " " ">"
    Gt(Some(Signed)) => ">s"
    Gt(Some(Unsigned)) => ">u"
    Le(None) => "<="
    Le(Some(Signed)) => "<=s"
    Le(Some(Unsigned)) => "<=u"
    Ge(None) => ">="
    Ge(Some(Signed)) => ">=s"
    Ge(Some(Unsigned)) => ">=u"
  }
}

///|
pub fn[Info] ModuleField::to_json(self : ModuleField[Info]) -> Json {
  match self {
    Func(name~, body~, attributes~, ..) =>
      tagged("fn", {
        "name": name.to_json(),
        "attrs": arr(attributes, a => Json::string(a.attr_name)),
        "label": opt(body.0, l => l.to_json()),
        "body": arr(body.1, i => i.to_json()),
      })
    Global(name~, mut_~, def~, attributes~, ..) =>
      tagged("global", {
        "name": name.to_json(),
        "mut": Json::boolean(mut_),
        "attrs": arr(attributes, a => Json::string(a.attr_name)),
        "value": def.to_json(),
      })
    Type(_) => tagged("type", {})
    Tag(name~, ..) => tagged("tag", { "name": name.to_json() })
    Memory(name~, ..) => tagged("memory", { "name": name.to_json() })
    Data(name~, ..) => tagged("data", { "name": opt(name, i => i.to_json()) })
    Table(name~, ..) => tagged("table", { "name": name.to_json() })
    Elem(name~, ..) => tagged("elem", { "name": name.to_json() })
    Import(module_~, decl~) =>
      tagged("import", {
        "module": bytes_json(module_.desc),
        "name": decl.desc.id.to_json(),
      })
    ImportGroup(module_~, decls~) =>
      tagged("import_group", {
        "module": bytes_json(module_.desc),
        "items": arr(decls, d => d.desc.id.to_json()),
      })
    ModuleAnnotation(attrs) =>
      tagged("module_annotation", {
        "attrs": arr(attrs, a => Json::string(a.attr_name)),
      })
    Conditional(then_fields~, else_fields~, ..) =>
      tagged("conditional", {
        "then": arr(then_fields.desc, f => f.desc.to_json()),
        "else": opt(else_fields, e => arr(e.desc, f => f.desc.to_json())),
      })
  }
}

///|
/// The whole module.
pub fn[Info] module_to_json(m : Module[Info]) -> Json {
  arr(m, f => f.desc.to_json())
}

///|
/// A byte string, rendered for a snapshot.
///
/// Valid UTF-8 renders as its text, so the common case stays readable in a
/// diff. Anything else renders as `hex:<...>`, because a snapshot that decoded
/// binary lossily would compare equal for two segments that differ.
fn bytes_json(b : Bytes) -> Json {
  match (Some(@utf8.decode(b[:])) catch { _ => None }) {
    Some(t) => Json::string(t)
    None => {
      let out = StringBuilder::new()
      out.write_string("hex:")
      for byte in b {
        let digits = "0123456789abcdef"
        let n = byte.to_int()
        out.write_char(
          digits.unsafe_get((n >> 4) & 0xF).to_int().unsafe_to_char(),
        )
        out.write_char(digits.unsafe_get(n & 0xF).to_int().unsafe_to_char())
      }
      Json::string(out.to_string())
    }
  }
}