From bbaacd5d2887f8a6d58af1797c806a533e081819 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Thu, 10 Aug 2017 16:31:10 +0200 Subject: [PATCH] added Async --- hscript/Async.hx | 380 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 hscript/Async.hx diff --git a/hscript/Async.hx b/hscript/Async.hx new file mode 100644 index 0000000..3d64649 --- /dev/null +++ b/hscript/Async.hx @@ -0,0 +1,380 @@ +package hscript; +import hscript.Expr; + +class Async { + + var varNames : Array; + var currentFun : String; + var currentLoop : hscript.Expr; + var currentBreak : hscript.Expr; + var uid = 0; + + /** + Convert a script into asynchronous one. + - calls such as foo(a,b,c) are translated to a_foo(function(r) ...rest, a,b,c) where r is the result value + - object access such obj.bar(a,b,c) are translated to obj.a_bar(function(r) ...rest, a, b, c) + - @split { e1; e2; e3; } is transformed to split(function(_) ...rest, [e1, e2, e3]) which + should execute asynchronously all expressions - until they return - before continuing the execution + - @async { e1; e2; e3; } is similar to @split but execution will continue immediately without wait + - for(i in v) block; loops are translated to the following: + var _i = makeIterator(v); + function _loop() { + if( !_i.hasNext() ) return; + var v = _i.next(); + block(function(_) _loop()); + } + _loop() + - while loops are translated similar to for loops + - break and continue are correctly handled + - you can use @sync to disable async transformation in some code parts (for performance reason) + - a few expressions are still not supported (complex calls, try/catch, and a few others) + + In these examples ...rest represents the continuation of execution of the script after the expression + **/ + public static function toAsync( e : Expr, topLevelSync = false ) { + var a = new Async(); + return a.build(e, topLevelSync); + } + + public function build( e : Expr, topLevelSync = false ) { + if( topLevelSync ) { + return buildSync(e); + } else { + var nothing = EFunction([{ name : "_", t : null }], EBlock([])); + return new Async().toCps(e, nothing, nothing); + } + } + + function buildSync( e : Expr ) { + switch( e ) { + case EFunction(_): + return toCps(e, null, null); + case EBlock(el): + var v = saveVars(); + for( e in el ) + switch( e ) { + case EFunction(_, _, name, _) if( name != null ): varNames.push(name); + default: + } + var e = EBlock([for(e in el) buildSync(e)]); + restoreVars(v); + return e; + default: + return hscript.Tools.map(e, buildSync); + } + } + + public function new() { + varNames = []; + } + + function ignore(e) { + return EFunction([{ name : "_", t : null }], EBlock([e])); + } + + function retNull(e) { + return ECall(e, [EIdent("null")]); + } + + function makeCall( ecall, args : Array, rest : hscript.Expr, exit, sync = false ) { + var names = [for( i in 0...args.length ) "_a"+uid++]; + var rargs = [for( i in 0...args.length ) EIdent(names[i])]; + if( !sync ) + rargs.unshift(rest); + var rest = sync ? ECall(rest,[ECall(ecall, rargs)]) : ECall(ecall, rargs); + var i = args.length - 1; + while( i >= 0 ) { + rest = toCps(args[i], EFunction([ { name : names[i], t : null } ], rest), exit); + i--; + } + return rest; + } + + var syncFlag : Bool; + + function isSync( e : Expr ) { + syncFlag = true; + checkSync(e); + return syncFlag; + } + + function checkSync( e : Expr ) { + if( !syncFlag ) + return; + switch( e ) { + case ECall(_): + syncFlag = false; + case EReturn(_): + // require specific handling to pass it to exit expr + syncFlag = false; + default: + Tools.iter(e, checkSync); + } + } + + function saveVars() { + return varNames.length; + } + + function restoreVars(k) { + while( varNames.length > k ) varNames.pop(); + } + + public function toCps( e : hscript.Expr, rest : hscript.Expr, exit : hscript.Expr ) { + if( isSync(e) ) + return ECall(rest, [e]); + switch( e ) { + case EBlock(el): + var el = el.copy(); + var vold = saveVars(); + // local recursion + for( e in el ) + switch( e ) { + case EFunction(_, _, name): varNames.push(name); + default: + } + while( el.length > 0 ) { + var e = toCps(el.pop(), rest, exit); + rest = ignore(e); + } + restoreVars(vold); + return retNull(rest); + case EFunction(args, body, name, t): + var vold = saveVars(); + if( name != null ) + varNames.push(name); + for( a in args ) + varNames.push(a.name); + args.unshift( { name : "_onEnd", t : null } ); + var rest = EIdent("_onEnd"); + var oldFun = currentFun; + currentFun = name; + var body = toCps(body, rest, rest); + var f = EFunction(args, body, "a_" + name, t); + restoreVars(vold); + return rest == null ? f : ECall(rest, [f]); + case EParent(e): + return EParent(toCps(e, rest, exit)); + case EMeta("sync", _, e): + return buildSync(e); + case EMeta(name = "split" | "async", _, e): + var args = switch( e ) { case EBlock(el): el; default: [e]; }; + var args = [for( a in args ) EFunction([ { name : "_rest", t : null } ], toCps(EBlock([a]), EIdent("_rest"), exit))]; + return ECall(EIdent(name), [rest, EArrayDecl(args)]); + case ECall(EIdent(i), args): + return makeCall( EIdent( varNames.indexOf(i) < 0 ? "a_" + i : i) , args, rest, exit); + case ECall(EField(e, f), args): + return makeCall(EField(e,"a_"+f), args, rest, exit); + case EFor(v, eit, e): + var id = ++uid; + var it = EIdent("_i" + id); + var oldLoop = currentLoop, oldBreak = currentBreak; + var loop = EIdent("_loop" + id); + currentLoop = loop; + currentBreak = EBlock([ECall(rest, [EIdent("null")]), EReturn()]); + var e = EBlock([ + EVar("_i" + id, ECall(EIdent("makeIterator"),[eit])), + EFunction([{ name : "_", t : null }], EBlock([ + EIf(EUnop("!", true, ECall(EField(it, "hasNext"), [])), currentBreak), + EVar(v, ECall(EField(it, "next"), [])), + toCps(e, loop, exit), + ]),"_loop" + id), + ECall(loop, [EIdent("null")]), + ]); + currentLoop = oldLoop; + currentBreak = oldBreak; + return e; + case EUnop(op = "!", prefix, e): + return toCps(e, EFunction([ { name:"_r", t:null } ], ECall(rest, [EUnop(op, prefix, EIdent("_r"))])), exit); + case EBinop(op, e1, e2): + switch( op ) { + case "=", "+=", "-=", "/=", "*=", "%=", "&=", "|=", "^=": + switch( e1 ) { + case EIdent(_): + var id = "_r" + uid++; + return toCps(e2, EFunction([ { name:id, t:null } ], ECall(rest, [EBinop(op, e1, EIdent(id))])), exit); + case EField(e1, f): + var id1 = "_r" + uid++; + var id2 = "_r" + uid++; + return toCps(e1, EFunction([ { name:id1, t:null } ], toCps(e2, EFunction([ { name : id2, t : null } ], ECall(rest, [EBinop(op, EField(EIdent(id1),f), EIdent(id2))])), exit)), exit); + case EArray(earr, eindex): + var idArr = "_r" + uid++; + var idIndex = "_r" + uid++; + var idVal = "_r" + uid++; + return toCps(earr, + EFunction([ { name:idArr, t:null } ], toCps(eindex, + EFunction([ { name : idIndex, t : null } ], toCps(e2, + EFunction([ { name : idVal, t : null } ], + ECall(rest, [EBinop(op, EArray(EIdent(idArr), EIdent(idIndex)), EIdent(idVal))]) + ), exit) + ), exit) + ),exit); + default: + throw "assert " + e1; + } + case "||": + var id1 = "_r" + uid++; + var id2 = "_r" + uid++; + return toCps(e1, EFunction([ { name:id1, t:null } ], EIf(EBinop("==", EIdent(id1), EIdent("true")),ECall(rest,[EIdent("true")]),toCps(e2, rest, exit))), exit); + case "&&": + var id1 = "_r" + uid++; + var id2 = "_r" + uid++; + return toCps(e1, EFunction([ { name:id1, t:null } ], EIf(EBinop("!=", EIdent(id1), EIdent("true")),ECall(rest,[EIdent("false")]),toCps(e2, rest, exit))), exit); + default: + var id1 = "_r" + uid++; + var id2 = "_r" + uid++; + return toCps(e1, EFunction([ { name:id1, t:null } ], toCps(e2, EFunction([ { name : id2, t : null } ], ECall(rest, [EBinop(op, EIdent(id1), EIdent(id2))])), exit)), exit); + } + case EIf(cond, e1, e2), ETernary(cond, e1, e2): + return toCps(cond, EFunction([ { name : "_c", t : null } ], EIf(EIdent("_c"), toCps(e1, rest, exit), e2 == null ? retNull(rest) : toCps(e2, rest, exit))), exit); + case EWhile(cond, e): + var id = ++uid; + var loop = EIdent("_loop" + id); + var oldLoop = currentLoop, oldBreak = currentBreak; + currentLoop = loop; + currentBreak = EBlock([ECall(rest, [EIdent("null")]), EReturn()]); + var ewhile = EBlock([ + EFunction([{ name : "_r", t : null }], + toCps(cond, EFunction([ { name : "_c", t : null } ], EIf(EIdent("_c"), toCps(e, loop, exit), ECall(rest,[EIdent("null")]))), exit) + , "_loop"+id), + ECall(loop, [EIdent("null")]), + ]); + currentLoop = oldLoop; + currentBreak = oldBreak; + return ewhile; + case EReturn(e): + return e == null ? ECall(exit, [EIdent("null")]) : toCps(e, exit, exit); + case EObject(fields): + var id = "_o" + uid++; + var rest = ECall(rest, [EIdent(id)]); + fields.reverse(); + for( f in fields ) + rest = toCps(f.e, EFunction([ { name : "_r", t : null } ], EBlock([ + EBinop("=", EField(EIdent(id), f.name), EIdent("_r")), + rest, + ])),exit); + return EBlock([ + EVar(id, EObject([])), + rest, + ]); + case EArrayDecl(el): + var id = "_a" + uid++; + var rest = ECall(rest, [EIdent(id)]); + var i = el.length - 1; + while( i >= 0 ) { + rest = toCps(el[i], EFunction([ { name : "_r", t : null } ], EBlock([ + EBinop("=", EArray(EIdent(id), EConst(CInt(i))), EIdent("_r")), + rest, + ])), exit); + i--; + } + return EBlock([ + EVar(id, EArrayDecl([])), + rest, + ]); + case EArray(e, eindex): + var id1 = "_r" + uid++; + var id2 = "_r" + uid++; + return toCps(e, EFunction([ { name:id1, t:null } ], toCps(eindex, EFunction([ { name : id2, t : null } ], ECall(rest, [EArray(EIdent(id1), EIdent(id2))])), exit)), exit); + case EVar(v, t, ev): + if( ev == null ) + return EBlock([e, ECall(rest, [EIdent("null")])]); + return EBlock([ + EVar(v, t), + toCps(ev, EFunction([ { name : "_r", t : null } ], EBlock([ + EBinop("=", EIdent(v), EIdent("_r")), + ECall(rest,[EIdent("null")]), + ])), exit), + ]); + case EConst(_), EIdent(_), EUnop(_), EField(_): + return ECall(rest, [e]); + case ENew(cl, args): + var names = [for( i in 0...args.length ) "_a"+uid++]; + var rargs = [for( i in 0...args.length ) EIdent(names[i])]; + var rest = ECall(rest,[ENew(cl, rargs)]); + var i = args.length - 1; + while( i >= 0 ) { + rest = toCps(args[i], EFunction([ { name : names[i], t : null } ], rest), exit); + i--; + } + return rest; + case EBreak: + if( currentBreak == null ) throw "Break outside loop"; + return currentBreak; + case EContinue: + if( currentLoop == null ) throw "Continue outside loop"; + return EBlock([ECall(currentLoop, [EIdent("null")]), EReturn()]); + case ESwitch(v, cases, def): + var cases = [for( c in cases ) { values : c.values, expr : toCps(c.expr, rest, exit) } ]; + return toCps(v, EFunction([ { name : "_c", t : null } ], ESwitch(EIdent("_c"), cases, def == null ? retNull(rest) : toCps(def, rest, exit))), exit ); + case EThrow(v): + return toCps(v, EFunction([ { name : "_v", t : null } ], EThrow(v)), exit); + //case EDoWhile(_), ETry(_), ECall(_): + default: + throw "Unsupported async expression " + Printer.toString(e); + } + } + +} + + +class AsyncInterp extends hscript.Interp { + + public function setContext( api : Dynamic ) { + + variables.set("split", split); + variables.set("async", async); + variables.set("makeIterator", makeIterator); + + var c = Type.getClass(api); + for( f in (c == null ? Reflect.fields(api) : Type.getInstanceFields(c)) ) { + var fv = Reflect.field(api, f); + if( !Reflect.isFunction(fv) ) continue; + if( f.charCodeAt(0) == "_".code ) f = f.substr(1); + variables.set(f, fv); + // create the async wrapper if doesn't exists + if( f.substr(0, 2) != "a_" && !variables.exists("a_"+f) ) + variables.set("a_" + f, Reflect.makeVarArgs(function(args) { + var onEnd = args.shift(); + onEnd(Reflect.callMethod(api, fv, args)); + })); + } + } + + function async( rest : Dynamic -> Void, args : Array ) { + for( a in args ) + a(function(_) {}); + rest(null); + } + + function split( rest : Dynamic -> Void, args : Array ) { + if( args.length == 0 ) + rest(null); + else { + var count = args.length; + function next(_) { + if( --count == 0 ) rest(null); + } + for( a in args ) + a(next); + } + } + + override function fcall( o : Dynamic, f : String, args : Array ) : Dynamic { + var m = Reflect.field(o, f); + if( m == null ) { + if( f.substr(0, 2) == "a_" ) { + m = Reflect.field(o, f.substr(2)); + // fallback on sync version + if( m != null ) { + var onEnd = args.shift(); + onEnd(call(o, m, args)); + return null; + } + } + throw o + " has no method " + f; + } + return call(o, m, args); + } + +}