open StdLabels
open Shims

let prog_name = Filename.basename Sys.executable_name

let dump_ast = ref false

(* Table from positions to custom operators at these positions *)
let custom_operators = Hashtbl.create 128

module Wrap_lexer = struct
  let save_loc = Location.curr

  let restore_loc (lexbuf : Lexing.lexbuf) (loc : Location.t) =
    lexbuf.lex_start_p <- loc.loc_start;
    lexbuf.lex_curr_p <- loc.loc_end

  let encode_op (tok : Parser.token) op =
    (match tok with LET -> "let__" | AND -> "and__" | _ -> assert false) ^ op

  let pending = Queue.create ()

  let add (x : Parser.token * _) = Queue.push x pending

  let register_custom_operator tok op (loc1 : Location.t) (loc2 : Location.t) =
    let op = encode_op tok op in
    Hashtbl.add custom_operators loc1.loc_start
      ({ loc1 with loc_end = loc2.loc_end }, op)

  let wrap (lexer : Lexing.lexbuf -> Parser.token) lb =
    if not (Queue.is_empty pending) then (
      let tok, loc = Queue.pop pending in
      restore_loc lb loc;
      tok )
    else
      match lexer lb with
      | (LET | AND) as tok ->
          let loc = save_loc lb in
          ( match Let_trail.op lb with
          | None -> ()
          | Some op -> register_custom_operator tok op loc (save_loc lb) );
          restore_loc lb loc;
          tok
      | LPAREN ->
          let loc1 = save_loc lb in
          let tok2 = lexer lb in
          let loc2 = save_loc lb in
          let tok, loc =
            match tok2 with
            | LET | AND -> (
                match Let_trail.op lb with
                | None ->
                    add (tok2, loc2);
                    (Parser.LPAREN, loc1)
                | Some op -> (
                    let loc3 = save_loc lb in
                    match lexer lb with
                    | RPAREN ->
                        ( LIDENT (encode_op tok2 op),
                          { loc2 with loc_end = loc3.loc_end } )
                    | tok4 ->
                        let loc4 = save_loc lb in
                        add (tok2, loc2);
                        add (tok4, loc4);
                        register_custom_operator tok2 op loc2 loc3;
                        (LPAREN, loc1) ) )
            | _ ->
                add (tok2, loc2);
                (LPAREN, loc1)
          in
          restore_loc lb loc;
          tok
      | tok -> tok

  let () = Lexer.set_preprocessor (fun () -> Queue.clear pending) wrap
end

module Map_ast = struct
  open Ast_mapper
  open Asttypes
  open Parsetree
  open Ast_helper

  let get_op vb =
    match Hashtbl.find custom_operators vb.pvb_loc.loc_start with
    | exception Not_found -> None
    | loc, op -> Some (Exp.ident ~loc { txt = Lident op; loc })

  let mapper =
    let super = default_mapper in
    let expr self expr =
      let expr =
        match expr.pexp_desc with
        | Pexp_let (rf, (vb :: _ as vbs), body) -> (
            match get_op vb with
            | None -> expr
            | Some op ->
                if rf = Recursive then
                  Location.raise_errorf ~loc:expr.pexp_loc
                    "Custom 'let' operators cannot be recursive";
                let patts, exprs =
                  List.map vbs ~f:(fun vb ->
                      let {
                        pvb_pat = patt;
                        pvb_expr = expr;
                        pvb_attributes = attrs;
                        pvb_loc = loc;
                      } =
                        vb
                      in
                      ( match attrs with
                      | [] -> ()
                      | ({ loc; _ }, _) :: _ ->
                          Location.raise_errorf ~loc
                            "This attribute will be discarded" );
                      let op =
                        match get_op vb with
                        | Some op ->
                            Hashtbl.remove custom_operators vb.pvb_loc.loc_start;
                            op
                        | None ->
                            Location.raise_errorf ~loc
                              "Custom 'and' operator expected, got stantard \
                               'and' keyword"
                      in
                      (patt, (loc, op, expr)))
                  |> List.split
                in
                let patt =
                  List.fold_left (List.tl patts) ~init:(List.hd patts)
                    ~f:(fun acc patt ->
                      let loc = patt.ppat_loc in
                      Pat.tuple ~loc [ acc; patt ])
                in
                let vars =
                  List.mapi exprs ~f:(fun i _ ->
                      Printf.sprintf "__future_syntax__%d__" i)
                in
                let pvars =
                  List.map2 vars patts ~f:(fun v p ->
                      let loc = { p.ppat_loc with loc_ghost = true } in
                      Pat.var ~loc { txt = v; loc })
                in
                let evars =
                  List.map2 vars exprs ~f:(fun v (_, _, e) ->
                      let loc = { e.pexp_loc with loc_ghost = true } in
                      Exp.ident ~loc { txt = Lident v; loc })
                in
                let expr =
                  List.fold_left2 (List.tl evars) (List.tl exprs)
                    ~init:(List.hd evars) ~f:(fun acc var (loc, op, _) ->
                      Exp.apply ~loc op [ (nolabel, acc); (nolabel, var) ])
                in
                let body =
                  let loc = expr.pexp_loc in
                  Exp.apply ~loc op
                    [
                      (nolabel, expr);
                      (nolabel, Exp.fun_ ~loc nolabel None patt body);
                    ]
                in
                List.fold_right2 pvars exprs ~init:body
                  ~f:(fun var (loc, _, expr) acc ->
                    Exp.let_ Nonrecursive ~loc [ Vb.mk ~loc var expr ] acc) )
        | _ -> expr
      in
      super.expr self expr
    in
    { super with expr }

  let map f ast =
    let ast = f mapper ast in
    let fail _ (loc, _) =
      Location.raise_errorf ~loc "Invalid use of custom 'let' or 'and' operator"
    in
    Hashtbl.iter fail custom_operators;
    ast

  let structure = mapper.structure mapper

  let signature = mapper.signature mapper
end

let process_file fn ~magic ~parse ~print ~map ~mk_ext =
  let lexbuf = Lexing.from_channel (open_in_bin fn) in
  Location.init lexbuf fn;
  Location.input_lexbuf := Some lexbuf;
  let ast =
    try map (parse lexbuf)
    with exn -> (
      match error_of_exn exn with
      | Some error ->
          if !dump_ast then
            [
              mk_ext ?loc:None ?attrs:None (Ast_mapper.extension_of_error error);
            ]
          else (
            Location.report_error Format.err_formatter error;
            exit 1 )
      | None -> raise exn )
  in
  if !dump_ast then (
    set_binary_mode_out stdout true;
    output_string stdout magic;
    output_value stdout fn;
    output_value stdout ast;
    flush stdout )
  else Format.printf "%a@?" print ast

let process_file fn =
  let ext =
    match String.rindex fn '.' with
    | exception Not_found -> ""
    | i -> String.sub fn ~pos:i ~len:(String.length fn - i)
  in
  match ext with
  | ".ml" ->
      process_file fn ~magic:Config.ast_impl_magic_number
        ~parse:Parse.implementation ~print:Pprintast.structure
        ~map:Map_ast.structure ~mk_ext:Ast_helper.Str.extension
  | ".mli" ->
      process_file fn ~magic:Config.ast_intf_magic_number ~parse:Parse.interface
        ~print:Pprintast.signature ~map:Map_ast.signature
        ~mk_ext:Ast_helper.Sig.extension
  | _ ->
      Printf.eprintf "%s: Don't know what to do with %s.\n%!" prog_name fn;
      exit 2

let () =
  let args =
    Arg.align
      [
        ( "-dump-ast",
          Arg.Set dump_ast,
          " Output a binary AST rather than a pretty-printed source file" );
      ]
  in
  let usage = Printf.sprintf "Usage: %s [-dump-ast] FILES" prog_name in
  Arg.parse args process_file usage
