Notes for Circle video tutorial #1

1

#include <cstdio>

@meta printf("Hello circle\n");

int main() {
  return 0;
}

Whatever statement you put after the meta keyword is executed at compile time. For meta statements inside templates, the statement is parsed at definition and executed during instantiation.

2

#include <cstdio>

@meta for(int i = 0; i < 5; ++i)
  @meta printf("Hello circle %d\n", i);

int main() {
  return 0;
}

You can perform structured control at compile-time. The child statement is translated at each iteration. Here, the child printf statement is executed five times at compile time.

3

#include <cstdio>

@meta for(int i = 0; i < 5; ++i)
  int x = i;

int main() {
  return 0;
}

The child statement of the meta for is a real statement, so it gets injected in the innermost enclosing real scope, which in this case is the global namespace. Each loop iteration injects that object declaration: five object declarations means four redefinition errors.

To extend C++ for programmatic declarations, the @ operator translates strings and integers to identifiers. Identifiers are prepended with an underscore to make them valid identifiers.

#include <cstdio>

@meta for(int i = 0; i < 5; ++i)
  int @(i) = i;

int main() {
  return 0;
}

4

#include <cstdio>

@meta int count;
@meta printf("Enter array size: ");
@meta scanf("%d", &count);

double my_array[count];

@meta printf("declared array %s\n", @type_name(decltype(my_array)));

int main() {
  return 0;
}

Circle has an integrated C++ interpreter, so we can perform unrestricted i/o with the system. We can generate types and functions from data, enabling a new data-driven imperative metaprogramming paradigm.

5

#include <cstdio>

template<typename type_t>
struct foo_t {
  @meta printf("foo_t<%s>\n", @type_name(type_t));
};

int main() {
  foo_t<int> a;
  foo_t<char*> b;
  return 0;
}

Meta statements can be put in any braced scope: namespace, class definition, enum definition or block scope. In dependent contexts, the meta statement is executed during instantiation.

#include <cstdio>

template<typename... types_t>
struct foo_t {
  @meta int x[] { printf("%s\n", @type_name(types_t))... };
};

int main() {
  foo_t<int, double, char*> obj;
  return 0;
}

Meta stataments are compatible with template parameter packs. This is C++'s ridiculous way to call a function over a parameter pack. The pack expansion syntax is only allowed in function arguments and initializer lists, so here we create a compile-time integer array (which is a compile-time automatic variable, not a class member) and initialize it with the return object of the printf, expanded over each element of the types_t parameter pack.

#include <cstdio>

template<typename... types_t>
struct foo_t {
  @meta for(int i = 0; i < sizeof...(types_t); ++i)
    @meta printf("type %d is %s\n", i, types_t...[i]);
};

int main() {
  foo_t<int, double, const char*> obj;
  return 0;
}

Circle extends C++ with the ...[] operator to directly index any parameter pack kind: type pack, non-type pack, template pack or function parameter pack. This allows imperative access to parameter packs. There are also Circle extensions to create parameter packs from several kinds of data.

#include <cstdio>

template<typename... types_t>
struct tuple_t {
  @meta for(int i = 0; i < sizeof...(types_t); ++i)
    types_t...[i] @(i);
};

int main() {
  tuple_t<int, double, const char*> obj;
  obj._0 = 1;
  obj._1 = 3.14;
  obj._2 = "Hello tuple";
  return 0;
}

The Circle tuple is a flat data structure, with members given integer names and created straight from the indexed parameter pack.

6

We can use imperative metaprogramming to create types. Here we show how to use Circle's introspection keywords to generically access data inside types.

#include <iostream>
#include <type_traits>

template<typename... types_t>
struct tuple_t {
  @meta for(int i = 0; i < sizeof...(types_t); ++i)
    types_t...[i] @(i);
};

template<typename type_t>
void print_object(const type_t& obj) {
  static_assert(std::is_class<type_t>::value, 
    "argument of print_object must be a class type");

  @meta for(int i = 0; i < @member_count(type_t); ++i)
    std::cout<< @member_name(type_t, i)<< ": "<< @member_ref(obj, i)<< "\n";
}

struct type2_t {
  double x, y, z;
  const char* s;
};

int main() {
  tuple_t<int, double, const char*> obj;
  obj._0 = 1;
  obj._1 = 3.14;
  obj._2 = "Hello tuple";

  type2_t obj2 {
    5, 10, 15, "Another type"
  };

  print_object(obj);
  print_object(obj2);
  return 0;
}

We can also introspect on the enumerators in an enumeration. This is useful for converting between strings and enumerator names.

#include <cstdio>
#include <type_traits>
#include <string>

enum my_enum_t {
  a, b, c, d, e
};

template<typename type_t>
std::string enum_to_string(type_t e) {
  static_assert(std::is_enum<type_t>::value, 
    "argument to enum_to_string must be enumeration");

  switch(e) {
    @meta for enum(auto e2 : type_t) 
      case e2:
        return @enum_name(e2);
    
    default:
      return "<" + std::to_string(e) + ">";
  }
}


int main() {
  printf("%s\n", enum_to_string(my_enum_t::c).c_str());
  printf("%s\n", enum_to_string((my_enum_t)15).c_str());
  return 0;
}

7

#include <sstream>
#include <cstdio>
#include <type_traits>
#include <vector>
#include <map>

template<typename... types_t>
struct tuple_t {
  @meta for(int i = 0; i < sizeof...(types_t); ++i)
    types_t...[i] @(i);
};

template<typename type_t>
std::string enum_to_string(type_t e) {
  static_assert(std::is_enum<type_t>::value, 
    "argument to enum_to_string must be enumeration");

  switch(e) {
    @meta for enum(auto e2 : type_t)
      case e2:
        return @enum_name(e2);
    
    default:
      return "<" + std::to_string(e) + ">";
  }
}

template<typename type_t>
std::string print_object(const type_t& obj) {
  std::ostringstream oss;
  if constexpr(std::is_enum<type_t>::value) {
    oss<< std::string(@type_name(type_t)) + "::" + enum_to_string(obj);

  } else if constexpr(std::is_same<std::string, type_t>::value ||
    std::is_same<const char*, type_t>::value) {

    oss<< "\""<< obj<< "\"";

  } else if constexpr(std::is_scalar<type_t>::value) {
    oss<< obj;

  } else if constexpr(std::is_array<type_t>::value ||
    @is_class_template(type_t, std::vector)) {

    // Print all array elements inside []
    oss<< "[ ";
    bool comma = false;
    for(const auto& x : obj) {
      if(comma) oss<< ", ";
      oss<< print_object(x);    // Recursive call to print items.
      comma = true;
    }
    oss<< " ]";
    return oss.str();

  } else if constexpr(@is_class_template(type_t, std::map)) {
    // Print all pairs inside {}
    oss<< "{ ";
    bool comma = false;
    for(const auto& x : obj) {
      if(comma) oss<< ", ";
      oss<< print_object(x.first)<< " : "<< print_object(x.second);
      comma = true;
    }
    oss<< " }";

  } else {
    static_assert(std::is_class<type_t>::value, "expected class type");

    // Print all members inside {}
    oss<< "{ ";
    bool comma = false;
    @meta for(int i = 0; i < @member_count(type_t); ++i) {
      if(comma) oss<< ", ";
      // Print key-value pair.
      oss<< @member_name(type_t, i)<< " : ";
      oss<< print_object(@member_ref(obj, i));
      comma = true;
    }
    oss<< " }";
  } 
  return oss.str();
}

int main() {
  tuple_t<int, double[3], const char*, std::map<std::string, int> > obj {
    1,
    { 101.1, 202.2, 303.3 }, 
    "Hello tuple", 
    { { "Hello", 100 }, { "World", 200 } }
  };

  printf("%s\n", print_object(obj).c_str());
  return 0;
}