Skip to content

Instantly share code, notes, and snippets.

@Zshazz

Zshazz/A.hx

Last active Aug 29, 2015
Embed
What would you like to do?
My first build macro in Haxe plus example usage
package;
@:enum
abstract StateID(Int) to Int
{
var one = 0;
var two = 1;
var three = 2;
// must always have last number
var length = 3;
}
class State {
public var transition: Void -> State = function() { return this; }
public var exec = function() { }
}
@:build(SimpleFSMTransform.apply(StateID))
class A {
@:states
//var states: haxe.ds.Vector<State>;
var states: Array<State>;
@:injectStatesInit
public function new() {
var currentState: State = states[StateID.one];
for(i in 0...10) {
currentState.exec();
currentState = currentState.transition();
}
}
// Transition table
// 1 -> 2
// 2 -> 3
// 3 -> 2
@:stateFunc(one, transition)
function oneTrans()
return states[StateID.two];
@:stateFunc(two, transition)
function twoTrans()
return states[StateID.three];
@:stateFunc(three, transition)
function threeTrans()
return states[StateID.two];
// State actions
@:stateFunc(one, exec)
function oneExec()
trace("one!");
@:stateFunc(two, exec)
function twoExec()
trace("two!");
@:stateFunc(three, exec)
function threeExec()
trace("three!");
}
package;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
using tink.MacroApi;
#end
/**
* A build macro that transforms a class by automatically binding marked
* functions to states stored in an Array or Vector.
*
* This basically bolts on a very rudimentary FSM controlled behavior system
* to a class with little hassle. Very helpful for things such as games.
*
* @author Chris Cain
*/
class SimpleFSMTransform
{
static var storageTag = ":states";
static var initInjectTag = ":injectStatesInit";
static var stateFnTag = ":stateFunc";
macro static public function apply(stateEnum: Expr): Array<Field>
{
// This will check to make sure stateEnum is an identifier...
stateEnum.pos.getOutcome(stateEnum.getIdent());
var fields = Context.getBuildFields();
var statesField: Member = findUnique(storageTag, fields);
var statesName = statesField.name;
var statesType: TypePath;
var stateType: TypePath;
// initializes statesType and stateType and assures we can handle them properly.
switch(statesField.kind)
{
case FVar(TPath(p), _) if(p.name == "Array" || p.name == "Vector"):
statesType = p;
default:
Context.fatalError('Field annotated with ${storageTag} must ' +
'be a variable with a defined type of either Array or Vector.',
statesField.pos);
}
switch(statesType.params[0])
{
case TPType(TPath(p)):
stateType = p;
default:
Context.fatalError('Field annotated with ${storageTag} should ' +
'be Array<StateType> or Vector<StateType>, not ${statesType}',
statesField.pos);
}
// Inspects injectField to make sure it's a function and extracts
// the part we'll modify later.
var injectField: Member = findUnique(initInjectTag, fields);
var targetInject = injectField.pos.getOutcome(injectField.getFunction());
var blockContents: Array<Expr> = new Array();
// Creates a helpful "StateID has no field length" error message
// Technically, this is done automatically, but this makes sure
// the first error indicates the correct position and makes sure
// it's extra clear.
{
var stateEnumLenTest: Expr = macro $stateEnum.length;
stateEnumLenTest.pos = stateEnum.pos;
blockContents.push(stateEnumLenTest);
}
// ######
// Create all the lines necessary to populate the states field
// with all of the tagged state functions
// new syntax varies between Array and Vector
blockContents.push(
if(statesType.name == "Array")
macro $i{statesName} = new $statesType()
else // Vector
macro $i{statesName} = new $statesType($stateEnum.length)
);
// The two types must be initialized differently because
// Array has no "new" that takes length (thus has no elements
// at first) but Vector requires a length.
var init: Expr =
if(statesType.name == "Array")
macro $i{statesName}.push(new $stateType());
else // Vector
macro $i{statesName}[i] = new $stateType();
blockContents.push(macro
for(i in 0 ... $stateEnum.length)
$init
);
var stateFnMap: Map<String, Field> = new Map();
for(field in fields) for(datum in field.meta) if(datum.name == stateFnTag)
{
if(datum.params.length != 2)
Context.fatalError('${field.name}\'s @${stateFnTag}(stateName, ' +
'functionName) must have 2 parameters, not ${datum.params.length}',
datum.pos);
var stateTarget: String = switch(datum.params[0].expr) {
case EConst(CIdent(sn)) | EConst(CString(sn)):
sn;
default:
Context.fatalError('Identifier or String expected, not ' +
'${datum.params[0].toString()}', datum.pos);
"";
};
var functTarget: String = switch(datum.params[1].expr) {
case EConst(CIdent(fn)) | EConst(CString(fn)):
fn;
default:
Context.fatalError('Identifier or String expected, not ' +
'${datum.params[1].toString()}', datum.pos);
"";
};
// Make sure there's a unique mapping (so that we don't silently
// overwrite defined state-function combinations)
var mappingStr = '$stateTarget,$functTarget';
blockContents.push(
if (stateFnMap.exists(mappingStr)) {
var existing = stateFnMap.get(mappingStr);
field.pos.errorExpr('${field.name} duplicates ' +
'@$stateFnTag($mappingStr) of a previously seen field:\n' +
'${existing.name} ${existing.pos}');
}
else {
stateFnMap.set(mappingStr, field);
var stateEnumTargetExpr = stateEnum.field(stateTarget);
macro $i{statesName}[$stateEnumTargetExpr].$functTarget = $i{field.name}
});
}
// ######
// Append the lines of the target function to our newly generated body
switch (targetInject.expr.expr) {
case EBlock(oldContents):
for(expr in oldContents)
blockContents.push(expr);
default:
// When the user has a function without {}s
blockContents.push(targetInject.expr);
};
// swap out the old body with the new
targetInject.expr = EBlock(blockContents).at(targetInject.expr.pos);
return fields;
}
#if macro
// A little helper function to find a unique metadata tag and make sure
// that it's unique, displaying an error to the user otherwise.
static function findUnique(targetTag: String, fields: Array<Field>): Field
{
var resultArray: Array<Field> = new Array();
for(field in fields) {
for(entry in field.meta) {
if(entry.name == targetTag) {
resultArray.push(field);
}
}
}
if(resultArray.length == 0)
{
Context.fatalError('A field annotated with @${targetTag} is necessary.',
Context.currentPos());
}
else if(resultArray.length > 1)
{
Context.warning('Too many fields annotated with @${targetTag} found.', Context.currentPos());
for (field in resultArray)
{
Context.warning('${field.name} annotated with @${targetTag}', field.pos);
}
Context.fatalError('Fatal Error: Suggested action is to only annotate 1 field with @${targetTag}',
Context.currentPos());
}
return resultArray[0];
}
#end
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment