Skip to content

Instantly share code, notes, and snippets.

@player-03
Created February 25, 2022 17:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save player-03/9635b6ccc2c50249a1ffc606a32d4e5d to your computer and use it in GitHub Desktop.
Save player-03/9635b6ccc2c50249a1ffc606a32d4e5d to your computer and use it in GitHub Desktop.
My own take on the `async` and `await` keywords.
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
@player-03
Copy link
Author

This utility combines JavaScript's async and await keywords with Lime's Future 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment