Tutorial part 3: 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.

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 include the libgccjit++ header and make a gccjit::context.

#include <libgccjit++.h>

void test (void)
{
  gccjit::context ctxt;
  ctxt = gccjit::context::acquire ();

The function works with the C int type.

In the previous tutorial we acquired this via

gccjit::type the_type = ctxt.get_type (ctxt, GCC_JIT_TYPE_INT);

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

gccjit::type the_type = ctxt.get_type (ctxt, GCC_JIT_TYPE_DOUBLE);

For integer types we can use gccjit::context::get_int_type to directly bind a specific type:

gccjit::type the_type = ctxt.get_int_type <int> ();

Let’s build the function:

gcc_jit_param n = ctxt.new_param (the_type, "n");
std::vector<gccjit::param> params;
params.push_back (n);
gccjit::function func =
  ctxt.new_function (GCC_JIT_FUNCTION_EXPORTED,
                     return_type,
                     "loop_test",
                     params, 0);

Expressions: lvalues and rvalues

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 from the previous tutorial:

  1. the multiplication i * i:

gccjit::rvalue expr =
  ctxt.new_binary_op (
    GCC_JIT_BINARY_OP_MULT, int_type,
    param_i, param_i);

/* Alternatively, using operator-overloading: */
gccjit::rvalue expr = param_i * param_i;

which is a gccjit::rvalue, and

  1. 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 new kind of expression: we have two local variables. We create them by calling gccjit::function::new_local(), supplying a type and a name:

/* Build locals:  */
gccjit::lvalue i = func.new_local (the_type, "i");
gccjit::lvalue sum = func.new_local (the_type, "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.

Control flow

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:

gccjit::block b_initial = func.new_block ("initial");
gccjit::block b_loop_cond = func.new_block ("loop_cond");
gccjit::block b_loop_body = func.new_block ("loop_body");
gccjit::block b_after_loop = func.new_block ("after_loop");

We now populate each block with statements.

The entry block b_initial 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:

/* sum = 0; */
b_initial.add_assignment (sum, ctxt.zero (the_type));

/* i = 0; */
b_initial.add_assignment (i, ctxt.zero (the_type));

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

b_initial.end_with_jump (b_loop_cond);

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 could build the comparison using gccjit::context::new_comparison():

gccjit::rvalue guard =
  ctxt.new_comparison (GCC_JIT_COMPARISON_GE,
                       i, n);

and can then use this to add b_loop_cond’s sole statement, via gccjit::block::end_with_conditional():

b_loop_cond.end_with_conditional (guard,
                                  b_after_loop, // on_true
                                  b_loop_body); // on_false

However gccjit::rvalue has overloaded operators for this, so we express the conditional as

gccjit::rvalue guard = (i >= n);

and hence we can write the block more concisely as:

b_loop_cond.end_with_conditional (
  i >= n,
  b_after_loop, // on_true
  b_loop_body); // 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:

/* sum += i * i */
b_loop_body.add_assignment_op (sum,
                               GCC_JIT_BINARY_OP_PLUS,
                               i * i);

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

/* i++ */
b_loop_body.add_assignment_op (i,
                               GCC_JIT_BINARY_OP_PLUS,
                               ctxt.one (the_type));

Note

For numeric constants other than 0 or 1, we could use gccjit::context::new_rvalue(), which has overloads for both int and double.

The loop body completes by jumping back to the conditional:

b_loop_body.end_with_jump (b_loop_cond);

Finally, we populate the b_after_loop block, reached when the loop conditional is false. We want to generate the equivalent of:

return sum;

so the block is just one statement:

/* return sum */
b_after_loop.end_with_return (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:

gcc_jit_result *result;
result = ctxt.compile ();

ctxt.release ();

if (!result)
  {
    fprintf (stderr, "NULL result");
    return 1;
  }

typedef int (*loop_test_fn_type) (int);
loop_test_fn_type loop_test =
 (loop_test_fn_type)gcc_jit_result_get_code (result, "loop_test");
if (!loop_test)
  {
    fprintf (stderr, "NULL loop_test");
    gcc_jit_result_release (result);
    return 1;
  }
printf ("result: %d", loop_test (10));
result: 285

Visualizing the control flow graph

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

func.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

/* Usage example for libgccjit.so's C++ API
   Copyright (C) 2014-2021 Free Software Foundation, Inc.

This file is part of GCC.

GCC is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

GCC is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3.  If not see
<http://www.gnu.org/licenses/>.  */

#include <libgccjit++.h>

#include <stdlib.h>
#include <stdio.h>

void
create_code (gccjit::context ctxt)
{
  /*
    Simple sum-of-squares, to test conditionals and looping

    int loop_test (int n)
    {
      int i;
      int sum = 0;
      for (i = 0; i < n ; i ++)
      {
	sum += i * i;
      }
      return sum;
   */
  gccjit::type the_type = ctxt.get_int_type <int> ();
  gccjit::type return_type = the_type;

  gccjit::param n = ctxt.new_param (the_type, "n");
  std::vector<gccjit::param> params;
  params.push_back (n);
  gccjit::function func =
    ctxt.new_function (GCC_JIT_FUNCTION_EXPORTED,
                       return_type,
                       "loop_test",
                       params, 0);

  /* Build locals:  */
  gccjit::lvalue i = func.new_local (the_type, "i");
  gccjit::lvalue sum = func.new_local (the_type, "sum");

  gccjit::block b_initial = func.new_block ("initial");
  gccjit::block b_loop_cond = func.new_block ("loop_cond");
  gccjit::block b_loop_body = func.new_block ("loop_body");
  gccjit::block b_after_loop = func.new_block ("after_loop");

  /* sum = 0; */
  b_initial.add_assignment (sum, ctxt.zero (the_type));

  /* i = 0; */
  b_initial.add_assignment (i, ctxt.zero (the_type));

  b_initial.end_with_jump (b_loop_cond);

  /* if (i >= n) */
  b_loop_cond.end_with_conditional (
    i >= n,
    b_after_loop,
    b_loop_body);

  /* sum += i * i */
  b_loop_body.add_assignment_op (sum,
                                 GCC_JIT_BINARY_OP_PLUS,
                                 i * i);

  /* i++ */
  b_loop_body.add_assignment_op (i,
                                GCC_JIT_BINARY_OP_PLUS,
                                ctxt.one (the_type));

  b_loop_body.end_with_jump (b_loop_cond);

  /* return sum */
  b_after_loop.end_with_return (sum);
}

int
main (int argc, char **argv)
{
  gccjit::context ctxt;
  gcc_jit_result *result = NULL;

  /* Get a "context" object for working with the library.  */
  ctxt = gccjit::context::acquire ();

  /* Set some options on the context.
     Turn this on to see the code being generated, in assembler form.  */
  ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE,
                        0);

  /* Populate the context.  */
  create_code (ctxt);

  /* Compile the code.  */
  result = ctxt.compile ();

  ctxt.release ();

  if (!result)
    {
      fprintf (stderr, "NULL result");
      return 1;
    }

  /* Extract the generated code from "result".  */
  typedef int (*loop_test_fn_type) (int);
  loop_test_fn_type loop_test =
    (loop_test_fn_type)gcc_jit_result_get_code (result, "loop_test");
  if (!loop_test)
    {
      fprintf (stderr, "NULL loop_test");
      gcc_jit_result_release (result);
      return 1;
    }

  /* Run the generated code.  */
  int val = loop_test (10);
  printf("loop_test returned: %d\n", val);

  gcc_jit_result_release (result);
  return 0;
}

Building and running it:

$ gcc \
    tut03-sum-of-squares.cc \
    -o tut03-sum-of-squares \
    -lgccjit

# Run the built program:
$ ./tut03-sum-of-squares
loop_test returned: 285