Skip to content

Instantly share code, notes, and snippets.

@boozook
Forked from nadako/Main.hx
Last active November 17, 2015 19:59
Show Gist options
  • Save boozook/bf62356e751ea7e60483 to your computer and use it in GitHub Desktop.
Save boozook/bf62356e751ea7e60483 to your computer and use it in GitHub Desktop.
Signal builder using new Rest type parameter in Haxe
package hx.event.test;
import haxe.unit.TestRunner;
class Main
{
public static function main()
{
var runner = new TestRunner();
runner.add(new SignalTest());
runner.run();
#if sys
Sys.exit(runner.result.success ? 0 : 1);
#end
}
}
package hx.event;
import haxe.extern.Rest;
import haxe.Constraints;
@:genericBuild(hx.event.SignalMacro.build())
class Signal<Rest> extends Base<Rest -> Void>
{
public function dispatch(rest:Rest):Void {}
}
typedef Fugnal<T:Function> = Base<T>;
/* @:genericBuild(hx.event.SignalMacro.build())
class Fugnal<T:Function> extends Base<T>
{ public var dispatch(default, never):T; } */
typedef Opt<T> = T;
class Base<T:Function>
{
//----------- properties, fields ------------//
var head:Slot<T>;
var tail:Slot<T>;
var toAddHead:Slot<T>;
var toAddTail:Slot<T>;
var dispatching:Bool;
//--------------- constructor ---------------//
public function new()
{
dispatching = false;
}
//--------------- initialize ----------------//
public function dispose(slots:Bool = true):Void
{
if(slots)
{
end();
var slot = head;
while(slot != null)
{
slot.dispose();
slot = slot.next;
}
for(s in [head, tail, toAddHead, toAddTail])
if(s != null)
s.dispose();
}
head = tail = toAddHead = toAddTail = null;
}
//---------------- control ------------------//
public function add(listener:T, ?once:Bool):Slot<T>
{
var slot = new Slot(this, listener, once);
if(dispatching)
{
if(toAddHead == null)
toAddHead = toAddTail = slot;
else
{
toAddTail.next = slot;
slot.previous = toAddTail;
toAddTail = slot;
}
}
else
{
if(head == null)
head = tail = slot;
else
{
tail.next = slot;
slot.previous = tail;
tail = slot;
}
}
return slot;
}
/** Look like a stopPropagation **/
public function stop():Void if(dispatching) end();
// TODO: public EitherType<Slot, T:Function>
public function remove(slot:Slot<T>):Void
{
if(head == slot)
head = head.next;
if(tail == slot)
tail = tail.previous;
if(toAddHead == slot)
toAddHead = toAddHead.next;
if(toAddTail == slot)
toAddTail = toAddTail.previous;
if(slot.previous != null)
slot.previous.next = slot.next;
if(slot.next != null)
slot.next.previous = slot.previous;
}
inline function start():Void
{
dispatching = true;
}
function end():Void
{
dispatching = false;
if(toAddHead != null)
{
if(head == null)
{
head = toAddHead;
tail = toAddTail;
}
else
{
tail.next = toAddHead;
toAddHead.previous = tail;
tail = toAddTail;
}
toAddHead = toAddTail = null;
}
}
//----------- handlers, callbacks -----------//
//--------------- accessors -----------------//
}
@:allow(hx.event.Base)
@:access(hx.event.Base)
class Slot<T:Function>
{
var signal:Base<T>;
var listener:T;
var once:Bool;
var previous:Slot<T>;
var next:Slot<T>;
public var disposed(get_disposed, never):Bool;
function new(signal:Base<T>, listener:T, once:Bool)
{
this.signal = signal;
this.listener = listener;
this.once = once;
}
//--------------- initialize ----------------//
public function dispose():Void
{
if(signal != null)
{
signal.remove(this);
signal = null;
}
listener = null;
}
//--------------- accessors -----------------//
public function get_disposed():Bool
return listener == null && signal == null;
}
package hx.event;
import haxe.macro.Compiler;
import hx.event.Signal.Opt;
import haxe.macro.Printer;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
using haxe.macro.Tools;
class SignalMacro
{
//----------- properties, fields ------------//
static var signalTypes = new Map<Int, Map<String, Bool>>();
static inline var NAME_SIGNAL = "Signal";
static inline var NAME_FUGNAL = "Fugnal";
static inline var SUPER = "Base";
static inline var METHOD = "dispatch";
static var PACK(get, never):Array<String>;
//---------------- control ------------------//
static function build():ComplexType
{
return switch(Context.getLocalType())
{
case TInst(_.get() => {name: NAME_SIGNAL}, params):
buildSignalClass(params, NAME_SIGNAL);
case Type.TInst(_.get() => {name: NAME_FUGNAL}, params):
{
var args:Array<{name:String, opt:Bool, t:Type}> = switch(params[0])
{
case Type.TFun(args, ret): args;
default: [{t:Type.TMono({get:function()return null, toString:function() return "TMono(Null)"}), opt:false, name:"momo"}];
}
buildFugnalClass(args);
}
default:
throw Context.error("Wrong type. SignalMacro.build can build only Signal generic class.", Context.currentPos());
}
}
static function buildFugnalClass(args:Array<{name:String, opt:Bool, t:Type}>):ComplexType
{
var params:Array<Type> = [];
for(i in 0...args.length)
{
var t = args[i].t;
if(args[i].opt)
t = wrapToOptional(t);
params[i] = t;
}
var result = buildSignalClass(params, NAME_FUGNAL);
switch(result)
{
case ComplexType.TPath(tp):
{
tp.sub = tp.name;
tp.name = NAME_SIGNAL;
}
default:
}
return result;
}
static function buildSignalClass(params:Array<Type>, baseName:String):ComplexType
{
function filter(type:Type):Bool return switch(type.follow())
{
case Type.TAbstract(_.get() => {name: "Void"}, _): false;
default: true;
};
params = params.filter(filter);
var numParams = params.length;
var optParams = isOptionalTypes(params);
var optParamsID = followOptionalTypes(params);
var name = baseName + numParams + optParamsID;
if(!signalTypes.exists(numParams) || !(signalTypes.get(numParams):Map<String, Bool>).exists(name))
{
var typeParams:Array<TypeParamDecl> = [];
var superClassFunctionArgs:Array<ComplexType> = [];
var dispatchArgs:Array<FunctionArg> = [];
var listenerCallParams:Array<Expr> = [];
for(i in 0...numParams)
{
var opt = optParams[i];
var argT:ComplexType = TPath({name: 'T$i', pack: []});
// additional wrap into `Null<>`
//if(opt) argT = TPath({pack: [], name: "Null", params: [TypeParam.TPType(argT)]});
typeParams.push({name: 'T$i'});
superClassFunctionArgs.push(opt ? TOptional(argT) : argT);
dispatchArgs.push({name: 'arg$i', type: argT, opt:opt});
listenerCallParams.push(macro $i{'arg$i'});
}
var pos = Context.currentPos();
Context.defineType(
{
pack: PACK,
name: name,
pos: pos,
params: typeParams,
kind: TDClass(
{
pack: PACK,
name: baseName,
sub: SUPER,
params: [TPType(TFunction(superClassFunctionArgs, macro :Void))]
}),
fields: [
{
name: METHOD,
access: [APublic],
pos: pos,
kind: FFun(
{
args: dispatchArgs,
ret: macro :Void,
expr: macro
{
start();
var slot = head;
while(dispatching && slot != null)
{
slot.listener($a{listenerCallParams});
if(slot.once)
slot.dispose();
slot = slot.next;
}
end();
}
})
}
]
});
var map = signalTypes.get(numParams);
if(map == null)
signalTypes.set(numParams, (map = new Map()));
map.set(name, true);
}
return ComplexType.TPath({pack: PACK, name: name, params: [for(t in params) TPType(t.toComplexType())]});
}
static inline function isOptional(type:Type):Bool
{
return switch(type)
{
case Type.TType(_.get() => {name: "Null"}, params):
#if NullIsNotOptional false; #else true; #end
case Type.TType(_.get() => {name: "Opt"}, params):
true;
default:
false;
}
}
static function isOptionalTypes(types:Array<Type>):Array<Bool>
{
return [for(i in 0...types.length) isOptional(types[i])];
}
static function followOptionalTypes(types:Array<Type>):OrderingID
{
var opts = 0;
var result = "";
for(i in 0...types.length)
{
var isOpt = isOptional(types[i]);
result += isOpt ? "o" : "r";
while(isOptional(types[i]))
types[i] = types[i].follow(true);
opts++;
}
return opts > 0 ? "_" + result : "";
}
static inline function wrapToOptional(type:Type):Type
{
var opt:Type = Context.getType("hx.event.Opt");
return switch(opt)
{
case Type.TType(_.get() => {name: "Opt"}, params):
params[0] = type;
opt;
default: type;
}
}
//--------------- accessors -----------------//
static inline function get_PACK() return ["hx", "event"];
}
private typedef OrderingID = String;
package hx.event.test;
import haxe.io.Bytes;
import hx.event.Signal;
import hx.event.Signal.Fugnal;
import haxe.extern.EitherType;
import haxe.unit.TestCase;
class SignalTest extends TestCase
{
//----------- properties, fields ------------//
//--------------- constructor ---------------//
public function new()
{
super();
}
//----------------- tests -------------------//
public function testSignal():Void
{
var signal = new Signal<Int, String>();
var data:{a:Int, b:String};
var conn = signal.add(function(a, b) data = {a:a, b:b} );
signal.dispatch(42, "test");
assertTrue(data != null);
assertEquals(42, data.a);
assertEquals("test", data.b);
signal.dispatch(1, "");
assertTrue(data != null);
assertEquals(1, data.a);
assertEquals(0, data.b.length);
}
public function testSignalOnce():Void
{
var signal = new Signal<Int, String>();
var data:{a:Int, b:String};
var conn = signal.add(function(a, b) data = {a:a, b:b} );
signal.dispatch(42, "test");
assertTrue(data != null);
assertEquals(42, data.a);
assertEquals("test", data.b);
signal.dispatch(1, "");
assertTrue(data != null);
assertEquals(1, data.a);
assertEquals(0, data.b.length);
}
public function testSignalOption():Void
{
//var signal = new Signal<Int, Null<String>>();
var signal = new Signal<Int, Opt<Bytes>>();
var data:{a:Int, b:Bytes};
var conn = signal.add(function(a, ?b) data = {a:a, b:b} );
signal.dispatch(42, Bytes.alloc(42));
assertTrue(data != null);
assertEquals(42, data.a);
assertEquals(42, data.b.length);
signal.dispatch(1, null);
assertTrue(data != null);
assertEquals(1, data.a);
assertEquals(null, data.b);
// assertEquals(0, data.b.length);
}
public function testSignalTyping():Void
{
var signalA:Signal = new Signal();
var signalB:Signal<Void> = new Signal();
var signalC = new Signal<Opt<Null<String>>>();
var signalD = new Signal<Opt<String>, Bool>();
var signalE = new Signal<Opt<Null<String>>, Bool>();
var signalF = new Signal<Null<String>>();
var signalG = new Signal<EitherType<Array<Float>, Float>, Array<String>>();
var signalH = new Signal<Null<EitherType<Array<Float>, Float>>, String>();
var signalJ:Signal<Dynamic, Array<String>> = new Signal<EitherType<Array<Float>, Float>, Array<String>>();
//TODO:
//var signalK:Signal<Dynamic> = new Signal();
//var signalL:Signal<Int, Dynamic> = new Signal();
//var signalM:Signal<Opt<Dynamic>, Array<String>> = new Signal<EitherType<Array<Float>, Float>, Array<String>>();
$type(signalA).dispatch();
$type(signalB).dispatch();
$type(signalC).dispatch();
$type(signalD);
$type(signalD.dispatch);
signalD.dispatch(true);
signalD.dispatch("", true);
$type(signalE);
$type(signalE.dispatch);
signalE.dispatch(true);
signalE.dispatch("", true);
$type(signalF);
$type(signalF.dispatch);
signalF.dispatch();
$type(signalF);
$type(signalF.dispatch);
signalF.dispatch();
$type(signalG);
$type(signalG.dispatch);
signalG.dispatch([], []);
signalG.dispatch(42, [""]);
signalG.dispatch(42., [""]);
signalG.dispatch(null, []);
$type(signalH);
$type(signalH.dispatch);
signalH.dispatch("");
$type(signalJ);
$type(signalJ.dispatch);
signalJ.dispatch(null, []);
signalJ.dispatch("", []);
signalJ.dispatch(42, []);
signalJ.dispatch([], []);
///
/*$type(signalM);
$type(signalM.dispatch);
signalM.dispatch([]);
signalM.dispatch(null, []);
signalM.dispatch("", []);
signalM.dispatch(42, []);
signalM.dispatch([], []);*/
assertTrue(true);
}
public function testSignalDispatch():Void
{
// Ordinar:
var counter:Int = 0;
var signal = new Signal();
signal.add(function() counter++);
signal.dispatch();
signal.dispatch();
assertEquals(2, counter);
// Once
counter = 0;
signal.add(function() counter++, true);
signal.dispatch();
assertEquals(2, counter);
signal.dispatch();
assertEquals(3, counter);
signal = new Signal();
signal.dispose();
// Once & Ordinar:
counter = 0;
function handler() counter++;
signal.add(handler, true);
signal.add(handler, true);
signal.dispatch();
assertEquals(2, counter);
counter = 0;
signal.add(handler, false);
signal.add(handler, false);
signal.add(handler, false);
signal.add(handler, true);
signal.dispatch();
assertEquals(4, counter);
signal.dispatch();
assertEquals(4 + 3, counter);
}
public function testDispose():Void
{
var counter = 0;
var signal = new Signal();
var a = signal.add(function() counter++);
var b = signal.add(function() counter++, true);
var c = signal.add(function() counter++, true);
signal.dispatch();
assertEquals(3, counter);
counter = 0;
a.dispose();
signal.dispatch();
assertEquals(0, counter);
b.dispose();
c.dispose();
// test dispose in dispatching:
a = signal.add(function() counter++);
b = signal.add(function() {counter++; c.dispose();}, true);
c = signal.add(function() counter++, true);
signal.dispatch();
assertEquals(2, counter);
counter = 0;
a.dispose();
b.dispose();
c.dispose();
signal.dispatch();
assertEquals(0, counter);
// test dispose in dispatching:
a = signal.add(function() {counter++; b.dispose();});
b = signal.add(function() counter++);
c = signal.add(function() counter++);
signal.dispatch();
assertEquals(2, counter);
signal.dispatch();
assertEquals(4, counter);
counter = 0;
a.dispose();
b.dispose();
c.dispose();
signal.dispatch();
assertEquals(0, counter);
// test dispose in dispatching:
a = signal.add(function() {counter++; signal.dispose(false);}); // +
b = signal.add(function() counter += 2); // +
c = signal.add(function() counter += 10); // +
signal.dispatch();
assertEquals(13, counter);
signal.dispatch();
assertEquals(13, counter);
// test dispose in dispatching:
counter = 0;
var signal = new Signal();
a = signal.add(function() {counter++; signal.dispose(true);}); // +
b = signal.add(function() counter += 2); // -
c = signal.add(function() counter += 10); // -
signal.add(function() counter += 100); // -
signal.add(function() counter += 10000); // -
signal.dispatch();
assertEquals(1, counter);
signal.dispatch();
assertEquals(1, counter);
}
public function testStopPropagation():Void
{
var counter = 0;
var signal = new Signal();
var a = signal.add(function() counter++);
var b = signal.add(function() {counter++; signal.stop();});
var c = signal.add(function() counter++);
signal.dispatch();
assertEquals(2, counter);
counter = 0;
a.dispose();
b.dispose();
c.dispose();
signal.dispatch();
assertEquals(0, counter);
a = signal.add(function() {counter++; signal.stop();});
b = signal.add(function() counter++);
c = signal.add(function() counter++);
signal.dispatch();
assertEquals(1, counter);
signal.dispatch();
assertEquals(2, counter);
counter = 0;
a.dispose();
b.dispose();
c.dispose();
signal.dispatch();
assertEquals(0, counter);
counter = 0;
var signal = new Signal();
a = signal.add(function() counter++);
b = signal.add(function() counter += 2);
c = signal.add(function() counter += 10);
signal.add(function() {counter += 100; signal.stop();});
signal.add(function() counter += 10000);
signal.dispatch();
assertEquals(113, counter);
signal.dispatch();
assertEquals(113 * 2, counter);
}
public function testSignalHandlerTyping():Void
{
var data:{a:String, b:Bool};
var signal = new Signal<Opt<String>, Bool>();
function handler(?a:String, b:Bool):Void data = {a:a, b:b};
signal.add(handler);
signal.dispatch("test", true);
assertFalse(data == null);
assertEquals("test", data.a);
assertTrue(data.b);
data = null;
signal.dispatch(true);
assertFalse(data == null);
assertEquals(null, data.a);
assertTrue(data.b);
// a : String -> b : Bool -> Void should be ?String -> Bool -> Void
//signal.add(function(a:String, b:Bool){});
// a : String -> ?b : Null<Bool> -> Void should be ?String -> Bool -> Void
//signal.add(function(a:String, ?b:Bool){});
signal.add(function(?a:String, ?b:Bool){});
signal.dispatch(true);
}
public function testSignalCall():Void
{
var data:{a:String, b:Bool};
var signal = new Signal<Opt<String>, Bool>();
function handler(?a:String, b:Bool):Void data = {a:a, b:b};
signal.add(handler);
signal.dispatch("test", true);
assertFalse(data == null);
assertEquals("test", data.a);
assertTrue(data.b);
data = null;
signal.dispatch(true);
assertFalse(data == null);
assertEquals(null, data.a);
assertTrue(data.b);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment