// Code generation: one pair of read/write functions per container type, the codecs of
// every struct, and each service's registration and client. A field is then a single
// line, whatever its type — the nesting lives in the helper, once.

///|
fn moon_type(ty : TType) -> String {
  match ty {
    Bool => "Bool"
    Byte | I16 | I32 => "Int32"
    I64 => "Int64"
    Double => "Double"
    Str => "String"
    Binary => "Bytes"
    List(inner) => "Array[\{moon_type(inner)}]"
    Set(inner) => "Array[\{moon_type(inner)}]"
    Map(key, value) => "Map[\{moon_type(key)}, \{moon_type(value)}]"
    Named(name) => name
  }
}

///|
fn wire_type(ty : TType, enums : Array[String]) -> String {
  match ty {
    Bool => "@thrift.type_bool"
    Byte => "@thrift.type_byte"
    I16 => "@thrift.type_i16"
    I32 => "@thrift.type_i32"
    I64 => "@thrift.type_i64"
    Double => "@thrift.type_double"
    Str | Binary => "@thrift.type_string"
    List(_) => "@thrift.type_list"
    Set(_) => "@thrift.type_set"
    Map(_, _) => "@thrift.type_map"
    Named(name) =>
      if enums.contains(name) {
        "@thrift.type_i32"
      } else {
        "@thrift.type_struct"
      }
  }
}

///|
fn mangle(ty : TType) -> String {
  match ty {
    Bool => "bool"
    Byte => "byte"
    I16 => "i16"
    I32 => "i32"
    I64 => "i64"
    Double => "double"
    Str => "string"
    Binary => "binary"
    List(inner) => "list_" + mangle(inner)
    Set(inner) => "set_" + mangle(inner)
    Map(key, value) => "map_" + mangle(key) + "_" + mangle(value)
    Named(name) => name
  }
}

///|
/// The statement that writes `expr`, whose type is `ty`, to `proto`.
fn write_of(ty : TType, expr : String, enums : Array[String]) -> String {
  match ty {
    Bool => "proto.write_bool(\{expr})"
    Byte => "proto.write_byte(\{expr})"
    I16 => "proto.write_i16(\{expr})"
    I32 => "proto.write_i32(\{expr})"
    I64 => "proto.write_i64(\{expr})"
    Double => "proto.write_double(\{expr})"
    Str => "proto.write_string(\{expr})"
    Binary => "proto.write_bytes(\{expr})"
    Named(name) =>
      if enums.contains(name) {
        "proto.write_i32(\{expr}.to_i32())"
      } else {
        "\{expr}.encode(proto)"
      }
    _ => "write_\{mangle(ty)}(proto, \{expr})"
  }
}

///|
/// The expression that reads one value of `ty` from `proto`.
fn read_of(ty : TType, enums : Array[String]) -> String {
  match ty {
    Bool => "proto.read_bool()"
    Byte => "proto.read_byte()"
    I16 => "proto.read_i16()"
    I32 => "proto.read_i32()"
    I64 => "proto.read_i64()"
    Double => "proto.read_double()"
    Str => "proto.read_string()"
    Binary => "proto.read_bytes()"
    Named(name) =>
      if enums.contains(name) {
        "\{name}::from_i32(proto.read_i32())"
      } else {
        "\{name}::decode(proto)"
      }
    _ => "read_\{mangle(ty)}(proto)"
  }
}

///|
fn default_of(ty : TType, enums : Array[Enum]) -> String {
  match ty {
    Bool => "false"
    Byte | I16 | I32 => "0"
    I64 => "0L"
    Double => "0.0"
    Str => "\"\""
    Binary => "b\"\""
    List(inner) => "([] : Array[\{moon_type(inner)}])"
    Set(inner) => "([] : Array[\{moon_type(inner)}])"
    Map(key, value) => "(Map([]) : Map[\{moon_type(key)}, \{moon_type(value)}])"
    Named(name) =>
      match find_enum(enums, name) {
        Some(e) =>
          if e.values.length() > 0 {
            "\{name}::\{e.values[0].0}"
          } else {
            "\{name}::from_i32(0)"
          }
        None => "\{name}::default()"
      }
  }
}

///|
fn find_enum(enums : Array[Enum], name : String) -> Enum? {
  for item in enums {
    if item.name == name {
      return Some(item)
    }
  }
  None
}

///|
/// The empty array or map a reader starts from.
fn empty_of(ty : TType) -> String {
  match ty {
    List(inner) => "([] : Array[\{moon_type(inner)}])"
    Set(inner) => "([] : Array[\{moon_type(inner)}])"
    Map(key, value) => "(Map([]) : Map[\{moon_type(key)}, \{moon_type(value)}])"
    _ => "?"
  }
}

///|
struct Emitter {
  out : Array[String]
}

///|
fn Emitter::line(self : Emitter, indent : Int32, text : String) -> Unit {
  let mut pad = ""
  for _ in 0.. Unit {
  self.out.push("\n")
}

///|
fn Emitter::text(self : Emitter) -> String {
  self.out.join("")
}

// ---------------------------------------------------------------- containers

///|
fn walk_types(
  ty : TType,
  seen : Map[String, TType],
  order : Array[String],
) -> Unit {
  let add = () => {
    let key = mangle(ty)
    if seen.get(key) is None {
      seen[key] = ty
      order.push(key)
    }
  }
  match ty {
    List(inner) | Set(inner) => {
      walk_types(inner, seen, order)
      add()
    }
    Map(key, value) => {
      walk_types(key, seen, order)
      walk_types(value, seen, order)
      add()
    }
    _ => ()
  }
}

///|
/// Every container type reachable from the document, once each, sorted so the output
/// does not depend on the order the file happened to mention them in.
fn collect_containers(doc : Linked) -> Array[TType] {
  let seen : Map[String, TType] = Map([])
  let order : Array[String] = []
  for s in doc.structs {
    for f in s.fields {
      walk_types(f.ty, seen, order)
    }
  }
  for service in doc.services {
    for func in service.functions {
      for param in func.params {
        walk_types(param.ty, seen, order)
      }
      match func.ret {
        Some(ty) => walk_types(ty, seen, order)
        None => ()
      }
    }
  }
  order.sort()
  order.map(key => seen.get(key).unwrap())
}

///|
fn emit_container(e : Emitter, ty : TType, enums : Array[String]) -> Unit {
  let name = mangle(ty)
  let moon = moon_type(ty)
  match ty {
    List(inner) | Set(inner) => {
      e.line(
        0,
        "pub fn write_\{name}(proto : &@thrift.Protocol, value : \{moon}) -> Unit {",
      )
      e.line(
        1,
        "proto.write_list_begin(\{wire_type(ty, enums)}, value.length())",
      )
      e.line(1, "for item in value {")
      e.line(2, write_of(inner, "item", enums))
      e.line(1, "}")
      e.line(0, "}")
      e.blank()
      e.line(
        0,
        "pub fn read_\{name}(proto : &@thrift.Protocol) -> \{moon} raise @thrift.ThriftError {",
      )
      e.line(1, "let (elem_type, size) = proto.read_list_begin()")
      e.line(1, "ignore(elem_type)")
      e.line(1, "let out : \{moon} = \{empty_of(ty)}")
      e.line(1, "for _ in 0.. {
      e.line(
        0,
        "pub fn write_\{name}(proto : &@thrift.Protocol, value : \{moon}) -> Unit {",
      )
      e.line(
        1,
        "proto.write_map_begin(\{wire_type(key, enums)}, \{wire_type(value, enums)}, value.length())",
      )
      e.line(1, "for key, item in value {")
      e.line(2, write_of(key, "key", enums))
      e.line(2, write_of(value, "item", enums))
      e.line(1, "}")
      e.line(0, "}")
      e.blank()
      e.line(
        0,
        "pub fn read_\{name}(proto : &@thrift.Protocol) -> \{moon} raise @thrift.ThriftError {",
      )
      e.line(1, "let (key_type, value_type, size) = proto.read_map_begin()")
      e.line(1, "ignore(key_type)")
      e.line(1, "ignore(value_type)")
      e.line(1, "let out : \{moon} = \{empty_of(ty)}")
      e.line(1, "for _ in 0.. ()
  }
}

// ---------------------------------------------------------------- declarations

///|
fn emit_enum(e : Emitter, item : Enum) -> Unit {
  e.line(0, "pub(all) enum \{item.name} {")
  for value in item.values {
    e.line(1, "\{value.0}")
  }
  e.line(0, "}")
  e.blank()
  e.line(
    0,
    "pub impl Eq for \{item.name} with fn equal(self : \{item.name}, other : \{item.name}) -> Bool {",
  )
  e.line(1, "self.to_i32() == other.to_i32()")
  e.line(0, "}")
  e.blank()
  e.line(
    0,
    "pub impl Eq for \{item.name} with fn not_equal(self : \{item.name}, other : \{item.name}) -> Bool {",
  )
  e.line(1, "self.to_i32() != other.to_i32()")
  e.line(0, "}")
  e.blank()
  e.line(0, "pub fn \{item.name}::to_i32(self : \{item.name}) -> Int32 {")
  e.line(1, "match self {")
  for value in item.values {
    e.line(2, "\{value.0} => \{value.1}")
  }
  e.line(1, "}")
  e.line(0, "}")
  e.blank()
  e.line(
    0,
    "pub fn \{item.name}::from_i32(value : Int32) -> \{item.name} raise @thrift.ThriftError {",
  )
  e.line(1, "match value {")
  for value in item.values {
    e.line(2, "\{value.1} => \{value.0}")
  }
  e.line(2, "other => raise @thrift.BadType(other)")
  e.line(1, "}")
  e.line(0, "}")
  e.blank()
}

///|
/// A union carries exactly one of its fields, so it is an enum with a variant each. On
/// the wire it is a struct: the variant's field id and value, then STOP.
fn emit_union(
  e : Emitter,
  item : Struct,
  enums : Array[Enum],
  file : String,
) -> Unit raise Refused {
  let names = enums.map(x => x.name)
  if item.fields.length() == 0 {
    refuse(
      file,
      0,
      "`\{item.name}` is a union with no fields; a union carries one of them",
    )
  }
  let variants : Array[String] = []
  for f in item.fields {
    let variant = capitalise(f.name)
    if variants.contains(variant) {
      refuse(
        file,
        0,
        "`\{item.name}` has two fields written `\{variant}`; rename one",
      )
    }
    variants.push(variant)
  }
  e.line(0, "pub(all) enum \{item.name} {")
  for f in item.fields {
    e.line(1, "\{capitalise(f.name)}(\{moon_type(f.ty)})")
  }
  e.line(0, "}")
  e.blank()

  // The first field is what a missing value is built from: an unset union still needs
  // something the rest of the generated code can carry.
  let first = item.fields[0]
  e.line(0, "pub fn \{item.name}::default() -> \{item.name} {")
  e.line(1, "\{capitalise(first.name)}(\{default_of(first.ty, enums)})")
  e.line(0, "}")
  e.blank()

  e.line(
    0,
    "pub fn \{item.name}::encode(self : \{item.name}, proto : &@thrift.Protocol) -> Unit {",
  )
  e.line(1, "proto.write_struct_begin(\"\{item.name}\")")
  e.line(1, "match self {")
  for f in item.fields {
    e.line(2, "\{capitalise(f.name)}(value) => {")
    e.line(3, "proto.write_field_begin(\{wire_type(f.ty, names)}, \{f.id})")
    e.line(3, write_of(f.ty, "value", names))
    e.line(2, "}")
  }
  e.line(1, "}")
  e.line(1, "proto.write_field_stop()")
  e.line(1, "proto.write_struct_end()")
  e.line(0, "}")
  e.blank()

  e.line(
    0,
    "pub fn \{item.name}::decode(proto : &@thrift.Protocol) -> \{item.name} raise @thrift.ThriftError {",
  )
  e.line(1, "proto.read_struct_begin()")
  e.line(1, "let mut out : \{item.name}? = None")
  e.line(1, "for ;; {")
  e.line(2, "let (field_type, field_id) = proto.read_field_begin()")
  e.line(2, "if field_type == @thrift.type_stop {")
  e.line(3, "break")
  e.line(2, "}")
  e.line(2, "match field_id {")
  for f in item.fields {
    e.line(
      3,
      "\{f.id} => out = Some(\{capitalise(f.name)}(\{read_of(f.ty, names)}))",
    )
  }
  e.line(3, "_ => @thrift.skip(proto, field_type)")
  e.line(2, "}")
  e.line(1, "}")
  e.line(1, "proto.read_struct_end()")
  e.line(1, "match out {")
  e.line(2, "Some(value) => value")
  e.line(
    2,
    "_ => raise @thrift.Application(\"the union \{item.name} carried no field\")",
  )
  e.line(1, "}")
  e.line(0, "}")
  e.blank()
}

///|
fn emit_struct(
  e : Emitter,
  item : Struct,
  enums : Array[Enum],
  file : String,
) -> Unit raise Refused {
  if item.kind is UnionDecl {
    return emit_union(e, item, enums, file)
  }
  let names = enums.map(x => x.name)
  e.line(0, "pub(all) struct \{item.name} {")
  for f in item.fields {
    let ty = if f.optional {
      "Option[\{moon_type(f.ty)}]"
    } else {
      moon_type(f.ty)
    }
    e.line(1, "\{f.name} : \{ty}")
  }
  e.line(0, "}")
  e.blank()

  e.line(0, "pub fn \{item.name}::default() -> \{item.name} {")
  e.line(1, "{")
  for f in item.fields {
    let value = if f.optional { "None" } else { default_of(f.ty, enums) }
    e.line(2, "\{f.name}: \{value},")
  }
  e.line(1, "}")
  e.line(0, "}")
  e.blank()

  e.line(
    0,
    "pub fn \{item.name}::encode(self : \{item.name}, proto : &@thrift.Protocol) -> Unit {",
  )
  e.line(1, "proto.write_struct_begin(\"\{item.name}\")")
  for f in item.fields {
    if f.optional {
      e.line(1, "match self.\{f.name} {")
      e.line(2, "Some(value) => {")
      e.line(3, "proto.write_field_begin(\{wire_type(f.ty, names)}, \{f.id})")
      e.line(3, write_of(f.ty, "value", names))
      e.line(2, "}")
      e.line(2, "None => ()")
      e.line(1, "}")
    } else {
      e.line(1, "proto.write_field_begin(\{wire_type(f.ty, names)}, \{f.id})")
      e.line(1, write_of(f.ty, "self.\{f.name}", names))
    }
  }
  e.line(1, "proto.write_field_stop()")
  e.line(1, "proto.write_struct_end()")
  e.line(0, "}")
  e.blank()

  e.line(
    0,
    "pub fn \{item.name}::decode(proto : &@thrift.Protocol) -> \{item.name} raise @thrift.ThriftError {",
  )
  for f in item.fields {
    let ty = if f.optional {
      "Option[\{moon_type(f.ty)}]"
    } else {
      moon_type(f.ty)
    }
    let value = if f.optional { "None" } else { default_of(f.ty, enums) }
    e.line(1, "let mut \{f.name} : \{ty} = \{value}")
  }
  e.line(1, "proto.read_struct_begin()")
  e.line(1, "for ;; {")
  e.line(2, "let (field_type, field_id) = proto.read_field_begin()")
  e.line(2, "if field_type == @thrift.type_stop {")
  e.line(3, "break")
  e.line(2, "}")
  e.line(2, "match field_id {")
  for f in item.fields {
    let value = if f.optional {
      "Some(\{read_of(f.ty, names)})"
    } else {
      read_of(f.ty, names)
    }
    e.line(3, "\{f.id} => \{f.name} = \{value}")
  }
  e.line(3, "_ => @thrift.skip(proto, field_type)")
  e.line(2, "}")
  e.line(1, "}")
  e.line(1, "proto.read_struct_end()")
  let fields = item.fields.map(f => f.name).join(", ")
  e.line(1, "{ \{fields}, }")
  e.line(0, "}")
  e.blank()
}

///|
fn has_method(methods : Array[Function], name : String) -> Bool {
  for func in methods {
    if func.name == name {
      return true
    }
  }
  false
}

///|
/// A service's methods, its own first and then its base's, so an overriding method
/// shadows the inherited one.
fn service_methods(
  service : Service,
  by_service : Map[String, Service],
  seen : Array[String],
  file : String,
) -> Array[Function] raise Refused {
  if seen.contains(service.name) {
    refuse(
      file,
      0,
      "`\{service.name}` extends itself through its bases (\{seen.join(" -> ")})",
    )
  }
  seen.push(service.name)
  let out : Array[Function] = []
  for func in service.functions {
    out.push(func)
  }
  match service.base {
    Some(base) =>
      match by_service.get(base) {
        Some(parent) =>
          for func in service_methods(parent, by_service, seen, file) {
            if !has_method(out, func.name) {
              out.push(func)
            }
          }
        None =>
          refuse(
            file,
            0,
            "`\{service.name}` extends `\{base}`, which this file does not declare or include",
          )
      }
    None => ()
  }
  ignore(seen.pop())
  out
}

///|
/// The error type of one method: a variant per exception its `throws` clause names.
fn error_name(service : String, func : String) -> String {
  service + capitalise(func) + "Error"
}

///|
fn emit_error_enum(e : Emitter, service : String, func : Function) -> Unit {
  let name = error_name(service, func.name)
  e.line(0, "/// The declared failures of `\{func.name}`.")
  e.line(0, "pub(all) enum \{name} {")
  for ex in func.exceptions {
    let ty = moon_type(ex.ty)
    e.line(1, "\{ty}(\{ty})")
  }
  e.line(0, "}")
  e.blank()
}

///|
fn emit_service(
  e : Emitter,
  service : Service,
  enums : Array[Enum],
  methods : Array[Function],
) -> Unit {
  let prefix = lower_first(service.name)
  let names = enums.map(x => x.name)
  for func in methods {
    if func.exceptions.length() > 0 {
      emit_error_enum(e, service.name, func)
    }
  }

  // The server side: one handler per method, registered under its wire name.
  e.line(0, "pub fn register_\{prefix}(")
  e.line(1, "processor : @thrift.Processor,")
  for func in methods {
    let params = func.params.map(p => moon_type(p.ty)).join(", ")
    let ret = match func.ret {
      Some(ty) => moon_type(ty)
      None => "Unit"
    }
    let ret = if func.exceptions.length() > 0 {
      "Result[\{ret}, \{error_name(service.name, func.name)}]"
    } else {
      ret
    }
    e.line(1, "\{func.name} : (\{params}) -> \{ret},")
  }
  e.line(0, ") -> Unit {")
  for func in methods {
    e.line(1, "processor.add(\"\{func.name}\", proto => {")
    for param in func.params {
      e.line(
        2,
        "let mut arg_\{param.name} : \{moon_type(param.ty)} = \{default_of(param.ty, enums)}",
      )
    }
    e.line(2, "for ;; {")
    if func.params.length() > 0 {
      e.line(3, "let (field_type, field_id) = proto.read_field_begin()")
    } else {
      e.line(3, "let (field_type, _) = proto.read_field_begin()")
    }
    e.line(3, "if field_type == @thrift.type_stop {")
    e.line(4, "break")
    e.line(3, "}")
    if func.params.length() > 0 {
      e.line(3, "match field_id {")
      for param in func.params {
        e.line(
          4,
          "\{param.id} => arg_\{param.name} = \{read_of(param.ty, names)}",
        )
      }
      e.line(4, "_ => @thrift.skip(proto, field_type)")
      e.line(3, "}")
    } else {
      e.line(3, "@thrift.skip(proto, field_type)")
    }
    e.line(2, "}")
    let args = func.params.map(p => "arg_" + p.name).join(", ")
    if func.exceptions.length() > 0 {
      e.line(2, "match \{func.name}(\{args}) {")
      match func.ret {
        Some(ty) => {
          e.line(3, "Ok(value) => {")
          e.line(4, "proto.write_field_begin(\{wire_type(ty, names)}, 0)")
          e.line(4, write_of(ty, "value", names))
          e.line(3, "}")
        }
        None => e.line(3, "Ok(_) => ()")
      }
      for ex in func.exceptions {
        let ty = moon_type(ex.ty)
        e.line(3, "Err(\{ty}(value)) => {")
        e.line(4, "proto.write_field_begin(@thrift.type_struct, \{ex.id})")
        e.line(4, "value.encode(proto)")
        e.line(3, "}")
      }
      e.line(2, "}")
    } else {
      match func.ret {
        Some(ty) => {
          e.line(2, "let result = \{func.name}(\{args})")
          e.line(2, "proto.write_field_begin(\{wire_type(ty, names)}, 0)")
          e.line(2, write_of(ty, "result", names))
        }
        None => e.line(2, "\{func.name}(\{args})")
      }
    }
    e.line(1, "})")
  }
  e.line(0, "}")
  e.blank()

  // The client side: one function per method, over any transport.
  for func in methods {
    let name = "\{prefix}_\{func.name}"
    let throwing = func.exceptions.length() > 0
    let ret = match func.ret {
      Some(ty) =>
        if throwing {
          "Result[\{moon_type(ty)}, \{error_name(service.name, func.name)}]"
        } else {
          moon_type(ty)
        }
      None =>
        if throwing {
          "Result[Unit, \{error_name(service.name, func.name)}]"
        } else {
          "Unit"
        }
    }
    e.line(0, "pub async fn[T : @thrift.Transport] \{name}(")
    e.line(1, "transport : T,")
    e.line(1, "proto : &@thrift.Protocol,")
    for param in func.params {
      e.line(1, "\{param.name} : \{moon_type(param.ty)},")
    }
    e.line(0, ") -> \{ret} raise @thrift.ThriftError {")
    let mut call = "@thrift"
    if throwing {
      call = if func.ret is Some(_) {
        call + ".call_result"
      } else {
        call + ".call_result_unit"
      }
    } else if func.ret is Some(_) {
      call = call + ".call"
    } else {
      call = call + ".request"
    }
    if throwing {
      if func.params.length() == 0 {
        e.line(1, "\{call}(transport, proto, \"\{func.name}\", 1, _ => (),")
      } else {
        e.line(1, "\{call}(transport, proto, \"\{func.name}\", 1, proto => {")
        for param in func.params {
          e.line(
            2,
            "proto.write_field_begin(\{wire_type(param.ty, names)}, \{param.id})",
          )
          e.line(2, write_of(param.ty, param.name, names))
        }
        e.line(1, "},")
      }
      match func.ret {
        Some(ty) => e.line(1, "proto => \{read_of(ty, names)},")
        None => ()
      }
      e.line(1, "(proto, id) => {")
      e.line(2, "match id {")
      for ex in func.exceptions {
        let ty = moon_type(ex.ty)
        e.line(3, "\{ex.id} => Some(\{ty}(\{ty}::decode(proto)))")
      }
      e.line(3, "_ => None")
      e.line(2, "}")
      e.line(1, "})")
    } else if func.params.length() == 0 {
      e.line(
        1,
        "\{call}(transport, proto, \"\{func.name}\", 1, _ => ()\{read_tail(func.ret, names)})",
      )
    } else {
      e.line(1, "\{call}(transport, proto, \"\{func.name}\", 1, proto => {")
      for param in func.params {
        e.line(
          2,
          "proto.write_field_begin(\{wire_type(param.ty, names)}, \{param.id})",
        )
        e.line(2, write_of(param.ty, param.name, names))
      }
      e.line(1, "}\{read_tail(func.ret, names)})")
    }
    e.line(0, "}")
    e.blank()
  }
}

///|
/// The `, proto => ` that closes a `@thrift.call`, empty for a method that returns
/// nothing.
fn read_tail(ret : TType?, enums : Array[String]) -> String {
  match ret {
    Some(ty) => ", proto => \{read_of(ty, enums)}"
    None => ""
  }
}

///|
fn lower_first(name : String) -> String {
  if name.length() == 0 {
    return name
  }
  let first = name.get_char(0).unwrap_or('a')
  first.to_ascii_lowercase().to_string() + name[1:].to_owned()
}

// ---------------------------------------------------------------- validation

///|
fn owner_of(doc : Linked) -> Map[String, Struct] {
  let owner : Map[String, Struct] = Map([])
  for s in doc.structs {
    owner[s.name] = s
  }
  owner
}

///|
/// A struct that contains itself by value has no default and no end to its codec.
/// Containers and `optional` fields break the chain, because their default is empty.
fn visit_cycle(
  name : String,
  owner : Map[String, Struct],
  done : Map[String, Bool],
  path : Array[String],
  file : String,
) -> Unit raise Refused {
  if done.get(name) is Some(_) {
    return
  }
  if path.contains(name) {
    let chain = path.join(" -> ")
    refuse(
      file,
      0,
      "`\{name}` contains itself by value (\{chain} -> \{name}); make the field optional or put it in a list",
    )
  }
  path.push(name)
  match owner.get(name) {
    Some(s) =>
      for f in s.fields {
        if !f.optional {
          if f.ty is Named(inner) && owner.get(inner) is Some(_) {
            visit_cycle(inner, owner, done, path, file)
          }
        }
      }
    None => ()
  }
  ignore(path.pop())
  done[name] = true
}

///|
fn check_cycles(doc : Linked, file : String) -> Unit raise Refused {
  let owner = owner_of(doc)
  let done : Map[String, Bool] = Map([])
  for s in doc.structs {
    visit_cycle(s.name, owner, done, [], file)
  }
}

///|
fn check_type(
  ty : TType,
  enums : Array[String],
  structs : Array[String],
  file : String,
) -> Unit raise Refused {
  match ty {
    Named(name) =>
      if !enums.contains(name) && !structs.contains(name) {
        refuse(
          file,
          0,
          "`\{name}` is not declared in this file or in anything it includes",
        )
      }
    List(inner) | Set(inner) => check_type(inner, enums, structs, file)
    Map(key, value) => {
      if key is Named(_) {
        refuse(
          file,
          0,
          "a map key must be a primitive or a string; `\{moon_type(key)}` is not",
        )
      }
      check_type(key, enums, structs, file)
      check_type(value, enums, structs, file)
    }
    _ => ()
  }
}

///|
/// A `throws` clause may name only a declared `exception`; a struct there would
/// generate a variant nobody can construct and would blame the wrong declaration.
fn check_throws(
  func : Function,
  exceptions : Array[String],
  structs : Array[String],
  file : String,
) -> Unit raise Refused {
  for ex in func.exceptions {
    match ex.ty {
      Named(name) =>
        if !exceptions.contains(name) {
          if structs.contains(name) {
            refuse(
              file,
              0,
              "`\{func.name}` throws `\{name}`, which is a struct; declare it as an `exception`",
            )
          } else {
            refuse(
              file,
              0,
              "`\{func.name}` throws `\{name}`, which is not declared in this file or in anything it includes",
            )
          }
        }
      _ =>
        refuse(
          file,
          0,
          "`\{func.name}` throws something that is not a named exception",
        )
    }
  }
}

///|
fn check_names(doc : Linked, file : String) -> Unit raise Refused {
  let enums = doc.enums.map(e => e.name)
  let structs = doc.structs.map(s => s.name)
  let exceptions = doc.structs
    .filter(s => s.kind is ExceptionDecl)
    .map(s => s.name)
  for s in doc.structs {
    for f in s.fields {
      check_type(f.ty, enums, structs, file)
    }
  }
  for c in doc.constants {
    check_type(c.ty, enums, structs, file)
  }
  for service in doc.services {
    for func in service.functions {
      for param in func.params {
        check_type(param.ty, enums, structs, file)
      }
      match func.ret {
        Some(ty) => check_type(ty, enums, structs, file)
        None => ()
      }
      check_throws(func, exceptions, structs, file)
    }
  }
}

// ---------------------------------------------------------------- constants

///|
fn find_struct(structs : Array[Struct], name : String) -> Struct? {
  for item in structs {
    if item.name == name {
      return Some(item)
    }
  }
  None
}

///|
fn enum_has(item : Enum, tag : String) -> Bool {
  for pair in item.values {
    let (label, _) = pair
    if label == tag {
      return true
    }
  }
  false
}

///|
/// A string as a MoonBit literal, with the escapes a round trip needs.
fn quoted(text : String) -> String {
  let mut out = "\""
  for i in 0.. "\\\""
        '\\' => "\\\\"
        '\n' => "\\n"
        '\t' => "\\t"
        '\r' => "\\r"
        _ => c.to_string()
      })
  }
  out + "\""
}

///|
fn in_range(
  value : Int64,
  low : Int64,
  high : Int64,
  name : String,
  file : String,
  line : Int32,
) -> String raise Refused {
  if value < low || value > high {
    refuse(file, line, "`\{name}` does not fit in \{value}'s type")
  }
  value.to_string()
}

///|
fn double_literal(value : Double) -> String {
  let text = value.to_string()
  let mut plain = true
  for i in 0.. String raise Refused {
  let wrong = () => {
    refuse(
      file,
      line,
      "`\{owner}` is a \{moon_type(ty)} and the value it is given is not",
    )
  }
  match ty {
    Bool =>
      match value {
        CBool(b) => if b { "true" } else { "false" }
        _ => wrong()
      }
    Byte =>
      match value {
        CInt(v) => in_range(v, -128L, 127L, owner, file, line)
        _ => wrong()
      }
    I16 =>
      match value {
        CInt(v) => in_range(v, -32768L, 32767L, owner, file, line)
        _ => wrong()
      }
    I32 =>
      match value {
        CInt(v) => in_range(v, -2147483648L, 2147483647L, owner, file, line)
        _ => wrong()
      }
    I64 =>
      match value {
        CInt(v) => v.to_string() + "L"
        _ => wrong()
      }
    Double =>
      match value {
        CFloat(v) => double_literal(v)
        CInt(v) => v.to_string() + ".0"
        _ => wrong()
      }
    Str =>
      match value {
        CStr(text) => quoted(text)
        _ => wrong()
      }
    Binary =>
      match value {
        CStr(text) => "b" + quoted(text)
        _ => wrong()
      }
    List(inner) =>
      match value {
        CList([]) => "([] : Array[\{moon_type(inner)}])"
        CList(items) =>
          "[" +
          items
          .map(item => {
            render_value(inner, item, enums, structs, file, line, owner)
          })
          .join(", ") +
          "]"
        _ => wrong()
      }
    Set(inner) =>
      match value {
        CList([]) => "([] : Array[\{moon_type(inner)}])"
        CList(items) =>
          "[" +
          items
          .map(item => {
            render_value(inner, item, enums, structs, file, line, owner)
          })
          .join(", ") +
          "]"
        _ => wrong()
      }
    Map(key, inner) =>
      match value {
        CMap([]) => "(Map([]) : Map[\{moon_type(key)}, \{moon_type(inner)}])"
        CMap(entries) => {
          let parts : Array[String] = []
          for entry in entries {
            let (k, v) = entry
            let key_text = render_value(
              key, k, enums, structs, file, line, owner,
            )
            let value_text = render_value(
              inner, v, enums, structs, file, line, owner,
            )
            parts.push("(\{key_text}, \{value_text})")
          }
          "Map([" + parts.join(", ") + "])"
        }
        _ => wrong()
      }
    Named(name) =>
      match find_enum(enums, name) {
        Some(item) =>
          match value {
            CEnum(_, tag) => {
              if !enum_has(item, tag) {
                refuse(file, line, "`\{name}` has no value `\{tag}`")
              }
              "\{name}::\{tag}"
            }
            _ =>
              refuse(
                file,
                line,
                "`\{owner}` is an enum; write one of its values as \{name}.VALUE",
              )
          }
        None =>
          match find_struct(structs, name) {
            Some(item) =>
              match value {
                CMap(entries) => {
                  let seen : Array[String] = []
                  for entry in entries {
                    let (k, _) = entry
                    match k {
                      CStr(key) => {
                        if !item.fields.any(f => f.name == key) {
                          refuse(file, line, "`\{name}` has no field `\{key}`")
                        }
                        if seen.contains(key) {
                          refuse(
                            file,
                            line,
                            "`\{owner}` sets `\{name}.\{key}` twice",
                          )
                        }
                        seen.push(key)
                      }
                      _ =>
                        refuse(
                          file,
                          line,
                          "`\{name}` needs its field names as keys",
                        )
                    }
                  }
                  let parts : Array[String] = []
                  for f in item.fields {
                    let mut given : ConstValue? = None
                    for entry in entries {
                      let (k, v) = entry
                      if k is CStr(key) && key == f.name {
                        given = Some(v)
                      }
                    }
                    let text = match given {
                      Some(v) =>
                        render_value(f.ty, v, enums, structs, file, line, owner)
                      None => default_of(f.ty, enums)
                    }
                    let text = if f.optional {
                      match given {
                        Some(_) => "Some(\{text})"
                        None => "None"
                      }
                    } else {
                      text
                    }
                    parts.push("\{f.name}: \{text}")
                  }
                  "{ " + parts.join(", ") + " }"
                }
                _ =>
                  refuse(
                    file,
                    line,
                    "`\{owner}` is a \{name}; write its fields as `{ name: value }`",
                  )
              }
            None =>
              refuse(
                file,
                line,
                "`\{name}` is not declared in this file or in anything it includes",
              )
          }
      }
  }
}

///|
fn emit_constant(
  e : Emitter,
  item : Constant,
  enums : Array[Enum],
  structs : Array[Struct],
  file : String,
) -> Unit raise Refused {
  let value = render_value(
    item.ty,
    item.value,
    enums,
    structs,
    file,
    item.line,
    item.name,
  )
  e.line(0, "pub let \{item.name} : \{moon_type(item.ty)} = \{value}")
  e.blank()
}

// ---------------------------------------------------------------- the file

///|
/// Render one .thrift file as MoonBit. A file with no includes needs no file system.
pub fn render(file : String, source : String) -> String raise Refused {
  render_with(file, source, _ => None)
}

///|
/// Render one .thrift file with its includes folded in. `load` reads an included file;
/// a loader that always answers `None` compiles a file that has no includes.
pub fn render_with(
  file : String,
  source : String,
  load : Loader,
) -> String raise Refused {
  let doc = link(file, source, load)
  check_names(doc, file)
  check_cycles(doc, file)
  let e : Emitter = { out: [], }
  e.line(0, "// Generated from \{base_name(file)} by thriftc. Do not edit.")
  e.line(
    0, "// The package importing this file needs `moonbitstack/moonrpc/thrift`.",
  )
  e.blank()
  for item in doc.enums {
    emit_enum(e, item)
  }
  for item in doc.structs {
    emit_struct(e, item, doc.enums, file)
  }
  for ty in collect_containers(doc) {
    emit_container(e, ty, doc.enums.map(x => x.name))
  }
  for item in doc.constants {
    emit_constant(e, item, doc.enums, doc.structs, file)
  }
  for service in doc.services {
    emit_service(
      e,
      service,
      doc.enums,
      service_methods(service, doc.by_service, [], file),
    )
  }
  e.text()
}

///|
fn base_name(path : String) -> String {
  let mut start = 0
  for i in 0..