type arith_op = Plus | Minus | Times | Divide | Modulo
type rel_op = Eq | Neq | Lt | Lte | Gt | Gte
type logic_op = And | Or
type bin_op = ArithOp of arith_op | RelOp of rel_op | LogicOp of logic_op
type mon_op = UnaryMinus | Negation
type const = BoolConst of bool | IntConst of int
type expr =
    Enclosure of expr
    | BinOp of expr * bin_op * expr
    | MonOp of mon_op * expr
    | Id of string
    | Const of const

(* Test function that returns converts a typed expression to a string *)
let rec eval_expr =
    let eval_bin_op =
        let eval_arith_op = function
            Plus -> "+"
            | Minus -> "-"
            | Times -> "*"
            | Divide -> "/"
            | Modulo -> "mod"
        in
        let eval_rel_op = function
            Eq -> "="
            | Neq -> "!="
            | Lt -> "<"
            | Lte -> "<="
            | Gt -> ">"
            | Gte -> ">="
        in
        let eval_logic_op = function
            And -> "&&"
            | Or -> "||"
        in
        function
        ArithOp op -> eval_arith_op(op)
        | RelOp op -> eval_rel_op(op)
        | LogicOp op -> eval_logic_op(op)
    in
    let eval_mon_op = function
        UnaryMinus -> "-"
        | Negation -> "!"
    in
    let eval_const = function
        IntConst i -> string_of_int i
        | BoolConst b -> string_of_bool b
    in
    function
    Enclosure e -> "(" ^ eval_expr(e) ^ ")"
    | BinOp (e1, op, e2) -> eval_expr(e1) ^ " " ^ eval_bin_op(op)
                            ^ " " ^ eval_expr(e2)
    | MonOp (op, e) -> eval_mon_op(op) ^ eval_expr(e)
    | Id id -> id
    | Const c -> eval_const(c)
;;

(* (a) *)
print_endline (eval_expr (Enclosure (Id "a")));;

(* -a *)
print_endline (eval_expr (MonOp (UnaryMinus, Id "a")));;

(* a - b *)
print_endline (eval_expr (BinOp (Id "a", ArithOp Minus, Id "b")));;

(* a * (-b + 1) *)
let a_times b = BinOp (Id "a", ArithOp Times, b) in
let uminus a = MonOp (UnaryMinus, a) in
let plus a b = BinOp (a, ArithOp Plus, b) in
let one = Const (IntConst 1) in
print_endline (eval_expr (a_times (Enclosure (plus (uminus (Id "b")) one))));;

(* a = b && c *)
let b_and_c = BinOp (Id "b", LogicOp And, Id "c") in
print_endline (eval_expr (BinOp (Id "a", RelOp Eq, b_and_c)));;
