On a quest for the silver bullet..

Boo AstMacros explained

In this post I am going to explain how you write your own macros in Boo. Writing macros is a powerful way to use the compiler extensibility built into Boo. Macros in Boo actually let you create your own keywords which are resolved at compile time.

Before you read on, these posts might be useful to read:

To create a Macro, you simply have to create a class that inherits from AbstractAstMacro and overrides the Expand-method. In addition your class name must end with “Macro”.

Let’s demonstrate with my DevNull-macro:

1
2
3
4
5
6
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
 
class DevNullMacro(AbstractAstMacro):
	override def Expand(macro as MacroStatement):
		pass

How do you use it? In your code (in a different assembly than where DevNullMacro is situated) you can write something like this:

1
2
3
def DoTheMath(x as int) as int:
	devNull:
		x>0

Here you send in a “Block” to the macro (”X>0″). You can actually also send arguments:

1
2
3
def DoTheMath(x as int) as int:
	devNull x,10,"Hello":
		x>0

So, a Macro can take any number of arguments, and then it takes a block (which can be as big as you like).

Now why did I call this macro DevNull? I did it to demonstrate how the macros work. The point is if you choose to do nothing in the Expand method, nothing will happen. The compiler will actually remove the macro with its arguments and its Block. Meaning that since it is only our macro that handles the code inside the block sent into the macro we can send in whatever we’d like.

This code actually compiles:

1
2
3
4
def DoTheMath(x as int) as int:
	devNull x,10,"Hello":
		x>0
		I_can_write_whatever_here_actually

Ok, so when the compiler hits a macro statement in the compiling code, it finds the macro implementation (in this case the DevNullMacro), and sends it both the arguments and the code block. The compiler then removes all that code, and continues.

(I know this might seem a little confusing. When I started learning Boo it helped quite a lot to look at the compiled code using a tool like Reflector. You should try to write this code and look at it. It will clear up a few things, believe me.)

So what is the point then? The point is that within the macro, you can add code to the code structure. You can of course add whatever you like, but normally you add something based on the input to the macro.

To demonstrate something useful, I will show you how to implement Design By Contract. I have previously demonstrated it in several talks I’ve held, but then I have implemented it as AstAttributes like this:

1
2
3
4
5
6
7
8
class Account:	
	public Balance as int
 
	[Require(amount>=0)]
	[Ensure(Balance == originalBalance + amount)]
	def Deposit(amount as int):
		originalBalance = Balance
		Balance += amount

I will not go into details about this, but I’m pretty sure you will manage to do it yourself after reading my post about AstAttributes.

Now I want to implement Design By Contract as macros, in the same way it is implemented in Eiffel, the language that introduced it. I want to write my assertions like this:

1
2
3
4
5
6
7
8
9
10
11
12
class Account:
 
	public Balance as int
 
	def Deposit(amount as int):
		requires:
			amount>=0
		body:
			originalBalance = Balance
			Balance += amount
		ensures:
			Balance == originalBalance + amount

We have three macros here: requires, body, and ensures. I’m going to show you in detail how to implement the requires-part of the code, then I’ll add the source for both body and ensures as well. When we hit the requires macro in the code, we’re going to replace it with some actual code that forces those requirements. So, where we find this code in our Deposit-method,

1
2
requires:
	amount>=0

we want, at compile time, to put something like this to the output assembly:

1
2
if not (amount>=0):
	raise Exception("Some suitable error message")

To add code where the macro statement is situated, we simply return a Statement from our Expand-method. What we want to make is a Block (which is a derived type of Statement):

1
2
3
override def Expand(macro as MacroStatement) as Statement:
	blockToReturn = Block()
	return blockToReturn

Still nothing happens, since we’re just returning an empty Block. We have to fill the return block with something. So for each assertion (which turns out as Statements) in our code, we want to assert it. Let’s loop all our statements:

1
2
3
4
5
6
7
8
9
override def Expand(macro as MacroStatement) as Statement:
	blockToReturn = Block()
 
	for statement in macro.Block.Statements:
		expression as BinaryExpression = TryMakeBinaryExpression(statement)
		blockToReturn.Add(
			CreateAssertblockFromBinaryExpression(expression))
 
	return blockToReturn

In addition to the looping, which is pretty straight forward, we’re doing two new things here. We’re trying to turn every statement into a BinaryExpression, and we’re creating blocks for each BinaryExpression.

Let’s look at our TryMakeBinaryExpression first. The point is that if we want to assert something, we have to make sure they resolve to either true or false. First we need all the Statements to be ExpressionStatements. If you for instance had written an if-statement in you macro like this:

1
2
3
requires:
	if 1==1:
		do_something

This will not be an ExpressionStatement, it is an IfStatement (you should look into the Boo class-hierarchy, use Visual studio or Reflector).

So, when we have assured us it is all ExpressionStatements we have to deal with, we also want to ensure they are all BinaryExpressions. They all have to resolve into either true or false. Both of these checks are run here:

1
2
3
4
5
6
7
8
9
10
def TryMakeBinaryExpression(statement as Statement) as BinaryExpression:
	expression = statement as ExpressionStatement 
	if expression is null:
		RaiseNotBinaryException(statement.ToString())
 
	binaryexpression = expression.Expression as BinaryExpression 
	if binaryexpression is null:
		RaiseNotBinaryException(expression.Expression.ToString())
 
	return binaryexpression

Ok, now we either have a BinaryExpression or it has already failed. Let’s make it into a block:

1
2
3
4
5
6
7
def CreateAssertblockFromBinaryExpression(expression as BinaryExpression) as Block:
	exceptionText = "Voilated precondition: " + expression.ToString()
	return [|
			block:
				if not $(expression):
					raise Exception($exceptionText)
		|].Block

Here we’re using quasiquotations again. It’s understandable and easy to both read and write.

And that’s the complete macro. So now for every BinaryExpression written, the macro will assert its correctness. Here’s the complete code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import System
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
 
class RequiresMacro(AbstractAstMacro):
 
	override def Expand(macro as MacroStatement) as Statement:
		blockToReturn = Block()
		for statement in macro.Block.Statements:
			expression as BinaryExpression = TryMakeBinaryExpression(statement)
			blockToReturn.Add(
				CreateAssertblockFromBinaryExpression(expression))
		return blockToReturn
 
	def CreateAssertblockFromBinaryExpression(expression as BinaryExpression) as Block:
		exceptionText = "Voilated precondition: " + expression.ToString()
		return [|
				block:
					if not $(expression):
						raise Exception($exceptionText)
			|].Block
 
	def TryMakeBinaryExpression(statement as Statement) as BinaryExpression:
		expression = statement as ExpressionStatement 
		if expression is null:
			RaiseNotBinaryException(statement.ToString())
 
		binaryexpression = expression.Expression as BinaryExpression 
		if binaryexpression is null:
			RaiseNotBinaryException(expression.Expression.ToString())
 
		return binaryexpression
 
	def RaiseNotBinaryException(typeInfo as String):
		raise Exception("Not a binary expression: " + typeInfo)

The other two macros, BodyMacro and EnsuresMacro are listed here:

1
2
3
4
5
6
7
8
import System
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
 
class BodyMacro(AbstractAstMacro):
 
	override def Expand(macro as MacroStatement) as Statement:
		return macro.Block

Here we only need to return the actual code written within the macro.

And here’s the EnsuresMacro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import System
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
 
class EnsuresMacro(AbstractAstMacro):
 
	override def Expand(macro as MacroStatement):
		ancestor as Method = macro.GetAncestor(NodeType.Method)
 
		oldBody as Block = ancestor.Body
		ensuresBody as Block = RequiresMacro().Expand(macro)
		newBody = [|
			block:
				try:
					$oldBody
				ensure:
					$ensuresBody
		|].Block
 
		ancestor.Body = newBody

Here we take all the code in the method containing the macro, and surrounds it with a try-clause to ensure that we always run the ensures-part of the code. Notice also that we’re using the RequireMacro, since it creates the assert-block exactly the way we want it.

A helping hand:
When you explore this, I actually advice you to write some of your macro code in C#, as it gives you a lot more intellisense-help, then when you have found out what you want, write them back in Boo, as it is often much more readable, especially with the nice quasiquotation which you do not have in C#.

Hope it helps!

- Tore Vestues

December 22nd, 2008 at 23:15 (010)


3 Responses to “Boo AstMacros explained”

  1. Wyatt Says:

    I have enjoyed your articles, and would like to try these out for myself. However I get a compiler error:

    Unexpected token: |. (BCE0043) – …\RequiresMacro.boo:19,17

    Referring to the quasiquotation. Any ideas as to what is wrong.

  2. Tore Vestues Says:

    @Wyatt:

    If you post the method I can take a look at it.

    Remember that Boo is Indent-sensitive, it might have something to do with that.

  3. Wyatt Says:

    Sorry, I assumed I was using the latest version of SharpDevelop. Upgrading to the 3 Beta did the trick.

    Your articles really help explain what is going on. But I find myself wanting to more about how the quasiquotation works. I hope you write more about that in the future.

    Thanks again.

Leave a Reply