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 gcc_jit_context*.

#include <libgccjit.h>

void test (void)
{
  gcc_jit_context *ctxt;
  ctxt = gcc_jit_context_acquire ();

The function works with the C int type:

gcc_jit_type *the_type =
  gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT);
gcc_jit_type *return_type = the_type;

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

gcc_jit_type *the_type =
  gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_DOUBLE);

Let’s build the function:

gcc_jit_param *n =
  gcc_jit_context_new_param (ctxt, NULL, the_type, "n");
gcc_jit_param *params[1] = {n};
gcc_jit_function *func =
  gcc_jit_context_new_function (ctxt, NULL,
                                GCC_JIT_FUNCTION_EXPORTED,
                                return_type,
                                "loop_test",
                                1, params, 0);

Expressions: lvalues and rvalues#

The base class of expression is the gcc_jit_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 gcc_jit_type*.

Another important class is gcc_jit_lvalue*. A gcc_jit_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 gcc_jit_lvalue* is a subclass of gcc_jit_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:

gcc_jit_rvalue *expr =
  gcc_jit_context_new_binary_op (
    ctxt, NULL,
    GCC_JIT_BINARY_OP_MULT, int_type,
    gcc_jit_param_as_rvalue (param_i),
    gcc_jit_param_as_rvalue (param_i));

which is a gcc_jit_rvalue*, and the various function parameters: param_i and param_n, instances of gcc_jit_param*, which is a subclass of gcc_jit_lvalue* (and, in turn, of gcc_jit_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 gcc_jit_function_new_local(), supplying a type and a name:

/* Build locals:  */
gcc_jit_lvalue *i =
  gcc_jit_function_new_local (func, NULL, the_type, "i");
gcc_jit_lvalue *sum =
  gcc_jit_function_new_local (func, NULL, the_type, "sum");

These are instances of gcc_jit_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 gcc_jit_block* instances within the gcc_jit_function*:

gcc_jit_block *b_initial =
  gcc_jit_function_new_block (func, "initial");
gcc_jit_block *b_loop_cond =
  gcc_jit_function_new_block (func, "loop_cond");
gcc_jit_block *b_loop_body =
  gcc_jit_function_new_block (func, "loop_body");
gcc_jit_block *b_after_loop =
  gcc_jit_function_new_block (func, "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 gcc_jit_block_add_assignment() to add an assignment statement, and using gcc_jit_context_zero() to get the constant value 0 for the relevant type for the right-hand side of the assignment:

/* sum = 0; */
gcc_jit_block_add_assignment (
  b_initial, NULL,
  sum,
  gcc_jit_context_zero (ctxt, the_type));

/* i = 0; */
gcc_jit_block_add_assignment (
  b_initial, NULL,
  i,
  gcc_jit_context_zero (ctxt, the_type));

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

gcc_jit_block_end_with_jump (b_initial, NULL, 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 gcc_jit_rvalue*, in this case the comparison of i and n. We build the comparison using gcc_jit_context_new_comparison():

/* (i >= n) */
 gcc_jit_rvalue *guard =
   gcc_jit_context_new_comparison (
     ctxt, NULL,
     GCC_JIT_COMPARISON_GE,
     gcc_jit_lvalue_as_rvalue (i),
     gcc_jit_param_as_rvalue (n));

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

/* Equivalent to:
     if (guard)
       goto after_loop;
     else
       goto loop_body;  */
gcc_jit_block_end_with_conditional (
  b_loop_cond, NULL,
  guard,
  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 gcc_jit_block_add_assignment_op() to handle these operations:

/* sum += i * i */
gcc_jit_block_add_assignment_op (
  b_loop_body, NULL,
  sum,
  GCC_JIT_BINARY_OP_PLUS,
  gcc_jit_context_new_binary_op (
    ctxt, NULL,
    GCC_JIT_BINARY_OP_MULT, the_type,
    gcc_jit_lvalue_as_rvalue (i),
    gcc_jit_lvalue_as_rvalue (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++ */
gcc_jit_block_add_assignment_op (
  b_loop_body, NULL,
  i,
  GCC_JIT_BINARY_OP_PLUS,
  gcc_jit_context_one (ctxt, the_type));

Note

For numeric constants other than 0 or 1, we could use gcc_jit_context_new_rvalue_from_int() and gcc_jit_context_new_rvalue_from_double().

The loop body completes by jumping back to the conditional:

gcc_jit_block_end_with_jump (b_loop_body, NULL, 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 */
gcc_jit_block_end_with_return (
  b_after_loop,
  NULL,
  gcc_jit_lvalue_as_rvalue (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 = gcc_jit_context_compile (ctxt);

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)
  goto error;
printf ("result: %d", loop_test (10));
result: 285

Visualizing the control flow graph#

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

gcc_jit_function_dump_to_dot (func, "/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
   Copyright (C) 2014-2022 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 (gcc_jit_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;
   */
  gcc_jit_type *the_type =
    gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT);
  gcc_jit_type *return_type = the_type;

  gcc_jit_param *n =
    gcc_jit_context_new_param (ctxt, NULL, the_type, "n");
  gcc_jit_param *params[1] = {n};
  gcc_jit_function *func =
    gcc_jit_context_new_function (ctxt, NULL,
				  GCC_JIT_FUNCTION_EXPORTED,
				  return_type,
				  "loop_test",
				  1, params, 0);

  /* Build locals:  */
  gcc_jit_lvalue *i =
    gcc_jit_function_new_local (func, NULL, the_type, "i");
  gcc_jit_lvalue *sum =
    gcc_jit_function_new_local (func, NULL, the_type, "sum");

  gcc_jit_block *b_initial =
    gcc_jit_function_new_block (func, "initial");
  gcc_jit_block *b_loop_cond =
    gcc_jit_function_new_block (func, "loop_cond");
  gcc_jit_block *b_loop_body =
    gcc_jit_function_new_block (func, "loop_body");
  gcc_jit_block *b_after_loop =
    gcc_jit_function_new_block (func, "after_loop");

  /* sum = 0; */
  gcc_jit_block_add_assignment (
    b_initial, NULL,
    sum,
    gcc_jit_context_zero (ctxt, the_type));

  /* i = 0; */
  gcc_jit_block_add_assignment (
    b_initial, NULL,
    i,
    gcc_jit_context_zero (ctxt, the_type));

  gcc_jit_block_end_with_jump (b_initial, NULL, b_loop_cond);

  /* if (i >= n) */
  gcc_jit_block_end_with_conditional (
    b_loop_cond, NULL,
    gcc_jit_context_new_comparison (
       ctxt, NULL,
       GCC_JIT_COMPARISON_GE,
       gcc_jit_lvalue_as_rvalue (i),
       gcc_jit_param_as_rvalue (n)),
    b_after_loop,
    b_loop_body);

  /* sum += i * i */
  gcc_jit_block_add_assignment_op (
    b_loop_body, NULL,
    sum,
    GCC_JIT_BINARY_OP_PLUS,
    gcc_jit_context_new_binary_op (
      ctxt, NULL,
      GCC_JIT_BINARY_OP_MULT, the_type,
      gcc_jit_lvalue_as_rvalue (i),
      gcc_jit_lvalue_as_rvalue (i)));

  /* i++ */
  gcc_jit_block_add_assignment_op (
    b_loop_body, NULL,
    i,
    GCC_JIT_BINARY_OP_PLUS,
    gcc_jit_context_one (ctxt, the_type));

  gcc_jit_block_end_with_jump (b_loop_body, NULL, b_loop_cond);

  /* return sum */
  gcc_jit_block_end_with_return (
    b_after_loop,
    NULL,
    gcc_jit_lvalue_as_rvalue (sum));
}

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

  /* Get a "context" object for working with the library.  */
  ctxt = gcc_jit_context_acquire ();
  if (!ctxt)
    {
      fprintf (stderr, "NULL ctxt");
      goto error;
    }

  /* Set some options on the context.
     Let's see the code being generated, in assembler form.  */
  gcc_jit_context_set_bool_option (
    ctxt,
    GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE,
    0);

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

  /* Compile the code.  */
  result = gcc_jit_context_compile (ctxt);
  if (!result)
    {
      fprintf (stderr, "NULL result");
      goto error;
    }

  /* 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");
      goto error;
    }

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

 error:
  gcc_jit_context_release (ctxt);
  gcc_jit_result_release (result);
  return 0;
}

Building and running it:

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

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