Skip to content

Commit

Permalink
Monads now implemented via generators - reusing control flow logic, a…
Browse files Browse the repository at this point in the history
…nd to allow use of native generators implementation where available (this will be a command line flag later on).
  • Loading branch information
tehsenaus committed Jul 15, 2014
1 parent 9513d3a commit de97fbd
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 29 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -118,7 +118,8 @@ for (let i in [...]) {
Changed iterators & generators to reflect latest ES6 spec (no more StopIteration).
Added benchmarks (just for generators, initially).
Optimised generators for tail call emulation use case - performance is comparable to trampoline.

Monads now implemented via generators - reusing control flow logic, and to allow use of native
generators implementation where available (this will be a command line flag later on).

### 0.3.5
Allow monad binds in for loops.
Expand Down
20 changes: 20 additions & 0 deletions benchmark/generator_native.js
@@ -0,0 +1,20 @@

// Benchmark to compare Latte generator performance against native V8 generators.
// Run with --harmony flag:
// node --harmony benchmark/generator_native.js

var tests = require('./src/generator.latte').tests;

var suite = new (require('benchmark').Suite);

suite.add('trampoline', require('./src/generator.latte').tests['trampoline']);
suite.add('native generator', require('./src/generator.latte').tests['generator']);
suite.add('latte generator', require('./out/generator').tests['generator']);

suite.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})
.run();
10 changes: 6 additions & 4 deletions benchmark/src/generator.latte
@@ -1,18 +1,20 @@

var N = 15500;

function sum_rec() {
function sum(n, a) {
return n > 0 ? sum(n - 1, n + (a || 0)) : a;
}

sum(10000);
sum(N);
}

function sum_tco() {
var sum = recur(function (n, a) {
return n > 0 ? sum(n - 1, n + (a || 0)) : a;
})

sum(10000);
sum(N);

// https://github.com/Gozala/js-tail-call
function recur(f) {
Expand All @@ -35,7 +37,7 @@ function sum_gen() {
yield (n > 0 ? sum_r(n - 1, n + (a || 0)) : a);
})

sum(10000);
sum(N);


function rec(f) {
Expand All @@ -60,7 +62,7 @@ module.exports = {
name: 'Generator',
tests: {
'recursion': sum_rec,
'tail call (baseline)': sum_tco,
'trampoline': sum_tco,
'generator': sum_gen
}
};
9 changes: 9 additions & 0 deletions src/gen.latte
Expand Up @@ -169,6 +169,15 @@ module.exports = {
}
},

binary: function (left, op, right) {
return {
type: 'BinaryExpression',
operator: op,
left: this.ident(left),
right: this.ident(right)
}
},

newExpr: function (callee, arguments) {
return {
type: 'NewExpression',
Expand Down
3 changes: 3 additions & 0 deletions src/prelude/for-in-converter.latte
@@ -0,0 +1,3 @@
function __convertForIn(obj) {
return [k for (k in obj)];
}
17 changes: 8 additions & 9 deletions src/prelude/monad-to-generator.latte
@@ -1,13 +1,12 @@
function __monad(m, lv) {
function __monad(m, value) {
while (true) {
try {
var v = m.send(lv);
if ( v && typeof v.then === "function" ) {
return v.then(nv => __monad(m, nv))
}
} catch (e) {
if ( e === StopIteration ) return lv;
throw e;
var bind = m.send(value);
if ( bind.done ) return value;
value = bind.value;
if ( value && typeof value.then === "function" ) {
return value.then(nv => __monad(m, nv))
}
}
}
2 changes: 1 addition & 1 deletion src/transform.latte
Expand Up @@ -91,7 +91,7 @@ var transformers = [
require('./transformer/arrowFunc'),
require('./transformer/comprehension'),
require('./transformer/forOf'),
require('./transformer/monad'),
require('./transformer/monad-to-generator'),
require('./transformer/generator'),
require('./transformer/arrowFunc'),
arrayDestructuringTransformer,
Expand Down
136 changes: 130 additions & 6 deletions src/transformer/generator.latte
Expand Up @@ -23,8 +23,24 @@ var generatorTransformer = module.exports = traverser.explicit('generator', {
return bound;
},



varDecl: function (stmt, context) {
if ( stmt.declarations.length > 1 && stmtHasYields(stmt) ) {
// Multiple variable decl with yields -
// must split this up into separate stmts otherwise we may
// get undefined references
stmt = gen.block([
gen.varDecl(d.id, d.init, stmt)
for (d of stmt.declarations)
])
return this.stmt(stmt, context);
}

return this.super.varDecl(stmt, context);
},

ifStmt: function (stmt, context) {
var aHasYields = stmtHasYields(stmt.consequent)
bHasYields = stmt.alternate && stmtHasYields(stmt.alternate);
Expand Down Expand Up @@ -67,8 +83,43 @@ var generatorTransformer = module.exports = traverser.explicit('generator', {

forInOfStmt: function (stmt, context) {
if ( !stmt.generatoric && stmtHasYields(stmt.body) ) {
throw new Error('Yields not supported in for statement... yet!');
if ( exprHasYields(stmt.right) ) {
stmt.right = this.expr(stmt.right, context);
}

if ( stmtHasYields(stmt.body) ) {
// Convert to for loop
context.ast['requires for-in-converter prelude'] = true;
var keys = context.temp('keys'),
idx = context.temp('i');
stmt.left.declarations[0].init = gen.member(keys, gen.ident(idx), { computed: true });

stmt = gen.block([
gen.varDecl(
keys, /*=*/ gen.call('__convertForIn', [stmt.right])
),
gen.varDecl(
idx, gen.val(0)
),
gen.whileStmt(
gen.binary(idx, '<', gen.member(keys, 'length')),
gen.block([
stmt.left,
stmt.body,
gen.exprStmt(gen.assign(
idx, /*=*/ gen.binary(idx, '+', gen.val(1))
))
])
)
]);

stmt.body[2].update = stmt.body[2].body.body[2];

this.stmt(stmt, context);
return gen.empty;
}

// Doesn't contain yields, but may contain other generators
Expand Down Expand Up @@ -97,6 +148,8 @@ var generatorTransformer = module.exports = traverser.explicit('generator', {
gen.exprStmt(stmt.update)
]))
])
stmt.body[1].update = stmt.body[1].body.body[1];
return this.stmt(stmt, context);
}
Expand Down Expand Up @@ -134,12 +187,36 @@ var generatorTransformer = module.exports = traverser.explicit('generator', {

var rec = context.temp('loop');


// In loop body, replace 'continue' with calls to
// recursive closure, and 'break' with flag

var continueStmt = gen.block([
stmt.update || gen.empty,
gen.ret(gen.callWithThis(rec))
]);

stmt.body = replaceContinuesInStmt(stmt.body, continueStmt);

if ( stmtHasBreak(stmt.body) ) {
var flag = context.temp('flag');
stmt.test = gen.binary(flag, '&&', stmt.test);
this.stmt(gen.varDecl(flag, gen.val(true)), context);
stmt.body = replaceBreaksInStmt(stmt.body, gen.block([
gen.exprStmt(gen.assign(flag, gen.val(false))),
continueStmt
]));
}


context.generator.withcc(callcc => {
this.stmt( gen.ifStmt(
stmt.test,
gen.block(stmt.body).body.concat([
gen.ret( gen.call(rec) )
gen.ret( gen.callWithThis(rec) )
])
), context );
Expand All @@ -155,12 +232,12 @@ var generatorTransformer = module.exports = traverser.explicit('generator', {
}

// Doesn't contain yields, but may contain other generators
var oldMonad = context.monad;
var oldGenerator = context.generator;
context.generator = null;

stmt = this.super.whileDoWhileStmt(stmt, context);

context.generator = oldMonad;
context.generator = oldGenerator;

return stmt;
},
Expand Down Expand Up @@ -581,6 +658,53 @@ function genGeneratorExpr(expr, context) {
}


var hasBreakTraverser = createBreakContinueTraverser(['BreakStatement'])

function stmtHasBreak(stmt) {
return hasBreakTraverser.stmtHasBreakContinue(stmt);
}
function replaceBreaksInStmt(stmt, replacement) {
var tr = createBreakContinueTraverser(['BreakStatement']);
tr.breakContinue = () => replacement;
return tr.stmt(stmt, tr.context());
}
function replaceContinuesInStmt(stmt, replacement) {
var tr = createBreakContinueTraverser(['ContinueStatement']);
tr.breakContinue = () => replacement;
return tr.stmt(stmt, tr.context());
}

function createBreakContinueTraverser(types) {
return traverser({
stmtHasBreakContinue: function (stmt) {
var context = this.context();
context.hasBreakContinue = false;
this.stmt(stmt, context);
return context.hasBreakContinue;
},

stmt: function (stmt, context) {
if ( types.indexOf(stmt.type) >= 0 ) {
stmt = this.breakContinue(stmt, context);
}
return this.super.stmt(stmt, context);
},

breakContinue: function (stmt, context) {
context.hasBreakContinue = true;
return stmt;
},

// Stop points
forStmt: x => x,
forInStmt: x => x,
whileDoWhileStmt: x => x,
func: x => x
});
}


var YIELD = '__generatorYield',
STOP = '__generatorStop',
GENERATOR_INSTANCE = '__generator',
Expand Down
5 changes: 0 additions & 5 deletions src/transformer/monad-to-generator.latte
Expand Up @@ -26,7 +26,6 @@ var monadTransformer = module.exports = traverser({

unary: function (expr, context) {
if ( expr.operator === "<<" ) {
console.log('YIELD')
if (!context.monad) {
throw new Error('Unexpected "<<": not in monad block!\nIn:' + dbg(expr));
}
Expand All @@ -40,8 +39,6 @@ var monadTransformer = module.exports = traverser({
},

monad: function (m, context) {
console.log('MONAD');
var lastMonad = context.monad;
var monad = context.monad = true;
Expand All @@ -53,8 +50,6 @@ var monadTransformer = module.exports = traverser({
var g = gen.closure(body);
g.calleeFn.generator = true;
console.log(dbg(g));
return gen.call('__monad', [ g ])
}
})
Expand Down
6 changes: 3 additions & 3 deletions src/traverser.latte
Expand Up @@ -100,9 +100,9 @@ function traverser(args) {
case 'ForStatement':
return me.forStmt(stmt, context)
case 'ForInStatement':
return me.forInStmt(stmt, context)
return me.forInOfStmt(stmt, context)
case 'ForOfStatement':
return me.forOfStmt(stmt, context)
return me.forInOfStmt(stmt, context)
case 'WhileStatement':
case 'DoWhileStatement':
Expand Down Expand Up @@ -172,7 +172,7 @@ function traverser(args) {
}

me._forInOfStmt = (stmt, context) => {
//stmt.right = me.expr(stmt.right, context)
stmt.right = me.expr(stmt.right, context)
stmt.body = me.stmt(stmt.body, context)
return stmt;
}
Expand Down
1 change: 1 addition & 0 deletions test/src/monad.latte
Expand Up @@ -205,6 +205,7 @@ describe('Monad Expression', function () {
for ( var i in {a:1, b:1, c:1} ) {
var v =<< async(i);
console.log(v);
if ( v == 'b' ) continue;
x += v;
}
Expand Down

0 comments on commit de97fbd

Please sign in to comment.