Skip to content

Defining types from inside macros

Alex Zimin edited this page Jul 11, 2011 · 3 revisions

Table of Contents

Basics

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 the TypeBuilder) or
  • define a new top level type (using Define method of the GlobalEnv class).
Both methods take the same argument -- just a single quotation of the type 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.

Error mode

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 { }
    ]>);
}

Longer example

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.

Stages of TypeBuilder

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
Next the 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)
Now about the 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.
Clone this wiki locally