Last active
August 29, 2015 13:55
-
-
Save bmeck/8729010 to your computer and use it in GitHub Desktop.
sweet.js macro for async function, and the potential async function*
This file contains hidden or 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
// | |
// 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