-
-
Save Zshazz/64cd3e9609f9ef92d398 to your computer and use it in GitHub Desktop.
My first build macro in Haxe plus example usage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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