// Canonical JSON for the AST: the schema of docs/ast-json.md. `canonical`
// writes objects with their keys in code-unit order (MoonBit's `Map` keeps
// insertion order and its `String` compare is length-first, so neither is
// used). The Rust oracle (tools/gleam-ast-oracle) writes the same bytes.
//
// Leaves (spans, strings, numbers, payload-free enums) build their JSON
// directly. Nodes (`enc_*`) run on the step trampoline of `step.mbt`: they
// take a continuation, enter a child only through `node`, `maybe` or
// `each`, and hand their object to the continuation through `give`.

///|
/// An object; `canonical` sorts the keys when it writes.
fn obj(fields : Array[(String, Json)]) -> Json {
  Json::object(Map::from_array(fields))
}

///|
fn num(n : Int32) -> Json {
  Json::number(n.to_double())
}

///|
/// A `u64` of the compiler, written with its exact digits (a `Double`
/// alone loses precision above 2^53).
fn num64(u : UInt64) -> Json {
  Json::number(u.to_double(), repr=u.to_string())
}

///|
fn str(s : String) -> Json {
  Json::string(s)
}

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

///|
fn[T] list(xs : ArrayView[T], f : (T) -> Json) -> Json {
  Json::array(xs.iter().map(f).collect())
}

///|
fn arr(xs : ArrayView[Json]) -> Json {
  Json::array(xs.to_owned())
}

///|
/// The canonical text of an encoded value: keys in sorted (byte) order at
/// every level, no whitespace, serde_json's string escaping (`\"`, `\\`,
/// `\b`, `\f`, `\n`, `\r`, `\t`, other controls as `\u00xx`, everything
/// else raw). Map order is not relied on. Writes through the step
/// trampoline, so a value of any depth is written without recursion; the
/// builder is local to this call.
pub fn canonical(json : Json) -> String {
  let sb = StringBuilder()
  run(write_canonical(sb, json, () => Done(())))
  sb.to_string()
}

///|
/// Write one value, then continue with `k`. A continuation is never
/// called here: it is returned as `More(k)` for `run` to execute.
fn write_canonical(
  sb : StringBuilder,
  json : Json,
  k : () -> Step[Unit],
) -> Step[Unit] {
  match json {
    Null => {
      sb.write_string("null")
      More(k)
    }
    True => {
      sb.write_string("true")
      More(k)
    }
    False => {
      sb.write_string("false")
      More(k)
    }
    Number(d, repr~) => {
      // Byte offsets and small values fit a Double; a `u64` carries its
      // exact digits in `repr`.
      if repr is Some(digits) {
        sb.write_string(digits)
      } else if d == d.floor() && d.abs() < 9007199254740992.0 {
        sb.write_string(d.to_int64().to_string())
      } else {
        sb.write_string(d.to_string())
      }
      More(k)
    }
    String(s) => {
      write_json_string(sb, s)
      More(k)
    }
    Array(items) => {
      sb.write_char('[')
      write_items(sb, items[:], 0, () => {
        sb.write_char(']')
        More(k)
      })
    }
    Object(m) => {
      let keys = m.keys().collect()
      keys.sort_by(byte_order)
      sb.write_char('{')
      write_fields(sb, m, keys[:], 0, () => {
        sb.write_char('}')
        More(k)
      })
    }
  }
}

///|
fn write_items(
  sb : StringBuilder,
  items : ArrayView[Json],
  i : Int32,
  k : () -> Step[Unit],
) -> Step[Unit] {
  if i == items.length() {
    More(k)
  } else {
    if i > 0 {
      sb.write_char(',')
    }
    More(() => {
      write_canonical(sb, items[i], () => write_items(sb, items, i + 1, k))
    })
  }
}

///|
fn write_fields(
  sb : StringBuilder,
  m : Map[String, Json],
  keys : ArrayView[String],
  i : Int32,
  k : () -> Step[Unit],
) -> Step[Unit] {
  if i == keys.length() {
    More(k)
  } else {
    if i > 0 {
      sb.write_char(',')
    }
    write_json_string(sb, keys[i])
    sb.write_char(':')
    More(() => {
      write_canonical(sb, m.get(keys[i]).unwrap(), () => {
        write_fields(sb, m, keys, i + 1, k)
      })
    })
  }
}

///|
/// Keys compared code unit by code unit, as serde_json's `BTreeMap`
/// compares bytes (the keys are ASCII). MoonBit's `String` `Compare`
/// orders by length first, so it is not used.
fn byte_order(a : String, b : String) -> Int32 {
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  for i = 0; i < n; i = i + 1 {
    let d = a.code_unit_at(i).to_int() - b.code_unit_at(i).to_int()
    if d != 0 {
      return d
    }
  }
  a.length() - b.length()
}

///|
fn write_json_string(sb : StringBuilder, s : String) -> Unit {
  let hex = "0123456789abcdef"
  sb.write_char('"')
  for i = 0; i < s.length(); i = i + 1 {
    let c = s.code_unit_at(i).to_int()
    if c == '"'.to_int() {
      sb.write_string("\\\"")
    } else if c == '\\'.to_int() {
      sb.write_string("\\\\")
    } else if c == 0x08 {
      sb.write_string("\\b")
    } else if c == 0x0C {
      sb.write_string("\\f")
    } else if c == 0x0A {
      sb.write_string("\\n")
    } else if c == 0x0D {
      sb.write_string("\\r")
    } else if c == 0x09 {
      sb.write_string("\\t")
    } else if c < 0x20 {
      sb.write_string("\\u00")
      sb.write_char(hex.code_unit_at(c / 16).to_int().unsafe_to_char())
      sb.write_char(hex.code_unit_at(c % 16).to_int().unsafe_to_char())
    } else {
      sb.write_char(c.unsafe_to_char())
    }
  }
  sb.write_char('"')
}

///|
pub fn encode_span(s : @span.SrcSpan) -> Json {
  obj([("start", num(s.start)), ("end", num(s.end))])
}

///|
fn spanned_string(s : SpannedString) -> Json {
  obj([("location", encode_span(s.location)), ("name", str(s.name))])
}

///|
fn documentation(d : Documentation) -> Json {
  obj([("start", num(d.start)), ("text", str(d.text))])
}

///|
fn external(e : External) -> Json {
  obj([
    ("module", str(e.module_)),
    ("function", str(e.function)),
    ("location", encode_span(e.location)),
  ])
}

///|
pub fn encode_publicity(p : Publicity) -> Json {
  match p {
    Public => obj([("kind", str("public"))])
    Private => obj([("kind", str("private"))])
    Internal(attribute_location~) =>
      obj([
        ("kind", str("internal")),
        ("attribute_location", opt(attribute_location, encode_span)),
      ])
  }
}

///|
fn deprecation(d : Deprecation) -> Json {
  match d {
    NotDeprecated => obj([("kind", str("not_deprecated"))])
    Deprecated(message~) =>
      obj([("kind", str("deprecated")), ("message", str(message))])
  }
}

///|
fn assign_name(a : AssignName) -> Json {
  match a {
    Variable(name) => obj([("kind", str("variable")), ("name", str(name))])
    Discard(name) => obj([("kind", str("discard")), ("name", str(name))])
  }
}

///|
pub fn encode_parsed(p : Parsed) -> Json {
  run(enc_parsed(p, j => Done(j)))
}

///|
fn[R] enc_parsed(p : Parsed, k : (Json) -> Step[R]) -> Step[R] {
  node(p.module_, enc_module, module_ => {
    give(
      k,
      obj([
        ("module", module_),
        ("extra", encode_extra(p.extra)),
        ("warnings", list(p.warnings, encode_warning)),
      ]),
    )
  })
}

///|
pub fn encode_module(m : Module) -> Json {
  run(enc_module(m, j => Done(j)))
}

///|
fn[R] enc_module(m : Module, k : (Json) -> Step[R]) -> Step[R] {
  each(m.definitions, enc_targeted_definition, definitions => {
    give(k, obj([("definitions", arr(definitions))]))
  })
}

///|
pub fn encode_targeted_definition(d : TargetedDefinition) -> Json {
  run(enc_targeted_definition(d, j => Done(j)))
}

///|
fn[R] enc_targeted_definition(
  d : TargetedDefinition,
  k : (Json) -> Step[R],
) -> Step[R] {
  node(d.definition, enc_definition, definition => {
    give(
      k,
      obj([
        ("definition", definition),
        ("target", opt(d.target, t => str(t.name()))),
      ]),
    )
  })
}

///|
pub fn encode_definition(d : Definition) -> Json {
  run(enc_definition(d, j => Done(j)))
}

///|
fn[R] enc_definition(d : Definition, k : (Json) -> Step[R]) -> Step[R] {
  match d {
    Function(f) => enc_function(f, "function", k)
    TypeAlias(t) =>
      node(t.type_ast, enc_type_ast, type_ast => {
        give(
          k,
          obj([
            ("kind", str("type_alias")),
            ("location", encode_span(t.location)),
            ("alias", str(t.alias_)),
            ("name_location", encode_span(t.name_location)),
            ("parameters", list(t.parameters, spanned_string)),
            ("type_ast", type_ast),
            ("publicity", encode_publicity(t.publicity)),
            ("documentation", opt(t.documentation, documentation)),
            ("deprecation", deprecation(t.deprecation)),
          ]),
        )
      })
    CustomType(c) =>
      each(c.constructors, enc_record_constructor, constructors => {
        give(
          k,
          obj([
            ("kind", str("custom_type")),
            ("location", encode_span(c.location)),
            ("end_position", num(c.end_position)),
            ("name", str(c.name)),
            ("name_location", encode_span(c.name_location)),
            ("publicity", encode_publicity(c.publicity)),
            ("constructors", arr(constructors)),
            ("documentation", opt(c.documentation, documentation)),
            ("deprecation", deprecation(c.deprecation)),
            ("opaque", Json::boolean(c.opaque_)),
            ("parameters", list(c.parameters, spanned_string)),
            ("external_erlang", opt(c.external_erlang, external)),
            ("external_javascript", opt(c.external_javascript, external)),
          ]),
        )
      })
    Import(i) =>
      give(
        k,
        obj([
          ("kind", str("import")),
          ("documentation", opt(i.documentation, str)),
          ("location", encode_span(i.location)),
          ("module_location", encode_span(i.module_location)),
          ("module", str(i.module_)),
          (
            "as_name",
            opt(i.as_name, a => {
              obj([
                ("name", assign_name(a.name)),
                ("location", encode_span(a.location)),
              ])
            }),
          ),
          ("unqualified_values", list(i.unqualified_values, unqualified_import)),
          ("unqualified_types", list(i.unqualified_types, unqualified_import)),
        ]),
      )
    ModuleConstant(c) =>
      maybe(c.annotation, enc_type_ast, annotation => {
        node(c.value, enc_constant, value => {
          give(
            k,
            obj([
              ("kind", str("module_constant")),
              ("documentation", opt(c.documentation, documentation)),
              ("location", encode_span(c.location)),
              ("publicity", encode_publicity(c.publicity)),
              ("name", str(c.name)),
              ("name_location", encode_span(c.name_location)),
              ("annotation", annotation),
              ("value", value),
              ("deprecation", deprecation(c.deprecation)),
            ]),
          )
        })
      })
  }
}

///|
fn[R] enc_function(
  f : Function,
  kind : String,
  k : (Json) -> Step[R],
) -> Step[R] {
  each(f.arguments, enc_arg, arguments => {
    each(f.body, enc_statement, body => {
      maybe(f.return_annotation, enc_type_ast, return_annotation => {
        give(
          k,
          obj([
            ("kind", str(kind)),
            ("location", encode_span(f.location)),
            ("body_start", opt(f.body_start, num)),
            ("end_position", num(f.end_position)),
            ("name", opt(f.name, spanned_string)),
            ("arguments", arr(arguments)),
            ("body", arr(body)),
            ("publicity", encode_publicity(f.publicity)),
            ("deprecation", deprecation(f.deprecation)),
            ("return_annotation", return_annotation),
            ("documentation", opt(f.documentation, documentation)),
            ("external_erlang", opt(f.external_erlang, external)),
            ("external_javascript", opt(f.external_javascript, external)),
          ]),
        )
      })
    })
  })
}

///|
fn unqualified_import(u : UnqualifiedImport) -> Json {
  obj([
    ("location", encode_span(u.location)),
    ("name", str(u.name)),
    ("name_position", num(u.name_position)),
    ("as_name", opt(u.as_name, str)),
  ])
}

///|
fn[R] enc_record_constructor(
  r : RecordConstructor,
  k : (Json) -> Step[R],
) -> Step[R] {
  each(r.arguments, enc_record_constructor_arg, arguments => {
    give(
      k,
      obj([
        ("location", encode_span(r.location)),
        ("name_location", encode_span(r.name_location)),
        ("name", str(r.name)),
        ("arguments", arr(arguments)),
        ("documentation", opt(r.documentation, documentation)),
        ("deprecation", deprecation(r.deprecation)),
      ]),
    )
  })
}

///|
fn[R] enc_record_constructor_arg(
  a : RecordConstructorArg,
  k : (Json) -> Step[R],
) -> Step[R] {
  node(a.ast, enc_type_ast, ast => {
    give(
      k,
      obj([
        ("label", opt(a.label, spanned_string)),
        ("ast", ast),
        ("location", encode_span(a.location)),
        ("doc", opt(a.doc, documentation)),
      ]),
    )
  })
}

///|
fn[R] enc_arg(a : Arg, k : (Json) -> Step[R]) -> Step[R] {
  maybe(a.annotation, enc_type_ast, annotation => {
    give(
      k,
      obj([
        ("names", arg_names(a.names)),
        ("location", encode_span(a.location)),
        ("annotation", annotation),
      ]),
    )
  })
}

///|
fn arg_names(n : ArgNames) -> Json {
  match n {
    Discard(name~, location~) =>
      obj([
        ("kind", str("discard")),
        ("name", str(name)),
        ("location", encode_span(location)),
      ])
    LabelledDiscard(label~, label_location~, name~, name_location~) =>
      obj([
        ("kind", str("labelled_discard")),
        ("label", str(label)),
        ("label_location", encode_span(label_location)),
        ("name", str(name)),
        ("name_location", encode_span(name_location)),
      ])
    Named(name~, location~) =>
      obj([
        ("kind", str("named")),
        ("name", str(name)),
        ("location", encode_span(location)),
      ])
    NamedLabelled(label~, label_location~, name~, name_location~) =>
      obj([
        ("kind", str("named_labelled")),
        ("label", str(label)),
        ("label_location", encode_span(label_location)),
        ("name", str(name)),
        ("name_location", encode_span(name_location)),
      ])
  }
}

///|
pub fn encode_type_ast(t : TypeAst) -> Json {
  run(enc_type_ast(t, j => Done(j)))
}

///|
fn[R] enc_type_ast(t : TypeAst, k : (Json) -> Step[R]) -> Step[R] {
  match t {
    Constructor(c) =>
      each(c.arguments, enc_type_ast, arguments => {
        give(
          k,
          obj([
            ("kind", str("constructor")),
            ("location", encode_span(c.location)),
            ("name", type_ast_constructor_name(c.name)),
            ("arguments", arr(arguments)),
            ("start_parentheses", opt(c.start_parentheses, num)),
          ]),
        )
      })
    Fn(f) =>
      each(f.arguments, enc_type_ast, arguments => {
        node(f.return_, enc_type_ast, return_ => {
          give(
            k,
            obj([
              ("kind", str("fn")),
              ("location", encode_span(f.location)),
              ("arguments", arr(arguments)),
              ("return_", return_),
            ]),
          )
        })
      })
    Var(v) =>
      give(
        k,
        obj([
          ("kind", str("var")),
          ("location", encode_span(v.location)),
          ("name", str(v.name)),
        ]),
      )
    Tuple(t) =>
      each(t.elements, enc_type_ast, elements => {
        give(
          k,
          obj([
            ("kind", str("tuple")),
            ("location", encode_span(t.location)),
            ("elements", arr(elements)),
          ]),
        )
      })
    Hole(h) =>
      give(
        k,
        obj([
          ("kind", str("hole")),
          ("location", encode_span(h.location)),
          ("name", str(h.name)),
        ]),
      )
  }
}

///|
fn type_ast_constructor_name(n : TypeAstConstructorName) -> Json {
  match n {
    Qualified(module_~, module_location~, dot_location~, name~) =>
      obj([
        ("kind", str("qualified")),
        ("module", str(module_)),
        ("module_location", encode_span(module_location)),
        ("dot_location", num(dot_location)),
        ("name", opt(name, spanned_string)),
      ])
    Unqualified(name~, location~) =>
      obj([
        ("kind", str("unqualified")),
        ("name", str(name)),
        ("location", encode_span(location)),
      ])
  }
}

///|
pub fn encode_statement(s : Statement) -> Json {
  run(enc_statement(s, j => Done(j)))
}

///|
fn[R] enc_statement(s : Statement, k : (Json) -> Step[R]) -> Step[R] {
  match s {
    Expression(e) =>
      node(e, enc_expr, expression => {
        give(k, obj([("kind", str("expression")), ("expression", expression)]))
      })
    Assignment(a) =>
      node(a.value, enc_expr, value => {
        node(a.pattern, enc_pattern, pattern => {
          node(a.kind, enc_assignment_kind, assignment_kind => {
            maybe(a.annotation, enc_type_ast, annotation => {
              give(
                k,
                obj([
                  ("kind", str("assignment")),
                  ("location", encode_span(a.location)),
                  ("value", value),
                  ("pattern", pattern),
                  ("assignment_kind", assignment_kind),
                  ("annotation", annotation),
                ]),
              )
            })
          })
        })
      })
    Use(u) =>
      node(u.call, enc_expr, call => {
        each(u.assignments, enc_use_assignment, assignments => {
          give(
            k,
            obj([
              ("kind", str("use")),
              ("call", call),
              ("location", encode_span(u.location)),
              (
                "right_hand_side_location",
                encode_span(u.right_hand_side_location),
              ),
              ("assignments_location", encode_span(u.assignments_location)),
              ("assignments", arr(assignments)),
            ]),
          )
        })
      })
    Assert(a) =>
      node(a.value, enc_expr, value => {
        maybe(a.message, enc_expr, message => {
          give(
            k,
            obj([
              ("kind", str("assert")),
              ("location", encode_span(a.location)),
              ("value", value),
              ("message", message),
            ]),
          )
        })
      })
  }
}

///|
/// `Assignment.kind` is written under `assignment_kind`, because `kind`
/// names the statement. The `Assert` arm's message is an expression, so
/// this is a node: the statement encoder enters it through `node`.
fn[R] enc_assignment_kind(a : AssignmentKind, k : (Json) -> Step[R]) -> Step[R] {
  match a {
    Let => give(k, obj([("kind", str("let"))]))
    Generated => give(k, obj([("kind", str("generated"))]))
    Assert(location~, assert_keyword_start~, message~) =>
      maybe(message, enc_expr, message => {
        give(
          k,
          obj([
            ("kind", str("assert")),
            ("location", encode_span(location)),
            ("assert_keyword_start", num(assert_keyword_start)),
            ("message", message),
          ]),
        )
      })
  }
}

///|
fn[R] enc_use_assignment(u : UseAssignment, k : (Json) -> Step[R]) -> Step[R] {
  node(u.pattern, enc_pattern, pattern => {
    maybe(u.annotation, enc_type_ast, annotation => {
      give(
        k,
        obj([
          ("location", encode_span(u.location)),
          ("pattern", pattern),
          ("annotation", annotation),
        ]),
      )
    })
  })
}

///|
fn[A, R] enc_call_arg(
  a : CallArg[A],
  f : (A, (Json) -> Step[R]) -> Step[R],
  k : (Json) -> Step[R],
) -> Step[R] {
  node(a.value, f, value => {
    give(
      k,
      obj([
        ("label", opt(a.label, str)),
        ("location", encode_span(a.location)),
        ("value", value),
        ("implicit", opt(a.implicit, i => str(i.name()))),
      ]),
    )
  })
}

///|
fn[A, R] enc_record_being_updated(
  r : RecordBeingUpdated[A],
  f : (A, (Json) -> Step[R]) -> Step[R],
  k : (Json) -> Step[R],
) -> Step[R] {
  node(r.base, f, base => {
    give(k, obj([("base", base), ("location", encode_span(r.location))]))
  })
}

///|
fn[A, R] enc_record_update_arg(
  a : RecordUpdateArg[A],
  f : (A, (Json) -> Step[R]) -> Step[R],
  k : (Json) -> Step[R],
) -> Step[R] {
  node(a.value, f, value => {
    give(
      k,
      obj([
        ("label", str(a.label)),
        ("location", encode_span(a.location)),
        ("value", value),
      ]),
    )
  })
}

///|
fn[V, R] enc_bit_array_segment(
  s : BitArraySegment[V],
  f : (V, (Json) -> Step[R]) -> Step[R],
  k : (Json) -> Step[R],
) -> Step[R] {
  node(s.value, f, value => {
    each(s.options, (o, k2) => enc_bit_array_option(o, f, k2), options => {
      give(
        k,
        obj([
          ("location", encode_span(s.location)),
          ("value", value),
          ("options", arr(options)),
        ]),
      )
    })
  })
}

///|
fn[V, R] enc_bit_array_option(
  o : BitArrayOption[V],
  f : (V, (Json) -> Step[R]) -> Step[R],
  k : (Json) -> Step[R],
) -> Step[R] {
  let simple = (kind : String, location : @span.SrcSpan) => {
    give(k, obj([("kind", str(kind)), ("location", encode_span(location))]))
  }
  match o {
    Bytes(location~) => simple("bytes", location)
    Int(location~) => simple("int", location)
    Float(location~) => simple("float", location)
    Bits(location~) => simple("bits", location)
    Utf8(location~) => simple("utf8", location)
    Utf16(location~) => simple("utf16", location)
    Utf32(location~) => simple("utf32", location)
    Utf8Codepoint(location~) => simple("utf8_codepoint", location)
    Utf16Codepoint(location~) => simple("utf16_codepoint", location)
    Utf32Codepoint(location~) => simple("utf32_codepoint", location)
    Signed(location~) => simple("signed", location)
    Unsigned(location~) => simple("unsigned", location)
    Big(location~) => simple("big", location)
    Little(location~) => simple("little", location)
    Native(location~) => simple("native", location)
    Size(location~, value~, short_form~) =>
      node(value, f, value => {
        give(
          k,
          obj([
            ("kind", str("size")),
            ("location", encode_span(location)),
            ("value", value),
            ("short_form", Json::boolean(short_form)),
          ]),
        )
      })
    Unit(location~, value~) =>
      give(
        k,
        obj([
          ("kind", str("unit")),
          ("location", encode_span(location)),
          ("value", num(value)),
        ]),
      )
  }
}

///|
fn todo_kind(k : TodoKind) -> Json {
  match k {
    Keyword => obj([("kind", str("keyword"))])
    EmptyFunction(function_location~) =>
      obj([
        ("kind", str("empty_function")),
        ("function_location", encode_span(function_location)),
      ])
    IncompleteUse => obj([("kind", str("incomplete_use"))])
    EmptyBlock => obj([("kind", str("empty_block"))])
  }
}

///|
fn function_literal_kind(k : FunctionLiteralKind) -> Json {
  match k {
    Capture(hole~) =>
      obj([("kind", str("capture")), ("hole", encode_span(hole))])
    Anonymous(head~) =>
      obj([("kind", str("anonymous")), ("head", encode_span(head))])
    Use(location~) =>
      obj([("kind", str("use")), ("location", encode_span(location))])
  }
}

///|
pub fn encode_expr(e : Expr) -> Json {
  run(enc_expr(e, j => Done(j)))
}

///|
fn[R] enc_expr(e : Expr, k : (Json) -> Step[R]) -> Step[R] {
  match e {
    Int(location~, value~, int_value~) =>
      give(
        k,
        obj([
          ("kind", str("int")),
          ("location", encode_span(location)),
          ("value", str(value)),
          ("int_value", str(int_value.to_string())),
        ]),
      )
    Float(location~, value~, ..) =>
      give(
        k,
        obj([
          ("kind", str("float")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    String(location~, value~) =>
      give(
        k,
        obj([
          ("kind", str("string")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    Block(location~, statements~) =>
      each(statements, enc_statement, statements => {
        give(
          k,
          obj([
            ("kind", str("block")),
            ("location", encode_span(location)),
            ("statements", arr(statements)),
          ]),
        )
      })
    Var(location~, name~) =>
      give(
        k,
        obj([
          ("kind", str("var")),
          ("location", encode_span(location)),
          ("name", str(name)),
        ]),
      )
    Fn(
      location~,
      kind~,
      end_of_head_byte_index~,
      arguments~,
      body~,
      return_annotation~
    ) =>
      each(arguments, enc_arg, arguments => {
        each(body, enc_statement, body => {
          maybe(return_annotation, enc_type_ast, return_annotation => {
            give(
              k,
              obj([
                ("kind", str("fn")),
                ("location", encode_span(location)),
                ("fn_kind", function_literal_kind(kind)),
                ("end_of_head_byte_index", num(end_of_head_byte_index)),
                ("arguments", arr(arguments)),
                ("body", arr(body)),
                ("return_annotation", return_annotation),
              ]),
            )
          })
        })
      })
    List(location~, elements~, tail~) =>
      each(elements, enc_expr, elements => {
        maybe(tail, enc_expr, tail => {
          give(
            k,
            obj([
              ("kind", str("list")),
              ("location", encode_span(location)),
              ("elements", arr(elements)),
              ("tail", tail),
            ]),
          )
        })
      })
    Call(location~, fun~, arguments~, open_parenthesis~) =>
      node(fun, enc_expr, fun => {
        each(arguments, (a, k2) => enc_call_arg(a, enc_expr, k2), arguments => {
          give(
            k,
            obj([
              ("kind", str("call")),
              ("location", encode_span(location)),
              ("fun", fun),
              ("arguments", arr(arguments)),
              ("open_parenthesis", num(open_parenthesis)),
            ]),
          )
        })
      })
    BinOp(location~, operator~, operator_start~, left~, right~) =>
      node(left, enc_expr, left => {
        node(right, enc_expr, right => {
          give(
            k,
            obj([
              ("kind", str("bin_op")),
              ("location", encode_span(location)),
              ("operator", str(operator.name())),
              ("operator_start", num(operator_start)),
              ("left", left),
              ("right", right),
            ]),
          )
        })
      })
    Pipeline(expressions~) =>
      each(expressions, enc_expr, expressions => {
        give(
          k,
          obj([("kind", str("pipeline")), ("expressions", arr(expressions))]),
        )
      })
    Case(location~, subjects~, clauses~) =>
      each(subjects, enc_expr, subjects => {
        maybe(
          clauses,
          (cs, k2) => each(cs, enc_clause, js => give(k2, arr(js))),
          clauses => {
            give(
              k,
              obj([
                ("kind", str("case")),
                ("location", encode_span(location)),
                ("subjects", arr(subjects)),
                ("clauses", clauses),
              ]),
            )
          },
        )
      })
    FieldAccess(location~, label_location~, label~, container~) =>
      node(container, enc_expr, container => {
        give(
          k,
          obj([
            ("kind", str("field_access")),
            ("location", encode_span(location)),
            ("label_location", encode_span(label_location)),
            ("label", str(label)),
            ("container", container),
          ]),
        )
      })
    Tuple(location~, elements~) =>
      each(elements, enc_expr, elements => {
        give(
          k,
          obj([
            ("kind", str("tuple")),
            ("location", encode_span(location)),
            ("elements", arr(elements)),
          ]),
        )
      })
    TupleIndex(location~, index~, tuple~) =>
      node(tuple, enc_expr, tuple => {
        give(
          k,
          obj([
            ("kind", str("tuple_index")),
            ("location", encode_span(location)),
            ("index", num64(index)),
            ("tuple", tuple),
          ]),
        )
      })
    Todo(kind~, location~, message~) =>
      maybe(message, enc_expr, message => {
        give(
          k,
          obj([
            ("kind", str("todo")),
            ("todo_kind", todo_kind(kind)),
            ("location", encode_span(location)),
            ("message", message),
          ]),
        )
      })
    Panic(location~, message~) =>
      maybe(message, enc_expr, message => {
        give(
          k,
          obj([
            ("kind", str("panic")),
            ("location", encode_span(location)),
            ("message", message),
          ]),
        )
      })
    Echo(location~, keyword_end~, expression~, message~) =>
      maybe(expression, enc_expr, expression => {
        maybe(message, enc_expr, message => {
          give(
            k,
            obj([
              ("kind", str("echo")),
              ("location", encode_span(location)),
              ("keyword_end", num(keyword_end)),
              ("expression", expression),
              ("message", message),
            ]),
          )
        })
      })
    BitArray(location~, segments~) =>
      each(segments, (s, k2) => enc_bit_array_segment(s, enc_expr, k2), segments => {
        give(
          k,
          obj([
            ("kind", str("bit_array")),
            ("location", encode_span(location)),
            ("segments", arr(segments)),
          ]),
        )
      })
    RecordUpdate(location~, spread_start~, constructor_~, record~, arguments~) =>
      node(constructor_, enc_expr, constructor_ => {
        enc_record_being_updated(record, enc_expr, record => {
          each(arguments, (a, k2) => enc_record_update_arg(a, enc_expr, k2), arguments => {
            give(
              k,
              obj([
                ("kind", str("record_update")),
                ("location", encode_span(location)),
                ("spread_start", num(spread_start)),
                ("constructor", constructor_),
                ("record", record),
                ("arguments", arr(arguments)),
              ]),
            )
          })
        })
      })
    NegateBool(location~, value~) =>
      node(value, enc_expr, value => {
        give(
          k,
          obj([
            ("kind", str("negate_bool")),
            ("location", encode_span(location)),
            ("value", value),
          ]),
        )
      })
    NegateInt(location~, value~) =>
      node(value, enc_expr, value => {
        give(
          k,
          obj([
            ("kind", str("negate_int")),
            ("location", encode_span(location)),
            ("value", value),
          ]),
        )
      })
  }
}

///|
fn[R] enc_clause(c : Clause, k : (Json) -> Step[R]) -> Step[R] {
  each(c.pattern, enc_pattern, pattern => {
    each(
      c.alternative_patterns,
      (ps, k2) => each(ps, enc_pattern, js => give(k2, arr(js))),
      alternative_patterns => {
        maybe(c.guard_, enc_clause_guard, guard_ => {
          node(c.then, enc_expr, then => {
            give(
              k,
              obj([
                ("location", encode_span(c.location)),
                ("pattern", arr(pattern)),
                ("alternative_patterns", arr(alternative_patterns)),
                ("guard", guard_),
                ("then", then),
              ]),
            )
          })
        })
      },
    )
  })
}

///|
pub fn encode_clause_guard(g : ClauseGuard) -> Json {
  run(enc_clause_guard(g, j => Done(j)))
}

///|
fn[R] enc_clause_guard(g : ClauseGuard, k : (Json) -> Step[R]) -> Step[R] {
  match g {
    Block(location~, value~) =>
      node(value, enc_clause_guard, value => {
        give(
          k,
          obj([
            ("kind", str("block")),
            ("location", encode_span(location)),
            ("value", value),
          ]),
        )
      })
    BinaryOperator(location~, operator~, operator_start~, left~, right~) =>
      node(left, enc_clause_guard, left => {
        node(right, enc_clause_guard, right => {
          give(
            k,
            obj([
              ("kind", str("binary_operator")),
              ("location", encode_span(location)),
              ("operator", str(operator.name())),
              ("operator_start", num(operator_start)),
              ("left", left),
              ("right", right),
            ]),
          )
        })
      })
    Not(location~, expression~) =>
      node(expression, enc_clause_guard, expression => {
        give(
          k,
          obj([
            ("kind", str("not")),
            ("location", encode_span(location)),
            ("expression", expression),
          ]),
        )
      })
    Var(location~, name~) =>
      give(
        k,
        obj([
          ("kind", str("var")),
          ("location", encode_span(location)),
          ("name", str(name)),
        ]),
      )
    TupleIndex(location~, index~, tuple~) =>
      node(tuple, enc_clause_guard, tuple => {
        give(
          k,
          obj([
            ("kind", str("tuple_index")),
            ("location", encode_span(location)),
            ("index", num64(index)),
            ("tuple", tuple),
          ]),
        )
      })
    FieldAccess(label_location~, index~, label~, container~) =>
      node(container, enc_clause_guard, container => {
        give(
          k,
          obj([
            ("kind", str("field_access")),
            ("label_location", encode_span(label_location)),
            ("index", opt(index, num64)),
            ("label", str(label)),
            ("container", container),
          ]),
        )
      })
    Constant(c) =>
      node(c, enc_constant, constant => {
        give(k, obj([("kind", str("constant")), ("constant", constant)]))
      })
  }
}

///|
pub fn encode_pattern(p : Pattern) -> Json {
  run(enc_pattern(p, j => Done(j)))
}

///|
fn[R] enc_pattern(p : Pattern, k : (Json) -> Step[R]) -> Step[R] {
  match p {
    Int(location~, value~, int_value~) =>
      give(
        k,
        obj([
          ("kind", str("int")),
          ("location", encode_span(location)),
          ("value", str(value)),
          ("int_value", str(int_value.to_string())),
        ]),
      )
    Float(location~, value~, ..) =>
      give(
        k,
        obj([
          ("kind", str("float")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    String(location~, value~) =>
      give(
        k,
        obj([
          ("kind", str("string")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    Variable(location~, name~) =>
      give(
        k,
        obj([
          ("kind", str("variable")),
          ("location", encode_span(location)),
          ("name", str(name)),
        ]),
      )
    BitArraySize(s) =>
      node(s, enc_bit_array_size, size => {
        give(k, obj([("kind", str("bit_array_size")), ("size", size)]))
      })
    Assign(name~, location~, pattern~) =>
      node(pattern, enc_pattern, pattern => {
        give(
          k,
          obj([
            ("kind", str("assign")),
            ("name", str(name)),
            ("location", encode_span(location)),
            ("pattern", pattern),
          ]),
        )
      })
    Discard(name~, location~) =>
      give(
        k,
        obj([
          ("kind", str("discard")),
          ("name", str(name)),
          ("location", encode_span(location)),
        ]),
      )
    List(location~, elements~, tail~) =>
      each(elements, enc_pattern, elements => {
        maybe(
          tail,
          (t, k2) => {
            node(t.pattern, enc_pattern, pattern => {
              give(
                k2,
                obj([
                  ("location", encode_span(t.location)),
                  ("pattern", pattern),
                ]),
              )
            })
          },
          tail => {
            give(
              k,
              obj([
                ("kind", str("list")),
                ("location", encode_span(location)),
                ("elements", arr(elements)),
                ("tail", tail),
              ]),
            )
          },
        )
      })
    Constructor(location~, name_location~, name~, arguments~, module_~, spread~) =>
      each(arguments, (a, k2) => enc_call_arg(a, enc_pattern, k2), arguments => {
        give(
          k,
          obj([
            ("kind", str("constructor")),
            ("location", encode_span(location)),
            ("name_location", encode_span(name_location)),
            ("name", str(name)),
            ("arguments", arr(arguments)),
            ("module", opt(module_, spanned_string)),
            ("spread", opt(spread, encode_span)),
          ]),
        )
      })
    Tuple(location~, elements~) =>
      each(elements, enc_pattern, elements => {
        give(
          k,
          obj([
            ("kind", str("tuple")),
            ("location", encode_span(location)),
            ("elements", arr(elements)),
          ]),
        )
      })
    BitArray(location~, segments~) =>
      each(segments, (s, k2) => enc_bit_array_segment(s, enc_pattern, k2), segments => {
        give(
          k,
          obj([
            ("kind", str("bit_array")),
            ("location", encode_span(location)),
            ("segments", arr(segments)),
          ]),
        )
      })
    StringPrefix(
      location~,
      left_location~,
      left_side_assignment~,
      right_location~,
      left_side_string~,
      right_side_assignment~
    ) =>
      give(
        k,
        obj([
          ("kind", str("string_prefix")),
          ("location", encode_span(location)),
          ("left_location", encode_span(left_location)),
          (
            "left_side_assignment",
            opt(left_side_assignment, a => {
              obj([
                ("name", str(a.name)),
                ("name_start_position", num(a.name_start_position)),
                ("location", encode_span(a.location)),
              ])
            }),
          ),
          ("right_location", encode_span(right_location)),
          ("left_side_string", str(left_side_string)),
          ("right_side_assignment", assign_name(right_side_assignment)),
        ]),
      )
  }
}

///|
fn[R] enc_bit_array_size(s : BitArraySize, k : (Json) -> Step[R]) -> Step[R] {
  match s {
    Int(location~, value~, int_value~) =>
      give(
        k,
        obj([
          ("kind", str("int")),
          ("location", encode_span(location)),
          ("value", str(value)),
          ("int_value", str(int_value.to_string())),
        ]),
      )
    Variable(location~, name~) =>
      give(
        k,
        obj([
          ("kind", str("variable")),
          ("location", encode_span(location)),
          ("name", str(name)),
        ]),
      )
    BinaryOperator(location~, operator~, left~, right~) =>
      node(left, enc_bit_array_size, left => {
        node(right, enc_bit_array_size, right => {
          give(
            k,
            obj([
              ("kind", str("binary_operator")),
              ("location", encode_span(location)),
              ("operator", str(operator.name())),
              ("left", left),
              ("right", right),
            ]),
          )
        })
      })
    Block(location~, inner~) =>
      node(inner, enc_bit_array_size, inner => {
        give(
          k,
          obj([
            ("kind", str("block")),
            ("location", encode_span(location)),
            ("inner", inner),
          ]),
        )
      })
  }
}

///|
pub fn encode_constant(c : Constant) -> Json {
  run(enc_constant(c, j => Done(j)))
}

///|
fn[R] enc_constant(c : Constant, k : (Json) -> Step[R]) -> Step[R] {
  match c {
    Int(location~, value~, int_value~) =>
      give(
        k,
        obj([
          ("kind", str("int")),
          ("location", encode_span(location)),
          ("value", str(value)),
          ("int_value", str(int_value.to_string())),
        ]),
      )
    Float(location~, value~, ..) =>
      give(
        k,
        obj([
          ("kind", str("float")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    String(location~, value~) =>
      give(
        k,
        obj([
          ("kind", str("string")),
          ("location", encode_span(location)),
          ("value", str(value)),
        ]),
      )
    Tuple(location~, elements~) =>
      each(elements, enc_constant, elements => {
        give(
          k,
          obj([
            ("kind", str("tuple")),
            ("location", encode_span(location)),
            ("elements", arr(elements)),
          ]),
        )
      })
    List(location~, elements~, tail~) =>
      each(elements, enc_constant, elements => {
        maybe(tail, enc_constant, tail => {
          give(
            k,
            obj([
              ("kind", str("list")),
              ("location", encode_span(location)),
              ("elements", arr(elements)),
              ("tail", tail),
            ]),
          )
        })
      })
    Record(location~, arguments_start_position~, module_~, name~, arguments~) =>
      maybe(
        arguments,
        (xs, k2) => {
          each(xs, (a, k3) => enc_call_arg(a, enc_constant, k3), js => {
            give(k2, arr(js))
          })
        },
        arguments => {
          give(
            k,
            obj([
              ("kind", str("record")),
              ("location", encode_span(location)),
              ("arguments_start_position", num(arguments_start_position)),
              ("module", opt(module_, spanned_string)),
              ("name", str(name)),
              ("arguments", arguments),
            ]),
          )
        },
      )
    RecordUpdate(
      location~,
      constructor_location~,
      module_~,
      name~,
      record~,
      arguments~
    ) =>
      enc_record_being_updated(record, enc_constant, record => {
        each(arguments, (a, k2) => enc_record_update_arg(a, enc_constant, k2), arguments => {
          give(
            k,
            obj([
              ("kind", str("record_update")),
              ("location", encode_span(location)),
              ("constructor_location", encode_span(constructor_location)),
              ("module", opt(module_, spanned_string)),
              ("name", str(name)),
              ("record", record),
              ("arguments", arr(arguments)),
            ]),
          )
        })
      })
    BitArray(location~, segments~) =>
      each(segments, (s, k2) => enc_bit_array_segment(s, enc_constant, k2), segments => {
        give(
          k,
          obj([
            ("kind", str("bit_array")),
            ("location", encode_span(location)),
            ("segments", arr(segments)),
          ]),
        )
      })
    Var(location~, module_~, name~) =>
      give(
        k,
        obj([
          ("kind", str("var")),
          ("location", encode_span(location)),
          ("module", opt(module_, spanned_string)),
          ("name", str(name)),
        ]),
      )
    BinaryOperator(location~, operator_start~, operator~, left~, right~) =>
      node(left, enc_constant, left => {
        node(right, enc_constant, right => {
          give(
            k,
            obj([
              ("kind", str("binary_operator")),
              ("location", encode_span(location)),
              ("operator_start", num(operator_start)),
              ("operator", str(operator.name())),
              ("left", left),
              ("right", right),
            ]),
          )
        })
      })
    Todo(location~, message~) =>
      maybe(message, enc_constant, message => {
        give(
          k,
          obj([
            ("kind", str("todo")),
            ("location", encode_span(location)),
            ("message", message),
          ]),
        )
      })
  }
}

///|
pub fn encode_extra(x : ModuleExtra) -> Json {
  obj([
    ("module_comments", list(x.module_comments, encode_span)),
    ("doc_comments", list(x.doc_comments, encode_span)),
    ("comments", list(x.comments, encode_span)),
    ("empty_lines", list(x.empty_lines, num)),
    ("new_lines", list(x.new_lines, num)),
    ("trailing_commas", list(x.trailing_commas, num)),
  ])
}

///|
pub fn encode_warning(w : Warning) -> Json {
  let simple = (kind : String, location : @span.SrcSpan) => {
    obj([("kind", str(kind)), ("location", encode_span(location))])
  }
  match w {
    DeprecatedListPrepend(location~) =>
      simple("deprecated_list_prepend", location)
    DeprecatedListPattern(location~) =>
      simple("deprecated_list_pattern", location)
    DeprecatedListCatchAllPattern(location~) =>
      simple("deprecated_list_catch_all_pattern", location)
    DeprecatedListSpreadAsVariablePattern(location~, name~) =>
      obj([
        ("kind", str("deprecated_list_spread_as_variable_pattern")),
        ("location", encode_span(location)),
        ("name", str(name)),
      ])
    DeprecatedRecordSpreadPattern(location~) =>
      simple("deprecated_record_spread_pattern", location)
    DeprecatedEmptyClauseGuard(location~) =>
      simple("deprecated_empty_clause_guard", location)
    DeprecatedTargetShorthand(target~, location~) =>
      obj([
        ("kind", str("deprecated_target_shorthand")),
        ("target", str(target.name())),
        ("location", encode_span(location)),
      ])
    DetachedDocComment(location~) => simple("detached_doc_comment", location)
  }
}