Skip to content

Instantly share code, notes, and snippets.

@haxiomic
Last active July 18, 2017 09:04
Show Gist options
  • Save haxiomic/82af7f0c0a0f903afe45 to your computer and use it in GitHub Desktop.
Save haxiomic/82af7f0c0a0f903afe45 to your computer and use it in GitHub Desktop.
Recursive version of getSaveData macro for haxe group thread
-main Main
-cp src
-js bin/main.js
import haxe.macro.ComplexTypeTools;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.ExprTools;
import haxe.macro.Type.TInst;
import haxe.macro.TypeTools;
class ClassBuild{
//the entire contents of a class is contained in the class's fields
//a class-building-macro's job is to return an array of fields
static function saveClass():Array<Field>{
//we want to keep the currently defined fields intact, so first we get them from the context
var fields = Context.getBuildFields();
//so that you can manually override getSavedData if you want, we first check to see if there's a field with that name already
//if so, just return the fields without changing anything
for(field in fields){
if(field.name == 'getSavedData'){
return fields;
}
}
//the plan is to add a new field (which will be a function) named getSavedData
//we want to set the contents of that function to return an object containing all our @:save fields
//HOWEVER, if the class is a subclass of something that implements Savable, then we want to override the getSavedData field
//first we find the @:save fields and collect them in an array
var saveFields = new Array<Field>();
//iterate fields, look at field metas and find ones named :save
for(field in fields){
var metas = field.meta;//an array of meta objects (checkout std/haxe/macro/Expr to see the typedefs)
for(meta in metas){
switch meta.name {
case ':save':
//(we can also access any params here with meta.params)
//ok, we now know this field has @:save meta so we add it to our array
saveFields.push(field);
}
}
}
//create an array of expressions, either obj.field = field or obj.field = field.getSavedData()
//these are used on line 89 and 99 in populating our classes save data obj
var populateObjExpressions = [
for(field in saveFields){
var fieldName = field.name;
//the expression should be either obj.field = field, or obj.field = field.getSavedData() depending on if that field is also 'Savable'
//we call isFieldOfTypeSavable (defined below) to check
isFieldOfTypeSavable(field) ?
macro obj.$fieldName = $i{fieldName}.getSavedData() :
macro obj.$fieldName = $i{fieldName};
}
];
//now we want to know if our class directly implements Savable or is just a decentant of something which does
//if so, we don't need to override getSaveData, otherwise we do!
var directlyImplements:Bool;
//to understand what's going on here, read the comments in isFieldOfTypeSavable()
var savableType = ComplexTypeTools.toType(macro :Savable);
var savableClassType = TypeTools.getClass(savableType);
var localClass = Context.getLocalClass().get();
directlyImplements = false;
for(i in localClass.interfaces){
var iClassType = i.t.get();
//does interface match Savable
if(
savableClassType.name == iClassType.name &&
savableClassType.module == iClassType.module &&
savableClassType.pack.join('.') == iClassType.pack.join('.')
){
directlyImplements = true;
break;
}
}
//we generate a different version of getSavedData() depending on if we need to override a super class or not
var classObject = if(directlyImplements){
macro class SomeName{
public function getSavedData():Dynamic{
var obj:Dynamic = {};
$b{populateObjExpressions};//essentially writes out our list of expressions obj.a = a; obj.b = b; etc
return obj;
}
};
}else{
macro class SomeName{
public override function getSavedData():Dynamic{
var obj:Dynamic = super.getSavedData();//make sure we retain the behavior of the parent (and save the relevant fields)
$b{populateObjExpressions};
return obj;
}
};
}
//now we append the field in our 'classObject' to the fields of the class we're building
fields = fields.concat(classObject.fields);
//job done
return fields;
}
static function isFieldOfTypeSavable(f:Field):Bool{
//we need to test if our field is also of type 'Saveable'
//to do this we look at the field's type and see if that type extends or implements Savable
//this would be easy outside a macro right? Just do something like Std.is(f, Savable)
//HOWEVER, it's not so simple inside a macro
//our Field object 'f' isn't much more than a string that's been parsed into a structure!
//so on its own we only get the name of the type (or what ever has been written after the : )
//but not the actual type information itself
//to get the real type, we can use the Context object to find the loaded types that match that name
//then, we can compare the actual type to the 'Savable' type to see if they unify (ComplexTypeTools.toType(...))
//heres an example:
//we parse some haxe code into a series of Expr structures
var savableTypeExpression = macro :Savable;
//now we can query the context for that type by passing it the type expression
var savableType = Context.typeof( { expr: ECheckType(macro null, savableTypeExpression), pos: Context.currentPos() } );
//(there's a slightly nicer way of doing this, ComplexTypeTools.toType( expr ) does the same job! We use that later on)
//now one method of seeing if our field is a 'Savable' type is to get it's real type and search it's super classes
//checking each to see if they're called Savable. This would work, but there's a better method.
//we use TypeTools.unify, which does the same job, but it's far more robust!
//first we get the type expression from our field
switch f.kind{
case FProp(_, _, fieldTypeExpression, _):
case FVar(fieldTypeExpression, _):
var fieldType = ComplexTypeTools.toType(fieldTypeExpression);
//check to see if the type is a class-type (see std/haxe/macros/Type.hx)
switch TypeTools.follow(fieldType){//follow in case Savable is behind a typedef
case TInst(classType, _):
//now test to see if it unifies with our 'Savable' type
return TypeTools.unify(fieldType, savableType);
default:
//field isn't a class, could be something like a Float an anonymous type, or lots of other things
//see std/haxe/macros/Type.hx
}
default:
//field is a function
}
return false;
}
}
@:autoBuild(ClassBuild.saveClass())
interface Savable{
public function getSavedData():Dynamic;
}
class Main implements Savable{
@:save var gravity:Float = 9.81;
@:save var player:Player;
@:save var player2:Main.SubClassOfPlayer;
@:save var thing:SomethingThatDoesntImplementSavable;
var dontSave = 'this string';
function new(){
player = new Player();
player2 = new SubClassOfPlayer();
thing = new SomethingThatDoesntImplementSavable();
}
static function main(){
var m = new Main();
trace(m.getSavedData());
}
}
class Player implements Savable{
@:save var health:Float = 100;
var unwantedField = [
"some data" => "that we don't want to save"
];
public function new(){}
}
class SubClassOfPlayer extends Player{
@:save var ammo:Float = 999;
}
class SomethingThatDoesntImplementSavable {
var a:Float = 3;
var b:Float = 4;
public function new(){}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment