-
-
Save player-03/9635b6ccc2c50249a1ffc606a32d4e5d to your computer and use it in GitHub Desktop.
My own take on the `async` and `await` keywords.
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 com.player03.async; | |
import haxe.extern.EitherType; | |
import haxe.macro.Context; | |
import haxe.macro.Expr; | |
import haxe.macro.Printer; | |
using haxe.macro.Context; | |
using haxe.macro.ExprTools; | |
using StringTools; | |
/** | |
`Async` offers an alternate way to use Lime's `Future` class, mirroring | |
JavaScript's `async` and `await` keywords. | |
Sample usage: | |
Async.async({ | |
trace("Start time: " + Timer.stamp()); | |
var value = Async.await(future); | |
trace("Value: " + value); | |
Async.await(future2); | |
trace("End time: " + Timer.stamp()); | |
}); | |
In JavaScript, you can use the `await` keyword by passing a `js.lib.Promise` | |
instance to `Async.await()`. | |
@see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function | |
**/ | |
class Async { | |
private static inline var AWAIT_ERROR:String = "Invalid location for a call to Async.await()."; | |
#if macro | |
private static var printer:Printer = new Printer(); | |
#end | |
/** | |
Runs a block of code asynchronously. | |
@param code A block of code containing zero or more calls to | |
`Async.await()`. | |
@return A `Future` awaiting the code block's result. It won't report | |
progress but will report completion and errors. | |
**/ | |
#if !macro | |
public static macro function async<T>(code:ExprOf<T>):ExprOf<lime.app.Future<Null<T>>> | |
#else | |
public static function async(code:Expr):Expr | |
#end | |
{ | |
switch (code.expr) | |
{ | |
case EBlock(block): | |
code = processBlock(block); | |
default: | |
Context.warning("Async.async() requires a code block.", Context.currentPos()); | |
return code; | |
} | |
// In JS, add the `async` keyword. | |
if (Context.defined("js")) | |
{ | |
var jsCode:Expr = #if haxe4 macro js.Syntax.code #else macro untyped __js__ #end; | |
jsCode = macro $jsCode("(async {0})()", function() $code); | |
var promiseType:ComplexType = switch(jsCode.typeof().followWithAbstracts().toComplexType()) | |
{ | |
case TPath({ pack: ["js", "lib"], name: "Promise", params: [TPType(param)] }): | |
param; | |
case TPath({ name: "Void" }): | |
null; | |
case x: | |
x; | |
}; | |
// The `async` keyword produces a `js.lib.Promise`; convert this to | |
// `lime.app.Promise`. | |
return macro { | |
var promise:lime.app.Promise<$promiseType> = new lime.app.Promise(); | |
try | |
$jsCode.then(promise.complete, promise.error) | |
catch (e:Dynamic) | |
promise.error(e); | |
promise.future; | |
}; | |
} | |
else | |
{ | |
return macro try $code catch (e:Dynamic) lime.app.Future.withError(e); | |
} | |
} | |
/** | |
Pauses the current `async()` call until the given value is available. | |
Call this only from inside an `async()` block. | |
@param value A `lime.app.Future` or `js.lib.Promise` instance. | |
@return `future.value`, once available. | |
**/ | |
#if (macro || js) | |
public static #if !macro macro #end function await<T>(future:ExprOf<FutureType<T>>):ExprOf<T> | |
{ | |
if (Context.defined("js")) | |
{ | |
var jsCode:Expr = #if haxe4 macro js.Syntax.code #else macro untyped __js__ #end; | |
return macro $jsCode("await {0}", $future); | |
} | |
else | |
{ | |
return macro throw $v{AWAIT_ERROR}; | |
} | |
} | |
#else | |
public static inline function await<T>(future:FutureType<T>):T | |
{ | |
throw AWAIT_ERROR; | |
} | |
#end | |
#if macro | |
/** | |
Inserts pauses in a block of code wherever `await()` is called. | |
@param block The code. | |
@param index For recursion only; otherwise leave this alone. | |
@param declaredAwaitResult For recursion only. | |
**/ | |
private static function processBlock(block:Array<Expr>, index:Int = -1, declaredAwaitResult:Bool = false):Expr | |
{ | |
// Determine the return type before doing any processing, to avoid | |
// typing errors. | |
var returnType:String = printer.printComplexType(block[block.length - 1].typeof().followWithAbstracts().toComplexType()); | |
// Find the next `await()` call. | |
var state:{ ?awaitCode:Expr } = {}; | |
while (state.awaitCode == null && ++index < block.length) | |
{ | |
block[index] = processExpr(block[index], state); | |
block[index] = postProcessExpr(block[index]); | |
} | |
// If found, rearrange the rest of the block. | |
if (index < block.length) | |
{ | |
if (state.awaitCode == null) | |
{ | |
throw "Assertion failed"; | |
} | |
// Special case: remove `__awaitResult` if it's unused. | |
switch (block[index].expr) | |
{ | |
case EConst(CIdent("__awaitResult")): | |
block.splice(index, 1); | |
// If this was the last line, there's no need to wait. | |
if (index >= block.length) | |
{ | |
block.push(state.awaitCode); | |
return macro $b{block}; | |
} | |
default: | |
} | |
var awaitCodeType:String = printer.printComplexType(state.awaitCode.typeof().followWithAbstracts().toComplexType()); | |
if (Context.defined("js") && awaitCodeType.startsWith("js.lib.Promise")) | |
{ | |
// Call `await()`, making sure not to redeclare `__awaitResult`. | |
// (While JS allows this, Haxe doesn't.) | |
block.insert(index, declaredAwaitResult | |
? macro __awaitResult = com.player03.async.Async.await(${state.awaitCode}) | |
: macro var __awaitResult = com.player03.async.Async.await(${state.awaitCode}) | |
); | |
// Keep going. Don't increment `index` since the `while` loop | |
// handles that. Do pass `declaredAwaitResult`. | |
return processBlock(block, index, true); | |
} | |
else if (awaitCodeType.startsWith("lime.app.Future")) | |
{ | |
// Move the rest of this block into a `then` listener. | |
// Extract the lines to move. | |
var innerExprs:Array<Expr> = block.splice(index, block.length - index); | |
// Process. | |
processBlock(innerExprs); | |
// Make sure to return a `Future`. | |
if (returnType == "StdTypes.Void") | |
{ | |
innerExprs.push(macro lime.app.Future.withValue(true)); | |
} | |
else if (!returnType.startsWith("lime.app.Future")) | |
{ | |
innerExprs[innerExprs.length - 1] = macro lime.app.Future.withValue(${innerExprs[innerExprs.length - 1]}); | |
} | |
// Catch errors. | |
var innerBlock:Expr = macro try $b{innerExprs} catch (e:Dynamic) cast lime.app.Future.withError(e); | |
// Wait for `then()`. | |
block.push(macro ${state.awaitCode}.then(function(__awaitResult) return $innerBlock)); | |
return macro $b{block}; | |
} | |
else if (Context.defined("js")) | |
{ | |
Context.error('$awaitCodeType should either be lime.app.Future or js.lib.Promise.', state.awaitCode.pos); | |
} | |
else | |
{ | |
Context.error('$awaitCodeType should be lime.app.Future.', state.awaitCode.pos); | |
} | |
} | |
if (returnType == "StdTypes.Void") | |
{ | |
block.push(macro lime.app.Future.withValue(true)); | |
} | |
return macro $b{block}; | |
} | |
/** | |
Call this only inside `processBlock()`. | |
**/ | |
private static function processExpr(code:Expr, state:{ ?awaitCode:Expr }):Expr | |
{ | |
return switch (code.expr) | |
{ | |
case ECall(isAwaitCall(_) => true, [awaitCode]): | |
// If possible, create a new `async` context. | |
switch (awaitCode.expr) | |
{ | |
case EBlock(block): | |
state.awaitCode = async(awaitCode); | |
default: | |
state.awaitCode = awaitCode; | |
} | |
// Replace `code` with a reference to `__awaitResult`, which | |
// represents the eventual return value. `processBlock()` will | |
// make sure this value is available. | |
macro @:pos(${code.pos}) __awaitResult; | |
case EVars(vars): | |
// One of the few approved locations for `await()`; use | |
// recursion to check. | |
if (vars.length > 0 && vars[0].expr != null) | |
{ | |
vars[0].expr = processExpr(vars[0].expr, state); | |
} | |
code; | |
case EBinop(OpAssign, e1, e2): | |
// One of the few approved locations for `await()`; use | |
// recursion to check. | |
code.expr = EBinop(OpAssign, e1, processExpr(e2, state)); | |
code; | |
default: | |
// No other instances of `await()` are accepted. | |
code; | |
}; | |
} | |
private static function postProcessExpr(code:Expr):Expr | |
{ | |
return switch (code.expr) | |
{ | |
case EBlock(_): | |
// Create a new `async` context. | |
Context.warning("Implicitly wrapping this block in an Async.async() call.", code.pos); | |
async(code); | |
case ECall(isAsyncCall(_) => true, [asyncCode]): | |
// Create a new `async` context without the warning. | |
async(asyncCode); | |
case ECall(isAwaitCall(_) => true, [awaitCode]) if (!Context.defined("js")): | |
Context.warning(AWAIT_ERROR, code.pos); | |
code; | |
default: | |
code.map(postProcessExpr); | |
}; | |
} | |
private static function isAwaitCall(call:Expr):Bool | |
{ | |
var string:String = printer.printExpr(call); | |
return string == "Async.await" || string == "com.player03.async.Async.await"; | |
} | |
private static function isAsyncCall(call:Expr):Bool | |
{ | |
var string:String = printer.printExpr(call); | |
return string == "Async.async" || string == "com.player03.async.Async.async"; | |
} | |
private static function isVoid(expr:Expr):Bool | |
{ | |
switch (Context.typeof(expr).toComplexType()) | |
{ | |
case TPath({ name: "Void" }): | |
return true; | |
default: | |
return false; | |
} | |
} | |
#end | |
} | |
#if macro | |
typedef FutureType<T> = Dynamic; | |
#elseif js | |
typedef FutureType<T> = EitherType<lime.app.Future<T>, js.lib.Promise<T>>; | |
#else | |
typedef FutureType<T> = lime.app.Future<T>; | |
#end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This utility combines JavaScript's
async
andawait
keywords with Lime'sFuture
class. This will use the built-in language features in JS, and emulate them on other targets. With certain limitations, which are why I abandoned the project.If you want to use the JavaScript language features, alternatives include jsasync and hxasync. If you don't need the language features but do want cross-platform syntax sugar, alternatives include tink_await.