Cone Language Reference

Macros

On this page

Macros are similar to generic functions. They generate code inline wherever they are used.

// A macro that evaluates to the largest value
macro max[a, b] {
	imm aa = a;
	imm bb = b;
	if aa > bb {aa;} else {bb;}
}
	
// Now use it
fn tester(x i32, y i32) {
  imm maxnbr = max(x, y);
}

Wherever the macro is used, its call site is effectively replaced with the logic of the macro, substituting all arguments wherever the macro's parameters are specified. A macro's logic must be syntactically complete and valid. Macros are hygienic: any names defined in the macro are inaccessible to the call site, and vice versa.

Notice that the 'max' macro captured its parameters into variables before using their values. This ensures each parameter's expression is only evaluated once. If this had not been done, both expressions would have been evaluated more than once, possibly to different values.

The other side of that is an argument that is never evaluated at all, when the macro's logic does not reach it. Core's assertDebug (Panics) is a macro for that reason: in a release build its condition is not evaluated.

Written in a macro's logic, srcFile() and srcLine() answer where the macro is used, not where its logic is written; in a macro used inside another's logic, where the outermost one is used. A macro may take no default argument.

A macro need not declare any parameters. When it does not, its name is the whole of its use, and that name expands to the macro's logic wherever a value is expected: as an initializer, as either operand of an operator, as an argument, as what a function returns, and as the value a block hands back.

// A macro that evaluates to the same number every time
macro two { 2; }

fn tester(x i32) i32 {
  imm a = two;          // an initializer
  imm b = two + x;      // either operand of an operator
  two * b;              // the value the function hands back
}

Writing an empty argument list, as in two(), means exactly the same thing.

Macros as methods#

A macro may be declared inside a struct, trait or enum, where it becomes a member of that type. As with a function, it is a method when its first parameter is named self. It is used like a method, with the receiver written before the dot and any further arguments in parentheses:

enum Maybe {
  pub struct Nothing {}
  pub struct Just {value i32;}

  // The value held, or a default when nothing is
  pub macro orElse[self, dflt] {
    imm s = self;
    match s {
      case imm j &Just {j.value;}
      case imm n &Nothing {dflt;}
    }
  }
}

fn tester(m &imm Maybe) i32 {
  m.orElse(0);
}

The receiver stands in for self exactly as an argument stands in for any other macro parameter: it is a piece of source, substituted wherever self appears in the macro's logic, and evaluated as many times as it appears. That is why orElse captures it into a variable first. A macro method with no parameters beyond self is used as the bare member name, m.isSet, or with an empty argument list.

Inside the macro's logic, the type's fields and methods are reached through self, as in self.value, and never by their bare names. The logic expands into whichever function uses the macro, so it may reach the type's private members through self just as a method would, wherever it is used. Inside a method of the same type, a macro method may itself be named bare, as a method may, and means self.name.