Loops and variables

Consider this C function:

int loop_test (int n)
{
  int sum = 0;
  for (int i = 0; i < n; i++)
    sum += i * i;
  return sum;
}

This example demonstrates some more features of libgccjit, with local variables and a loop.

Let’s construct this from Python. To break this down into libgccjit terms, it’s usually easier to reword the for loop as a while loop, giving:

int loop_test (int n)
{
  int sum = 0;
  int i = 0;
  while (i < n)
  {
    sum += i * i;
    i++;
  }
  return sum;
}

Here’s what the final control flow graph will look like:

image of a control flow graph

As before, we import the libgccjit Python bindings and make a gccjit.Context:

>>> import gccjit
>>> ctxt = gccjit.Context()

The function works with the C int type:

>>> the_type = ctxt.get_type(gccjit.TypeKind.INT)

though we could equally well make it work on, say, double:

>>> the_type = ctxt.get_type(gccjit.TypeKind.DOUBLE)

Let’s build the function:

>>> return_type = the_type
>>> param_n = ctxt.new_param(the_type, b"n")
>>> fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED,
...                        return_type,
...                        b"loop_test",
...                        [param_n])
>>> print(fn)
loop_test

The base class of expression is the gccjit.RValue, representing an expression that can be on the right-hand side of an assignment: a value that can be computed somehow, and assigned to a storage area (such as a variable). It has a specific gccjit.Type.

Anothe important class is gccjit.LValue. A gccjit.LValue is something that can of the left-hand side of an assignment: a storage area (such as a variable).

In other words, every assignment can be thought of as:

LVALUE = RVALUE;

Note that gccjit.LValue is a subclass of gccjit.RValue, where in an assignment of the form:

LVALUE_A = LVALUE_B;

the LVALUE_B implies reading the current value of that storage area, assigning it into the LVALUE_A.

So far the only expressions we’ve seen are i * i:

ctxt.new_binary_op(gccjit.BinaryOp.MULT,
                   int_type,
                   param_i, param_i)

which is a gccjit.RValue, and the various function parameters: param_i and param_n, instances of gccjit.Param, which is a subclass of gccjit.LValue (and, in turn, of gccjit.RValue): we can both read from and write to function parameters within the body of a function.

Our new example has a couple of local variables. We create them by calling gccjit.Function.new_local(), supplying a type and a name:

>>> local_i = fn.new_local(the_type, b"i")
>>> print(local_i)
i
>>> local_sum = fn.new_local(the_type, b"sum")
>>> print(local_sum)
sum

These are instances of gccjit.LValue - they can be read from and written to.

Note that there is no precanned way to create and initialize a variable like in C:

int i = 0;

Instead, having added the local to the function, we have to separately add an assignment of 0 to local_i at the beginning of the function.

This function has a loop, so we need to build some basic blocks to handle the control flow. In this case, we need 4 blocks:

  1. before the loop (initializing the locals)
  2. the conditional at the top of the loop (comparing i < n)
  3. the body of the loop
  4. after the loop terminates (return sum)

so we create these as gccjit.Block instances within the gccjit.Function:

>>> entry_block = fn.new_block(b'entry')
>>> cond_block = fn.new_block(b"cond")
>>> loop_block = fn.new_block(b"loop")
>>> after_loop_block = fn.new_block(b"after_loop")

We now populate each block with statements.

The entry block consists of initializations followed by a jump to the conditional. We assign 0 to i and to sum, using gccjit.Block.add_assignment() to add an assignment statement, and using gccjit.Context.zero() to get the constant value 0 for the relevant type for the right-hand side of the assignment:

>>> entry_block.add_assignment(local_i, ctxt.zero(the_type))
>>> entry_block.add_assignment(local_sum, ctxt.zero(the_type))

We can then terminate the entry block by jumping to the conditional:

>>> entry_block.end_with_jump(cond_block)

The conditional block is equivalent to the line while (i < n) from our C example. It contains a single statement: a conditional, which jumps to one of two destination blocks depending on a boolean gccjit.RValue, in this case the comparison of i and n. We build the comparison using gccjit.Context.new_comparison():

>>> guard = ctxt.new_comparison(gccjit.Comparison.LT, local_i, param_n)
>>> print(guard)
i < n

and can then use this to add cond_block‘s sole statement, via gccjit.Block.end_with_conditional():

>>> cond_block.end_with_conditional(guard,
...                                 loop_block, # on true
...                                 after_loop_block) # on false

Next, we populate the body of the loop.

The C statement sum += i * i; is an assignment operation, where an lvalue is modified “in-place”. We use gccjit.Block.add_assignment_op() to handle these operations:

>>> loop_block.add_assignment_op(local_sum,
...                              gccjit.BinaryOp.PLUS,
...                              ctxt.new_binary_op(gccjit.BinaryOp.MULT,
...                                                 the_type,
...                                                 local_i, local_i))

The i++ can be thought of as i += 1, and can thus be handled in a similar way. We use gccjit.Context.one() to get the constant value 1 (for the relevant type) for the right-hand side of the assignment:

>>> loop_block.add_assignment_op(local_i,
...                              gccjit.BinaryOp.PLUS,
...                              ctxt.one(the_type))

The loop body completes by jumping back to the conditional:

>>> loop_block.end_with_jump(cond_block)

Finally, we populate the after_loop block, reached when the loop conditional is false. At the C level this is simply:

return sum;

so the block is just one statement:

>>> after_loop_block.end_with_return(local_sum)

Note

You can intermingle block creation with statement creation, but given that the terminator statements generally include references to other blocks, I find it’s clearer to create all the blocks, then all the statements.

We’ve finished populating the function. As before, we can now compile it to machine code:

>>> jit_result = ctxt.compile()
>>> void_ptr = jit_result.get_code(b'loop_test')

and use ctypes to turn it into a Python callable:

>>> import ctypes
>>> int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
>>> callable = int_int_func_type(void_ptr)

Now we can call it:

>>> callable(10)
285

Visualizing the control flow graph

You can see the control flow graph of a function using gccjit.Function.dump_to_dot():

>>> fn.dump_to_dot('/tmp/sum-of-squares.dot')

giving a .dot file in GraphViz format.

You can convert this to an image using dot:

$ dot -Tpng /tmp/sum-of-squares.dot -o /tmp/sum-of-squares.png

or use a viewer (my preferred one is xdot.py; see https://github.com/jrfonseca/xdot.py; on Fedora you can install it with yum install python-xdot):

image of a control flow graph

Full example

Here’s what the above looks like as a complete program:

import ctypes

import gccjit

def populate_ctxt(ctxt):
    the_type = ctxt.get_type(gccjit.TypeKind.INT)
    return_type = the_type
    param_n = ctxt.new_param(the_type, b"n")
    fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED,
                           return_type,
                           b"loop_test",
                           [param_n])
    # Build locals
    local_i = fn.new_local(the_type, b"i")
    local_sum = fn.new_local(the_type, b"sum")

    assert str(local_i) == 'i'

    # Build blocks
    entry_block = fn.new_block(b'entry')
    cond_block = fn.new_block(b"cond")
    loop_block = fn.new_block(b"loop")
    after_loop_block = fn.new_block(b"after_loop")

    # entry_block: #########################################

    # sum = 0
    entry_block.add_assignment(local_sum, ctxt.zero(the_type))

    # i = 0
    entry_block.add_assignment(local_i, ctxt.zero(the_type))

    entry_block.end_with_jump(cond_block)

    ### cond_block: ########################################

    # while (i < n)
    cond_block.end_with_conditional(ctxt.new_comparison(gccjit.Comparison.LT,
                                                         local_i, param_n),
                                    loop_block,
                                    after_loop_block)

    ### loop_block: ########################################

    # sum += i * i
    loop_block.add_assignment_op(local_sum,
                                 gccjit.BinaryOp.PLUS,
                                 ctxt.new_binary_op(gccjit.BinaryOp.MULT,
                                                    the_type,
                                                    local_i, local_i))

    # i++
    loop_block.add_assignment_op(local_i,
                                 gccjit.BinaryOp.PLUS,
                                 ctxt.one(the_type))

    # goto cond_block
    loop_block.end_with_jump(cond_block)

    ### after_loop_block: ##################################

    # return sum
    after_loop_block.end_with_return(local_sum)

def create_fn():
    # Create a compilation context:
    ctxt = gccjit.Context()

    if 0:
        ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_TREE, True)
        ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_GIMPLE, True)
        ctxt.set_bool_option(gccjit.BoolOption.DUMP_EVERYTHING, True)
        ctxt.set_bool_option(gccjit.BoolOption.KEEP_INTERMEDIATES, True)
    if 0:
        ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3)

    populate_ctxt(ctxt)

    jit_result = ctxt.compile()
    return jit_result

def test_calling_fn(i):
    jit_result = create_fn()

    int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
    code = int_int_func_type(jit_result.get_code(b"loop_test"))

    return code(i)

if __name__ == '__main__':
    print(test_calling_fn(10))