Circle Quick Reference

Download Circle

Circle white paper

Circle is a new language that extends C++ 17 to support data-driven imperative metaprogramming. Circle combines the immediacy and flexibility of a scripting language with the type system, performance and universality of C++. Three new features make this a more effective programming tool than Standard C++:

  1. An integrated interpreter supports the execution of normal C++ statements at compile time.
  2. Same-language reflection programmatically allows for the generation of new code without involving an elaborate DOM or API for modelling the AST.
  3. Introspection keywords inform the program about the content of types, and expose data members and enumerators as iterable entities.

Circle accepts the C++ language as a starting point, and rotates that language from the runtime to the compile-time axis, allowing you to finally metaprogram C++ using C++.

Meta statements

Unlike real statements, meta statements are not executed when a function is run, but rather when the compiler translates source to AST. Like statements in a scripting language, meta statements are essentially executed in top-to-bottom order, following meta control flow and the rules of C++.

// Use a normal means to pull declarations into the translation unit. These
// declarations are available to both real and meta code.
#include <cstdio>

// This expression-statement executes when it's parsed. Unlike a real 
// expression statement, it doesn't need to be executed from a function.
@meta printf("Hello global\n");

namespace ns {
  // We can execute meta statements in namespace scope.
  @meta printf("Hello namespace\n");
}

struct struct_t {
  // We can execute meta statements in class-specifiers. In the case of 
  // class templates, execution of the statement is deferred until 
  // instantiation
  @meta printf("Hello struct\n");
};

enum enum_t {
  // The enum-specifier syntax has been extended to allow semicolon-separated
  // statements. Meta statements are parsed and execution during translation.
  // Meta control flow 
  @meta printf("Hello enum\n");
};

void func() {
  // The meta statement is executed during translation of func's definition,
  // or its instantiation in the case of a function template. It is not 
  // executed when the function is called.
  @meta printf("Hello function\n");
}

Meta control flow statements create a new meta block scope. Real declarations fall through this meta scope and embed themselves in the enclosing real scope. Same-language reflection is achieved by using meta control flow to find the point at which you want to deposit an object, member or enumerator declaration, and using a real statement to make the declaration.

template<typename... types_t>
struct tuple_t {
  @meta for(int i = 0; i < sizeof...(types_t); ++i)
    types_t...[i] @(i);     // Convert i to an _-prefixed identifier.
};

tuple_t<int, double, char> obj;
obj._0 = 1;
obj._1 = 3.14;
obj._2 = 'x';

This is the simplest interesting Circle program. We use meta control flow in the definition of a class template to programmatically declare its non-static data members. When the template is instantiated the meta for-statement is executed, performing three iterations. At each iteration, the child statement, which is a real statement, is injected into the innermost real scope, which is the class's scope. Contextually this implies that the real statement be a member-specifier. Therefore, the substituted statement creates not an object, but a data member, at each of its three injections.

The ...[] operator performs subscripting on a parameter pack, in this case yielding the i'th type. The Circle extension @() is the dynamic name operator. It converts a string or integer known at compile time to an identifier. Taken together, the real statement declares non-static data members int _0, double _1 and char _2 over its three iterations.

int x;
@meta int y;

// block scope introduced by @meta is meta, but the enclosed statements are in
// a real context.
@meta for(int i = 0; i < 5; ++i) {
  ++x;        // OK: Emit a ++x statement five times.
  @meta ++x;  // Error: Cannot modify a real object from meta context.
  ++y;        // Error: Cannot modify a meta object from real context.
  @meta ++y;  // OK: Evaluate ++y five times during translation.
}

// block scope introduced by @meta+ is meta and the enclosed statements are in
// a meta context.
@meta+ for(int i = 0; i < 5; ++i) {
  ++x;        // Error: Cannot modify a real object from meta context.
  @emit ++x;  // OK: Emit a ++x statement five times.
  ++y;        // OK: Evaluate ++y five times during translation.
  @emit ++y;  // Error: Cannot modify a meta object from real context.
}

As a convenience, the @meta+ token sets the context of the current statement and all of its descendents as meta. Inside a @meta+ block, use the @emit token to escape out and re-establish a real context.

Dynamic names

Classes

The class-specifier syntax has been extended to allow meta statements, including declaration, expression and control-flow statements. Meta control flow may guide source translation of class-specifier.

Enums

The enum-specifier syntax has been extended to allow multiple statements. Meta control flow may guide translation of enum-specifier.

enum class my_enum {
  a, b;                       // Declare a, b

  // Loop from 'c' until 'f'
  @meta for(char x = 'c'; x != 'f'; ++x) {
    // Get a string ready.
    @meta char name[2] = { x, 0 };

    // Declare the enum.
    @(name);
  }

  f, g;                       // Declare f, g
};

// Declares enumerators a, b, c, d, e, f, g.

The for-enum-statement extends ranged-based for-statement to enumerations: each iteration yields one enumerator from the enum. This is especially useful when generating case statements:

enum enum_t { 
  a, b, c, d
};

template<enum_t e>
void func(double x);

void dispatch(enum_t e, double x) {
  switch(e) {
    @meta for enum(enum_t e2 : enum_t)
      case e2:
        func<e2>(x);
        break;
  }
}

Typed enums

Typed enums are a special data type where each enumerator has an associated type. A type-id must be provided for each enumerator. An identifier may be provided; if it is not, one will be assigned in the pattern _0, _1 and so on. Typed enums may be scoped or unscoped (by specifying class or struct) and may have a fixed underlying type.

enum typename [class | struct] my_typed_enum [: underlying-type] {
  [identifier = ] type-id, [identifier = ] type-id
};

To convert parameter packs to typed-enum definitions, the typed-enum-specifier supports pack expansion:

template<typename... types_t>
struct foo_t;

enum typename my_types_t {
  int, 
  char*,
  void
};

// Expand all the types in my_types_t, then expand pointers to all those
// types. This creates a typedef:
// typedef foo_t<int, char*, void, int*, char**, void*> my_foo_t;
typedef foo_t<
  @enum_types(my_types_t)..., 
  @enum_types(my_types_t)*...
> my_foo_t;

enum typename my_types2_t {
  // Specify single-types
  int, double,

  // Or expanded parameter packs
  @enum_types(my_types_t)..., @enum_types(my_types_t)*...
};

When a typed enum contains only unique associated types, the case-typename syntax allows us to switch over an enumeration by its associated type.

int main() {
  enum typename int_types_t {
    Short = short,
    Int = int,
    Long = long
  };

  switch(Int) {
    case typename short:
      break;

    case typename int:
      break;

    case typename long:
      break;
  }
  return 0;
}

Type manipulation

Code injection

mtype builtin type

mtype is a pointer-sized builtin type that holds a type. It allows compile-time meta code to manipulate types like values. mtype has comparison operators to assist in type array manipulation.

template<typename... types_t>
struct unique_type_tuple_t {
  enum { count = sizeof...(types_t) };

  // Create an array of mtype objects. Each object is created using 
  // @dynamic_type, which boxes a type-id into an @mtype.
  @meta @mtype x[] { @dynamic_type(types_t)... };

  // Use std::sort and std::unique to create an array of unique mtypes.
  @meta std::sort(x, x + count);
  @meta size_t count2 = std::unique(x, x + count) - x;

  // Convert the mtype array to an expanded type parameter pack, so we can 
  // specialized tuple_t
  typedef tuple_t<@pack_type(x, count2)...> type;
};

Array extensions

Valid expressions

Metafunctions

A function declared after the @meta token is a metafunction. Metafunctions generate inline, anonymous functions every time they are called. Prepend the @meta token to metafunction parameters to make meta parameters. The values of arguments for meta parameters must be known at compile time, and in the function definition, those values are are accessible during source translation.

template<typename... args_t>
@meta int cirprint(@meta const char* fmt, args_t&&... args);

Each call to the cirprint metafunction generates a new function definition. When the definition is translated, the fmt metaparameter is available as a meta object. The function definition performs meta control flow over the contents of the format specifier in order to access the args function parameter pack.

Macros

Circle macros are provide a way to inject content into a scope at definition or instantiation time. Use @macro on a void-returning function in global scope to define a macro. Expand a macro by calling it on its own @macro-prefixed line.

Circle macros participate in template argument deduction and overload resolution. All parameters are implicitly metaparameters, meaning their values are accessible during source translation.

Macros may be expanded from any statement-accepting scope.

// Define a macro. It has access to the count value during expansion.
@macro void my_macro(int count) { 
  @meta for(int i = 0; i < count; ++i)
    int @(i);
}

template<int Count>
struct foo_t {
  // Expand a macro. Since this is a dependent context, it's expanded at
  // instantiation, when the value of Count is available.
  @macro my_macro(Count);
};

int main() {
  foo_t<3> obj;
  obj._0 = 0;
  obj._1 = 1;
  obj._2 = 2;
  obj._3 = 3;   // Error! '_3' is not a member of class foo_t<3>
  return 0;
}

CUDA support

#include <cstdio>

@meta for enum(nvvm_arch_t arch : nvvm_arch_t)
  @meta printf("%s - %d\n", @enum_name(arch), (int)arch);
$ circle -cuda-path /usr/local/cuda-10.0/ -sm_35 -sm_52 -sm_61 -sm_70 kernel3.cu
sm_35 - 35
sm_52 - 52
sm_61 - 61
sm_70 - 70

The collection of target architectures is crucial for Circle's kernel dispatch strategy. In a kernel, loop over each target architecture. Then select the architecture being targeted for code generation by the backend with @codegen if.

template<typename type_t>
__global__ void kernel(type_t a, const type_t* x, type_t* y, size_t count) {
  // Loop over each target architecture.
  @meta for enum(nvvm_arch_t sm : nvvm_arch_t) {

    // The backend only emits this code when __nvvm_arch == sm. This
    @codegen if(__nvvm_arch == sm) {
      // Find parameters for this architecture being targeted by the backend. 
      @meta auto it = kernel_config.lower_bound((int)sm);
      static_assert(it != kernel_config.end(), 
        "requested SM version has no kernel details!");

      // Execute the behavior over the parameters for this architecture.
      details_t details = it->second;
      do_it<details>();
    }
  }
}