LeMP Macro Reference: Code generation & compile-time decision-making

20 Mar 2016
Standard macros in the LeMP namespace

Introduction

One of the main uses of LeMP is generating code.

Most code generation tasks can be accomplished with just four macros: macro, matchCode, quote, and compileTime. Most of the other macros here were introduced before compileTime and macro, and they are mostly useful for simple code generation scenarios that do not need to invoke the heavyweight C# Interactive engine.

In addition, there is a macro for the unary $ operator, which looks up and inserts a syntax variable captured by static deconstruct, static tryDeconstruct or the `staticMatches` operator. (This works differently from static matchCode, because static matchCode performs replacements “in advance” inside the “handler” below the matching case, whereas $ replaces lazily. In most cases, however, the net effect is the same.)

alt class: Algebraic Data Type

alt class Color { 
  alt this(byte Red, byte Green, byte Blue, byte Opacity = 255);
}
void DrawLine(Color c) {
  if (c.Opacity > 0) {
    (var r, var g, var b) = c;
    ...
  }
}

Expands a short description of an ‘algebraic data type’ (a.k.a. disjoint union) into a set of classes with a common base class. All data members are read-only, and for each member (e.g. Item1 and Item2 above), a With() method is generated to let users create modified versions. Example:`

// A binary tree
public partial abstract alt class Tree<T> 
	where T: IComparable<T>
{
  alt this(T Value);
  alt Leaf();
  alt Node(Tree<T> Left, Tree<T> Right);
}
// Output of LeMP
// A binary tree
public partial abstract class Tree<T>
 where T: IComparable<T> {
  public Tree(T Value) {
    this.Value = Value;
  }
  public T Value { get; private set; }
  public abstract Tree<T>
  WithValue(T newValue);
  [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public T Item1 {
    get {
      return Value;
    }
  }
}
// A binary tree
public partial abstract static partial class Tree {
  public static Tree<T>
  New<T>(T Value) where T: IComparable<T> {
    return new Tree<T>
    (Value);
  }
}
class Leaf<T> : Tree<T>
 where T: IComparable<T> {
  public Leaf(T Value)
     : base(Value) { }
  public override Tree<T>
  WithValue(T newValue) {
    return new Leaf<T>(newValue);
  }
}
class Node<T> : Tree<T>
 where T: IComparable<T> {
  public Node(T Value, Tree<T> Left, Tree<T> Right)
     : base(Value) {
    this.Left = Left;
    this.Right = Right;
  }
  public Tree<T> Left { get; private set; }
  public Tree<T> Right { get; private set; }
  public override Tree<T>
  WithValue(T newValue) {
    return new Node<T>(newValue, Left, Right);
  }
  public Node<T> WithLeft(Tree<T> newValue) {
    return new Node<T>(Value, newValue, Right);
  }
  public Node<T> WithRight(Tree<T> newValue) {
    return new Node<T>(Value, Left, newValue);
  }
  [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public Tree<T> Item2 {
    get {
      return Left;
    }
  }
  [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public Tree<T> Item3 {
    get {
      return Right;
    }
  }
}
static partial class Node {
  public static Node<T> New<T>(T Value, Tree<T> Left, Tree<T> Right) where T: IComparable<T> {
    return new Node<T>(Value, Left, Right);
  }
}

compileTime, compileTimeAndRuntime

These two macros run code at compile-time using Microsoft’s C# Interactive engine. The code can be either executable statements, like string text = File.ReadAllText("File.txt");, or declarations like class Foo { ... }. The code executes as though you had typed it into C# Interactive, except that the code is not expected to “return” any value or produce any output. These macros are usually used together with the precompute macro.

The C# interactive engine has some limitations that you should keep in mind when using this feature. For example, namespaces are not supported, and extension methods are not allowed inside classes (extension methods are allowed at the top level of the compileTime block, except in the initial release, v2.8.1).

To reference an assembly, use the #r directive as shown in the following example. The path to the assembly can be absolute, or relative to the current value of #get(#inputFolder), which is normally the folder that contains the source file that was given to LeMP (if one source file includes another using includeFile, the input folder doesn’t change, so by default #r is relative to the original file, not the current file.)

compileTime {
	#r "../../ecsharp/Lib/LeMP/Loyc.Utilities.dll"
	using Loyc.Utilities;
  
	var stat = new Statistic();
	stat.Add(5);
	stat.Add(7);
}
class Foo {
  const int Six = precompute((int) stat.Avg());
}
class Foo {
  const int Six = 6;
}

The code in a compileTime block automatically has references to assemblies with basic types such as tuples and List<T>, but it is necessary to add a using statement to access some of these types, e.g. using System(.Collections.Generic, .Linq, .Text);. Some Loyc core libraries are also referenced automatically: Loyc.Interfaces.dll, Loyc.Essentials.dll, Loyc.Collections.dll, Loyc.Syntax.dll, Loyc.Ecs.dll, and LeMP.StdMacros.dll.

The following namespaces are imported automatically and do not require a using statement: System, Loyc, Loyc.Syntax.

The code passed to compileTime (and related macros, including precompute) runs in the same context as LeMP itself, so it will run under the same version of .NET as LeMP itself uses. As of July 2020, The LeMP extension for Visual Studio expects to run under .NET version 4.7.2, but the actual environment is set up by Visual Studio itself. Therefore, certain C# features such as #nullable enable may or may not work depending on the environment.

compileTime blocks should appear at the top level of the file, outside any namespace or class declarations, because there is no support for scoping. For example, consider this code:

class X {
  // Not allowed
  compileTime {
    using System;
    string text = "Text";
    string folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
  }
}
class Y {
  compileTime {
    File.WriteAllText(Path.Combine(folder, "temp.txt"), text));
  }
}

This code would naturally work because the compileTime blocks are simply given to C# Interactive to run. But this is confusing, because text and folder appear to be defined in class X and used in class Y. To reduce confusion, this code produces the following error: “compileTime is designed for use only at the top level of the file. It will be executed as though it is at the top level.”

LeMP will evaluate macros in the code block before executing it. For example, you could use the quote macro or the matchCode macro to create or analyze syntax trees.

compileTime blocks do not appear in the output file, but compileTimeAndRuntime blocks are both executed and also copied to the output file. If you are unsure what effect a macro has inside a compileTime block, try changing it to compileTimeAndRuntime temporarily and look at the output file. Also, when a compiler error occurs in C# Interactive, LeMP will include, in the error message, the final line of code that was given to C# Interactive, instead of the original EC# code.

There are four kinds of errors that can occur when using code-execution macros:

  1. Enhanced C# parser errors. These errors do not include an error code, only a location.
  2. Messages from the macro. These errors or warnings begin with the qualified macro name, e.g. LeMP.StandardMacros.compileTime
  3. “Compile-time” errors from C# interactive, which begin with a macro name (LeMP.StandardMacros.compileTime) but also include an error code from the C# compiler (e.g. CS0103).
  4. Execution-time errors, which occur when the code is valid but encounters an error when it is executed. These errors will be followed by a stack trace in the Error List.

Note: As of July 2020, warnings from C# Interactive are not supported and are never shown.

Warning: compileTime and related macros are somewhat dangerous, in a couple of ways:

  1. There are no restrictions on what the code can do: it can read or write files, access the internet, or engage in destructive behavior. This makes it a security risk if you or your team did not write the code being given to LeMP.
  2. The code can accidentally cause problems. Visual Studio will run the code every time you save the file, and it will also run the code every time you switch to a different file in the editor. So if your code is incomplete or buggy, it could do something unexpected just because you pressed Ctrl+Tab or saved the file.

For example, when using the LeMP extension, you can crash Visual Studio with code like this:

compileTime {
    int Foo() => Foo();
    Foo();
}

This causes StackOverflowException, which is unrecoverable and terminates the host process. I considered fixing this, but avoiding the problem would be a substantial amount of work, and I noticed that T4 templates (which Visual Studio has supported for over 10 years) have the same problem. If it’s good enough for Microsoft, I thought to myself, it’s good enough for an open-source project with zero funding. In fact, T4 templates are even worse. <# while(true) {} #> will freeze Visual Studio forever, but LeMP has a system to stop itself after (by default) 10 seconds.

define

// General syntax
[Passive]
define (Foo[$index] = $value) {
	Foo.SetAt($index, $value);
}
x = Foo[y] = z;

// Method syntax
define MakeSquare($T) { 
	void Square($T x) => x*x;
}
MakeSquare(int);
MakeSquare(double);
MakeSquare(float);
// Output of LeMP
x = Foo.SetAt(y, z);

void Square(int x) => x * x;
void Square(double x) => x * x;
void Square(float x) => x * x;

Defines a new macro, scoped to the current braced block, that matches the specified pattern and replaces it with the specified output code. define has two forms. The method syntax resembles a method, and in EC# you can use either lambda syntax like define MacroName(...) => ..., or brace syntax like define MacroName(...) { ... }. Brace syntax is more general, since it allows you to put multiple statements in the output, and you can also include type declarations.

The general syntax has the form define (pattern) { replacement; }, where the braces are mandatory and are not included in the output. The syntax define (pattern) => replacement; is not (and cannot be) supported, because it is not valid Enhanced C# syntax.

The [Passive] option in the above example prevents warning messages when assignment operators are encountered that do not fit the specified pattern (e.g. X = Y and X[index] = Y do not match the pattern). See MacroMode for a list of available options.

Matching and replacement occur the same way as in the older replace macro. One difference is worth noting: if you are using the method syntax and there are braces around a matching argument, those braces are treated literally, not ignored (even though the braces around the replacement code are not considered part of the replacement code; they are ignored). If you are using the general syntax then it has no effect to enclose the entire pattern in braces, but braces are significant inside the pattern. In the following example, the braces around $body are significant, but the braces around the entire while loop are ignored:

// Fun fact: while(true) {...} is equivalent to for(;;) {...}
[Passive]
define ({ while (true) { $body; } }) 
{
  for (;;) $body;
}

while (true) {
  Keep.Going();
}

The general syntax ignores the outer braces in order to allow you to use statement syntax while (true) {...} (which is not a legal EC# expression). Note: if you want to write a pattern that matches braces themselves, you simply need to use double braces, i.e. define ({ { $body; } }) { ... }

define has the ability to generate unique names.

Example:

define change_temporarily($lhs = $rhs)
{
	var old_unique# = $lhs; // save previous value
	$lhs = $rhs;
	on_finally { $lhs = old_unique#; } // restore
};

void Example()
{
	change_temporarily(Environment.CurrentDirectory = @"C:\");
	change_temporarily(Console.ForegroundColor = ConsoleColor.Red);
	Console.WriteLine("Text color is temporarily red...");
	Console.WriteLine("And we're in " + Environment.CurrentDirectory);
}

The technical difference between define and the older replace macro is that replace performs a find-and-replace operation directly, whereas this one creates a macro with a specific name. This leads to a couple of differences in behavior which ensure that the old macro is still useful in certain situations.

The first difference is that define works recursively, but replace doesn’t:

replace (Foo => Bar(Foo($x)));
Foo(5);
// Output of LeMP
Bar(Foo($x))(5);

Currently, define doesn’t handle this well; Foo(...) is expanded recursively up to the iteration limit (or until stack overflow).

The second difference is that the older macro performs replacements immediately, while define generates a macro whose expansions are interleaved with other macros in the usual way. For example, if you write

    replace (A => B);  
    define macro2($X) => $X * $X;  
    macro1(macro2(macro3(A)));

The order of replacements is

  1. A is replaced with B
  2. macro1 is executed (if there is a macro by this name)
  3. macro2 is executed (if it still exists in the output of macro1)
  4. macro3 is executed (if there is a macro by this name)

Often, define has higher performance than replace because, by piggybacking on the usual macro expansion process, it avoids performing an extra pass on the syntax tree.

Note: define and replace are not hygienic. For example, variables defined in the replacement expression are not renamed to avoid conflicts with variables defined at the point of expansion.

macro

The macro macros combine define’s power to create macros with compileTime’s power to run C# code at compile time. There’s a lot to learn to use macro effectively, so let’s start simple.

This macro converts an identifier to uppercase:

macro toUpper($id) {
    return LNode.Id(id.Name.Name.ToUpper());
}

The input is expected to be an identifier, which is converted to uppercase (e.g. toUpper(xen) is XEN). The Name of a Loyc tree is a Symbol, so id.Name.Name is used to get the string stored inside the Symbol in order to convert it to uppercase. The Name of a call like Foo(x, y) is Foo. Literals and complex calls have zero-length names, so toUpper(3) would return the empty identifier, denoted @`` in Enhanced C# (I’m considering changing this to null, though, in which case toUpper(3) would produce an error).

LNode is short for “Loyc node”, a reference to the underlying syntax tree, called a Loyc tree. A Loyc tree is a simple data structure inspired by the Lisp family of languages; it enables LeMP to be language-agnostic (though still C#-centric as of 2021). The LNode API is discussed here.

Here you can see two macros that do almost the same thing, but one is implemented with define and the other with macro:

define WriteLine1($message, $(..args)) => 
  MessageSink.Default.Write(Severity.Note, "WriteLine1", $message, $args);

macro WriteLine2($message, $(..args)) => 
  quote(MessageSink.Default.Write(Severity.Note,
    new LineColumnFile(
      $(LNode.Literal(message.Range.Start.Line)),
      $(LNode.Literal(message.Range.Start.Column)),
      $(LNode.Literal(message.Range.Source.FileName))),
    $message, $(..args)));

compileTime{
	WriteLine1("Hello {0}", "world");
	WriteLine2("Goodbye {0}", "Bob");
}

Both of these macros allow you to print formatted messages from inside a compileTime or macro block.

However, the second one can include any C# expression. The macro uses this ability to send information to MessageSink.Default about the location of the message in the source code, by constructing a new LineColumnFile object. When you double-click the second message in your development environment, it should take you to the location where the message was printed.

Please note that the quote macro does not have access to the data type of the arguments being inserted, and assumes they are LNode; therefore the macro must convert the line, column and filename into LNodes via LNode.Literal.

A subtle difference between these two macros is that WriteLine1 refers to $args while WriteLine2 refers to $(..args). In fact, you can change $args to $(..args) and WriteLine1 will still work properly, but if you change $(..args) to $args in the quote macro, it will not compile. This is because the second macro uses quote, and quote is not aware that args is a list of syntax trees, so you must inform it that args is a list using $(..args). In contrast, define is already aware that args is a list, so you do not have to tell it.

macro relies on compileTime to function. It implicitly references the same libraries and implicitly includes the same using directives as compileTime, such as Loyc.Syntax which contains LNode. The macro macro preprocesses the method body before giving it to the C# scripting engine, so LeMP macros can be used inside the method body.

Like define, macro has two different syntaxes. The following two macros are equivalent:

macro toUpper($id) {
    return LNode.Id(id.Name.Name.ToUpper());
}
macro (toUpper($id)) {
    return LNode.Id(id.Name.Name.ToUpper());
}

The second syntax is more flexible because it allows you to match arbitrary expressions. For example,

macro ({ 
    if ($x == null)
        $then;
}) {
    return quote {
        if ($x is null)
            $then;
    };
}

// The condition is changed to `result is null`. (The macro also, unfortunately, 
// deletes these comments and some extra work is needed to preserve them.)
if (result == null) {
    CancelTransaction();
    // This comment is preserved though, because it's part of $then
}

The macro body has access to two parameter variables, LNode node and IMacroContext context. node is the syntax tree of the entire macro call including the macro name (of any). context provides access to all functionality of IMacroContext.

MacroMode values can be attached as attributes. For example, the [PriorityOverride] attribute will cause your macro to supercede other (default-priority) macros that have the same name and match the same patterns. On the other hand you can use [PriorityFallback] to define a polyfill: a macro with a lower priority than other macros.

Here’s another example. This macro’s job is to convert a UTF-8 string into a byte array, e.g. var hello = stringToBytes("hi!") becomes var hello = new byte[] { (byte) 'h', (byte) 'i', (byte) '!' };

using System;
using System.Linq;
using System.Text;

macro stringToBytes($str) {
    var s = (string) str.Value;
    var bytes = Encoding.UTF8.GetBytes(s).Select(
        b => quote((byte) $(LNode.Literal((char) b))));
    return quote(new byte[] { $(..bytes) });
}`

The stringToBytes macro uses the quote macro to generate a syntax tree for the cast to (byte) and for the literal characters, created with the LNode.Literal method.

The macro macro must appear at the top level of the file, and it is affected by using statements above it. These using statements cannot reference namespaces from assemblies that are not already referenced by LeMP. using statements that are meant only for use at runtime must be moved down; put them after your macros.

Inside the macro body it should be possible to reference assemblies with #r in the same way as you would do in C# Interactive in Visual Studio (but, honestly, I haven’t figured out the best way to reference system assemblies or NuGet assemblies and I could use help figuring it out… the problem is that system assemblies are located in different places on different machines and operating systems, so writing portable code is a challenge. Also, as of early 2021, the official LeMP release is still using .NET 4.7, so .NET Core DLLs cannot be used. There will eventually be a transition to .NET 6.)

My TO-DO list says to write a macro-writing guide. In the meantime, please leave your questions here.

precompute, rawPrecompute

These macros evaluate an expression using the C# interactive engine. The expression is allowed to return either a primitive value, a syntax tree (Loyc tree), or a list of syntax trees. In all cases, the macro call is replaced with its result.

double Talk() { 
	precompute(new List<LNode> {
		quote(Console.WriteLine("hello")),
		quote { return $(LNode.Literal(Math.PI)); }
	});
}

precompute shares the same instance of C# Interactive as compileTime, and most of the documentation of that macro applies to this one.

rawCompute is like precompute except that macros are blocked:

  1. macros are not evaluated inside the expression (e.g. rawCompute(quote(x)) produces an error). Note: other macros such as replace could still be used to change the code before rawPrecompute sees it.
  2. if the expression returns a syntax tree, macros are not evaluated on it afterward.

Runtime variables and functions are not visible at compile-time, and variables created inside precompute disappear immediately afterward. For example, in the following code, the second call to precompute cannot access array or x, so an error occurs.

compileTime {
  var dict = new Dictionary<int,int>() { [0] = 123 };
}
var array = new[] {
    precompute(dict.TryGetValue(0, out int x)),
    precompute(x) // error CS0103: the name 'x' does not exist [...]
};

replace

replace (x => xxx) { Foo.x(); }
replace (Polo() => Marco(),
    Marco($x) => Polo($x));
if (Marco(x + y)) Polo();
// Output of LeMP
Foo.xxx();
if (Polo(x + y))
  Marco();

Finds one or more patterns in a block of code and replaces each matching expression with another expression. The braces are omitted from the output (and are not matchable). This macro can be used without braces, in which case it affects all the statements/arguments that follow it in the current statement or argument list.

The patterns can include both literal elements (e.g. 123 matches the integer literal 123 and nothing else, and F() only matches a call to F with no arguments) and “captures” like $x, which match any syntax tree and assign it to a “capture variable”. Captures can be repeated in the replacement expression (after =>) to transfer subexpressions and statements from the original expression to the new expression.

For example, above you can see that the expression Marco(x + y) matched the search expression Marco($x). The identifier Marco was matched literally. $x was associated with the expression x + y, and it was inserted into the output, Polo(x + y).

The match expression and/or the replacement expression (left and right sides of =>, respectively) can be enclosed in braces to enable statement syntax. Example:

// Limitation: can't check $w and $s are the same.
replace ({ 
  List<$T> $L2 = $L1
    .Where($w => $wc)
    .Select($s => $sc).ToList(); 
} => {
  List<$T> $L2 = new List<$T>();
  foreach (var $w in $L1) {
    if ($wc) {
      static if (!($w `code==` $s))
      	var $s = $w;
      $L2.Add($sc);
    }
  }
});

void LaterThatDay()
{
  List<Item> paidItems = 
    items.Where(it => it.IsPaid)
         .Select(it => it.SKU).ToList();
}
// Output of LeMP
void LaterThatDay()
{
  List<Item> paidItems = new List<Item>();
  foreach (var it in items) {
    if (it.IsPaid) {
      paidItems.Add(it.SKU);
    }
  }
}

The braces are otherwise ignored; for example, { 123; } really just means 123. If you actually want to match braces literally, use double braces: { { statement list; } }

You can match a sequence of zero or more expressions using syntax like $(..x) on the search side (left side of =>). For example,

replace (WL($fmt, $(..args)) => Console.WriteLine($fmt, $args));
WL(); // not matched
WL("Hello!");
WL("Hello {0}!", name);
// Output of LeMP
WL();	// not matched
Console.WriteLine("Hello!");
Console.WriteLine("Hello {0}!", name);

The alternate name replacePP(...) additionally preprocesses the match and replacement expressions, which may be useful to get around problems with macro execution order. Caution: replacePP runs the macro processor twice on the replacement expression: once at the beginning, and again on the final output.

unroll

unroll ((X, Y) in ((X, Y), (Y, X)))
{ 
	DoSomething(X, Y);
	DoSomethingElse(X, Y);
	DoSomethingMore(X, Y);
}
// Output of LeMP
DoSomething(X, Y);
DoSomethingElse(X, Y);
DoSomethingMore(X, Y);
DoSomething(Y, X);
DoSomethingElse(Y, X);
DoSomethingMore(Y, X);

Produces variations of a block of code, by replacing an identifier left of in with each of the corresponding expressions on the right of in.

The left hand side of unroll must be either a simple identifier or a tuple. The braces are not included in the output.

The right-hand side of in can be a tuple in parentheses, or a list of statements in braces, or a call to the #splice(...) pseudo-operator. If the right-hand side of in is none of these things, unroll() runs macros on the right-hand side of in in the hope that doing so will produce a list. However, note that this behavior can cause macros to be executed twice in some cases: once on the right-hand side of in, and then again on the final output of unroll. For example, the noMacros macro doesn’t work if macros run twice.

static deconstruct a.k.a. #deconstruct

  #snippet tree = 8.5 / 11;
  #deconstruct($x * $y | $x / $y = #get(tree));
  var firstNumber = $x;
  var secondNumber = $y;
// Output of LeMP
var firstNumber = 8.5;
var secondNumber = 11;

Syntax:

#deconstruct(pattern1 | pattern2 = tree);

Deconstructs the syntax tree tree into constituent parts which are assigned to compile-time syntax variables marked with $ that can be used later in the same braced block. For example, #deconstruct($a + $b = x + y + 123) creates a syntax variable called $a which expands to x + y, and another variable $b that expands to 123. These variables behave like macros in their own right that can be used later in the same braced block (although technically $ is a macro in the LeMP namespace).

The left-hand side of = can specify multiple patterns separated by |. If you want = or | themselves (or other low-precedence operators, such as &&) to be part of the pattern on the left-hand side, you should enclose the pattern in braces (note: expressions in braces must end with ; in EC#). If the pattern itself is intended to match a braced block, use double braces (e.g. { { $stuff; } }).

Macros are expanded in the right-hand side (tree) before deconstruction occurs.

If multiple arguments are provided, e.g. #deconstruct(e1 => p1, e2 => p2), it has the same effect as simply writing multiple #deconstruct commands.

An error is printed when a deconstruction operation fails.

static tryDeconstruct a.k.a. #tryDeconstruct

Same as static deconstruct, except that an error message is not printed when deconstruction fails.

static if

// Normally this variable is predefined
#set #inputFile = "Foo.cs"; 

static if (#get(#inputFile) `code==` "Foo.cs")
	WeAreInFoo();
else
	ThisIsNotFoo();

var t = static_if(true, T(), F());
// Output of LeMP

WeAreInFoo();
var t = T();

A basic “compile-time if” facility.

The static if (cond) { then; } else { otherwise; } statement or static_if(cond, then, otherwise) expression is replaced with the then clause or the otherwise clause according to whether the first argument - a boolean expression - evaluates to true or false.

The otherwise clause is optional; if it is omitted and the boolean expression evaluates to false, the entire static_if statement disappears from the output.

Currently, the condition supports only boolean math (e.g. !true || false can be evaluated but not 5 > 4). static_if is often used in conjunction with the `staticMatches` operator.

static matchCode

static matchCode (expr) { case ...: ... }

Compares an expression or statement to a list of cases at compile-time and selects a block of code at compile-time to insert into the output.

#snippet expression = apples > oranges; 
static matchCode(#get(expression)) {
  case $x > $y, $x < $y:
    Compare($x, $y);
  case $call($(..args)): 
    void $call(unroll(arg in $(..args)) { int arg; }) 
      { base.$call($args); }
  default:
    DefaultAction($#);
}
// Output of LeMP
Compare(apples, oranges);

For example, case $a + $b: expects a tree that calls + with two parameters, placed in compile-time variables called $a and $b.

If expr is a single statement inside braces, the braces are stripped. Next, macros are executed on expr to produce a new syntax tree to be matched. matchCode then scans the cases to find one that matches. Finally, the entire static matchCode construct is replaced with the handler associated with the matching case.

If none of the cases match and there is no default: case, the entire static matchCode construct and all its cases are eliminated from the output.

Use case pattern1, pattern2: to handle multiple cases with the same handler. Unlike C# switch, this statement does not expect break at the end of each case. If break is present at the end of the matching case, it is emitted literally into the output.

staticMatches operator

bool b1 = (x * y + z) `staticMatches` ($a * $b);
bool b2 = (x * y + z) `staticMatches` ($a + $b);

static if (Pie("apple") `staticMatches` Pie($x))
{
  ConfectionMode = $x;
}
// Output of LeMP
bool b1 = false;
bool b2 = true;

ConfectionMode = "apple";

syntaxTree `staticMatches` pattern returns the literal true if the form of the syntax tree on the left matches the pattern on the right.

The pattern can use $variables to match any subtree. $(..lists) (multiple statements or arguments) can be matched too. In addition, if the result is true then a syntax variable is created for each binding in the pattern other than $_. For example, Foo(123) `codeMatches` Foo($arg) sets $arg to 123; you can use $arg later in your code.

The syntax tree on the left is macro-preprocessed, but the argument on the right is not. If either side is a single statement in braces (before preprocessing), the braces are ignored.