Append an Expr in haxe macro context


#1

Hello all!

It’s been a while since I touched the OpenFL forums, primarily because most of the time I am able to resolve Haxe-related issues on my own. Macros is very powerful, too powerful in fact, and complex.

Currently, I am attempting to build a macro to read my story files and parsing it into a series of function calls. I have successfully generated a class that contains all of the files interpreted as a Conversation, which is a class containing update and render calls in my game engine.

The Conversation also contains functions such as overlay which takes one argument.

An example of this in the files I interpret looks like this:

~ Cultism has sought to wreak havoc upon humanity.

I would go through each file line by line and generate a function that initialises the Conversation and calls each of these procedures accordingly.


class Convos
{
    // Generated from macro:

    public static var ch1_intro:Conversation;

    public static function ch1_intro_init():Conversation
    {
        ch1_intro = new Conversation(); // This is as far as I can generate without needing help
        ch1_intro.overlay("Cultism has sought to wreak havoc upon humanity.");

        return ch1_intro;
    }
}

The code I use to generate this is as follows:

class ConvoBuilder
{

	public static macro function build():Array<Field>
	{
		var fields = Context.getBuildFields();
		var path = "D:\\Business\\IT\\Internal\\Story\\Age of Atlantis";
		
		var chapters = FileSystem.readDirectory(path);
		for (chapter in chapters)
		{
			if (FileSystem.isDirectory(path + "\\" + chapter))
			{
				var files = FileSystem.readDirectory(path + "\\" + chapter);
				for (file in files)
				{
					if (!FileSystem.isDirectory(path + "\\" + chapter + "\\" + file))
					{
						var name = chapter + "_" + file.split(" ").join("_").toLowerCase();
						fields.push({
							name: name,
							doc: null,
							meta: [],
							access: [APublic, AStatic],
							kind: FVar(macro: render.Conversation, macro null),
							pos: Context.currentPos()
						});

						var initExpr = macro {
							$i{name} = new render.Conversation();

							return $i{name};
						};

						fields.push({
							name: name + "_init",
							doc: null,
							meta: [],
							access: [APublic , AStatic],
							kind: FFun({
								ret: Context.toComplexType(Context.getType("render.Conversation")),
								params: null,
								expr: initExpr,
								args: []
							}),
							pos: Context.currentPos()
						});

					}
				}
			}
		}

		return fields;
	}

	public static macro function buildProcedures(file:String):Void
	{
		var contents = File.getContent(file);
		var lines = contents.split("\n");

		for (line in lines)
		{
			if (line == "\r")
				continue;
			
			if (line.startsWith("~"))
			{
				var text = line.substr(2);
				Context.makeExpr(macro { overlay($v{text}); }, Context.currentPos());
			}
		}
	}

}

The bit I am stuck on is appending the Expr value initExpr in the above code such that when I parse the file, I can generate the above example code. I know how to accomplish the code generation, but I am unsure what the procedures are to append an existing Expr, or inject a macro function such as the buildProcedures function below to generate an Array<Expr>

Can anyone help provide their macro expertise? Any help would be appreciated!


#2

Okay, then…

It turns out I didn’t need help after all.

Here is how I accomplished it for anyone who might find this useful:

class ConvoBuilder
{

	public static macro function build():Array<Field>
	{
		var fields = Context.getBuildFields();
		var path = "D:\\Business\\IT\\Internal\\Story\\Age of Atlantis";
		
		var chapters = FileSystem.readDirectory(path);
		for (chapter in chapters)
		{
			if (FileSystem.isDirectory(path + "\\" + chapter))
			{
				var files = FileSystem.readDirectory(path + "\\" + chapter);
				for (file in files)
				{
					var fullPath = path + "\\" + chapter + "\\" + file;
					if (!FileSystem.isDirectory(fullPath))
					{
						var name = chapter + "_" + file.split(" ").join("_").toLowerCase();
						fields.push({
							name: name,
							doc: null,
							meta: [],
							access: [APublic, AStatic],
							kind: FVar(macro: render.Conversation, macro null),
							pos: Context.currentPos()
						});

						var calls = buildProcedures(name, fullPath);

						var initExpr = macro {
							$i{name} = new render.Conversation();

							$a{calls};

							return $i{name};
						};

						fields.push({
							name: name + "_init",
							doc: null,
							meta: [],
							access: [APublic , AStatic],
							kind: FFun({
								ret: Context.toComplexType(Context.getType("render.Conversation")),
								params: null,
								expr: initExpr,
								args: []
							}),
							pos: Context.currentPos()
						});

					}
				}
			}
		}

		return fields;
	}

	public static function buildProcedures(name:String, file:String):Array<Expr>
	{
		var procedures = new Array<Expr>();
		var contents = File.getContent(file);
		var lines = contents.split("\n");

		for (line in lines)
		{
			if (line == "\r")
				continue;
			
			if (line.startsWith("~"))
			{
				var text = line.substr(2);
				procedures.push (macro { $i{name}.overlay($v{text}); });
			}
		}

		return procedures;
	}

}

#3

Yep, Array<Expr> is the way to go, but for brevity, you should replace “Context.toComplexType(Context.getType("render.Conversation"))” with “macro:render.Conversation.”


Not 100% relevant, but here’s what I do when I want to append code to a function that already exists:

//Find the current contents of init().
var initBody:Array<Expr> = null;
var initPos:Position = Context.currentPos();

for(field in fields) {
	if(field.name == "init") {
		initPos = field.pos;
		
		switch(field.kind) {
			case FFun(f):
				switch(f.expr.expr) {
					case EBlock(exprs):
						//Save all the existing expressions.
						initBody = exprs;
					default:
						//Save the single existing expression.
						initBody = [f.expr];
				}
			default:
				//Nothing to save.
		}
		
		//Remove this field so it can be replaced.
		fields.splice(fields.indexOf(field));
		
		break;
	}
}

//If there still isn't an array, make one.
if(initBody == null) {
	initBody = [];
}

//Add one or more lines of code as necessary.
for(fieldName in fieldsToInitialize) {
    initBody.push(macro $i{fieldName} = new SomethingOrOther());
}

//Save the new function.
fields.push({
	name:"init",
	pos:initPos,
	kind:FFun({
		args:[],
		ret:macro:Void,
		expr:macro $b{initBody}
	}),
	access:[APublic]
});

return fields;

With this, I can write part of my init() function in normal Haxe code, and then the macro will add the rest.