test-transforms.js
'use strict';
var _ = require('underscore');
var parse = require('esprima').parse;
var unparse = require('escodegen').generate;
var thunkify = require('../src/syntax').thunkify;
var fail = require('../src/syntax').fail;
var naming = require('../src/transforms/naming').naming;
var cps = require('../src/transforms/cps').cps;
var store = require('../src/transforms/store').store;
var optimize = require('../src/transforms/optimize').optimize;
var trampoline = require('../src/transforms/trampoline').trampoline;
var varargs = require('../src/transforms/varargs').varargs;
var fooObj = {
bar: 1,
baz: {
blubb: 2,
bla: 3
}
};
var plus, minus, times, and, plusTwo;
function compose() {
var fs = Array.prototype.concat.apply([], arguments);
return function(x) {
return fs.reduceRight(function(x, f) {
return f(x);
}, x);
};
}
function runTest(test, code, expected, transformAst, run) {
var newCode = unparse(transformAst(parse(code)));
try {
run(test, code, newCode, expected);
}
catch (e) {
console.log('Exception:', e);
console.log(newCode);
test.ok(false);
test.done();
}
}
function check(test, code, newCode, expected, actual) {
var success = _.isEqual(expected, actual);
test.ok(success);
if (!success) {
console.log(code);
console.log(newCode);
console.log('Expected:', expected);
console.log('Actual:', actual);
}
test.done();
}
var transformAstNaming = compose(naming, function(node) {
return thunkify(node, fail('transform', node));
});
function runNaming(test, code, newCode, expected) {
check(test, code, newCode, expected, eval(newCode)(''));
}
var transformAstCps = compose(cps, transformAstNaming);
function runCps(test, code, newCode, expected) {
eval(newCode)(function(actual) {
check(test, code, newCode, expected, actual);
}, '');
}
var transformAstStorepassing = compose(store, transformAstCps);
function runStorepassing(test, code, newCode, expected) {
eval(newCode)({}, function(store, actual) {
check(test, code, newCode, expected, actual);
}, '');
}
var transformAstOptimize = compose(optimize, transformAstStorepassing);
var runOptimize = runStorepassing;
var transformAstVarargs = compose(varargs, transformAstOptimize);
var runVarargs = runOptimize;
var transformAstTrampoline = compose(trampoline, transformAstVarargs);
var runTrampoline = runVarargs;
var selectNamingPrimitives = function() {
// Set global definitions
plus = function(a, x, y) {
return (x + y);
};
minus = function(a, x, y) {
return (x - y);
};
times = function(a, x, y) {
return (x * y);
};
and = function(a, x, y) {
return (x && y);
};
plusTwo = function(a, x, y) {
return (x + 2);
};
};
function selectCpsPrimitives() {
// Set global definitions
plus = function(k, a, x, y) {
return k(x + y);
};
minus = function(k, a, x, y) {
return k(x - y);
};
times = function(k, a, x, y) {
return k(x * y);
};
and = function(k, a, x, y) {
return k(x && y);
};
plusTwo = function(k, a, x, y) {
return k(x + 2);
};
}
function selectStorePrimitives() {
// Set global definitions
plus = function(s, k, a, x, y) {
return k(s, x + y);
};
minus = function(s, k, a, x, y) {
return k(s, x - y);
};
times = function(s, k, a, x, y) {
return k(s, x * y);
};
and = function(s, k, a, x, y) {
return k(s, x && y);
};
plusTwo = function(s, k, a, x, y) {
return k(s, x + 2);
};
}
var selectOptimizePrimitives = selectStorePrimitives;
var selectVarargsPrimitives = selectOptimizePrimitives;
var selectTrampolinePrimitives = selectVarargsPrimitives;
function runNamingTest(test, code, expected) {
selectNamingPrimitives();
return runTest(test, code, expected, transformAstNaming, runNaming);
}
function runCpsTest(test, code, expected) {
selectCpsPrimitives();
return runTest(test, code, expected, transformAstCps, runCps);
}
function runStorepassingTest(test, code, expected) {
selectStorePrimitives();
return runTest(test, code, expected, transformAstStorepassing, runStorepassing);
}
function runOptimizeTest(test, code, expected) {
selectOptimizePrimitives();
return runTest(test, code, expected, transformAstOptimize, runOptimize);
}
function runVarargsTest(test, code, expected) {
selectVarargsPrimitives();
return runTest(test, code, expected, transformAstVarargs, runVarargs);
}
function runTrampolineTest(test, code, expected) {
selectTrampolinePrimitives();
return runTest(test, code, expected, transformAstTrampoline, runTrampoline);
}
function generateTestFunctions(allTests, testRunner) {
var exports = {};
for (var testClassName in allTests) {
if (allTests.hasOwnProperty(testClassName)) {
var tests = allTests[testClassName];
exports[testClassName] = {};
tests.forEach(
function(obj) {
exports[testClassName][obj.name] = function(test) {
if (!obj.runners || _.contains(obj.runners, testRunner)) {
return testRunner(test, obj.code, obj.expected);
} else {
test.done();
}
};
});
}
}
return exports;
}
var tests = {
testFunctionExpression: [
{ name: 'testFunc1',
code: 'var f = function(x){return plus(x, 10)}; f(3)',
expected: 13 },
{ name: 'testRecursion',
code: 'var f = function(x, n){return n==0 ? x : f(plusTwo(x), n-1);}; f(3, 4)',
expected: 11 },
{ name: 'testDefinitionOnly1',
code: 'var bar = function(){ var foo = function(){ return 3;} }; 5;',
expected: 5 },
{ name: 'testDefinitionOnly2',
code: 'var bar = function(){ var foo = function(){ return 3;}; var foo2 = function(){ return 4;} }; 5;',
expected: 5 },
{ name: 'testReturn1',
code: 'var foo = function(){ return 1; return 2; }; foo()',
expected: 1 },
{ name: 'testReturn2',
code: 'var foo = function(){ (function(){ return 1})(); return 2; }; foo()',
expected: 2 }
],
testCallExpression: [
{ name: 'testPrimitive',
code: 'plusTwo(3)',
expected: 5 },
{ name: 'testCompound1',
code: '(function(y){return plusTwo(y)})(123)',
expected: 125 },
{ name: 'testCompound2',
code: '(function(y){return y})(plusTwo(123))',
expected: 125 },
{ name: 'testHigherOrder1',
code: ['var foo = function(func1, func2) {',
' return function(x) {',
' return func1(x)(func2(x))',
' }',
'};',
'var f1 = function(y){return function(x){return x * y;}};',
'var f2 = function(x){return x + 1;}',
'foo(f1, f2)(3)'].join('\n'),
expected: 12 },
{ name: 'testBinaryFuncPlus',
code: 'plus(3, 5)',
expected: 8 },
{ name: 'testBinaryFuncMinus',
code: 'minus(3, 5)',
expected: -2 },
{ name: 'testBinaryFuncAnd',
code: 'and(true, false)',
expected: false }
],
testLiterals: [
{ name: 'testNumber',
code: '456',
expected: 456
},
{ name: 'testString',
code: "'foobar'",
expected: 'foobar'
},
{ name: 'testBool1',
code: 'true',
expected: true
},
{ name: 'testBool2',
code: 'false',
expected: false
}
],
testEmptyStatement: [
{ name: 'testEmptyAlone',
code: ';',
expected: undefined },
{ name: 'testEmptyInBlock',
code: 'plusTwo(3); ; plusTwo(5);',
expected: 7 }
],
testDebuggerStatement: [
{ name: 'test',
code: 'debugger; true;',
expected: true }
],
testBlockStatement: [
{ name: 'testProgram',
code: 'plusTwo(3); plusTwo(4); plusTwo(5);',
expected: 7 },
{ name: 'testBlock1',
code: '{ plusTwo(3) }; plusTwo(5);',
expected: 7 },
{ name: 'testBlock2',
code: 'plusTwo(1); { plusTwo(3) }; plusTwo(5);',
expected: 7 },
{ name: 'testBlock3',
code: 'plusTwo(1); { plusTwo(3); plusTwo(4); }; plusTwo(5);',
expected: 7 },
{ name: 'testBlock4',
code: 'plusTwo(1); { plusTwo(3); plusTwo(4); }',
expected: 6 },
{ name: 'testBlock5',
code: ('var identity = function(x){return x};' +
"var obj1 = identity({ 'X': 10, 'Y': 20 });" +
'var foo = function(){ return baz(); };' + // forward-recursive
"var baz = function(){ return obj1['X']; };" +
'foo();'),
expected: 10,
runners: [runOptimizeTest, runTrampolineTest] }
],
testVariableDeclaration: [
{ name: 'testVar1',
code: 'var x = 1; x',
expected: 1 },
{ name: 'testVar2',
code: 'var x = plus(1, 2); var y = times(x, 4); y',
expected: 12 }
],
testConditionalExpression: [
{ name: 'testConditional1',
code: 'false ? 1 : 2',
expected: 2 },
{ name: 'testConditional2',
code: 'true ? 1 : 2',
expected: 1 },
{ name: 'testConditional3',
code: 'and(true, false) ? 2 : 3',
expected: 3 },
{ name: 'testConditional4',
code: [
'var id = function(x){return x};',
'var x = undefined;',
'var a = ((x === undefined) ? false : id(x.foo)) || true;',
'a'
].join('\n'),
expected: true }
],
testIfExpression: [
{ name: 'testIf1',
code: 'var foo = function(x){if (x > 2) { return 1 } else { return 2 }}; foo(3)',
expected: 1 },
{ name: 'testIf2',
code: 'var foo = function(x){if (x > 2) { return 1 } else { return 2 }}; foo(1)',
expected: 2 },
{ name: 'testIf3',
code: 'var foo = function(x){if (x > 2) { return 1 } else { return 2 }}; foo(foo(5))',
expected: 2 },
{ name: 'testIfWithoutElse1',
code: 'var foo = function(x){if (x > 2) { return 1 }}; foo(5)',
expected: 1 },
{ name: 'testIfWithoutElse2',
code: 'var foo = function(x){if (x > 2) { return 1 }}; foo(0)',
expected: undefined },
{ name: 'testIfWithoutElse3',
code: 'var f = function(){ if (1 < 2) { var x = 1; var y = 2; return x + y; }}; f();',
expected: 3 },
{ name: 'testIfWithoutElse4',
code: 'var f = function(){ if (1 < 0) { return 1; }; return 5; }; f();',
expected: 5 },
{ name: 'testIfWithoutElse5',
code: 'var f = function(){ if (false) { "bad"; }; }; f();',
expected: undefined },
{ name: 'testNestedIf',
code: 'if (1 > 2) { 3 } else { if (4 < 5) { 6 } else { 7 }}',
expected: 6 },
{ name: 'testNestedIf2',
code: 'var f = function(){ if (true) { "bad"; }; return "good"; }; f();',
expected: 'good' },
{ name: 'testIfWithReturn',
code: 'var foo = function(){ if (true) { return 3 } return 4 }; foo()',
expected: 3 },
{ name: 'testIfInNestedFunction',
code: ('var foo = function(x){' +
' var bar = function(y){' +
' if (y == 10) {' +
' return 3;' +
' } else {' +
' return 4;' +
' }' +
' };' +
' var z = bar(x);' +
' if (z === 3){' +
' return 1;' +
' } else {' +
' return 2;' +
' };' +
'};' +
'foo(10);'),
expected: 1 }
],
testArrayExpression: [
{ name: 'testArray1',
code: '[1, 2, 3]',
expected: [1, 2, 3] },
{ name: 'testArray2',
code: '[plusTwo(1), plus(2, 5), 3]',
expected: [3, 7, 3] }
],
testObjectExpression: [
{ name: 'testObjectAtomic',
code: 'var x = {a: 1, b: 2}; x',
expected: {a: 1, b: 2} },
{ name: 'testObjectCompound',
code: 'var x = {a: 1+2, b: [4,5]}; x',
expected: {a: 3, b: [4, 5]} },
{ name: 'testNestedObject',
code: ['var box = {',
' sub: {',
' f: function(x1){',
' return function(x2){',
' return x1 + x2;',
' }',
' }',
' }',
'}',
'',
'var g = box.sub.f;',
'g(1)(2)'].join('\n'),
expected: 3 }
],
testMemberExpression: [
{ name: 'testMember1',
code: 'fooObj.bar',
expected: 1 },
{ name: 'testMember2',
code: 'fooObj.baz.blubb',
expected: 2 },
{ name: 'testMember3',
code: 'var a = [1,2]; a[1]',
expected: 2 }
],
testNAryExpression: [
{ name: 'testPlus',
code: '3 + 4',
expected: 7 },
{ name: 'testUnary',
code: '-5',
expected: -5 },
{ name: 'testCompound1',
code: '(-3 + (4 * 5)) - 10',
expected: 7 },
{ name: 'testCompound2',
code: 'var f = function(x){return 2*x + 4;}; (-3 + f(4 * 5)) - f(10)',
expected: 17 }
],
testLogicalExpression: [
{ name: 'testLogicalOr',
code: 'true || false',
expected: true },
{ name: 'testLogicalNot',
code: '!(true || true)',
expected: false },
{ name: 'testLogicalAnd',
code: 'true && false',
expected: false },
{ name: 'testLogicalCompound1',
code: 'true && (false || false || true)',
expected: true },
{ name: 'testLogicalCompound2',
code: '!(true && (false || false || true))',
expected: false },
{ name: 'testLazyLogical',
code: [
'var id = function(x){return x};',
'var x = undefined;',
'var a = true || id(x.foo);',
'a'
].join('\n'),
expected: true }
],
testPrimitiveWrapping: [
{ name: 'testMath',
code: 'Math.log(Math.exp(5))',
expected: 5 },
{ name: 'testCompound',
code: 'var f = function(x){return Math.log(x);}; Math.exp(f(17))',
expected: 17 },
{ name: 'testMemberFromFn',
code: 'var foo = function() {return [1]}; foo().concat([2])',
expected: [1, 2] }
],
testVarargs: [
{ name: 'testVarargs1',
code: ('var foo = function(){return arguments[0] + arguments[1]};' +
'foo(3, 4);'),
expected: 7,
runners: [runVarargsTest, runTrampolineTest] },
{ name: 'testVarargs2',
code: ('var bar = function(){return arguments[0]*2};' +
'var foo = function(){return bar(arguments[0] + arguments[1]);};' +
'foo(3, 4);'),
expected: 14,
runners: [runVarargsTest, runTrampolineTest] },
{ name: 'testVarargs3',
code: ('var foo = function(x, y){var f = function(){ return arguments[0]}; return f(y)};' +
'foo(3, 4);'),
expected: 4,
runners: [runVarargsTest, runTrampolineTest] },
// FIXME: This test currently fails because varargs happens after
// cps which introduces additional closures. To fix this,
// move the varargs transform up earlier in the order of
// transforms?
// { name: 'testVarargs4',
// code: ("var bar = function(){return function(xs){return xs;}};;" +
// "var foo = function(){return bar()(arguments)};" +
// "foo(3, 4);"),
// expected: [3, 4],
// runners: [runVarargsTest, runTrampolineTest] },
{ name: 'testApply',
code: ('var foo = function(x, y){return x + y};' +
'var bar = function(){ return apply(foo, arguments); };' +
'bar(3, 4);'),
expected: 7,
runners: [runVarargsTest, runTrampolineTest] }
]
};
exports.testNaming = generateTestFunctions(tests, runNamingTest);
exports.testCps = generateTestFunctions(tests, runCpsTest);
exports.testStorepassing = generateTestFunctions(tests, runStorepassingTest);
exports.testOptimize = generateTestFunctions(tests, runOptimizeTest);
exports.testTrampoline = generateTestFunctions(tests, runTrampolineTest);
exports.testVarargs = generateTestFunctions(tests, runVarargsTest);
exports.testTrampoline = generateTestFunctions(tests, runTrampolineTest);