https://www.circle-lang.org/ Circle The C++ Automation Language 2021 Sean Baxter Follow me on Twitter @seanbax for compiler updates. NEW CUDA NEW Pattern Matching NEW Circle Imperative Arguments Member traits Circle conditionals Universal member access Circle C++ for shaders Circle reflection and typed enums List comprehensions, slices, ranges, for-expressions, functional folds and expansion expressions The Circle format library Compile-time regular expressions Parameter packs in Circle Download the compiler Build 138 with C++23 pattern matching, CUDA, SPIR-V and DXIL shaders, file embed, extended slices, list comprehension and more! Circle is experimental software. As you run into issues, please, pardon my bugs. What is Circle? Circle is a new programming language that extends C++ 20 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++-10-dev sudo apt install binutils I use Linux Mint 20, which is based on Ubuntu 20.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_138.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 #include 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 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(); 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: * With one pass per file, compile to .o and use clang to link. * With one pass per file, compile to .bc, use llvm-link-8 to link the bitcodes, llc-8 to optimize and generate a .o file, and clang to link. This provides inter-procedural optimization across translation units. For single translation unit programs, circle will link to executables or shared objects as specified by the -filetype argument or inferred from the output filename extension. Building with Circle Circle adopts the same general command-line arguments practiced by gcc and clang: * -S emits an ASM file (.s). * -S -emit-llvm emits a human-readable LLVM IR file (.ll). * -c -emit-llvm emits a binary LLVM IR bitcode file (.bc). * -c only compiles to an object file, without linking. * -console prints the output (if text) to the terminal. This is very useful when paired with -S or -S -emit-llvm * -O0, -O1, -O2 and -O3 are the usual optimization levels. * -g generates debug info. * -M names a compile-time shared object library. This enables compile-time foreign function calls to compiled code. * -stat reports on files opened and lines lexed. * -no-demangle prints mangled names in link errors. 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. License Copyright (c) 2021, 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.