branch: master commit 7c8e32c2ab1da09dbd82a9a7f395c829022766dc Author: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com> Commit: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com>
Restore microooptimized, add more benchmarks, add tests. --- benchmark/scenarios.js | 35 +++--- scopifier-microoptimized.js | 123 ++++++++++++++++++++ test/.jslintrc | 4 + test/fixtures/monad.js | 12 -- test/fixtures/vow.js | 270 +++++++++++++++++++++++++++++++++++++++++++ test/fixtures/vow.json | 1 + test/scopifier.js | 11 -- test/specs.js | 53 +++++++++ 8 files changed, 469 insertions(+), 40 deletions(-) diff --git a/benchmark/scenarios.js b/benchmark/scenarios.js index f2fa386..879da5c 100644 --- a/benchmark/scenarios.js +++ b/benchmark/scenarios.js @@ -3,6 +3,7 @@ var fs = require('fs'), path = require('path'), scopifier = require('../scopifier'), + scopifierMicrooptimized = require('../scopifier-microoptimized'), jqueryPath = path.join(__dirname, 'fixtures', 'jquery-2.1.1.js'), lodashPath = path.join(__dirname, 'fixtures', 'lodash-2.4.1.js'), @@ -19,21 +20,18 @@ suite('scopifier', function () { next(error); }); }); - before(function (next) { fs.readFile(lodashPath, 'utf8', function (error, contents) { lodash = contents; next(error); }); }); - before(function (next) { fs.readFile(asyncPath, 'utf8', function (error, contents) { async = contents; next(error); }); }); - before(function (next) { fs.readFile(mkdirpPath, 'utf8', function (error, contents) { mkdirp = contents; @@ -41,20 +39,23 @@ suite('scopifier', function () { }); }); - bench('jquery', function () { - scopifier(jquery); - }); - - bench('lodash', function () { - scopifier(lodash); - }); - - bench('async', function () { - scopifier(async); - }); - - bench('mkdirp', function () { - scopifier(mkdirp); + [scopifier, scopifierMicrooptimized].forEach(function (scopifier, index) { + var message = ''; + if (index === 1) { + message = ' (microoptimized)'; + } + bench('jquery' + message, function () { + scopifier(jquery); + }); + bench('lodash' + message, function () { + scopifier(lodash); + }); + bench('async' + message, function () { + scopifier(async); + }); + bench('mkdirp' + message, function () { + scopifier(mkdirp); + }); }); }); diff --git a/scopifier-microoptimized.js b/scopifier-microoptimized.js new file mode 100644 index 0000000..1ef067d --- /dev/null +++ b/scopifier-microoptimized.js @@ -0,0 +1,123 @@ +'use strict'; + +var escope = require('escope'), + esprima = require('esprima'); + +// Given code, returns an array of `[level, start, end]' tokens for +// context-coloring. +module.exports = function (code) { + var analyzedScopes, + ast, + comment, + comments, + definition, + definitions, + i, + isDefined, + j, + k, + mappedDefinitions, + range, + reference, + references, + scope, + scopes = [], + symbols = [], + variable; + + // Gracefully handle parse errors by doing nothing. + try { + ast = esprima.parse(code, { + comment: true, + range: true + }); + analyzedScopes = escope.analyze(ast).scopes; + } catch (error) { + process.exit(1); + } + + for (i = 0; i < analyzedScopes.length; i += 1) { + scope = analyzedScopes[i]; + // Having its level set implies it was already annotated. + if (scope.level === undefined) { + if (scope.upper) { + if (scope.upper.functionExpressionScope) { + // Pretend function expression scope doesn't exist. + scope.level = scope.upper.level; + scope.variables = scope.upper.variables.concat(scope.variables); + } else { + scope.level = scope.upper.level + 1; + } + } else { + // Base case. + scope.level = 0; + } + // We've only given the scope a level for posterity's sake. We're + // done now. + if (!scope.functionExpressionScope) { + range = scope.block.range; + scopes.push([ + scope.level, + range[0] + 1, + range[1] + 1 + ]); + definitions = []; + for (j = 0; j < scope.variables.length; j += 1) { + variable = scope.variables[j]; + mappedDefinitions = []; + for (k = 0; k < variable.defs.length; k += 1) { + definition = variable.defs[k]; + range = definition.name.range; + mappedDefinitions.push([ + scope.level, + range[0] + 1, + range[1] + 1 + ]); + } + Array.prototype.push.apply(definitions, mappedDefinitions); + } + references = []; + for (j = 0; j < scope.references.length; j += 1) { + reference = scope.references[j]; + range = reference.identifier.range; + isDefined = false; + // Determine if a definition already exists for the + // range. (escope detects variables twice if they are + // declared and initialized simultaneously; this filters + // them.) + for (k = 0; k < definitions.length; k += 1) { + definition = definitions[k]; + if (definition[1] === range[0] + 1 && + definition[2] === range[1] + 1) { + isDefined = true; + break; + } + } + if (!isDefined) { + references.push([ + // Handle global references too. + reference.resolved ? reference.resolved.scope.level : 0, + range[0] + 1, + range[1] + 1 + ]); + } + } + Array.prototype.push.apply(symbols, definitions); + Array.prototype.push.apply(symbols, references); + } + } + } + + comments = []; + for (i = 0; i < ast.comments.length; i += 1) { + comment = ast.comments[i]; + range = comment.range; + comments.push([ + -1, + range[0] + 1, + range[1] + 1 + ]); + } + + return scopes.concat(symbols).concat(comments); +}; diff --git a/test/.jslintrc b/test/.jslintrc index 1202114..539f357 100644 --- a/test/.jslintrc +++ b/test/.jslintrc @@ -1,5 +1,9 @@ { "predef": [ + "after", + "afterEach", + "before", + "beforeEach", "describe", "it" ] diff --git a/test/fixtures/monad.js b/test/fixtures/monad.js deleted file mode 100644 index 6366fad..0000000 --- a/test/fixtures/monad.js +++ /dev/null @@ -1,12 +0,0 @@ -/* A monad. */ -function MONAD() { - abc = 3; - return function unit(value) { - // Some details. - var monad = Object.create(null); - monad.bind = function (func) { - return func(value); - }; - return monad; - }; -} diff --git a/test/fixtures/vow.js b/test/fixtures/vow.js new file mode 100644 index 0000000..374853e --- /dev/null +++ b/test/fixtures/vow.js @@ -0,0 +1,270 @@ +// vow.js +// Douglas Crockford +// 2013-09-20 + +// Public Domain + +/*global setImmediate */ + + +var VOW = (function () { + 'use strict'; + +// The VOW object contains a .make function that is used to make vows. +// It may also contain other useful functions. +// In some mythologies, 'VOW' is called 'deferrer'. + + function enlighten(queue, fate) { + +// enlighten is a helper function of herald and .when. It schedules the +// processing of all of the resolution functions in either the keepers queue +// or the breakers queue in later turns with the promise's fate. + + queue.forEach(function (func) { + setImmediate(func, fate); + }); + } + + return { + make: function make() { + +// The make function makes new vows. A vow contains a promise object and the +// two resolution functions (break and keep) that determine the fate of the +// promise. + + var breakers = [], // .when's broken queue + fate, // The promise's ultimate value + keepers = [], // .when's kept queue + status = 'pending'; // 'broken', 'kept', or 'pending' + + function enqueue( + resolution, // 'keep' or 'break' + func, // A function that was registered with .when + vow // A vow that provides the resolution functions + ) { + +// enqueue is a helper function used by .when. It will append a function to +// either the keepers queue or the breakers queue. + + var queue = resolution === 'keep' ? keepers : breakers; + queue[queue.length] = typeof func !== 'function' + +// If func is not a function, push the resolver so that the value passes to +// the next cascaded .when. + + ? vow[resolution] + +// If the func is a function, push a function that calls func with a value. +// The result can be a promise, or not a promise, or an exception. + + : function (value) { + try { + var result = func(value); + +// If the result is a promise, then register our resolver with that promise. + + if (result && result.is_promise === true) { + result.when(vow.keep, vow.break); + +// But if it is not a promise, then use the result to resolve our promise. + + } else { + vow.keep(result); + } + +// But if func throws an exception, then break our promise. + + } catch (e) { + vow.break(e); + } + }; + } + + function herald(state, value, queue) { + +// The herald function is a helper function of break and keep. +// It seals the promise's fate, updates its status, enlightens +// one of the queues, and empties both queues. + + if (status !== 'pending') { + throw "overpromise"; + } + fate = value; + status = state; + enlighten(queue, fate); + keepers.length = 0; + breakers.length = 0; + } + +// Construct and return the vow object. + + return { + 'break': function (value) { + +// The break method breaks the promise. + + herald('broken', value, breakers); + }, + keep: function keep(value) { + +// The keep method keeps the promise. + + herald('kept', value, keepers); + }, + promise: { + +// The promise is an object with a .when method. + + is_promise: true, + +// The .when method is the promise monad's bind. The .when method can take two +// optional functions. One of those functions may be called, depending on the +// promise's resolution. Both could be called if the the kept function throws. + + when: function (kept, broken) { + +// Make a new vow. Return the new promise. + + var vow = make(); + switch (status) { + +// If this promise is still pending, then enqueue both kept and broken. + + case 'pending': + enqueue('keep', kept, vow); + enqueue('break', broken, vow); + break; + +// If the promise has already been kept, then enqueue only the kept function, +// and enlighten it. + + case 'kept': + enqueue('keep', kept, vow); + enlighten(keepers, fate); + break; + +// If the promise has already been broken, then enqueue only the broken +// function, and enlighten it. + + case 'broken': + enqueue('break', broken, vow); + enlighten(breakers, fate); + break; + } + return vow.promise; + } + } + }; + }, + every: function every(array) { + +// The every function takes an array of promises and returns a promise that +// will deliver an array of results only if every promise is kept. + + var remaining = array.length, results = [], vow = VOW.make(); + + if (!remaining) { + vow.break(array); + } else { + array.forEach(function (promise, i) { + promise.when(function (value) { + results[i] = value; + remaining -= 1; + if (remaining === 0) { + vow.keep(results); + } + }, function (reason) { + remaining = NaN; + vow.break(reason); + }); + }); + } + return vow.promise; + }, + first: function first(array) { + +// The first function takes an array of promises and returns a promise to +// deliver the first observed kept promise, or a broken promise if all of +// the promises are broken. + + var found = false, remaining = array.length, vow = VOW.make(); + + function check() { + remaining -= 1; + if (remaining === 0 && !found) { + vow.break(); + } + } + + if (remaining === 0) { + vow.break(array); + } else { + array.forEach(function (promise) { + promise.when(function (value) { + if (!found) { + found = true; + vow.keep(value); + } + check(); + }, check); + }); + } + return vow.promise; + }, + any: function any(array) { + +// The any function takes an array of promises and returns a promise that +// will deliver a possibly sparse array of results of any kept promises. +// The result will contain an undefined element for each broken promise. + + var remaining = array.length, results = [], vow = VOW.make(); + + function check() { + remaining -= 1; + if (remaining === 0) { + vow.keep(results); + } + } + + if (!remaining) { + vow.keep(results); + } else { + array.forEach(function (promise, i) { + promise.when(function (value) { + results[i] = value; + check(); + }, check); + }); + } + return vow.promise; + }, + kept: function (value) { + +// Returns a new kept promise. + + var vow = VOW.make(); + vow.keep(value); + return vow.promise; + }, + broken: function (reason) { + +// Returns a new broken promise. + + var vow = VOW.make(); + vow.break(reason); + return vow.promise; + } + }; +}()); + + +// If your system does not have setImmediate, then simulate it with setTimeout. + +if (typeof setImmediate !== 'function') { + setImmediate = function setImmediate(func, param) { + 'use strict'; + return setTimeout(function () { + func(param); + }, 0); + }; +} diff --git a/test/fixtures/vow.json b/test/fixtures/vow.json new file mode 100644 index 0000000..8eb2d6d --- /dev/null +++ b/test/fixtures/vow.json @@ -0,0 +1 @@ +[[0,92,8470],[1,103,8174],[2,311,656],[3,583,648],[2,685,5093],[3,1155,2682],[4,1964,2667],[5,2566,2645],[3,2696,3201],[3,3290,3423],[3,3447,3579],[3,3961,5050],[2,5110,5975],[3,5469,5917],[4,5526,5770],[4,5772,5897],[2,5992,6931],[3,6282,6446],[3,6568,6873],[4,6622,6846],[2,6946,7814],[3,7277,7437],[3,7555,7756],[4,7612,7729],[2,7830,7986],[2,8004,8165],[1,8323,8467],[2,8408,8456],[0,96,99],[0,8273,8285],[0,8308,8320],[1,320,329],[2,330,335],[2,337,341],[2,569,574],[3,593,597],[0,613,62 [...] diff --git a/test/scopifier.js b/test/scopifier.js deleted file mode 100644 index 3f87ef4..0000000 --- a/test/scopifier.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var assert = require('assert'); - -describe('scopifier', function () { - - it('should work', function () { - assert.strictEqual(true, true); - }); - -}); diff --git a/test/specs.js b/test/specs.js new file mode 100644 index 0000000..c015ddb --- /dev/null +++ b/test/specs.js @@ -0,0 +1,53 @@ +'use strict'; + +var assert = require('assert'), + fs = require('fs'), + path = require('path'), + + scopifier = require('../scopifier'), + scopifierMicrooptimized = require('../scopifier-microoptimized'), + + inputPath = path.join(__dirname, 'fixtures', 'vow.js'), + outputPath = path.join(__dirname, 'fixtures', 'vow.json'); + +describe('scopifier', function () { + + var input, output; + + before(function (done) { + fs.readFile(inputPath, 'utf8', function (error, contents) { + if (error) { + done(error); + return; + } + input = contents; + done(); + }); + }); + + before(function (done) { + fs.readFile(outputPath, 'utf8', function (readError, contents) { + if (readError) { + done(readError); + return; + } + try { + output = JSON.parse(contents); + } catch (parseError) { + done(parseError); + } + done(); + }); + }); + + [scopifier, scopifierMicrooptimized].forEach(function (scopifier, index) { + var message = ''; + if (index === 1) { + message = ' (microoptimized)'; + } + it('should work' + message, function () { + assert.deepEqual(scopifier(input), output); + }); + }); + +});