(** Bombs-Must-Detonate: BMDLang Type Checker @author Brian Go *)


type bmdtype = 
    TyInt
  | TyFloat
  | TyString
  | TyBool
  | TyVoid
  | TyList of bmdtype
  | TyEnumType of string
  | TyEnum of string
  | TyStruct of string
  | TyArray of bmdtype
  | TyArrow of bmdtype list * bmdtype
  | TyRef of bmdtype
  | TyTemplate of string

(** Used to keep track of the typing environment during typechecking *)

type typeenvdef =
    TyEnumDef of string * string list
  | TyStructDef of string * (string * bmdtype) list * Ast.data_type list
  | TyFunDef of string * bmdtype
  | TyVarDef of string * bmdtype
  | TyTemplateDef of string list
  | TyIncludeDef of string

exception Type_error of string

(** Eliminates duplicate entries in a list *)

let rec elim_repeats lst = match lst with
    cur::rest when List.mem cur rest -> elim_repeats rest
  | cur::rest -> cur::(elim_repeats rest)
  | [] -> []


(** Linker header information buffer *)

let link_buffer = ref []

(** Clears the linker header information buffer *)

let reset_link_buffer () = link_buffer := []

(** Gets the contents of the linker header infromation *)

let get_link_buffer () = String.concat "" ((elim_repeats !link_buffer)@["END_HEADER\n"])

(** Appends a string to the linker buffer *)

let append_link_buffer str = 
  link_buffer := str::!link_buffer

(** Raises a type error from the concatenated string list *)

let raise_type_error s_list =
  let str = String.concat "" s_list in
    raise (Type_error str)

(** Renaming of String.concat " *)

let concat s = String.concat "" s

(** Concatenates a list with commas. No comma after the final element. *)

let rec concat_commas slist = 
  match slist with 
      x::[] -> x
    | x::rest -> concat [x;", ";concat_commas rest]
    | [] -> ""

(** Replaces an association list by a list of keys *)

let get_first lst = 
  let helper (x,y) = x in
    List.map helper lst

(** Replaces an association list by a list of values *)

let get_second lst = 
  let helper (x,y) = y in
    List.map helper lst

(** Checks if the given struct name is defined in the environment *)

let rec isdef_struct name env = 
  match env with
      (TyStructDef (s,_,_))::rest -> ((s = name) || isdef_struct name rest)
    | _::rest -> isdef_struct name rest
    | [] -> false

(** Checks if the given enum is defined in the environment *)

let rec isdef_enum name env = 
  match env with
      (TyEnumDef (s,_))::rest -> ((s = name) || isdef_enum name rest)
    | _::rest -> isdef_enum name rest
    | [] -> false

(** Checks if the given variable is defined in the environment *)

let rec isdef_var name env =
  match env with
      (TyVarDef (s,_))::rest -> ((s = name) || isdef_var name rest)
    | _::rest -> isdef_var name rest
    | [] -> false

(** Checks if the given function is defined in the environment *)

let rec isdef_fun name env = 
  match env with
      (TyFunDef (s,_))::rest -> ((s = name) || isdef_fun name rest)
    | _::rest -> isdef_fun name rest
    | [] -> false

(** Checks if the given template definition is defined in the environment *)

let rec isdef_templated name env = 
  match env with
      (TyTemplateDef slist)::rest -> List.mem name slist
    | _::rest -> isdef_templated name rest
    | [] -> false

(** Removes the most recent template definition from the environment *)

let rec remove_template_def env = 
  match env with
      (TyTemplateDef _) :: rest -> rest
    | cur :: rest -> cur :: (remove_template_def rest)
    | [] -> []

(** Checks if the given identifier is defined as a struct, enum, variable, or function in the environment *)

let rec isdef_ident name env = 
  isdef_struct name env
  || isdef_enum name env
  || isdef_var name env
  || isdef_fun name env


(** Gets the type of the given variable from the environment. Raises Not_found if not defined.*)

let rec gettype_var name env = 
  match env with
      (TyVarDef (s,t))::rest when (s = name) -> t
    | _::rest -> gettype_var name rest
    | [] -> raise Not_found

(** Gets the type of the given function from the environment. Raises Not_found if not defined. *)

let rec gettype_fun name env = 
  match env with
      (TyFunDef (s,t))::rest when (s = name) -> t
    | _::rest -> gettype_fun name rest
    | [] -> raise Not_found

(** Checks if s2 is an enum value of enum type s1. Raises Not_found if s1 is not an enum type. *)

let rec is_enum_value s1 s2 env =
  match env with 
      (TyEnumDef (s,lst))::rest when (s = s1) -> List.mem s2 lst
    | _::rest -> is_enum_value s1 s2 rest
    | [] -> raise Not_found

(** Checks if file " has been included in the given environment *)

let rec isdef_include name env = 
  match env with
      (TyIncludeDef s)::rest when (s = name) -> true
    | _::rest -> isdef_include name rest
    | [] -> false

(** Finds the index of a value in a list. Raises Not_found if it is not in the list *)

let rec indexof str lst index =
  match lst with
      cur::rest when cur = str -> index
    | _::rest -> indexof str rest (index+1)
    | [] -> raise Not_found

(** Casts enum value s1.s2 to an integer. Raises Not_found if s1.s2 is not defined *)

let rec get_enum_value s1 s2 env = 
  match env with 
      (TyEnumDef (s,lst))::rest when (s = s1) -> indexof s2 lst 0
    | _::rest -> get_enum_value s1 s2 rest
    | [] -> raise Not_found

(** Gets the total number of values of enum type ename. Raises Not_found if ename is not an enum type *)

let rec get_nvalues_enum ename env = 
  match env with
      (TyEnumDef (s,lst))::rest when (s = ename) -> List.length lst
    | _::rest -> get_nvalues_enum ename rest
    | [] -> raise Not_found

(** Checks if s2 is a field of struct s1 from the environment. Raises Not_found if s1 is not a struct *)

let rec is_struct_value s1 s2 env = 
  match env with 
      (TyStructDef (s,lst,_))::rest when (s = s1) -> List.mem s2 (get_first lst)
    | _::rest -> is_struct_value s1 s2 rest
    | [] -> raise Not_found

(** Gets the data type of field s2 of struct s1 from the environment. Raises Not_found if s1 is not a struct *)

let rec gettype_struct s1 s2 env = 
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> List.assoc s2 lst
    | _::rest -> gettype_struct s1 s2 rest
    | [] -> raise Not_found

(** Returns the integer index of field s2 in struct s1 as determined by environment env *)

let rec getfield_struct s1 s2 env = 
  let t_field = gettype_struct s1 s2 env in
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> indexof (s2,t_field) lst 0
    | _::rest -> getfield_struct s1 s2 rest
    | [] -> raise Not_found

(** Returns the (string * bmdtype) list of struct fields of struct s1. Raises Not_found if s1 is not defined. *)

let rec getfields_struct s1 env =
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> lst
    | _::rest -> getfields_struct s1 rest
    | [] -> raise Not_found

(** Gets the number of fields of struct sname from the environment. Raises Not_found if sname is not defined. *)

let rec nfields_struct sname env =
  match env with
      (TyStructDef (s,lst,_))::rest when (s = sname) -> List.length lst
    | _::rest -> nfields_struct sname rest
    | [] -> raise Not_found

(** Gets the AST data types of the struct fields *)
 
let rec get_struct_data_type_list sname env =
  match env with 
      (TyStructDef (s,_,dtlist))::rest when (s = sname) -> dtlist
    | _::rest -> get_struct_data_type_list sname rest
    | [] -> raise Not_found


(** Annotates the data type AST element with information that will be needed by the IR *)

let annotate_data_type dt env = match dt with
    Ast.SynEnumOrStructType (s,tlist) ->
      if isdef_struct s env then 
        tlist := get_struct_data_type_list s env
      else if isdef_enum s env then
        tlist := [] 
      else 
        raise_type_error [s;" is not an enum or struct type but was used in a struct declaration."]
  | x -> ()
      
(** Creates a list of AST data type elements correlated with the struct fields *)

let rec make_struct_type_list varlist env = match varlist with
    (Ast.SynVarDeclareNoInit (dt,_))::rest ->
      let _ = annotate_data_type dt env in 
        dt::(make_struct_type_list rest env)
  | _::rest -> raise_type_error ["Struct declaration cannot contain a field initialization."]
  | [] -> []

(** Gives a string representation of the given type *)

let rec string_of_type ty = match ty with
    TyInt -> "int"
  | TyFloat -> "float"
  | TyString -> "string"
  | TyBool -> "bool"
  | TyVoid -> "void"
  | TyList t_list -> String.concat "" ["list(";string_of_type t_list;")"]
  | TyEnumType s -> String.concat "" ["enumType(";s;")"]
  | TyEnum s -> String.concat "" ["enum(";s;")"]
  | TyStruct s -> String.concat "" ["struct(";s;")"]
  | TyArray (t_arr) -> String.concat "" ["array(";string_of_type t_arr;")"]
  | TyArrow (t_args,t_ret) -> 
      let arg_string = List.map string_of_type t_args in
      let arg_string = if arg_string = []  then ["void"else arg_string in
      String.concat " "
        (["function("]@arg_string@["->";string_of_type t_ret;")"])
  | TyRef t_ref -> String.concat "" ["ref(";string_of_type t_ref;")"]
  | TyTemplate s -> if s = "_unknown" then "_" else String.concat "" ["templated(";s;")"]

(** Gives an expanded string representation of the given enum type detailing its total number of values *)

let rec expanded_string_of_enum ename env =
  let nfields = get_nvalues_enum ename env in
    concat [ename;" : ";string_of_int nfields]

(** Gives an expanded string representation of the given struct type which includes the expanded representation of each of its fields *)

and expanded_string_of_struct sname env = 
  let struct_dts = get_second (getfields_struct sname env) in
  let helper x = x env in
  let string_list = List.map helper (List.map expanded_string_of_type struct_dts) in
    concat [sname;" : ";concat_commas string_list]
    
(** Gives a string representation of the given type with expanded represnetations of structs and enums *)

and expanded_string_of_type ty env = match ty with
    TyInt -> "int"
  | TyFloat -> "float"
  | TyString -> "string"
  | TyBool -> "bool"
  | TyVoid -> "void"
  | TyList t_list -> concat ["list(";expanded_string_of_type t_list env;")"]
  | TyEnumType s -> concat ["enumType(";expanded_string_of_enum s env;")"]
  | TyEnum s -> concat ["enum(";expanded_string_of_enum s env;")"]
  | TyStruct s -> concat ["struct(";expanded_string_of_struct s env;")"]
  | TyArray (t_arr) -> concat["array(";expanded_string_of_type t_arr env;")"]
  | TyArrow (t_args,t_ret) -> 
      let helper x = x env in
      let arg_string = List.map helper (List.map expanded_string_of_type t_args) in
      let arg_string = if arg_string = []  then ["void"else arg_string in
      String.concat " "
        (["function("]@arg_string@["->";expanded_string_of_type t_ret env;")"])
  | TyRef t_ref -> concat ["ref(";expanded_string_of_type t_ref env;")"]
  | TyTemplate s -> if s = "_unknown" then "_" else concat ["templated(";s;")"]

(** Checks if op is a valid operator on values of type t1 and t2 *)

let rec isallowed_binop op t1 t2 = match (op,t1,t2) with
    (Ast.SynBinopAndTyBoolTyBool-> true
  | (Ast.SynBinopOrTyBoolTyBool-> true
  | (Ast.SynBinopAdd, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopSub, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopMul, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopDiv, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopIDivTyIntTyInt-> true
  | (Ast.SynBinopModTyIntTyInt-> true
  | (Ast.SynBinopConcatTyStringTyString-> true
  | (Ast.SynBinopConcatTyList t_list1, TyList t_list2) -> (t_list1 = t_list2)
  | (Ast.SynBinopCons, t_item, TyList t_list) -> (t_item = t_list)
  | (Ast.SynCompLt, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompGt, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompLte, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompGte, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompEq, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompNeq, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompEq, x, y) when (x = y) -> true
  | (Ast.SynCompNeq, x, y) when (x = y) -> true
  | _ -> false

(** Gets the type of the result of operator op on values of type t1 and t2. Type error if invalid operator. *)

let rec gettype_binop op t1 t2 = match (op,t1,t2) with
    (Ast.SynBinopAndTyBoolTyBool-> TyBool
  | (Ast.SynBinopOrTyBoolTyBool-> TyBool
  | (Ast.SynBinopAddTyIntTyInt-> TyInt
  | (Ast.SynBinopSubTyIntTyInt-> TyInt
  | (Ast.SynBinopMulTyIntTyInt-> TyInt
  | (Ast.SynBinopDivTyIntTyInt-> TyFloat
  | (Ast.SynBinopIDivTyIntTyInt-> TyInt
  | (Ast.SynBinopModTyIntTyInt-> TyInt
  | (Ast.SynBinopAdd, (TyInt | TyFloat), TyFloat-> TyFloat
  | (Ast.SynBinopAddTyFloatTyInt-> TyFloat
  | (Ast.SynBinopSub, (TyInt | TyFloat), TyFloat-> TyFloat
  | (Ast.SynBinopSubTyFloatTyInt-> TyFloat
  | (Ast.SynBinopMul, (TyInt | TyFloat), TyFloat-> TyFloat
  | (Ast.SynBinopMulTyFloatTyInt-> TyFloat
  | (Ast.SynBinopDiv, (TyInt | TyFloat), TyFloat-> TyFloat
  | (Ast.SynBinopDivTyFloatTyInt-> TyFloat
  | (Ast.SynBinopConcatTyStringTyString-> TyString
  | (Ast.SynBinopConcatTyList t_list1, TyList t_list2) when (t_list1 = t_list2) -> TyList t_list1
  | (Ast.SynBinopCons, t_item, TyList t_list) when (t_item = t_list) -> TyList t_list
  | (Ast.SynCompLt, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompGt, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompLte, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompGte, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompEq, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompNeq, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompEq, x, y) when (x = y) -> TyBool
  | (Ast.SynCompNeq, x, y) when (x = y) -> TyBool
  | _ -> raise_type_error ["Attempted to perform an invalid binary operation ";Ast.string_of_binop op;" on values of type ";string_of_type t1;" and ";string_of_type t2]

(** Types a list of AST data type element *)

let rec type_data_types dt_lst env =
  match dt_lst with
      Ast.SynIntType::rest -> TyInt::(type_data_types rest env)
    | Ast.SynFloatType::rest -> TyFloat::(type_data_types rest env)
    | Ast.SynStringType::rest -> TyString::(type_data_types rest env)
    | Ast.SynBoolType::rest -> TyBool::(type_data_types rest env)
    | Ast.SynVoidType::rest -> TyVoid::(type_data_types rest env)
    | (Ast.SynEnumOrStructType (s,field_types))::rest ->
        if isdef_struct s env then 
          let _ = field_types  := get_struct_data_type_list s env in
            (TyStruct s)::(type_data_types rest env)
        else if isdef_enum s env then 
          (TyEnum s)::(type_data_types rest env)
        else if isdef_templated s env then
          (TyTemplate s)::(type_data_types rest env)
        else raise_type_error ["Invalid data type: ";s;" expected enum, struct, or templated type."]
    | (Ast.SynArrayType (dt,n))::rest ->
        let t_n,_ = type_value_producer n env in
          if t_n = TyInt then 
            (TyArray (type_data_type dt env))::(type_data_types rest env)
          else
            raise_type_error ["Invalid array index type ";string_of_type t_n;" expected an integer value."]
    | (Ast.SynListType dt)::rest ->
        (TyList (type_data_type dt env))::(type_data_types rest env)
    | (Ast.SynRefType dt)::rest ->
        (TyRef (type_data_type dt env))::(type_data_types rest env)
    | (Ast.SynArrowType (arglist_types,ret_type))::rest ->
        let arglist_types = type_data_types arglist_types env in
          (TyArrow ((if arglist_types = [] then [TyVoidelse arglist_types), 
                    type_data_type ret_type env))::(type_data_types rest env)
    | [] -> []

(** Types a single AST data type element *)

and type_data_type dt env = 
  let verified_type_lst = type_data_types [dt] env in
    match verified_type_lst with 
        x::rest -> x
      | [] -> raise_type_error ["Unexpected type error."]

(** Types the declaration of a list of struct fields. Initial values not allowed, type error if they are. *)

and structdef_list lst env = match lst with
    (Ast.SynVarDeclareNoInit (dt,s))::[] -> [s,type_data_type dt env]
  | (Ast.SynVarDeclareNoInit (dt,s))::rest -> (s,type_data_type dt env)::(structdef_list rest env)
  | _ -> raise_type_error ["Expected uninitialized variable declaration in struct definition."

(** Types an argument list to a function, adding the arguments as variable declarations to the environment. *)

and vardef_arglist arglist env = 
  match arglist with 
      (dt,name)::rest -> vardef_arglist rest ((TyVarDef (name,type_data_type dt env))::env)
    | [] -> env

(** Types a value *)

and type_value v env = match v with
    Ast.SynIntValue _ -> TyInt, env
  | Ast.SynFloatValue _ -> TyFloat, env
  | Ast.SynBoolValue _ -> TyBool, env
  | Ast.SynStringValue _ -> TyString, env

(** Produces a comma-separated string representation of a list of types *)

and string_of_type_list lst = match lst with
    t::[] -> string_of_type t
  | t::rest -> String.concat "" [string_of_type t;", ";string_of_type_list rest]
  | [] -> ""

(** Checks if association list l1 has no conflicting key/value pairs with association list l2 *)

and are_assoclists_compatible l1 l2 = match l1 with
    (x,y)::rest -> 
      if List.mem_assoc x l2 
      then y = List.assoc x l2
      else are_assoclists_compatible rest l2
  | [] -> true

(** Returns an association list of template types to bmdtypes. Type error if t2 cannot be unified to t1. *)

and unify_types t1 t2 = match t1,t2 with
    x,y when x = y -> []
  | TyList t_list1, TyList t_list2 -> unify_types t_list1 t_list2
  | TyArray t_arr1, TyArray t_arr2 -> unify_types t_arr1 t_arr2
  | TyArrow (args1,ret1), TyArrow (args2,ret2) -> 
      let assoc1 = unify_types ret1 ret2 in
      let assoc2 = List.flatten (List.map2 unify_types args1 args2) in
        if are_assoclists_compatible assoc1 assoc2 then
          assoc1 @ assoc2
        else 
          raise_type_error ["Could not unify templated arrow type ";
                            string_of_type t2;
                            " with ";
                            string_of_type t1]
  | TyRef t_ref1, TyRef t_ref2 -> unify_types t_ref1 t_ref2
  | TyTemplate s, t2 -> [(s,t2)]
  | _ -> raise_type_error ["Expected type ";
                           string_of_type t1;
                           " but got type ";
                           string_of_type t2]

(** Uses the association list (substitution list) template_map (key: template string, value: bmdtype) to type the given data type with those substitutions *)

and insert_template_types template_map env dt = 
  let insert_types = insert_template_types template_map env in
    match dt with
        TyList t -> TyList (insert_types t)
      | TyArray t -> TyArray (insert_types t)
      | TyArrow (args,ret) -> TyArrow (List.map insert_types args, insert_types ret)
      | TyRef t -> TyRef (insert_types t)
      | TyTemplate s -> 
          if List.mem_assoc s template_map then
            List.assoc s template_map
          else if isdef_templated s env then
            TyTemplate s
          else 
            raise_type_error ["Expression has free template type ";string_of_type dt;" in result."]
      | x -> x

(** Apply argtypes to the arrow type, making sure there are no template inconsistencies, and get the return type *)

and apply_arrow_type arrowtype argtypes env = 
  let lhs_types, ret_type = match arrowtype with
      TyArrow (x,y) -> x,y
    | _ -> raise_type_error ["Attempted to apply arguments to something of non-arrow type ";string_of_type arrowtype;"."]
  in
  let isVoidArgs = lhs_types = [TyVoid&& (List.length argtypes = 0) in
  let _ = 
    if (not isVoidArgs) && ((List.length lhs_types) != (List.length argtypes))
    then raise_type_error ["Attempted to apply an incorrect number of arguments to arrow type ";string_of_type arrowtype;"."]
    else ()
  in
  let template_map = ref [] in
  let _ = 
    if isVoidArgs then () else
      for i = 0 to (List.length lhs_types)-1 do
        let cur_lhs, cur_arg = List.nth lhs_types i, List.nth argtypes i in
        let unify_result = unify_types cur_lhs cur_arg in
          if are_assoclists_compatible unify_result !template_map then
            template_map := unify_result @ !template_map
          else
            raise_type_error ["Could not apply argument types ";
                              string_of_type_list argtypes;
                              " to a function of type ";
                              string_of_type arrowtype]
      done
  in
    insert_template_types !template_map env ret_type
      
(** Types a function call as a value-producer (checks argument types, gives the return type). Updates the linker header with inferred remote call types. *)

and type_function_call_value funcall env = match funcall with
    Ast.SynLocalCall (s,vprod_list,isvar) -> 
      if isdef_fun s env || isdef_var s env then
        let fun_type = 
          if isdef_fun s env then 
            gettype_fun s env 
          else 
            let _ = isvar := true in
              gettype_var s env 
        in
        let vplist_types,_ = type_value_producer_list_nonuniform vprod_list env in
          try
            apply_arrow_type fun_type vplist_types env, env
          with
              Type_error err_string ->
                raise_type_error ["Call to (local) function ";s;" was malformed. ";err_string]
      else 
        raise_type_error ["(Local) function call to undefined function ";s;"."]
  | Ast.SynRemoteCall (s,vprod_list, var_ident) -> 
       (** the linker must do most of the work here **)

      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype,_ = type_variable_identifier var_ident env in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n";]) in
        TyBool, env 
          (** the result is true or false depending on call success/failure **)

  | Ast.SynRemoteCallNoResult (s,vprod_list) -> 
      (** again, the linker will do most of the work **)

      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype = TyTemplate ("_unknown"in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n";]) in
        TyBool, env
        
(** Types a binary operation vp1 op vp2. Type error if not a valid operation *)

and type_binop vp1 op vp2 env =
  let t_vp1,_ = type_value_producer vp1 env in
  let t_vp2,_ = type_value_producer vp2 env in
    if isallowed_binop op t_vp1 t_vp2 then
      (gettype_binop op t_vp1 t_vp2), env
    else 
      raise_type_error ["Attempted to perform an invalid binary operation ";Ast.string_of_binop op;" on values of type ";string_of_type t_vp1;" and ";string_of_type t_vp2;"."]
    
(** Types a unary operation expression. Type error if not a valid operation. *)

and type_prefix_unop op vp env = 
  let t_vp,_ = type_value_producer vp env in
    match (op,t_vp) with 
        Ast.SynUnopNotTyBool -> TyBool, env
      | Ast.SynUnopCarTyList t_list -> t_list, env
      | Ast.SynUnopCdrTyList t_list -> TyList t_list, env
      | Ast.SynUnopTrunc, (TyFloat | TyInt-> TyInt, env
      | Ast.SynUnopDerefTyRef t_ref -> t_ref, env
      | Ast.SynUnopNegTyInt -> TyInt, env
      | Ast.SynUnopNegTyFloat -> TyFloat, env
      | Ast.SynUnopNull, _ -> TyBool, env
      | _ -> raise_type_error ["Attempted to perform an invalid prefix unary operation on a value of type ";string_of_type t_vp;"."]

(** Types an array value producer (i.e. . Error if empty (we need to infer the type), or if values of different types are used. *)

and type_array_value_producer vp_list env = match vp_list with
    vp::[] ->
      let vp_type,_ = type_value_producer vp env in
        TyArray vp_type, env
  | vp::rest ->
      let vp_type,_ = type_value_producer vp env in
      let rest_type,_ = type_array_value_producer rest env in
        (match rest_type with
             TyArray rest_arr_type when rest_arr_type = vp_type ->
               TyArray vp_type, env
           | _ -> raise_type_error ["An element of type ";string_of_type vp_type;" is being used with an array of type ";string_of_type rest_type;"."])
  | [] -> raise_type_error ["Constant array values must be non-empty."]

(** Types a list of value producers (i.e. elem1; elem2;...) Error if empty or if values of different types are used. *)

and type_value_producer_list vp_list env = match vp_list with
    vp::[] ->
      let vp_type,_ = type_value_producer vp env in
        TyList vp_type, env
  | vp::rest ->
      let vp_type,_ = type_value_producer vp env in
      let rest_type,_ = type_value_producer_list rest env in
        if TyList vp_type = rest_type then
          TyList vp_type, env
        else 
          raise_type_error ["An element of type ";string_of_type vp_type;" is being used with list of type ";string_of_type rest_type;"."]
  | [] -> raise_type_error ["Constant list values must be non-empty."]

(** Types a list value producer *)

and type_list_value_producer vp_list env = match vp_list with
    Ast.SynListNil dt -> 
      let t_dt = type_data_type dt env in
        TyList t_dt, env
  | Ast.SynListList vp_list ->
      type_value_producer_list vp_list env
  | Ast.SynListCons (vp1,vp2) ->
      let t_vp1,_ = type_value_producer vp1 env in
      let t_vp2,_ = type_value_producer vp2 env in
        (match t_vp2 with 
            TyList t_list when t_list = t_vp1 -> t_vp2, env
          | _ -> raise_type_error ["Cannot cons a value of type ";string_of_type t_vp1;" to a list of type ";string_of_type t_vp2;"."])

(** Types a value producer *)

and type_value_producer vp env = match vp with
    Ast.SynValue v -> type_value v env
  | Ast.SynFunctionCallValue funcall -> type_function_call_value funcall env
  | Ast.SynVarIdentifier vident -> type_variable_identifier vident env 
  | Ast.SynBinop (vp1,op,vp2) -> type_binop vp1 op vp2 env
  | Ast.SynPrefixUnop (op,vp) -> type_prefix_unop op vp env
  | Ast.SynParenthesized vp -> type_value_producer vp env
  | Ast.SynArrayValueProducer vp_list -> type_array_value_producer vp_list env
  | Ast.SynListValueProducer vp_list -> type_list_value_producer vp_list env

(** Returns a list of types, i.e. for parameters *)

and type_value_producer_list_nonuniform vp_list environment =  
  let helper vplist env = 
    match vplist with
        vp::rest -> 
          let t_vp,_ = type_value_producer vp env in
          let t_rest,_ = type_value_producer_list_nonuniform rest env in
            t_vp::t_rest
      | [] -> []
  in
    helper vp_list environment, environment

(** Types a variable identifier *)

and type_variable_identifier v_ident env = match v_ident with
    Ast.SynVarName s -> 
      if isdef_var s env then
        gettype_var s env, env
      else if isdef_enum s env then
        TyEnumType s, env
      else if isdef_struct s env then
        TyStruct s, env
      else if isdef_fun s env then
        gettype_fun s env, env
      else
        raise_type_error ["Variable ";s;" was used but not defined."]
  | Ast.SynStructOrEnumValue (vid1,s2,field_info) ->
      let t_vid,_ = type_variable_identifier vid1 env in
        (match t_vid with
             TyEnumType s1 -> 
               if is_enum_value s1 s2 env then
                 let index = get_enum_value s1 s2 env in
                 let _ = field_info := (Ast.VIdEnumValue index) in
                   TyEnum s1, env
               else 
                 raise_type_error [s1;".";s2;" is not an enum value of enum type ";s1;"."]
           | TyStruct s1 ->
               if isdef_struct s1 env then
                 if is_struct_value s1 s2 env then
                   let index = getfield_struct s1 s2 env in
                   let _ = field_info := (Ast.VIdStructField index) in
                   gettype_struct s1 s2 env, env
                 else
                   raise_type_error [s1;".";s2;" refers to a non-existent field of struct type ";string_of_type t_vid;"."]
               else
                 raise_type_error [s1;" is used as a struct but is not a struct."]
           | _ -> raise_type_error ["An identifier of type ";string_of_type t_vid;" is not a struct or enum type in this scope."])
  | Ast.SynArrayCell (vp,i) -> 
      let t,_ = type_variable_identifier vp env in
      let t_index,_ = type_value_producer i env in
        (match (t,t_index) with
             TyArray t_arr, TyInt ->
               t_arr, env
           | _ -> raise_type_error ["A value of non-array-type ";string_of_type t;" is used as an array type."])            

(** Types a variable assignment *)

let type_var_assign vasgn env = match vasgn with 
    Ast.SynVarAssignment (var,vprod) ->
      let var_type, temp_env = type_variable_identifier var env in
      let vp_type, new_env = type_value_producer vprod temp_env in
        if vp_type = var_type or TyRef vp_type = var_type then 
          TyVoid, new_env
        else 
          raise_type_error ["Cannot assign a value of type ";string_of_type vp_type;" to something of type ";string_of_type var_type;"."]
  | Ast.SynVarModify (vident, op, vprod) -> 
      let var_type, temp_env = type_variable_identifier vident env in
      let vp_type, new_env = type_value_producer vprod temp_env in
      let t1, t2 = 
        (match op with 
             Ast.SynBinopCons -> vp_type, var_type
           | _ -> var_type, vp_type) 
                (** flip order for cons, the list is being mutated **)

      in
        if isallowed_binop op t1 t2 then
          TyVoid, new_env
        else 
          raise_type_error ["Cannot use operator ";Ast.string_of_binop op;" to mutate a value of type ";string_of_type var_type;" with an argument of type ";string_of_type vp_type;"."]

(** Types a variable declaration *)

let type_var_declaration vdecl env = match vdecl with
    Ast.SynVarDeclareNoInit (dt,s) ->
      if isdef_ident s env then 
        raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else
        let new_dt = type_data_type dt env in
          (match new_dt with
               TyTemplate s -> raise_type_error ["Cannot declare templated variable ";s;" without explicit initialization."]
             | _ ->
                 let new_env = (TyVarDef (s,new_dt))::env in
                   TyVoid, new_env)
  | Ast.SynVarDeclareWithInit (dt,s,vp) ->
      if isdef_ident s env then
        raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else
        let new_dt = type_data_type dt env in
        let temp2_env = (TyVarDef (s,new_dt))::env in
        let vp_dt,new_env = type_value_producer vp temp2_env in
          if vp_dt = new_dt then
            TyVoid, new_env
          else 
            raise_type_error ["Variable ";s;" has type ";string_of_type new_dt;" but is being initialized with a value of type ";string_of_type vp_dt;"."]

(** Types a conditional expression *)

let rec type_conditional cond env rettype = match cond with 
    Ast.SynIf (vprod,expr_list) ->
      let vp_type, new_env = type_value_producer vprod env in
        if vp_type = TyBool then
          let body_type,_ = type_expression_list expr_list env rettype in
            if body_type = TyVoid then
              TyVoid, new_env
            else 
              raise_type_error ["Conditional body was not well-formed."]
        else
          raise_type_error ["Conditional test expression must have a boolean type."]
  | Ast.SynIfCase (vprod,expr_list,continued_cond) ->
      let vp_type, new_env = type_value_producer vprod env in
        if vp_type = TyBool then
          let body_type,_ = type_expression_list expr_list env rettype in
            if body_type = TyVoid then
              type_continued_conditional continued_cond new_env rettype
            else 
              raise_type_error ["Conditional case body was not well-formed."]
        else 
          raise_type_error["Conditional case test expression must have a boolean type."]

(** Types an else case *)

and type_continued_conditional continued_cond env rettype = match continued_cond with
    Ast.SynFinalElse expr_list -> 
      let body_type,_ = type_expression_list expr_list env rettype in
        if body_type = TyVoid then
          TyVoid, env
        else
          raise_type_error ["Final else body was not well-formed."]
  | Ast.SynElse cond -> type_conditional cond env rettype

(** Types a loop *)

and type_loop loop env rettype = match loop with
    Ast.SynWhile (vprod, loop_expr_list) -> 
      let vp_type, new_env = type_value_producer vprod env in
        (match vp_type with
             TyBool ->
               let body_type,_ = type_expression_list loop_expr_list env rettype in
                 (match body_type with
                      TyVoid -> TyVoid, new_env
                    | _ -> raise_type_error ["While loop body was not well-formed."])
           | _ -> raise_type_error ["While loop test expression must have a boolean type."])
  | Ast.SynDoWhile (loop_expr_list, vprod) -> 
      let vp_type, new_env = type_value_producer vprod env in
        (match vp_type with
             TyBool ->
               let body_type,_ = type_expression_list loop_expr_list env rettype in
                 (match body_type with
                      TyVoid -> TyVoid, new_env
                    | _ -> raise_type_error ["While loop body was not well-formed."])
           | _ -> raise_type_error ["While loop test expression must have a boolean type."])
  | Ast.SynFor (init_expr, test_vprod, step_expr, loop_expr_list) ->
      let _, init_env = type_expression init_expr env rettype in
      let vp_type, _ = type_value_producer test_vprod init_env in
      let _ = type_expression step_expr init_env rettype in
        (match vp_type with
             TyBool ->
               let _ = type_expression step_expr in 
                     (** we dont care about the type as long as it has one **)

               let body_type,_ = type_expression_list loop_expr_list init_env rettype in
                 (match body_type with
                      TyVoid -> TyVoid, env
                    | _ -> raise_type_error ["For loop body was not well-formed."])
           | _ -> raise_type_error ["For loop test expression must have a boolean type."])

(** Types a function call expression. This is not a value producer, so regardless of return type the expression will have type void *)

and type_function_call funcall env rettype = match funcall with
    Ast.SynLocalCall (s,vprod_list,isvar) -> 
     if isdef_fun s env || isdef_var s env then
        let fun_type = 
          if isdef_fun s env then 
            gettype_fun s env 
          else 
            let _ = isvar := true in
              gettype_var s env 
        in
        
        let vplist_types,_ = type_value_producer_list_nonuniform vprod_list env in
          try
            let _ = apply_arrow_type fun_type vplist_types env in
              TyVoid, env
          with
              Type_error err_string ->
                raise_type_error ["Call to (local) function ";s;" was malformed. ";err_string]
     else 
        raise_type_error ["(Local) function call to undefined function ";s;"."]
  | Ast.SynRemoteCall (s,vprod_list, var_ident) -> 
       (** the linker must do most of the work here **)

      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype,_ = type_variable_identifier var_ident env in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n"]) in
        TyVoid, env
  | Ast.SynRemoteCallNoResult (s,vprod_list) -> 
      (** again, the linker will do most of the work **)

      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype = TyTemplate "_unknown" in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n"]) in
        TyVoid, env

(** Types an expression. We use void to indicate a successful typing, since expressions have no actual type. *)

and type_expression expr env rettype = match expr with
    Ast.SynVarDeclare vdecl -> type_var_declaration vdecl env
  | Ast.SynEnumDeclare (s, slist) ->
      if isdef_ident s env then
        raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else 
        TyVoid, (TyEnumDef (s,slist))::env
  | Ast.SynVarAssign vasgn -> type_var_assign vasgn env
  | Ast.SynCond cond -> type_conditional cond env rettype
  | Ast.SynLoop loop -> type_loop loop env rettype
  | Ast.SynFunctionCall funcall -> type_function_call funcall env rettype
  | Ast.SynReturnStatement ret -> 
      (match ret with
           Ast.SynVoidReturn -> 
             if rettype = TyVoid then
               TyVoid, env
             else 
               raise_type_error ["Invalid return statement: expected a value of type void but received one of type ";string_of_type rettype;"."]
         | Ast.SynValueReturn vprod ->
             let vp_type, new_env = type_value_producer vprod env in
               if vp_type = rettype then
                 TyVoid, new_env
               else 
                 raise_type_error ["Invalid return statement: expected a value of type ";string_of_type rettype;" but received one of type ";string_of_type vp_type;"."])
  | Ast.SynBreak -> TyVoid, env
  | Ast.SynContinue -> TyVoid, env

(** Types an expression list. Checks if all expressions are validly (void) typed, and then returns void type. Otherwise raises type errors *)

and type_expression_list lst env rettype = match lst with
    expr::rest -> 
      let ty,new_env = type_expression expr env rettype in
        (match ty with
             TyVoid -> type_expression_list rest new_env rettype
           | _ -> raise_type_error ["Invalid expression encountered."])
  | [] -> TyVoid, env

(** Types a program (top-level expression list *)

and type_program p env = match p with
    expr::rest -> 
      let t,new_env = type_global_expression expr env in
        (match t with
             TyVoid -> type_program rest new_env
           | _ -> raise_type_error ["Global expression was not well formed (non-void type)."])
  | [] -> TyVoid, env

(** Types a top level expression *)

and type_global_expression ast env = 
  match ast with
      Ast.SynFunctionDeclare (dt,lbl,arg_list) ->
        if isdef_ident lbl env then
          raise_type_error ["Function ";lbl;" is defined more than once."]
        else
          let fun_type = 
            let arglist_types = type_data_types (get_first arg_list) env in
            TyArrow ((if arglist_types = [] then [TyVoidelse arglist_types), 
                     type_data_type dt env) in
          let new_env = (TyFunDef (lbl,fun_type))::env in
            TyVoid, new_env
    | Ast.SynRemotableFunctionDeclare (dt,lbl,arg_list) ->
        let fun_type = 
          let arglist_types = type_data_types (get_first arg_list) env in
            TyArrow ((if arglist_types = [] then [TyVoidelse arglist_types), 
                     type_data_type dt env) 
        in
        let _ = append_link_buffer (concat ["provide ";lbl;" = ";expanded_string_of_type fun_type env;"\n"]) in
        let new_env = (TyFunDef (lbl,fun_type))::env in
          TyVoid, new_env
    | Ast.SynFunctionDefine ((dt,lbl,arg_list),expr_list) ->
        let ret_type = type_data_type dt env in
        let fun_type =
          let arglist_types = type_data_types (get_first arg_list) env in
            TyArrow ((if arglist_types = [] then [TyVoidelse arglist_types),
                     ret_type) in
        let new_env = (TyFunDef (lbl,fun_type))::env in
        let body_env = vardef_arglist arg_list new_env in
        let body_type,_ = type_expression_list expr_list body_env ret_type in
          (match body_type with 
              TyVoid -> TyVoid, new_env
            | _ -> raise_type_error ["Function body had an invalid type."])
    | Ast.SynTemplatedDeclare (typelist, expr) ->
        let new_env = (TyTemplateDef typelist)::env in
        let result_type,result_env = type_global_expression expr new_env in
          result_type, remove_template_def result_env
    | Ast.SynTemplatedDefine (typelist, expr) ->
        let new_env = (TyTemplateDef typelist)::env in
        let result_type,result_env = type_global_expression expr new_env in
          result_type, remove_template_def result_env
    | Ast.SynStructDeclare (s,varlist) -> 
        let typed_varlist = structdef_list varlist env in
        let type_list = make_struct_type_list varlist env in
        let new_env = (TyStructDef (s, typed_varlist, type_list))::env in
          TyVoid, new_env
    | Ast.SynGlobalEnumDeclare (s, lst) ->
        TyVoid, (TyEnumDef (s,lst))::env
    | Ast.SynGlobalVarDeclare vdecl -> 
        (match vdecl with
             Ast.SynVarDeclareNoInit (dt, s) ->
               let new_dt = type_data_type dt env in
               let new_env = (TyVarDef (s,new_dt))::env in
                 TyVoid, new_env
           | _ -> raise_type_error ["Global variable declarations cannot be initialized."])
    | Ast.SynInclude data ->
        
        (** If this is the first time processing this include, parses the include and inserts its AST after typechecking it. If not, skips it. *)

        (match !data with
             Ast.IncludeFileName s ->
               if isdef_include s env then
                 let _ = begin
                   data := (Ast.IncludeAst []);
                   print_string "Skipping repeat include ";
                   print_string s;
                   print_string ".\n";
                   flush stdout;
                 end
                 in
                   TyVoid, env
               else
                 (let ast = ref [] in
                    try
                      let inchan = open_in s in
                      let lexbuf = Lexing.from_channel inchan in
                        begin
                          ast := Bmdparse.program Bmdlex.bmd_lang lexbuf;
                          print_string "Type checking include ";
                          print_string s;
                          print_string ".\n";
                          flush stdout;
                          let t,new_env = type_program !ast env in
                            (match t with 
                                 TyVoid -> 
                                   let _ = data := (Ast.IncludeAst !ast) in
                                     TyVoid, (TyIncludeDef s)::new_env
                               | _ -> raise_type_error ["Typechecking of include file ";s;" failed.\n"])
                        end
                    with 
                        Parsing.Parse_error -> raise_type_error ["Syntax error in include file ";s;" line ";string_of_int !Bmdlex.line_count;".\n"])
           | _ ->
               raise_type_error ["Unexpected type error: include should be specified by file name, not as an ast."])
    | Ast.SynStateMachine _ ->
        
        (** State machine declarations should be eliminated from code during source-to-source compilation. *)

        raise_type_error ["Unexpected type error: source-to-source compilation of state machine failed."]

(** Determines whether an AST is validly typed. Prints typing exceptions. *)

let is_typed p = 
  try
    let t,_ = type_program p [] in
      match t with
          TyVoid -> true
        | _ -> false
  with Type_error s -> 
    let _ = output_string stderr s in
    let _ = output_string stderr "\n" in
    let _ = flush stderr in
      false