Skip to content

Instantly share code, notes, and snippets.

@bmeck
Last active August 29, 2015 13:55
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/8729010 to your computer and use it in GitHub Desktop.
Save bmeck/8729010 to your computer and use it in GitHub Desktop.
sweet.js macro for async function, and the potential async function*
//
// This assumes the runner supports
// - generators (for a transpiler see http://facebook.github.io/regenerator/)
// - Promises (for a polyfill see https://github.com/petkaantonov/bluebird)
//
// This does not need outside libraries to be loaded
//
// This survives direct eval semantics, unless you use regenerator, in which case the unwinding will cause variable renaming
//
//
// `async function id() {}`
// - will always return a promise
// - once the function resolves (throw/return) the promise will resolve
// - has `await $expr` that will cast the `$expr` to a promise and not continue the function until the promise is resolved
// - if the promise is rejected it will throw at the location of `await`
// - if the promise is fulfilled `await` will resolve to the value of the promise
//
//
// `async function* id() {}`
// - will always return a promise (`.then(fn)/.catch(fn)`) that is also a generator iterator (`.next(value)`)
// - once the function resolves (throw/return) the promise will resolve
// - the function execution is controlled in the same manner as a generator
// - first `.next()` should have undefined as its argument
// - `.next(value)` will always return a promise
// - these promises will resolve one at a time FIFO when the function `yield`s
// - you can queue promises past the end of the function's resolution, but when the function resolves they will be rejected
// - has `await $expr` that will cast the `$expr` to a promise and not continue the function until the promise is resolved
// - if the promise is rejected it will throw at the location of `await`
// - if the promise is fulfilled `await` will resolve to the value of the promise
// - has `yield $expr`
// - this will resolve the first promise (if any) left unresolved from calling `.next(value)` on the function
// - yielding a promise will not cause the function to go into an `awaiting` state
//
macro async {
case { $macro function ($args ...) { $body ... } } => {
return #{
$macro function anonymous ($args ...) { $body ... }
}
}
case { $macro function* ($args ...) { $body ... } } => {
return #{
$macro function* anonymous ($args ...) { $body ... }
}
}
case { $macro function $id ($args ...) { $body ... } } => {
var body = #{ $body ... };
function expand(orig) {
var body = orig.concat();
for (var i = 0; i < body.length; i++) {
var token = body[i].token;
if (token.type === parser.Token.Identifier || token.type === parser.Token.Keyword) {
if (token.value === 'function') {
var expr=getExpr(body.slice(i));
i--;
i+=expr.result.length;
}
else if (token.value === 'await') {
var expr = getExpr(body.slice(i+1));
if (expr.success) {
var yieldWord = makeKeyword('yield', #{ $id });
body.splice(i, 1, yieldWord);
}
}
}
else if (token.inner) {
token.inner = expand(token.inner);
}
}
return body;
}
var nbody = expand(body);
letstx $nbody ... = nbody;
return #{
function $id () {
function* $id($args ...) {
$nbody ...
}
return new Promise(function (f,r) {
var generator = $id();
function nextForValue(v) {
run(function () {
return generator.next(v);
});
}
function throwForValue(e) {
run(function () {
return generator.throw(e);
});
}
function run(step) {
var result;
try {
result = step();
}
catch (e) {
r(e);
return;
}
if (result.done) {
f(result.value);
return;
}
else {
Promise.cast(result.value).then(nextForValue, throwForValue);
}
}
// should this be on the next tick?
nextForValue();
})
}
}
}
case { $macro function* $id ($args ...) { $body ...} } => {
var body = #{ $body ... };
function expand(orig) {
var body = orig.concat();
for (var i = 0; i < body.length; i++) {
var token = body[i].token;
if (token.inner) {
token.inner = expand(token.inner);
}
else if (token.type === parser.Token.Identifier || token.type === parser.Token.Keyword) {
if (token.value === 'function') {
var expr=getExpr(body.slice(i));
i--;
i+=expr.result.length;
}
else if (/^(?:yield|await)$/.test(token.value)) {
function generateYield(type,i) {
var nextIndex = i + 1;
var expr = getExpr(body.slice(nextIndex));
if (expr.success) {
for (var ii = 0; ii < expr.result.length; ii++) {
if (expr.result[ii].token.value === ',') {
expr.result = expr.result.slice(0, ii);
break;
}
}
var yieldWord = makeKeyword('yield', #{ $id });
if (expr.result.length === 1 && /^(?:yield|await)$/.test(expr.result[0].token.value)) {
var inner = generateYield(expr.result[0].token.value, nextIndex);
var arr = makeDelim('[]', [makeValue(type, #{ $id }), makePunc(',', #{ $id })].concat(inner.arr), #{ $id });
return {
length: 1+inner.length,
arr: [yieldWord, arr]
}
}
var arr = makeDelim('[]', [makeValue(type, #{ $id }), makePunc(',', #{ $id })].concat(expr.result), #{ $id });
return {
length: 1+expr.result.length,
arr: [yieldWord, arr]
}
}
else {
throw new Error(type + ' should be followed by an expression');
}
}
var validYield = generateYield(token.value, i);
body.splice(i, validYield.length, validYield.arr[0], validYield.arr[1]);
}
}
}
return body;
}
letstx $nbody ... = expand(body);
return #{
function $id () {
return function proxy(innerFn, innterFnThis, innerFnArguments) {
function* scheduleGenerator(asyncFulfill, asyncReject) {
var innerGenerator = innerFn.apply(innterFnThis, innerFnArguments);
// simple linked list of queued promises
var head = null;
var tail = null;
var done = false;
// calls the inner generator .next and figures out next course of action
function nextForValue(v) {
iterate(function () {
return innerGenerator.next(v);
});
}
// calls the inner generator .throw and figures out next course of action
function throwForValue(e) {
iterate(function () {
return innerGenerator.throw(e);
});
}
// calls the inner generator somehow and figures out next course of action
function iterate(how) {
var innerResult;
try {
// invoking the inner generator
innerResult = how();
}
// generator threw an error
catch (e) {
// we want the first error to kill the outer promise
// when we start back up all the inner promises will die as well
if (!done) {
done = true;
// generator threw
asyncReject(e);
head.start();
}
// this will cause a chain of rejections to pending inner promises
else {
head.reject(e);
}
return;
}
if (!innerResult.done) {
// generator used await
if (innerResult.value[0] === 'await') {
Promise.cast(innerResult.value[1]).then(nextForValue, throwForValue);
}
// generator used yield
else {
head.fulfill(innerResult.value[1]);
}
}
// generator used return
else {
done = true;
asyncFulfill(innerResult.value);
// this will cause a chain of rejections to pending ones
head.start();
}
}
// method for promise to dequeue on resolution, only used by head
function dequeue() {
// if we are the last promise we should cleanup
if (head === tail) {
head = tail = null;
}
else {
head = head.next;
head.start();
}
}
function enqueueValue(v) {
return enqueue(function start() {
nextForValue(v);
});
}
function enqueueThrow(v) {
return enqueue(function start() {
throwForValue(v);
});
}
// queues a promise and value for execution and resolution
function enqueue(start) {
var helper;
// promise we will return to person calling our outer generator (async function)
var outerPromise = new Promise(function (f,r) {
helper = {
start: start,
fulfill: function (resultValue) {
f(resultValue);
dequeue();
},
reject: function (e) {
r(e);
dequeue();
}
};
// if we are not running already we should be
if (!head) {
head = helper;
tail = helper;
head.start();
}
// we are running already, put this on the tail
else {
tail.next = helper;
tail = helper;
}
});
return outerPromise;
}
// running
var passedValue = void 0;
while (true) {
try {
passedValue = yield enqueueValue(passedValue);
}
catch (error) {
enqueueThrow(error);
}
}
}
// combine a Promise and a GeneratorIterator...
var asyncFulfill, asyncReject;
var result = new Promise(function (f,r) {
asyncFulfill = f, asyncReject = r;
});
var outerGenerator = scheduleGenerator(asyncFulfill, asyncReject);
result.next = function () {
return outerGenerator.next.apply(outerGenerator, arguments);
};
result.throw = function () {
return outerGenerator.throw.apply(outerGenerator, arguments);
};
return result;
// modified original function
}(function* $id($args ...) {
$nbody ...
}, this, arguments);
}
}
}
case { _ } => { _ }
}
async function* asyncGenFn() {
// nesting example
await await 1;
function* nestingTest() {yield "should not be transpiled in nested functions";}
async function asyncFn() {await 1;}
console.log(arguments, 'PASSED IN AS GENERATOR ARGUMENTS');
console.log(await 'first await operand', 'PASSED IN AS FIRST AWAIT VALUE');
console.log(await 'second await operand', 'PASSED IN AS SECOND AWAIT VALUE');
console.log(yield 'first yield operand', 'PASSED IN AS FIRST YIELD VALUE');
console.log(yield 'second yield operand', 'PASSED IN AS SECOND YIELD VALUE');
console.log(yield new Promise(function (f, r) {
setTimeout(function () {
f('in async generators promises can be yielded without awaiting');
}, 2000);
}), 'PASSED IN AS THIRD YIELD VALUE');
return 'generator return value';
}
var iter = asyncGenFn('first generator argument', 'second generator argument');
iter.then(function (v) {
console.log('GENERATOR RETURN VALUE ENDED UP AS', v);
}, function (e) {
console.log('GENERATOR ERROR THROWN ENDED UP AS', e);
});
var promises = [iter.next(),iter.next('second next operand'),iter.next('third next operand'),iter.next('fourth next operand'),iter.next('fifth next operand')];
promises.forEach(function (result, i) {
var promise = result.value;
var onFulfill = console.log.bind(console, 'promise', i, 'fulfilled as');
var onReject = console.error.bind(console, 'promise', i, 'rejected as');
promise.then(onFulfill, onReject);
});
async function asyncFn() {
// nesting example
await await 1;
console.log(arguments, 'PASSED IN AS FUNCTION ARGUMENTS');
console.log('function first await result', await new Promise(function (f, r) {
setTimeout(function () {
f('in functions there is only await');
}, 2000);
}));
throw 'function throw value';
}
var iter = asyncFn('first function argument', 'second function argument');
iter.then(function (v) {
console.log('FUNCTION RETURN VALUE ENDED UP AS', v);
}, function (e) {
console.log('FUNCTION ERROR THROWN ENDED UP AS', e);
});
async function () { await await 1 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment