User Tools

Site Tools


Photon Compiler Development: Function Calls

This is the eigth article of a series (table of contents) about compiler development with LLVM using OCaml. We intend to develop a compiler for a subset of OCaml large enough to allow our compiler to compile itself.

In this article, we will add function calls to our language. This addition is relatively straightforward. We will also add unit values so that we can call functions expecting a unit-type argument.


We add a new Unit literal in Ast.

type lit =
   | Unit

We also add a Call AST node, which comprises both the applied value (the function) and its arguments.

} and 'a node =
   | Call of 'a t * 'a t list (** fully applied call [(function, arguments)] *)

In the parser, we extend expr to allow function calls as follows.

   | atom atoms            { label 0 (Call($1, List.rev $2)) }

atoms is just a list of atoms.

   : atom                  { [$1] }
   | atoms atom            { $2 :: $1 }

Finally, we must parse literal unit values, which have the concrete syntax ().

   | LPAR RPAR             { Unit }


Typing unit literals is trivial, it suffices to return Type.Unit as their type.

Function calls are more interesting. We first type the applied value, then check it is indeed a function. If it is not, we throw a new Wrong_apply error.

If the applied value is indeed a function, we then check it has the right number of arguments. We support neither partial function application (too few arguments) nor functions returning functions (which would look as too many arguments) yet. The number of arguments in Call should then match exactly the number of arguments in the Fun. If it is not the case, we throw a new Wrong_nbr_of_args error.

If the number of arguments is correct, we type all the arguments. We then check that the inferred argument types match the expected argument types.

Finally, we return as type for the call the return type of the applied value.

let rec annotate type_env ast =
     match ast.node with
     | Call(f, args) ->
          let f = annotate type_env f in
          begin match snd f.label with
            Fun(ret_type, arg_types) ->
               let n_actual = List.length args in
               let n_expected = List.length arg_types in
               if n_actual <> n_expected then
                  raise (Error(fst f.label,
                               Wrong_nbr_of_args(n_actual, n_expected)));
               let args = (annotate type_env) args in
               let check_arg arg t =
                  let (arg_pos, arg_type) = arg.label in
                  if arg_type <> t then
                     raise (Error(arg_pos, Mismatch(arg_type, t))) in
               List.iter2 check_arg args arg_types;
               { label = (ast.label, ret_type); node = Call(f, args) }
          | _ -> raise(Error(fst f.label, Wrong_apply))


First, we compile unit literals as a 1-bit integer constant. The actual value is irrelevant, we use 0.

To avoid storing uselessly unit multiple times, we create a .unit global constant containing () in create.

If we try to store the result of a function returning unit in a variable, it will fail because unit return types are converted to void return types. Before storing a value, we thus check its type is not unit. If it is, we ignore it and use the .unit global variable instead. It is also more economical as unit will be stored only once by proceeding that way.

let compile_evaluator_and_printer comp id ast =
   (* Store result if an [id] is not "-" *)
   if id <> "-" then (
      let g =
         if val_type = Unit then (
            (* Unit, re-use already defined global *)
            unsafe_lookup_global ".unit" comp.module_
         ) else if is_constant value then (
            let g = define_global id value comp.module_ in
            set_global_constant true g;
         ) else (
            let undefined = undef (type_of value) in
            let g = define_global id undefined comp.module_ in
            ignore (build_store value g builder);
         ) in
      Hashtbl.add comp.globals id g

To compile function calls, we first compile the applied value. We then compile all arguments from left to right, including unit-type arguments (if they have side-effects, these should be produced). Finally, we call the compiled applied value with the compiled arguments which are not of a unit type.

let rec compile comp builder ast =
   match ast.node with
   | Call(f, args) ->
        let f' = compile comp builder f in
        let args' = (compile comp builder) args in
        (* Filter out unit-type arguments *)
        let filter_out_unit arg arg' args' =
             if snd arg.label = Unit then args'
             else arg' :: args' in
        let args' = List.fold_right2 filter_out_unit args args' [] in
        build_call f' (Array.of_list args') "" builder


We can now add arbitrary primitives to our language using its foreign function interface, although not always in a type-safe way (we have no way to define custom types yet). For example, we could extend our calculator (our interpreter is nothing more at the moment) by adding trigonometric and other mathematical functions.

# extern atan : float -> float = "atan";;
# extern sin : float -> float = "sin";;
# let pi = atan 1. *. 4.;;
pi : float = 3.141593
# sin (pi /. 2.);;
- : float = 1.000000

We will now turn our attention to function definitions, i.e. functions which are written entirely in Photon. However, we will first dedicate the next installment to a little re-factoring, before moving on to function definitions.

Source Code

The code accompanying this article is available in archive photon-tut-08.tar.xz or through the git repository:

 git clone
 cd photon
 git checkout 8-function_calls


Enter your comment. Wiki syntax is allowed:
blog/2011/01/28/function_calls.txt · Last modified: 2011/02/20 20:28 (external edit)