Prexonite Script

a .NET hosted scripting language with a focus on meta-programming and embedded DSLs

View the Project on GitHub SealedSun/prx

Prexonite Macro Support

Posted on 2010-10-11

Over a year ago I considered the implementation of meta programming facilities in Prexonite Script. In that post, I concluded that advanced meta-programming features like  quotation and splicing were not realistic for a compiler architecture as convoluted as Prexonite.

I, however, think that the implementation of macro functions is feasible and useful. Macros won’t revolutionise how the language is used, but will complement the existing dynamic features to enable concise solutions, should the programmer decide to invest enough time into the macro system.

And that’s just what I did. Prexonite now supports a new keyword `macro` that can be used to define macro functions. Macro functions are invoked at compile time (more precisely just before code generation) with the AST nodes of their arguments passed as parameters. They’re expected to return a single AST node that is then inserted into the final AST in place of the macro function call.

The mechanism is already in use: The implementation of the `struct` mechanism has been ported to the macro system.

Detour: struct

Prexonite Script does not provide any mechanism for users to define their own data types (classes, structures, records). Instead there is a special type of object that you can extend with new fields and methods: The `Structure`. However, using this object to create custom structures/objects is quite verbose and cumbersome. Fortunately there is a PSR file `psr\struct.pxs` that contains the handy function of the same name. `struct` creates an empty structure and adds all nested functions of the calling function to it. This comes quite close to JavaScript-prototype-definition level of verbosity.

build does require(@"psrstruct.pxs");
function create_foo(x){
  function report() {
    println(x);
  }
  function increment(this) {
    x++;
    this.report();
    return x;
  }
  return struct;
}

function main(){
  var foo1 = new foo(5); 
  //fancy syntax for `create_foo(5)`
  var foo2 = create_foo(foo1.increment()); 
  //prints 6
  foo2.report; 
  //prints 6 too
}

The function `create_foo` (you can call it a constructor if you like) creates a new `foo` object every time it is invoked. The resulting object has two methods `report` and `increment`. The variable `x` is shared by all methods via the nested function/closure mechanism. It is not formally part of the foo object1.

Macro functions

The implementation of the struct function is actually quite simple. It makes heavy use of helper functions defined in `psr\macro.pxs`. All of the `ast<something>` functions plus `tempalloc` and `tempfree` are defined in that file.

//part of `macro struct()` 
//  in `psr\struct.pxs`
//creates a new block expression node and allocates a local
//  variable for the structure object.
var block = ast("BlockExpression");

var structV = tempalloc;

//create the structure object and assign it to
//  the local variable
var assignStructure = astlvar(SI.set, structV);
assignStructure.Arguments.Add(ast\new("Structure"));
block.Add(assignStructure);
//don't forget to add the statement to the block
//assign the "ctorId" (name of the constructor function) 
//  to the structure
var assignCtorId = astmember(ast\lvar(SI.get, structV), 
                               SI.get,@"");
assignCtorId.Arguments.Add(ast\const(CTORID));
assignCtorId.Arguments.Add(ast\const(parentId));
block.Add(assignCtorId);
//Add all of the methods to the struct
foreach(var method in methods){
    var addMethod = ast\member(ast\lvar(SI.get, structV), 
                                 SI.get, "\\");
    addMethod.Arguments.Add(ast\const(method.Key));
    addMethod.Arguments.Add(ast\lvar(SI.get, method.Value));
    block.Add(addMethod);
}
//return the constructed structure 
objectblock.Expression = astlvar(SI.get, structV);
//Don't forget to free our temporary variable and
//  actually return the code 
blocktempfree(structV);
return block;

The macro code is not that difficult to understand. So writing macros in Prexonite Script is easy right? Not exactly, no. You’re directly operating within the compilers AST, an API that was never designed to be consumed by user code. That `ast(”BlockExpression”)` creates an object of type `Prexonite.Compiler.Ast.AstBlockExpression` and the `Arguments` member of the `assignCtorId` node is the same member that the compiler uses. Why is that a bad thing? Well for one it creates a very strong dependency on an implementation detail of the Prexonite Script compiler, and it relies heavily on good IDEs, that means the API is ugly (often many parameters) and irregular (I myself have to constantly lookup the ever changing parameter ordering and exact member names). In other words: You won’t get far without a copy of the compiler source code next to your Prexonite Script code editor.

Ok, so its a bit tricky to use, anything else I should know?

Yes.

Footnotes

  1.  

    In 8/10 cases, this is a good thing. The variable is almost completely inaccessible from the outside, essentially enforcing information hiding. Not even reflection over the member functions of a struct is not 100% reliable: while a shared variable called `x` could refer to the parameter variable `x`, it could just as well refer to a variable captured from a different context.

    The only scenario where this treatment of "private" variables is not transparent is cloning. If you attempted to clone a struct, you'd get a distinct Structure object, yes, but it would share all its internal state with the original struct object. This is because a shallow copy of the struct would only copy the member functions, which are really closures. The variables shared by these closures are not duplicated. This is, again, because there is no distinction between "private" variables and other captured variables.

    So all in all the struct solution is not ideal as automatic cloning can be quite useful, especially for a dynamic language. Therefore should Prexonite ever get a more sophisticated user data structure feature (or, gasp classes), it would probably not follow the struct approach.