Domain Specific Languages with Boo: AST Macros
For those of you who don’t know what Boo is its a statically typed CLR language with Python like syntax that lets you extend it’s compiler, and the language itself easily by giving you access to the AST (Abstract Syntax Tree) and compiler’s context directly. This gives you very powerful tools for building your own language or Domain Specific Language or DSL from here on out. Some examples of DSL’s include rSpec and Fluent NHibernate. In fact the entire subject of what is a DSL and what types of DSL there are and how to create a proper DSL could be a book itself and a fascinating one at that.
Which is why I’ve been reading Ayende’s book DSLs in Boo: Domain Specific Languages in .NET. To make sure I understood the concepts I’ve taken to building a toy BDD DSL called bSpec, it’s got a long way to go to be something useful and I may not care to take it that far, however I did get my brain wrapped around a really cool thing called AST Macros.
AST macros give you FULL access to the compiler context and full AST of the code. I could describe this in more detail but a code sample goes light years to making this helpful.
Let’s say I want to have a “shouldFail” method that fails my spec immediately how would I do that?
macro should_fail:
#create code using Quasi Quotation ( ”[|” and ”|]” )
codeblock = [|
raise AssertionError(“failed by request”)
|] #exit macro statement
return codeblock #now replace ”should_fail” with ”raise AssertionError() ”</p>
#client code
should_fail #throws AssertionError </div> </div>
In reflector in C# the code looks like so:
So how did this happen? Well as part of Boo’s compiler pipeline it will run the macros first and replace the macro statement itself with what the macro returns. So far this doesn’t seem particularly interesting until you try and actually manipulate and work with the AST itself or get access to compiler objects. For a simple example lets look at what I did with the “describe” macro.
macro describe:
#creates a reference to the first argument
itemtodescribe = cast(ReferenceExpression, describe.Arguments[])
#using a simple trick to prevent compiler errors. I just want access to the body of ”block” so
# you can safely ignore block in my example
#note the key is using $itemtodescribe . this is how you pass in macro variables to the AST
logdescribe = [|
block:
_spechash[$itemtodescribe] = null
|].Body
yield logdescribe #this will become the first statement
yield describe.Body #the specified code block after ”describe FooBart: ” will now be placed
#Unit Test Code with asserts removed
class DescribeSpecs_When_Not_Nested:
private _spechash as Hash
private _called as List[of string]
public def constructor():
_spechash = {}
self._called = List[of string]()
describe FooBart:
_called.Add(“called from FooBart spec”)
#compiled output via reflector
class DescribeSpecs_When_Not_Nested:
private _spechash as Hash
private _called as List[of string]
public def constructor():
_spechash = {}
self._spechash[typeof(FooBart)] = null; #this is the key change
self._called = List[of string]()
So our macro has replaced code again, but this time references a field in a class that it had no prior knowledge of, yet it safely compiles. However, it would not have if there had been no field called “_spechash”. Amazingly this simple trick is only one of many ways you can extend the Boo language in a late binding fashion yet still get all the benefits of the CLR and compile time error checking. Follow my blog in the coming weeks for more of the Boo language.