Circle feature preview

Twitter: @seanbax
circlelang@gmail.com

New Circle whitepaper - rapid fire examples

The docs!

Circle reference and examples
Kernel launches with Circle
Automatic struct-of-array
Generating tensor contractions with Circle and TACO
Reverse-mode automatic differentiation with Circle and Apex
Circle video tutorial #1 - Serialization
Circle video tutorial #2 - Typed enums
RPN as an embedded Circle compiler
Parameter packs in Circle
Implementing a DSL using an open source dynamic PEG parser
Walkthrough 1: Injecting functions from text
Walkthrough 2: Evaluating expressions from text
Walkthrough 3: Deserializing JSON to classes
Type erasure with Circle
Pattern-matching expressions and enhanced structured bindings
Spaceship operator
F#-style type providers in Circle
Video - Circle compiler walkthrough
NEW Compile-time regular expressions
NEW The Circle format library

The program!

Public preview build 81

For my pals, I'm making a preview build of Circle available.

Download the compiler.

The compiler is experimental. As you run into issues, please, pardon my bugs.

What is Circle?

Circle is a new programming 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++.

Installation

The Linux build of Circle targets libstdc++, which is the C/C++ library and C++ ABI implementation maintained by the GCC project. You'll need a recent version of this library (Circle looks in /usr/include for the latest version) and the GNU linker ld to build programs with circle. From a clean Debian distribution, install these dependencies by running:

sudo apt update
sudo apt install libstdc++-8-dev
sudo apt install binutils

I use Linux Mint 19, which is based on Ubuntu 18.04. The compiler also works on the Ubuntu installation on Windows Subsystem for Linux. It's also been tested on Redhat (Fedora 30) and Arch Linux.

The Circle distributable is just an executable and sanity-check file in a tarball archive. You can install the compiler in any folder like this:

tar xvf build_81.tgz
chmod +x circle

You'll want to run the included sanity check to confirm that libstdc++ installed correctly:

$ ./circle sanity.cxx
Hello printf at compile time
Hello cout at compile time
$ ./sanity
Hello printf at runtime
Hello cout at runtime

Installing from Docker

If you're running an incompatible distribution, or a different OS altogether, you can install Circle in a Ubuntu image. Download this into an empty directory, change to that directory and build the Dockerfile and run the resulting image, as follows:

$ docker build --tag=circle_docker .
$ docker run -it circle_docker bash
# cd /home/circle_docs/examples/special
# circle special.cxx 
Generating code for function 'E1'
  note: first Einstein special function
  Injecting expression 'sq(x) * exp(x) / sq(exp(x) - 1)'
  Injecting Taylor series
Generating code for function 'factorial'
  Injecting statements 'double y = 1; while(x > 1) y *= x--; return y;'
Generating code for function 'sigmoid'
  Injecting expression '1 / (1 + exp(-x))'
  Injecting Taylor series
Generating code for function 'sin'
  Injecting expression 'sin(x)'
  Injecting Taylor series
Generating code for function 'tanh'
  Injecting expression 'tanh(x)'
  Injecting Taylor series

The meta context

In compiled languages, we employ language features to guide execution of our program. Circle adds a new dimension of programmability, allowing you to program the compiler. Rather than introducing a complicated API for accessing front-end features (like a DOM for C++), C++ itself is used to guide C++ translation. The @meta keyword at the beginning of a statement causes that statement to be executed at source translation rather than at execution.

For example,

int main(int argc, char** argv) {
  printf("Hello world\n");
  @meta printf("Hello circle\n");
}
$ circle hello.cxx
Hello circle
$ ./hello
Hello world

During translation, normal statements are parsed, analyzed and inserted into the abstract syntax tree. During code generation, the abstract syntax tree is recursed and lowered into an intermediate representation like LLVM IR, and from that, an architecture-specific binary is generated.

In Circle, statements prefixed with the @meta token are parsed, analyzed and executed during translation (at definition or at instantiation in the case of template-dependent contexts). Because statements may be executed at compile time, the package includes an interpreter with the ability execute C++ AST and make foreign function calls (in this example, to printf in libc.so).

Why is this so impactful? We can guide source translation by employing compile-time control flow with data loaded at compile time by the interpreter.

The Circle language consists of three primary feature domains:

  1. Integrated interpreter
  2. Same-language reflection
  3. Introspection keywords

Combine these features to design your own metaprogramming idioms.

As a trivial example, consider listing the types for each data member of a structure in a plain text file in the source directory. We'll use ordinary C library functions to open the file and read out its contents, line-by-line, during source translation. For each type in the file, we'll declare a data member in the structure foo_t and name them _0, _1 and so on.

We'll use Circle's introspection keywords as a sanity check. print_struct iterates over a class's data members and prints the type and name of each of them.

struct.fields

int
double
char*

fields.cxx

#include <cstdio>
#include <cstdlib>

struct foo_t {
  // Load struct.fields from the source folder.
  @meta FILE* f = fopen("struct.fields", "r");

  // Declare one member of foo_t per line in the file.
  // @type_id converts a string (known at compile time) to a type.
  // @(XX) converts an integral expression XX to an identifier of the
  //   form _XX.
  // Since the body of the loop is not a meta statement, it falls out of the
  // loop (which is a meta scope) and into the enclosing non-meta scope, which
  // is the struct definition, where it is translated as a member-specifier.
  @meta char line[256];
  @meta for(int i = 0; fgets(line, 256, f); ++i)
    @type_id(line) @(i);
  
  // Be kind, rewind.
  @meta fclose(f);
};

// Loop over each non-static data member in the parameter type to convince
// ourselves that the trick above worked.
template<typename type_t>
void print_struct() {
  printf("%s\n", @type_name(type_t));
  @meta for(int i = 0; i < @member_count(type_t); ++i)
    printf(
      "  %s %s\n", 
      @type_name(@member_type(type_t, i)), // print the type of the member
      @member_name(type_t, i)              // print the name of the member
    );
}

int main(int argc, char** argv) {
  // Print all the fields in foo_t:
  print_struct<foo_t>();
  return 0;
}
$ circle fields.cxx
$ ./fields 
foo_t
  int _0
  double _1
  char* _2

The white paper and examples repository has deeper examples, and focuses on a "configuration-oriented programming" paradigm and on improving C++ template metaprogramming.

Usage

Like any compiler, --help lists options. Start with that.

Clone the white paper and examples project to get started with the examples.

Specify the Circle/C++ file as the single positional argument. circle does not currently serve as an indepedent linker, so if you have multiple translation units, you can:

Filetypes

Use -filetype=XX to select which kind of file to emit. If -o XX is provided, the filetype may be inferred from the extension.

Paths

Circle searches some common paths to establish default system header and meta library paths. Use --print-paths to show which default paths it uses on your installation.

The gcc-style command-line arguments -isystem, -I and -iquote add #include paths with different precedences to the preprocessor. Additionally, in Circle, the -M command-line argument adds shared object files to be loaded by the interpreter to support the foreign-function calls to non-standard libraries. For convenience, the compiler also checks the files circle.isystem, circle.I, circle.iquote and circle.M in the working directory--if one of those files isn't found, it checks the compiler's directory for the same file. These files contain the header and library paths in plain text, typed out one per line.

The gcc-style -D command-line argument supports defining simple preprocessor macros at invocation. Additionally, the file circle.D is opened (first from the working directory, and if not found there, from the compiler's directory), and its contents are injected verbatim into the top of the translation unit. To clarify, -DVERSION=5 injects an object-like macro VERSION set to 5. A file circle.D containing the text #define VERSION 5 achieves the same thing. The circle.D feature is more flexible than the command-line argument, as it allows you to define function-like macros in addition to object-like macros.

GPU targets

To enable GPU builds, specify one or more of these flags:

Unlike the nvcc or clang compilers, only a single front-end pass is performed over each translation unit, no matter how NVVM targets are specified. However, like both of those established compilers, circle will perform a separate code generation pass for each target, link the resulting PTX and CUBIN files into a single FATBIN, and register that FATBIN's kernel symbols with the CUDA Runtime API to support chevron-style kernel launches.

The codegen constant mechanism Circle uses to to target multiple GPU device generations with a single frontend pass is described here.

The CUDA support is very early. I'll be focusing on this in the next couple of weeks and will port moderngpu to the new language.

Compiling for CUDA

Circle requires declarations from the CUDA Toolkit to implement the chevron launch syntax. Specify the path to your CUDA Toolkit install (eg, from here, and not just the one installed at /usr/include) with -cuda-path.

Follow this useful tutorial to learn how to launch CUDA kernels.

Read here on using Circle's introspection and reflection to implement struct-of-array and array-of-struct generic GPU kernels.

Issues

License

Copyright (c) 2019, Sean Baxter
All rights reserved.

Redistribution and use in binary forms, with or without modification, are 
permitted provided that the following condition is met:

1. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the Circle compiler project.