type monop = Neg | Not
type binop = Add | Sub | Mul | Div | Mod    (* arithmetic operators *)
           | Eq | Ne | Lt | Le | Gt | Ge    (* relational operators *)
           | And | Or                       (* logic operators *)
type expr = Num of int                      (* integer constant *)
          | Bool of bool                    (* boolean constant *)
          | Var of string                   (* variable *)
          | MonopAp of monop * expr         (* unary operator application *)
          | BinopAp of binop * expr * expr  (* binary operator application *)
          | Cond of expr * expr * expr      (* conditional expression *)
          | Fun of string * expr            (* function abstraction *)
          | FunAp of expr * expr            (* function application *)
          | Let of string * expr * expr     (* variable binding *)
          | LetRec of string * expr * expr  (* recursive variable binding *)

let rec expr2string =
    let monop2string = function
          Neg -> "-"
        | Not -> "not "
    in
    let binop2string = function
          Add -> "+"
        | Sub -> "-"
        | Mul -> "*"
        | Div -> "/"
        | Mod -> "mod"
        | Eq -> "="
        | Ne -> "!="
        | Lt -> "<"
        | Le -> "<="
        | Gt -> ">"
        | Ge -> ">="
        | And -> "&&"
        | Or -> "||"
    in
    function
      Num i -> string_of_int i
    | Bool b -> string_of_bool b
    | Var v -> v
    | MonopAp (op, e) -> monop2string(op) ^ expr2string(e)
    | BinopAp (op, e1, e2) -> expr2string(e1) ^ " " ^ binop2string(op)
                              ^ " " ^ expr2string(e2)
    | Cond (cond, e1, e2) -> "if " ^ expr2string(cond)
                             ^ " then " ^ expr2string(e1)
                             ^ " else " ^ expr2string(e2)
    | Fun (arg, e) -> "fun " ^ arg ^ " -> " ^ expr2string(e)
    | FunAp (e1, e2) -> "(" ^ expr2string(e1) ^ " " ^ expr2string(e2) ^ ")"
    | Let (var, e1, e2) -> "let " ^ var ^ " = " ^ expr2string(e1)
                           ^ " in " ^ expr2string(e2)
    | LetRec (var, e1, e2) -> "let rec " ^ var ^ " = " ^ expr2string(e1)
                              ^ " in " ^ expr2string(e2)
;;

(* let a = 1 in b *)
print_endline (expr2string (Let ("a", (Num 1), (Var "b"))));;

(* a + 3 *)
print_endline (expr2string (BinopAp (Add, Var "a", Num 3)));;

(* -a *)
print_endline (expr2string (MonopAp (Neg, Var "a")));;
