Skip to content

Instantly share code, notes, and snippets.

@Simn
Forked from anonymous/FixPropertyReferences.hx
Last active September 6, 2019 10:58
Show Gist options
  • Save Simn/b00d7ffcfaae9cd8840d to your computer and use it in GitHub Desktop.
Save Simn/b00d7ffcfaae9cd8840d to your computer and use it in GitHub Desktop.
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Printer;
import haxe.macro.Type;
using haxe.macro.Tools;
// The original code used interfaces to define properties, and some interfaces
// had the same property name with different types. These interfaces are then
// implemented by classes that provide the implementation of those properties.
//
// The problem is that using interfaces is costly, and we have some techniques
// (not described here) that allow us to, if interfaces are not used, reduce
// the size of our binary drastically.
//
// So we have a technique whereby we declare a single "Interfaces" base class
// that "implements" all of the interface properties, and that base class is
// inherited by all other classes. Now all of those classes have access to
// a union of all of the interfaces's properties.
//
// The problem is that when the Interfaces base class is defined, there are
// some properties that have the same name but different type, and thus they
// collide.
//
// The goal is to use macros to allow those properties to be renamed using a
// type-specific name in the Interfaces base class, but have references to the
// properties using their 'original' names to replaced with a reference to the
// new name.
class FixPropertyReferences
{
macro static function process() : Array<Field>
{
var fields = Context.getBuildFields();
for (field in fields) {
processField(field);
}
return fields;
}
#if macro
static function processField(field : Field)
{
switch (field.kind) {
case FFun(f) if (f.expr != null):
f.expr = processExpr(f.expr);
case _:
}
}
static function processExpr(e : Expr)
{
return switch (e) {
case macro $e.$name:
macro FixPropertyReferences.replace($e, $v{name});
case { expr: EBinop(op, macro $e1.$name, e2) }:
var e2 = processExpr(e2);
var e = {
expr: EBinop(op, e1, e2),
pos: e.pos
}
macro FixPropertyReferences.replaceBinop($e1, $v{name}, $e);
case _:
e.map(processExpr);
}
}
static function doReplace(e : Expr, name : String) {
if (name != "property") {
var e = macro $e.$name;
return e;
}
var typeName : String = null;
switch (e.expr) {
case EConst(c):
switch (c) {
case CIdent(s):
typeName = s;
default:
}
default:
}
if (typeName == null) {
return macro @pos(e.pos) $e.$name;
}
if (typeName == "classA") {
return macro $e.intProperty;
}
else if (typeName == "classB") {
return macro $e.stringProperty;
}
return macro @pos(e.pos) $e.$name;
}
#end
macro static public function replace(e : Expr, name : String)
{
var e = doReplace(e, name);
return e;
}
macro static public function replaceBinop(e : Expr, name : String, eOp : Expr) {
return switch (eOp.expr) {
case EBinop(op, _, e2):
var e1 = doReplace(e, name);
{
expr: EBinop(op, e1, e2),
pos: eOp.pos
}
case _:
throw "should never happen";
}
return macro null;
}
}
#if USE_INTERFACES
// Define USE_INTERFACES when building to use the interfaces-based version.
// Everything works normally and sanely.
interface InterfaceA
{
public var property(get, set) : Int;
}
interface InterfaceB
{
public var property(get, set) : String;
}
#else
// If USE_INTERFACES is *not* refined, then the "interfaces" become a
// base class with a union of all properties declared by the interfaces,
// and the original interface names just become typedefs of this base class.
// To avoid collisions with the property name, the properties are re-named to
// include some type info to keep them separate.
// The goal is to use macros to allow anyone to refer to the 'property'
// property of the sub-classes of Interfaces, and to get either intProperty or
// stringProperty, depending upon which subclass is involved.
class Interfaces
{
public function new()
{
mIntProperty = 10;
mStringProperty = "ten";
}
public var intProperty(get, set) : Int;
public var stringProperty(get, set) : String;
private function get_intProperty() : Int
{
return mIntProperty;
}
private function set_intProperty(v : Int) : Int
{
mIntProperty = v;
return v;
}
private function get_stringProperty() : String
{
return mStringProperty;
}
private function set_stringProperty(v : String) : String
{
mStringProperty = v;
return v;
}
private var mIntProperty : Int;
private var mStringProperty : String;
}
typedef InterfaceA = Interfaces;
typedef InterfaceB = Interfaces;
#end
#if USE_INTERFACES
// If USE_INTERFACES is set, then interfaces are used and there are no
// name collisions. Simple.
class ClassA implements InterfaceA
{
public function new()
{
mProperty = 10;
}
public var property(get, set) : Int;
private function get_property() : Int
{
return mProperty;
}
private function set_property(v : Int) : Int
{
mProperty = v;
return v;
}
private var mProperty : Int;
}
class ClassB implements InterfaceB
{
public function new()
{
mProperty = "ten";
}
public var property(get, set) : String;
private function get_property() : String
{
return mProperty;
}
private function set_property(v : String) : String
{
mProperty = v;
return v;
}
private var mProperty : String;
}
#else
// If NO_INTERFACES is not set, then the implementation classes don't need to
// have anything, it's all in the base class
class ClassA extends Interfaces
{
public function new()
{
super();
}
}
class ClassB extends Interfaces
{
public function new()
{
super();
}
}
#end
#if !USE_INTERFACES
@:build(FixPropertyReferences.process())
#end
class MacroTest
{
public static function main()
{
var classA = new ClassA();
var classB = new ClassB();
classA.intProperty += 10;
//classB.property = "twenty";
// The following will succeed if USE_INTERFACES is set. But if
// USE_INTERFACES is not set, then the compiler will give an error
// because there is no such property 'property' in either classA or
// classB.
// The goal is to use macros so that:
// classA.property turns into classA.intProperty
// classB.property turns into classB.stringProperty
Sys.println("classA.property is " + classA.property);
Sys.println("classB.property is " + classB.property);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment