-
Notifications
You must be signed in to change notification settings - Fork 89
Defining types from inside macros
Macros can define new types, as well as add members to existing types.
There are two ways of defining types, you can either:
- define a nested type inside some other type (using
DefineNestedType
method of theTypeBuilder
) or - define a new top level type (using
Define
method of theGlobalEnv
class).
decl
. TypeBuilder
object can be obtained from a parameter
of a macro-on-declaration or from Nemerle.Macros.ImplicitCTX
().CurrentTypeBuilder
. Current GlobalEnv
is available in
Nemerle.Macros.ImplicitCTX ().Env
.
macro BuildClass ()
{
def ctx = Nemerle.Macros.ImplicitCTX ();
def builder = ctx.Env.Define (<[ decl:
internal class FooBar
{
public static SomeMethod () : void
{
System.Console.WriteLine ("Hello world");
}
}
]>);
builder.Compile ();
<[ FooBar.SomeMethod () ]>
}
This macro will add a new class inside the current namespace,
the class will be named FooBar
. The macro will return a call to
a function inside this class. That is this code:
module Some {
Main () : void
{
BuildClass ();
}
}
will print the famous message.
One gotcha here is that the following code:
module Some {
Main () : void
{
BuildClass ();
BuildClass ();
}
}
will give redefinition error instead of a second message. Macros do not guarantee hygiene of global symbols.
Another gotcha is the builder.Compile()
call. If you forget it,
then the compiler will throw ICE when the macro is used.
One important issue to remember when defining type from expression macros is to check if we are running in error mode. Compiler sometimes runs typing of method twice if it encounters some errors and it tries to produce nicer error messages in such additional pass. Unfortunately during this pass compiler expands all expression macros for the second time.
For this reason it is generally a good idea to make all expression macros purely functional (no side effects), so evaluating them several times does no harm. However, if you need to define new classes in such a macro (which is a side effect of expanding it), we provide a property IsMainPass of context, which is true if compiler is expanding macro for the first time.
You can use this property to switch off those part of macro, which should be evaluated only once:
macro Foo()
{
def ctx = Nemerle.Macros.ImplicitCTX ();
when (ctx.IsMainPass)
ctx.Env.Define (<[ decl:
public class Bar { }
]>);
}
macro BuildClass ()
{
def ctx = Nemerle.Macros.ImplicitCTX ();
when (ctx.IsMainPass)
{
def builder = ctx.Env.Define (<[ decl:
internal class FooBar
{
public static SomeMethod () : void
{
System.Console.WriteLine ("Hello world");
}
}
]>);
builder.Define (<[ decl: foo : int; ]>);
builder.Define (<[ decl:
public Foo : int
{
get { foo }
}
]>);
def nested_builder = builder.DefineNestedType (<[ decl:
internal class SomeClass { }
]>);
}
<[ FooBar () ]>
}
The GlobalEnv.Define
(as well as TypeBuilder.Compile
)
return a fresh TypeBuilder
object, that can be used to add new
members using the Define
and DefineNestedType
methods.
There is also a DefineAndReturn
method, that works much like
Define
, but returns the added member (as option [IMember]
).
As you can see, you can add new member to already built class.
To understand the CannotFinalize
stuff properly we need to talk
a bit about the internals of the compiler.
During compilation it first scans through the entire program to look for global definitions. Then there are several passes dealing with them. You can plug macros in most places of this process. Once the global iteration passes are done, the compiler proceeds with typing and code generation for each method in turn. Then the regular macros are called.
Now we want to add new types during typing. However the passes setting up various things in types have already been run.
Therefore the Define/DefineNestedType
call a few functions right
after the type is created. The most important are:
- setting the
CannotFinalize
property to true - resolving type names used in definitions
- adding default constructor if no constructor was found
- running any macros attached to the type and definitions within it
Compile()
call does a few other things:
- set
CannotFinalize
property to false - check implemented interfaces (that is, if you declared that you will implement some interface and failed to provide implementation for some methods, you will get an error here)
- add SRE declarations (this in particular means that the members of this type can be used during expression typing only after Compile() is called)
- queue compilation of methods inside the type (the compilation cannot occur just at the moment Compile() is called, because we support only one method compilation at a time, and Compile() can be called during compilation of some method)
CannotFinalize
property. If it's true, the type won't
be finalized, that is finished up. This can happen if it gets to regular typer
queues, so if you want to add members after Compile(), better set it to true.
But don't forget to set it to false before the end of the compilation,
otherwise you'll get an ICE.