Circle Quick Reference
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++:
- An integrated interpreter supports the execution of normal C++ statements at compile time.
- Same-language reflection programmatically allows for the generation of new code without involving an elaborate DOM or API for modelling the AST.
- 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
-
@meta
- Execute a statement or subexpression in a meta context.- Expression statement
- Declaration statement
- Compound statement
- try-statement
- if-, for-, while- and do-statements
- break- and continue-statements
-
@meta+
- Like@meta
, but the meta context propages through all child scopes. -
@emit
- Leave a@meta+
context and return to real context.
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 declarations and statements are in the declarative region of the innermost enclosing scope.
- Real declarations and statements are in the declarative region of the innermost enclosing real scope.
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
-
@(string-returning-expression)
- Converts a string known at compile time to an identifier. -
@(integer-returning-expression)
- Converts an integer known at compile time to an identifier. Prefixes _ to the number, making it a valid identifier. Negative numbers are prefixed by _n.
Classes
-
@member_count(type)
- Return number of non-static data members of class type. -
@member_name(type, i)
- Return name of i'th non-static data member of class type as a string literal lvalue. -
@member_ptr(type, i)
- Return pointer-to-member-data of i'th non-static data member of class type. -
@member_ref(object, i)
- Return lvalue to i'th non-static data member of object. -
@member_type(type, i)
- Yield type of i'th non-static data member of class type.
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
-
@enum_count(type)
- Return number of uniquely-valued enumerators in enum type. -
@enum_name(type, i)
- Return name of i'th enumerator in enum type. -
@enum_value(type, i)
- Return i'th enumerator constant in enum type. -
for enum(auto i : enum-type)
- Range-based for over enumerators in an enum. This must be a meta statement.
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
-
__is_typed_enum(type)
- Returns true if the type is a typed enum. -
@enum_type(type, i)
- Yield type associated with the i'th enumerator of typed enum. -
@enum_types(type)
- Yields associated types of a typed enum as an unexpanded parameter pack. -
case typename type-id:
- Matches an enumeration in a switch by its associated type.
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
@type_name(type)
-
@type_name(type, bool)
- Return the name of a type as a string literal lvalue. Pass 'true' as a second argument to print the canonical name of a type, versus the name of a typedef. -
@type_id(string)
- Yield a type from a type-id string. May be a string literal or aconst char*
orstd::string
known at compile time. -
...[i]
- Returns the i'th element of a type, non-type, template or function parameter pack.
Code injection
-
@expression(string)
- Inject an expression from a string known at compile time. The string is run through the preprocessor, then the tokens are parsed and run through the compiler. -
@statements(string)
- Inject one or more semicolon-terminated delimited statements from a string known at compile time. -
@include(filename)
- Inject the contents of the named file from a filename known at compile time.
mtype builtin type
-
@mtype
- Compile-time builtin type. -
@dynamic_type(type-id)
- Return an mtype object from a type-id. -
@static_type(mtype-returning-expression)
- Yields a type from an mtype object. -
@pack_type(mtype-array, mtype-array-count)
- Yields a type parameter pack from an array of mtype objects.
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
-
@array(pointer, count)
- Yields an array initializer from an array of values known at compile time. -
@string(string)
- Yields a string literal lvalue from aconst char*
orstd::string
known at compile time.
Valid expressions
-
@sfinae(expression)
- A SFINAE expression. If during template instantiation the enclosed expression substitutes successful, the extension returns true. For substitution failure, the expression returns false.
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
-
enum class nvvm_arch_t
- An implicitly-defined scoped enumeration, defined with one enumerator per NVPTX target specified at the command line. -
@codegen nvvm_arch_t __nvvm_arch
- An implicitly-declared enumeration object. When the code for a device function is generated,__nvvm_arch
evaluates to the enumerator corresponding to the target architecture. This availability occurs during code generation, not source translation. A value of__nvvm_arch
is unavailable when generating host code.
#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>();
}
}
}