///|
async fn main {
  let files = []
  @ArgParser.parse([], path => files.push(path), "", @env.args()[1:].to_array())
  for file in files {
    if !@fs.exists(file) {
      abort("file not found: \{file}")
    }
    let result = @fs.read_file(file).text() |> process_file(name=file)
    @fs.write_file(file, result, truncate=true)
    let _ = @process.collect_output("moonfmt", ["-w", file])
    println("written to \{file}")
  }
}

///|
let jmop_pkg = "@Yoorkin/jmop"

///|
let jmop_optional = "\{jmop_pkg}.Optional"

///|
let jmop_object = "\{jmop_pkg}.Object"

///|
let jmop_value = "\{jmop_pkg}.Value"

///|
let jmop_nullable = "\{jmop_pkg}.Nullable"

///|
let jmop_convert_error = "\{jmop_pkg}.convert_error"

///|
let jmop_is_value = "\{jmop_pkg}.IsValue"

///|
let jmop_is_object = "\{jmop_pkg}.IsObject"

///|
let jmop_is_any = "\{jmop_pkg}.IsAny"

///|
let jmop_dyn_cast = "\{jmop_pkg}.DynCast"

///|
let jmop_fail_cast = "\{jmop_pkg}.fail_cast"

///|
fn process_file(content : String, name~ : String) -> String {
  let (impls, reports) = parse_string(content, name~)
  if !reports.is_empty() {
    println(reports)
    abort("failed to parse \{name}")
  }
  let directives = impls.filter_map(extract_directive)
  let patches = generate_patch(directives)
  apply_patch0(content, patches)
}

///|
enum TypeAnnotation {
  Constr(@syntax.LongIdent, Array[TypeAnnotation])
  Option(TypeAnnotation)
  Tuple(Array[TypeAnnotation])
  Arrow(Array[TypeAnnotation], TypeAnnotation, err~ : ErrorTypeAnnoatation)
  Object(@syntax.LongIdent)
  Any(Location)
} derive(ToJson)

///|
pub(all) enum ErrorTypeAnnoatation {
  ErrorType(TypeAnnotation)
  DefaultErrorType
  NoErrorType
  Noraise
  MaybeError(TypeAnnotation)
} derive(ToJson)

///|
enum RefineDirective {
  UnionType(Array[String])
  UnionTypeOf(String)
  Nothing
} derive(ToJson)

///|
enum ParamDirective {
  Positional(
    name~ : String,
    ty~ : TypeAnnotation,
    mut refine~ : RefineDirective
  )
  Optional(name~ : String, ty~ : TypeAnnotation, mut refine~ : RefineDirective)
  Labeled(name~ : String, ty~ : TypeAnnotation, mut refine~ : RefineDirective)
} derive(ToJson)

///|
fn ParamDirective::set_refine(self : Self, value : RefineDirective) -> Unit {
  match self {
    Positional(..) as v => v.refine = value
    Optional(..) as v => v.refine = value
    Labeled(..) as v => v.refine = value
  }
}

///|
fn ParamDirective::get_refine(self : Self) -> RefineDirective {
  match self {
    Positional(..) as v => v.refine
    Optional(..) as v => v.refine
    Labeled(..) as v => v.refine
  }
}

///|
struct ResultDirective {
  ty : TypeAnnotation
  throws : Array[String]
  optional : Bool
  nullable : Bool
} derive(ToJson)

///|
pub fn ParamDirective::name(self : Self) -> String {
  match self {
    Positional(name~, ..) | Optional(name~, ..) | Labeled(name~, ..) => name
  }
}

///|
fn process_error_type(ety : @syntax.ErrorType) -> ErrorTypeAnnoatation {
  match ety {
    ErrorType(ty~) => ErrorType(process_type_annotation(ty))
    DefaultErrorType(..) => DefaultErrorType
    NoErrorType => NoErrorType
    Noraise(..) => Noraise
    MaybeError(ty~) => MaybeError(process_type_annotation(ty))
  }
}

///|
fn process_type_annotation(ty : @syntax.Type) -> TypeAnnotation {
  match ty {
    Option(ty~, ..) => Option(process_type_annotation(ty))
    Name(constr_id~, tys~, ..) =>
      Constr(constr_id.id, tys.map(process_type_annotation).to_array())
    Tuple(tys~, ..) => Tuple(tys.map(process_type_annotation).to_array())
    Arrow(args~, res~, err~, ..) =>
      Arrow(
        args.map(process_type_annotation).to_array(),
        process_type_annotation(res),
        err=process_error_type(err),
      )
    Object(constr_id) => Object(constr_id.id)
    Any(loc~) => Any(loc)
  }
}

///|
fn process_parameter(param : @syntax.Parameter) -> ParamDirective {
  match param {
    DiscardPositional(ty~, loc~) => {
      ignore((ty, loc))
      abort("discard parameter is not supported")
    }
    Positional(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Positional(
        name=binder.name,
        ty=process_type_annotation(ty),
        refine=Nothing,
      )
    }
    Labelled(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Labeled(name=binder.name, ty=process_type_annotation(ty), refine=Nothing)
    }
    Optional(binder~, default~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      ignore(default) //TODO: use default value
      Optional(name=binder.name, ty=process_type_annotation(ty), refine=Nothing)
    }
    QuestionOptional(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Optional(name=binder.name, ty=process_type_annotation(ty), refine=Nothing)
    }
  }
}

///|
enum Directive {
  Type(name~ : String, hole~ : Location)
  Constructor(
    ty~ : String,
    name~ : String,
    arguments~ : Map[String, ParamDirective],
    result~ : ResultDirective,
    bound_loc~ : BoundPosition,
    hole~ : Location,
    source_loc~ : String?
  )
  Getter(
    name~ : String,
    ty~ : String,
    result~ : ResultDirective,
    bound_loc~ : BoundPosition,
    hole~ : Location,
    source_loc~ : String?
  )
  Setter(
    name~ : String,
    ty~ : String,
    value~ : ParamDirective?,
    result~ : ResultDirective,
    bound_loc~ : BoundPosition,
    hole~ : Location,
    source_loc~ : String?
  )
  Function(
    name~ : String,
    ty~ : String,
    arguments~ : Map[String, ParamDirective],
    result~ : ResultDirective,
    bound_loc~ : BoundPosition,
    hole~ : Location,
    source_loc~ : String?
  )
  Method(
    name~ : String,
    ty~ : String,
    arguments~ : Map[String, ParamDirective],
    result~ : ResultDirective,
    bound_loc~ : BoundPosition,
    hole~ : Location,
    source_loc~ : String?
  )
  Union(name~ : String, tys~ : Array[String], loc~ : Location)
  Generated(hole~ : Location)
} derive(ToJson)

///|
enum BoundPosition {
  InsertPos(Position)
  ReplaceLoc(Location)
  Nothing
} derive(ToJson)

///|
fn extract_directive(toplevel : @syntax.Impl) -> Directive? {
  let attrs = match toplevel {
    TopTypeDef(t) => t.attrs
    TopFuncDef(fun_decl~, ..) => fun_decl.attrs
    TopTrait(decl) => decl.attrs
    TopImpl(attrs~, ..) => attrs
    TopImplRelation(..) => return None
    _ =>
      abort(
        "\{toplevel.loc()}: only impl, type def and function def are supported",
      )
  }
  let exprs = attrs.filter_map(attr => match attr.parsed {
    Some(Apply({ qual: Some("webgen"), name }, props)) =>
      Some((name, props, attr.loc))
    Some(Ident({ qual: Some("webgen"), name })) =>
      Some((name, @list.empty(), attr.loc))
    _ => None
  })
  guard exprs is More(main, tail=rest) else {
    abort(
      "error (\{toplevel.loc()}, \{attrs}, \{toplevel.to_json()}): expected ty, constructor, getter, setter",
    )
  }
  if main.0 == "generated" {
    return Some(Generated(hole=toplevel.loc()))
  }
  let mut is_result_nullable = false
  let mut is_result_optional = false
  let throws = []
  let arguments = {}
  let (name, ty, hole, bound_loc, result_ty) = match toplevel {
    TopFuncDef(
      fun_decl={
        name: { name, .. },
        type_name: Some({ name: Ident(name=ty), loc: type_name_loc }),
        decl_params,
        return_type,
        quantifiers,
        ..,
      },
      decl_body=DeclBody(expr~, ..),
      ..
    ) => {
      let name = if main.0 is ("getter" | "setter") {
        let trimed = if name.has_prefix("get_") || name.has_prefix("set_") {
          try! name[4:]
        } else if name.has_prefix("is_") {
          try! name[3:]
        } else {
          name
        }
        snake_to_small_camel(trimed)
      } else {
        name
      }
      for param in decl_params.unwrap() {
        let d = process_parameter(param)
        arguments[d.name()] = d
      }
      let result = return_type.map(process_type_annotation)
      let bound_loc = if quantifiers.is_empty() {
        InsertPos(type_name_loc.start)
      } else {
        let start = quantifiers.head().unwrap().name_loc.start
        let last = quantifiers.last().unwrap()
        let end = match last.constraints.last() {
          Some(c) => c.loc.end
          None => last.name_loc.end
        }
        ReplaceLoc(Location::{ start, end })
      }
      (name, ty, expr.loc(), bound_loc, result)
    }
    TopTypeDef({ tycon, loc, .. }) => (tycon, tycon, loc, Nothing, None)
    TopTrait({ name: { name, .. }, loc, .. }) =>
      (name, name, loc, Nothing, None)
    TopImpl(trait_={ name, loc: trait_loc }, ..) => {
      let n = longident_to_string(name)
      (n, n, toplevel.loc(), InsertPos(trait_loc.start), None)
    }
    _ => abort("\{toplevel.loc()}: missing type name or binder name")
  }
  fn filter_name_prop(props : List[@attribute.Prop]) {
    let result = props.filter_map(fn(prop) {
      match prop {
        Labeled("name", String(v)) => Some(v)
        _ => None
      }
    })
    match result {
      More(r, tail=Empty) => Some(r)
      Empty => None
      _ => panic() // report error
    }
  }

  let name = if filter_name_prop(main.1) is Some(v) { v } else { name }
  for expr in rest {
    let (name, props, loc) = expr
    match name {
      "result" =>
        for prop in props {
          match prop {
            Expr(Ident({ qual: None, name: "nullable" })) =>
              is_result_nullable = true
            Expr(Ident({ qual: None, name: "optional" })) =>
              is_result_optional = true
            Expr(Apply({ qual: None, name: "error" }, props)) =>
              for prop in props {
                match prop {
                  Expr(String(s)) => throws.push(s)
                  _ => abort("\{loc}: invalid throws argument")
                }
              }
            _ => abort("\{loc}: invalid result argument")
          }
        }
      "refine" =>
        for prop in props {
          match prop {
            Labeled(arg, String(v)) =>
              match arguments.get(arg) {
                None => abort("\{loc}: no such argument: \{arg}")
                Some(d) => d.set_refine(UnionTypeOf(v))
              }
            Labeled(arg, Apply({ qual: None, name: "union" }, props)) => {
              let tys = props.map(fn(p) {
                match p {
                  Expr(String(s)) => s
                  _ => abort("\{loc}: invalid union argument")
                }
              })
              match arguments.get(arg) {
                None => abort("\{loc}: no such argument: \{arg}")
                Some(d) => d.set_refine(UnionType(tys.to_array()))
              }
            }
            _ => abort("\{loc}: invalid refine argument")
          }
        }
      _ => abort("\{loc}: unknown webgen attribute: \{name}")
    }
  }
  // TODO: better error handling
  let source_loc = if arguments.contains("loc") { Some("loc") } else { None }
  match main.0 {
    "ty" => Some(Type(name~, hole~))
    "constructor" => {
      guard result_ty is Some(result_ty) else {
        abort("constructor must have return type")
      }
      let result = {
        ty: result_ty,
        throws,
        optional: is_result_optional,
        nullable: is_result_nullable,
      }
      Some(
        Constructor(
          arguments~,
          name~,
          ty~,
          result~,
          hole~,
          source_loc~,
          bound_loc~,
        ),
      )
    }
    "getter" => {
      guard result_ty is Some(result_ty) else {
        abort("getter must have return type")
      }
      let result = {
        ty: result_ty,
        throws,
        optional: is_result_optional,
        nullable: is_result_nullable,
      }
      Some(Getter(name~, ty~, result~, hole~, source_loc~, bound_loc~))
    }
    "setter" => {
      guard result_ty is Some(result_ty) else {
        abort("getter must have return type")
      }
      let result = {
        ty: result_ty,
        throws,
        optional: is_result_optional,
        nullable: is_result_nullable,
      }
      let value = arguments.get("value")
      Some(Setter(name~, ty~, value~, result~, hole~, source_loc~, bound_loc~))
    }
    "function" => {
      guard result_ty is Some(result_ty) else {
        abort("getter must have return type")
      }
      let result = {
        ty: result_ty,
        throws,
        optional: is_result_optional,
        nullable: is_result_nullable,
      }
      Some(
        Function(
          name~,
          ty~,
          arguments~,
          result~,
          hole~,
          source_loc~,
          bound_loc~,
        ),
      )
    }
    "method" => {
      guard result_ty is Some(result_ty) else {
        abort("getter must have return type")
      }
      let result = {
        ty: result_ty,
        throws,
        optional: is_result_optional,
        nullable: is_result_nullable,
      }
      Some(
        Method(name~, ty~, arguments~, result~, hole~, source_loc~, bound_loc~),
      )
    }
    "union" => {
      let tys = []
      for prop in main.1 {
        match prop {
          Expr(String(s)) => tys.push(s)
          _ => abort("invalid union argument")
        }
      }
      // TODO: fix trait location
      let loc = main.2.merge(toplevel.loc())
      Some(Union(name~, tys~, loc~))
    }
    _ => abort("\{hole}, unknown webgen attribute: \{main.0}")
  }
}

///|
enum Patch {
  Insert(Position, String)
  Replace(Location, String)
  Append(String)
  Remove(Location)
} derive(Show, ToJson)

///|
fn longident_to_string(id : @syntax.LongIdent) -> String {
  match id {
    Dot(pkg~, id~) => "@\{pkg}.\{id}"
    Ident(name~) => name
  }
}

///|
fn generate_ffi_type_annotation(
  ty : TypeAnnotation,
  refine : RefineDirective,
) -> String {
  fn go(ty) {
    match ty {
      Arrow(args, res, err~) => {
        ignore((args, res, err))
        abort("function type is not supported")
      }
      Tuple(tys) => "(" + tys.map(go).join(", ") + ")"
      Object(name) => "&" + longident_to_string(name)
      Option(ty) => "Nullable[\{go(ty)}]"
      Constr(name, args) => {
        let name = longident_to_string(name)
        if args.is_empty() {
          name
        } else {
          name + "[" + args.map(go).join(", ") + "]"
        }
      }
      Any(loc) => "Any"
    }
  }

  match refine {
    Nothing => go(ty)
    UnionType(_) | UnionTypeOf(_) => jmop_value
  }
}

///|
fn generate_ffi_params(arguments : Map[String, ParamDirective]) -> String {
  let buf = StringBuilder::new()
  let mut first = true
  for _, arg in arguments {
    if first {
      first = false
    } else {
      buf.write_string(", ")
    }
    match arg {
      Positional(name~, ty~, refine~) =>
        buf.write_string(
          "\{name} : \{generate_ffi_type_annotation(ty, refine)}",
        )
      Optional(name~, ty~, refine~) =>
        buf.write_string(
          "\{name} : \{jmop_optional}[\{generate_ffi_type_annotation(ty, refine)}]",
        )
      Labeled(name~, ty~, refine~) =>
        buf.write_string(
          "\{name} : \{generate_ffi_type_annotation(ty, refine)}",
        )
    }
  }
  buf.to_string()
}

///|
fn generate_glude_args(arguments : Map[String, ParamDirective]) -> String {
  let buf = StringBuilder::new()
  let mut first = true
  fn handle_nullable(arg_name : String, ty : TypeAnnotation) -> String {
    match ty {
      Option(_) => "\{jmop_optional}::from_option(\{arg_name})"
      _ => arg_name
    }
  }

  for _, arg in arguments {
    if first {
      first = false
    } else {
      buf.write_string(", ")
    }
    match arg {
      Optional(name~, ty~, refine=_) =>
        buf
        ..write_string("\{jmop_optional}::from_option(")
        ..write_string(handle_nullable(name, ty))
        ..write_string(")")
      Positional(name~, ty~, refine=_) | Labeled(name~, ty~, refine=_) =>
        buf.write_string(handle_nullable(name, ty))
    }
    match arg {
      Optional(refine~, ..) | Positional(refine~, ..) | Labeled(refine~, ..) =>
        match refine {
          UnionType(_) | UnionTypeOf(_) => buf.write_string(".as_value()")
          Nothing => ()
        }
    }
  }
  buf.to_string()
}

///|
fn generate_wrapper_type_bounds(
  arguments : Map[String, ParamDirective],
) -> String {
  let bounds = []
  for name, arg in arguments {
    match arg.get_refine() {
      UnionType(tys) => abort("not implemented")
      UnionTypeOf(trait_name) => {
        let name = snake_to_big_camel(name)
        bounds.push("\{name} : \{trait_name}")
      }
      Nothing => ()
    }
  }
  match bounds {
    [] => ""
    _ => bounds.join(", ")
  }
}

///|
fn generate_ffi_result(result : ResultDirective, self_ty : String) -> String {
  match result.ty {
    Option(Option(ty)) if result.nullable && result.optional =>
      "\{jmop_optional}[\{jmop_nullable}[\{generate_ffi_type_annotation(ty, Nothing)}]]"
    Option(ty) if result.nullable =>
      "\{jmop_nullable}[\{generate_ffi_type_annotation(ty, Nothing)}]"
    Option(ty) if result.optional =>
      "\{jmop_optional}[\{generate_ffi_type_annotation(ty, Nothing)}]"
    Option(_) => abort("result type need to be marked as nullable or optional")
    Constr(Ident(name="Self"), []) => self_ty
    _ => generate_ffi_type_annotation(result.ty, Nothing)
  }
}

///|
fn generate_args_name(
  arguments : Map[String, ParamDirective],
) -> (String?, String) {
  let ins_name = arguments.get("self").map(d => d.name())
  arguments.remove("self")
  let args = StringBuilder::new()
  let mut first = true
  for _, arg in arguments {
    if first {
      first = false
    } else {
      args.write_string(", ")
    }
    args.write_string(arg.name())
  }
  (ins_name, args.to_string())
}

///|
fn generate_glude_result_handling(
  glude : String,
  result : ResultDirective,
  source_loc : String?,
) -> String {
  let glude = if result.optional && result.nullable {
    "\{glude}.to_option().map(x => x.to_option())"
  } else if result.optional || result.nullable {
    "\{glude}.to_option()"
  } else {
    glude
  }
  if !result.throws.is_empty() && source_loc is Some(name) {
    "\{jmop_convert_error}(\{name}, fn(){\{glude}})"
  } else {
    // TODO: report error if throws is not empty but source_loc is None
    glude
  }
}

///|
fn generate_patch(directives : List[Directive]) -> Array[Patch] {
  let patches = []
  for directive in directives {
    match directive {
      Method(name~, ty~, arguments~, result~, bound_loc~, hole~, source_loc~) => {
        let ffi_name = "ffi_\{ty}_\{name}"
        let (ins_name, args_name) = generate_args_name(arguments)
        guard ins_name is Some(ins_name) else {
          abort("method must have self parameter")
        }
        let wrapper_type_bounds = generate_wrapper_type_bounds(arguments)
        let glude_args = generate_glude_args(arguments)
        let glude =
          $|\{ffi_name}(self, \{glude_args})
        let glude = generate_glude_result_handling(glude, result, source_loc)
        let ffi_result = generate_ffi_result(result, ty)
        let ffi_params = generate_ffi_params(arguments)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn \{ffi_name}(ins : \{ty}, \{ffi_params}) -> \{ffi_result} =
          $|   "(\{ins_name},\{args_name}) => \{ins_name}.\{name}(\{args_name})" 
        match bound_loc {
          InsertPos(pos) if wrapper_type_bounds != "" =>
            patches.push(Insert(pos, "[" + wrapper_type_bounds + "]"))
          ReplaceLoc(loc) if wrapper_type_bounds != "" =>
            patches.push(Replace(loc, wrapper_type_bounds))
          _ => ()
        }
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Constructor(
        ty~,
        name~,
        arguments~,
        result~,
        bound_loc~,
        hole~,
        source_loc~
      ) => {
        let ffi_name = "ffi_\{ty}_\{name}"
        let (_, args_name) = generate_args_name(arguments)
        let ffi_params = generate_ffi_params(arguments)
        let wrapper_type_bounds = generate_wrapper_type_bounds(arguments)
        let glude_args = generate_glude_args(arguments)
        let glude =
          $|\{ffi_name}(\{glude_args})
        let glude = generate_glude_result_handling(glude, result, source_loc)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn \{ffi_name}(\{ffi_params}) -> \{ty} = 
          $|    "(\{args_name}) => new \{ty}(\{args_name})" 
        match bound_loc {
          InsertPos(pos) if wrapper_type_bounds != "" =>
            patches.push(Insert(pos, "[" + wrapper_type_bounds + "]"))
          ReplaceLoc(loc) if wrapper_type_bounds != "" =>
            patches.push(Replace(loc, wrapper_type_bounds))
          _ => ()
        }
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Getter(ty~, name~, result~, hole~, source_loc~, bound_loc~) => {
        let glude =
          $|ffi_\{ty}_get_\{name}(self)
        let glude = generate_glude_result_handling(glude, result, source_loc)
        let ffi_result = generate_ffi_result(result, ty)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}) -> \{ffi_result}
          $|  = "(ins) => ins.\{name}" 
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Setter(ty~, name~, value~, hole~, source_loc~, result~, bound_loc~) => {
        guard value is Some(value) else {
          abort("setter must have value parameter")
        }
        let ffi_params = generate_ffi_params({ "value": value })
        let glude =
          $|ffi_\{ty}_set_\{name}(self, \{value.name()})
        let glude = generate_glude_result_handling(glude, result, source_loc)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}, \{ffi_params}) = 
          $|    "(ins) => ins.\{name}" 
        let wrapper_type_bounds = generate_wrapper_type_bounds({
          "value": value,
        })
        match bound_loc {
          InsertPos(pos) if wrapper_type_bounds != "" =>
            patches.push(Insert(pos, "[" + wrapper_type_bounds + "]"))
          ReplaceLoc(loc) if wrapper_type_bounds != "" =>
            patches.push(Replace(loc, wrapper_type_bounds))
          _ => ()
        }
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Generated(hole~) => patches.push(Remove(hole))
      Type(name~, hole~) => {
        // TODO: support tuple struct syntax
        let typedef =
          #|#webgen.ty
          $|struct \{name} (\{jmop_object})
        let impl_is_any =
          #|#webgen.generated 
          $|pub impl \{jmop_is_any} for \{name}
        let impl_is_value =
          #|#webgen.generated 
          $|pub impl \{jmop_is_value} for \{name} with as_value(self) {
          $|  object_from_\{name}(self).as_value()
          $|}
          $|
          $|#webgen.generated
          $|fn object_from_\{name}(x : \{name}) -> \{jmop_object} = "%identity"
        let impl_is_object =
          #|#webgen.generated
          $|pub impl \{jmop_is_object} for \{name}
        let impl_dyn_cast =
          #|#webgen.generated
          $|pub impl \{jmop_dyn_cast} for \{name} with from_value(x) {
          $|  if x.instance_of("\{name}") {
          $|    unsafe_value_to_\{name}(x)
          $|  } else {
          $|    \{jmop_fail_cast}(target="\{name}")
          $|  }
          $|}
          $|
          #|#webgen.generated
          $|fn unsafe_value_to_\{name}(x : \{jmop_value}) -> \{name} = "%identity"
        patches
        ..push(Replace(hole, typedef))
        ..push(Append(impl_is_any))
        ..push(Append(impl_is_value))
        ..push(Append(impl_is_object))
        ..push(Append(impl_dyn_cast))
        ignore((name, hole))
      }
      Union(name~, tys~, loc~) => {
        let tys_str = tys.map(s => "\"\{s}\"").join(", ")
        let trait_def =
          $|#webgen.union(\{tys_str})
          $|trait \{name} : \{jmop_is_value} {}
        let impls = tys
          .map(s => (
            $|#webgen.generated
            $|pub impl \{name} for \{s}
          ))
          .join("\n\n")
        patches..push(Replace(loc, trait_def))..push(Append(impls))
      }
      Function(..) => ()
    }
  }
  patches
}

///|
fn snake_to_small_camel(s : StringView) -> String {
  // TODO: verify input string
  let parts = s.split("_").collect()
  let buf = StringBuilder::new()
  for i, part in parts {
    if i == 0 {
      buf.write_string(part.to_lower().to_string())
    } else {
      buf
      ..write_string(try! part[0:1].to_upper().to_string())
      ..write_string(try! part[1:].to_lower().to_string())
    }
  }
  buf.to_string()
}

///|
fn snake_to_big_camel(s : StringView) -> String {
  // TODO: verify input string
  let parts = s.split("_").collect()
  let buf = StringBuilder::new()
  for i, part in parts {
    buf
    ..write_string(try! part[0:1].to_upper().to_string())
    ..write_string(try! part[1:].to_lower().to_string())
  }
  buf.to_string()
}

///|
test "snake_to_small_camel" {
  inspect(snake_to_small_camel("hello_world"), content="helloWorld")
  inspect(snake_to_small_camel("Hello_World_one"), content="helloWorldOne")
  inspect(snake_to_small_camel("hello"), content="hello")
  inspect(snake_to_small_camel("h"), content="h")
  inspect(snake_to_small_camel(""), content="")
}

///|
fn remove_block_line(source : String) -> String {
  let lines = source.split("\n").collect()
  let filtered = lines.filter_map(fn(line) {
    if line.has_prefix("///|") {
      None
    } else {
      Some(line)
    }
  })
  filtered.join("\n")
}

///|
fn apply_patch0(source : String, patches : Array[Patch]) -> String {
  let appended = []
  let replacements = []
  for patch in patches {
    match patch {
      Insert(pos, s) =>
        replacements.push(point(pos.lnum - 1, pos.column() - 1, s))
      Replace(loc, s) =>
        replacements.push(
          multiline(
            (loc.start.lnum - 1, loc.start.column() - 1),
            (loc.end.lnum - 1, loc.end.column() - 2),
            s,
          ),
        )
      Remove(loc) =>
        replacements.push(
          multiline(
            (loc.start.lnum - 1, loc.start.column() - 1),
            (loc.end.lnum - 1, loc.end.column() - 2),
            "",
          ),
        )
      Append(text) => appended.push(text)
    }
  }
  let buf = StringBuilder::new(size_hint=source.length() * 2)
  buf.write_string(splice(source, replacements))
  for text in appended {
    buf.write_string("\n")
    buf.write_string(text)
  }
  buf.to_string() |> remove_block_line
}