Macros

The macro capabilities of C3 reaches across several constructs: macros (prefixed with @ at invocation), generic functions, generic modules, compile time variables (prefixed with $), macro compile time execution (using $if, $for, $foreach, $switch) and attributes.

A quick comparison of C and C3 macros

Conditional compilation

// C
#if defined(x) && Y > 3
int z;
#endif

// C3
$if ($defined(x) && $y > 3):
    int z;
$endif;

Macros

// C
#define M(x) ((x) + 2)
#define UInt32 unsigned int

// Use:
int y = M(foo() + 2);
UInt32 b = y;

// C3
macro m(x)
{
    return x + 2;
}
typedef UInt32 = uint;

// Use:
int y = @m(foo() + 2);
UInt32 b = y;

Dynamic scoping

// C
#define Z() ptr->x->y->z
int x = Z();

// C3
... currently no corresponding functionality ...

Reference arguments

Use & in front of a parameter to capture the variable and pass it by reference without having to explicitly use & and pass a pointer. (Note that in C++ this is allowed for normal functions, whereas for C3 it is only permitted with macros.)

// C
#define M(x, y) x = 2 * (y);

// C3
macro m(int &x, int y)
{
    x = 2 * y;
}

First class types

// C
#define SIZE(T) (sizeof(T) + sizeof(int))

// C3
macro size($Type)
{
    return $Type.sizeof + int.sizeof;
}

Trailing blocks for macros

// C
#define FOR_EACH(x, list) \
for (x = (list); x; x = x->next)

// Use:
Foo *it;
FOR_EACH(it, list) 
{
    if (!process(it)) return;
}


// C3
macro for_each(list; @body(it))
{
    for ($typeof(list) x = list; x; x = x.next)
    {
        @body(x);
    }    
}

// Use:
@for_each(list; Foo* x)
{
    if (!process(x)) return;
}

First class names

// C
#define offsetof(T, field) (size_t)(&((T*)0)->field)

// C3
macro usz offset($Type, #field)
{
    $Type* t = null;
    return (usz)(uptr)&t.#field;
}

Declaration attributes

// C
#define PURE_INLINE __attribute__((pure)) __attribute__((always_inline))
int foo(int x) PURE_INLINE { ... }

// C3
define @PureInline = { @pure @inline };
fn int foo(int) @PureInline { ... }

Declaration macros

// C
#define DECLARE_LIST(name) List name = { .head = NULL };
// Use:
DECLARE_LIST(hello)

// C3
... currently no corresponding functionality ...

Stringification

#define CHECK(x) do { if (!x) abort(#x); } while(0)

// C3
macro fn check(#expr)
{
   if (!#expr) abort($stringify(#expr));
}

Top level evaluation

Script languages, and also upcoming languages like Jai, usually have unbounded top level evaluation. The flexibility of this style of meta programming has a trade off in making the code more challenging to understand.

In C3, top level compile time evaluation is limited to $if and $switch constructs + macros with constant expression evaluation. This makes the code easier to read, but at the cost of expressive power.

Macro declarations

A macro is defined using macro <name>(<parameters>). All user defined macros use the @ symbol if they use the & or # parameters.

The parameters have different sigils: $ means compile time evaluated (constant expression or type). # indicates an expression that is not yet evaluated, but is bound to where it was defined. Finally & is used to implicitly pass a parameter by reference. @ is required on macros that use # and & parameters.

A basic swap:

/**
 * @checked a = b, b = a
 */
macro void @swap(&a, &b)
{
    $typeof(a) temp = a;
    a = b;
    b = temp;
}

This expands on usage like this:

fn void test()
{
    int a = 10;
    int b = 20;
    @swap(a, b);
}
// Equivalent to:
fn void test()
{
    int a = 10;
    int b = 20;
    {
        int __temp = a;
        a = b;
        b = __temp;
    }
}

Note the necessary &. Here is an incorrect swap and what it would expand to:

macro void badswap(a, b)
{
    $typeof(a) temp = a;
    a = b;
    b = temp;
}

fn void test()
{
    int a = 10;
    int b = 20;
    badswap(a, b);
}
// Equivalent to:
fn void test()
{
    int a = 10;
    int b = 20;
    {
        int __a = a;
        int __b = b;
        int __temp = __a;
        __a = __b;
        __b = __temp;
    }
}

Macro methods

Similar to regular methods a macro may also be associated with a particular type:

struct Foo { ... }

macro Foo.generate(Foo *foo) { ... }
Foo f;
f.generate();

Capturing a trailing block

It is often useful for a macro to take a trailing compound statement as an argument. In C++ this pattern is usually expressed with a lambda, but in C3 this is completely inlined.

To accept a trailing block, ; @name(param1, ...) is placed after declaring the regular macro parameters.

Here's an example to illustrate its use:

/**
 * A macro looping through a list of values, executing the body once
 * every pass.
 *
 * @checked { int i = a.len; value2 = a[i]; }
 **/
macro @foreach(a; @body(index, value))
{
    for (int i = 0; i < a.len; i++)
    {
        @body(i, a[i]);
    }
}

fn void test()
{
    double[] a = { 1.0, 2.0, 3.0 };
    @foreach(a; int index, double value)
    {
        io::printfn("a[%d] = %f", index, value);
    }
}

// Expands to code similar to:
fn void test()
{
    int[] a = { 1, 2, 3 };
    {
        int[] __a = a;
        for (int __i = 0; i < __a.len; i++)
        {
            io::printfn("Value: %d, x2: %d", __value1, __value2);
        }
    }
}

Macros returning values

A macro may return a value, it is then considered an expression rather than a statement:

macro square(x)
{
    return x * x;
}

fn int getTheSquare(int x)
{
    return square(x);
}

fn double getTheSquare2(double x)
{
    return square(x);
}

Calling macros

It's perfectly fine for a macro to invoke another macro or itself.

macro square(x) { return x * x; }

macro squarePlusOne(x)
{
    return square(x) + 1; // Expands to "return x * x + 1;"
}

The maximum recursion depth is limited to the macro-recursion-depth build setting.

Macro vaargs

Macros support the typed vaargs used by C3 functions: macro void foo(int... args) and macro void bar(args...) but it also supports a unique set of macro vaargs that look like C style vaargs: macro void baz(...)

To access the arguments there is a family of $va-* built in functions to retrieve the arguments:

macro compile_time_sum(...)
{
   var $x = 0;
   $for (var $i = 0; $i < $vacount(); $i++):
       $x += $vaconst($i);
   $endfor;
   return $x;
}
$if (compile_time_sum(1, 3) > 2): // Will compile to $if (4 > 2)
  ...
$endif;

$vacount

Returns the number of arguments.

$vaarg

Returns the argument as a regular parameter. The argument is guaranteed to be evaluated once, even if the argument is used multiple times.

$vaconst

Returns the argument as a compile time constant, this is suitable for placing in a compile time variable or use for compile time evaluation, e.g. $foo = $vaconst(1). This corresponds to $ parameters.

$vaexpr

Returns the argument as an unevaluated expression. Multiple uses will evaluate the expression multiple times, this corresponds to # parameters.

$vatype

Returns the argument as a type. This corresponds to $Type style parameters, e.g. $vatype(2) a = 2

$varef

Returns the argument as an lvalue. This corresponds to &myref style parameters, e.g. $varef(1) = 123.

Untyped lists

Compile time variables may hold untyped lists. Such lists may be iterated over or implicitly converted to initializer lists:

var $a = { 1, 2 };
$foreach ($x : $a):
    io::printfn("%d", $x);
$endforeach;
int[2] x = $a;
io::printfn("%s", x);
io::printfn("%s", $a[1]);
// Will print
// 1
// 2
// [1, 2]
// 2

Macro directives

Inside of a macro, we can use the compile time statements $if, $for and $switch. Macros may also be recursively invoked. As previously mentioned, $if and $switch may also be invoked on the top level.

$if, $else and $elif

$if (<const expr>): takes a compile time constant value and evaluates it to true or false.

macro foo($x, $y)
{
    $if ($x > 3):
        $y += $x * $x;
    $else:
        $y += $x;
    $endif;    
}

const int FOO = 10;

fn void test()
{
    int a = 5;
    int b = 4;
    foo(1, a); // Allowed, expands to a += 1;
    // foo(b, a); // Error: b is not a compile time constant.
    foo(FOO, a); // Allowed, expands to a += FOO * FOO;
}

Loops using $foreach and $for

$foreach (<range> : <variable>): ... $endforeach; allows compile time recursion. $foreach may recurse over enums, struct fields or constant ranges. Everything must be known at compile time.

Compile time looping:

macro foo($a)
{
    $for (var $x = 0; $x < $a; $x++):
        io::printfn("%d", $x);     
    $endfor;
}

fn void test()
{
    foo(2);
    // Expands to ->
    // io::printfn("%d", 0);     
    // io::printfn("%d", 1);         
}

Looping over enums:

macro foo_enum($SomeEnum)
{
    $foreach ($x : $SomeEnum.values):
        io::printfn("%d", (int)$x);     
    $endforeach;
}

enum MyEnum
{
    A,
    B,
}

fn void test()
{
    foo_enum(MyEnum);
    // Expands to ->
    // io::printfn("%d", (int)MyEnum.A);
    // io::printfn("%d", (int)MyEnum.B);    
}

An important thing to note is that the content of the $foreeach or $for body must be a complete statement. It's not possible to compile partial statements.

Switching on type with $switch

It's possible to switch on type:

macro void foo(a, b)
{
    $switch(a, b):
        $case int, int: 
            return a * b;
    $endswitch;
    return a + b;
}

Conditional macros at the top level

A limitation with the macros is that they are only used within functions. This is deliberate – macros expanding at the top level are much harder to reason about since they should be able to define new types or change the meaning of the code that follows.

Still, the usefulness of top level macros is great, which is why C3 offers three pieces of functionality for the top level: conditional compilation, global constants and attributes

Conditional compilation

Conditional compilation is done with $if and $else, which works just like inside of functions.

$if ($defined(platform::OS) && platform::OS == WIN32):

fn void doSomethingWin32Specific()
{
    /* .... */
}

$endif;

Global constants

Global constant on the top level work like compile time variables in macros – with the exception that they must always be declared constant. They are evaluated in order, but will resolve on-demand if needed.