├── .gitignore
├── README.md
├── demo.html
├── esoptimize.js
├── esscope.js
├── package.json
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Esoptimize
2 |
3 | Esoptimize is a JavaScript optimizer that is designed to work well with [esprima](http://github.com/Constellation/esprima) and [escodegen](http://github.com/Constellation/escodegen).
4 |
5 | ### Usage
6 |
7 | Esoptimize can be installed using `npm install esoptimize` and used by calling `esoptimize.optimize(ast)` where `ast` is a JavaScript abstract syntax tree that conforms to the [SpiderMonkey Parser API](https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API) format.
8 |
9 | ### Features
10 |
11 | * Constant propagation
12 | * Dead code elimination
13 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Esoptimize Demo
6 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Esoptimize Demo
49 |
50 | This is a demo of esoptimize , a JavaScript AST optimizer.
51 | It performs constant folding and dead code elimination.
52 |
53 |
54 |
55 |
56 |
57 | Input
58 |
61 |
62 |
63 | Output
64 |
65 |
66 |
67 |
68 |
69 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/esoptimize.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | var esscope = typeof window !== 'undefined' ? window.esscope : require('./esscope');
5 | var estraverse = typeof window !== 'undefined' ? window.estraverse : require('estraverse');
6 | var esoptimize = typeof window !== 'undefined' ? (window.esoptimize = {}) : exports;
7 |
8 | var isValidIdentifier = new RegExp('^(?!(?:' + [
9 | 'do',
10 | 'if',
11 | 'in',
12 | 'for',
13 | 'let',
14 | 'new',
15 | 'try',
16 | 'var',
17 | 'case',
18 | 'else',
19 | 'enum',
20 | 'eval',
21 | 'false',
22 | 'null',
23 | 'this',
24 | 'true',
25 | 'void',
26 | 'with',
27 | 'break',
28 | 'catch',
29 | 'class',
30 | 'const',
31 | 'super',
32 | 'throw',
33 | 'while',
34 | 'yield',
35 | 'delete',
36 | 'export',
37 | 'import',
38 | 'public',
39 | 'return',
40 | 'static',
41 | 'switch',
42 | 'typeof',
43 | 'default',
44 | 'extends',
45 | 'finally',
46 | 'package',
47 | 'private',
48 | 'continue',
49 | 'debugger',
50 | 'function',
51 | 'arguments',
52 | 'interface',
53 | 'protected',
54 | 'implements',
55 | 'instanceof'
56 | ].join('|') + ')$)[$A-Z_a-z][$A-Z_a-z0-9]*$');
57 |
58 | var oppositeOperator = {
59 | '&&': '||',
60 | '||': '&&',
61 | '<': '>=',
62 | '>': '<=',
63 | '<=': '>',
64 | '>=': '<',
65 | '==': '!=',
66 | '!=': '==',
67 | '!==': '===',
68 | '===': '!=='
69 | };
70 |
71 | var parent = null;
72 | var scope = null;
73 |
74 | function assert(truth) {
75 | if (!truth) {
76 | throw new Error('assertion failed');
77 | }
78 | }
79 |
80 | function hasSideEffects(node) {
81 | if (node.type === 'Literal' || node.type === 'Identifier' || node.type === 'FunctionExpression') {
82 | return false;
83 | }
84 |
85 | if (node.type === 'MemberExpression') {
86 | return hasSideEffects(node.object) || hasSideEffects(node.property);
87 | }
88 |
89 | if (node.type === 'SequenceExpression') {
90 | return node.expressions.some(hasSideEffects);
91 | }
92 |
93 | if (node.type === 'ArrayExpression') {
94 | return node.elements.some(hasSideEffects);
95 | }
96 |
97 | if (node.type === 'ObjectExpression') {
98 | return node.properties.some(function(property) {
99 | return hasSideEffects(property.value);
100 | });
101 | }
102 |
103 | return true;
104 | }
105 |
106 | function declareScopeVariables() {
107 | var variables = scope.variables;
108 | var node = scope.node;
109 |
110 | variables = variables.filter(function(variable) {
111 | return variable.isVariable() && !variable.isArgument();
112 | });
113 |
114 | if (variables.length === 0) {
115 | return {
116 | type: 'EmptyStatement'
117 | };
118 | }
119 |
120 | return {
121 | type: 'VariableDeclaration',
122 | declarations: variables.map(function(variable) {
123 | return {
124 | type: 'VariableDeclarator',
125 | id: {
126 | type: 'Identifier',
127 | name: variable.name
128 | },
129 | init: null
130 | };
131 | }),
132 | kind: 'var'
133 | };
134 | }
135 |
136 | function normalize(node) {
137 | return estraverse.replace(node, wrapVisitorScope({
138 | leave: function(node) {
139 | // Hoist global variables
140 | if (node.type === 'Program') {
141 | return {
142 | type: 'Program',
143 | body: [declareScopeVariables()].concat(node.body)
144 | };
145 | }
146 |
147 | // Hoist local variables
148 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
149 | return {
150 | type: node.type,
151 | id: node.id,
152 | params: node.params,
153 | defaults: node.defaults,
154 | body: {
155 | type: 'BlockStatement',
156 | body: [declareScopeVariables()].concat(node.body.body)
157 | },
158 | rest: node.rest,
159 | generator: node.generator,
160 | expression: node.expression
161 | };
162 | }
163 |
164 | if (node.type === 'Property') {
165 | assert(node.key.type === 'Literal' || node.key.type === 'Identifier');
166 | return {
167 | type: 'Property',
168 | key: {
169 | type: 'Literal',
170 | value: node.key.type === 'Literal' ? node.key.value + '' : node.key.name
171 | },
172 | value: node.value,
173 | kind: node.kind
174 | };
175 | }
176 |
177 | if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier') {
178 | return {
179 | type: 'MemberExpression',
180 | computed: true,
181 | object: node.object,
182 | property: {
183 | type: 'Literal',
184 | value: node.property.name
185 | }
186 | };
187 | }
188 |
189 | if (node.type === 'VariableDeclaration') {
190 | var expressions = node.declarations.filter(function(node) {
191 | return node.init !== null;
192 | }).map(function(node) {
193 | return {
194 | type: 'AssignmentExpression',
195 | operator: '=',
196 | left: node.id,
197 | right: node.init
198 | }
199 | });
200 |
201 | if (expressions.length === 0) {
202 | return {
203 | type: 'EmptyStatement'
204 | };
205 | }
206 |
207 | return {
208 | type: 'ExpressionStatement',
209 | expression: {
210 | type: 'SequenceExpression',
211 | expressions: expressions
212 | }
213 | };
214 | }
215 |
216 | if (node.type === 'ForStatement' && node.init !== null) {
217 | return {
218 | type: 'ForStatement',
219 | init:
220 | node.init.type === 'EmptyStatement' ? null :
221 | node.init.type === 'ExpressionStatement' ? node.init.expression :
222 | node.init,
223 | test: node.test,
224 | update: node.update,
225 | body: node.body
226 | };
227 | }
228 | }
229 | }));
230 | }
231 |
232 | function denormalize(node) {
233 | return estraverse.replace(node, {
234 | leave: function(node) {
235 | if (node.type === 'Literal') {
236 | if (node.value === void 0) {
237 | return {
238 | type: 'UnaryExpression',
239 | operator: 'void',
240 | argument: {
241 | type: 'Literal',
242 | value: 0
243 | }
244 | };
245 | }
246 |
247 | if (typeof node.value === 'number') {
248 | if (isNaN(node.value)) {
249 | return {
250 | type: 'BinaryExpression',
251 | operator: '/',
252 | left: {
253 | type: 'Literal',
254 | value: 0
255 | },
256 | right: {
257 | type: 'Literal',
258 | value: 0
259 | }
260 | }
261 | }
262 |
263 | if (!isFinite(node.value)) {
264 | return {
265 | type: 'BinaryExpression',
266 | operator: '/',
267 | left: node.value < 0 ? {
268 | type: 'UnaryExpression',
269 | operator: '-',
270 | argument: {
271 | type: 'Literal',
272 | value: 1
273 | }
274 | } : {
275 | type: 'Literal',
276 | value: 1
277 | },
278 | right: {
279 | type: 'Literal',
280 | value: 0
281 | }
282 | }
283 | }
284 |
285 | if (node.value < 0) {
286 | return {
287 | type: 'UnaryExpression',
288 | operator: '-',
289 | argument: {
290 | type: 'Literal',
291 | value: -node.value
292 | }
293 | };
294 | }
295 |
296 | if (node.value === 0 && 1 / node.value < 0) {
297 | return {
298 | type: 'Literal',
299 | value: 0
300 | };
301 | }
302 | }
303 | }
304 |
305 | if (node.type === 'Property') {
306 | var key = node.key;
307 | assert(key.type === 'Literal');
308 | if (isValidIdentifier.test(key.value)) {
309 | key = {
310 | type: 'Identifier',
311 | name: key.value
312 | };
313 | }
314 | return {
315 | type: 'Property',
316 | key: key,
317 | value: node.value,
318 | kind: node.kind
319 | };
320 | }
321 |
322 | if (node.type === 'MemberExpression' && node.computed && node.property.type === 'Literal' && isValidIdentifier.test(node.property.value)) {
323 | return {
324 | type: 'MemberExpression',
325 | computed: false,
326 | object: node.object,
327 | property: {
328 | type: 'Identifier',
329 | name: node.property.value
330 | }
331 | };
332 | }
333 | }
334 | });
335 | }
336 |
337 | function foldConstants(node) {
338 | return estraverse.replace(node, {
339 | enter: function(node) {
340 | if (node.type === 'UnaryExpression' && node.operator === '!') {
341 | if (node.argument.type === 'BinaryExpression' && node.argument.operator in oppositeOperator) {
342 | return {
343 | type: 'BinaryExpression',
344 | operator: oppositeOperator[node.argument.operator],
345 | left: node.argument.left,
346 | right: node.argument.right
347 | };
348 | }
349 |
350 | if (node.argument.type === 'LogicalExpression' && node.argument.operator in oppositeOperator) {
351 | return {
352 | type: 'LogicalExpression',
353 | operator: oppositeOperator[node.argument.operator],
354 | left: {
355 | type: 'UnaryExpression',
356 | operator: '!',
357 | argument: node.argument.left
358 | },
359 | right: {
360 | type: 'UnaryExpression',
361 | operator: '!',
362 | argument: node.argument.right
363 | }
364 | };
365 | }
366 | }
367 | },
368 |
369 | leave: function(node) {
370 | if (node.type === 'SequenceExpression') {
371 | var expressions = node.expressions;
372 |
373 | expressions = Array.prototype.concat.apply([], expressions.map(function(node) {
374 | return node.type === 'SequenceExpression' ? node.expressions : node;
375 | }));
376 |
377 | expressions = expressions.slice(0, -1).filter(hasSideEffects).concat(expressions.slice(-1));
378 |
379 | if (expressions.length > 1) {
380 | return {
381 | type: 'SequenceExpression',
382 | expressions: expressions
383 | };
384 | }
385 |
386 | return expressions[0];
387 | }
388 |
389 | if (node.type === 'UnaryExpression' && node.argument.type === 'Literal') {
390 | var operator = new Function('a', 'return ' + node.operator + ' a;');
391 | return {
392 | type: 'Literal',
393 | value: operator(node.argument.value)
394 | }
395 | }
396 |
397 | if ((node.type === 'BinaryExpression' || node.type === 'LogicalExpression') && node.left.type === 'Literal' && node.right.type === 'Literal') {
398 | var operator = new Function('a', 'b', 'return a ' + node.operator + ' b;');
399 | return {
400 | type: 'Literal',
401 | value: operator(node.left.value, node.right.value)
402 | };
403 | }
404 |
405 | if (node.type === 'ConditionalExpression' && node.test.type === 'Literal') {
406 | return node.test.value ? node.consequent : node.alternate;
407 | }
408 |
409 | if (node.type === 'MemberExpression' && node.property.type === 'Literal' && !hasSideEffects(node.object)) {
410 | assert(node.computed);
411 |
412 | if (node.object.type === 'ObjectExpression') {
413 | for (var i = 0; i < node.object.properties.length; i++) {
414 | var property = node.object.properties[i];
415 | assert(property.key.type === 'Literal' && typeof property.key.value === 'string');
416 | if (property.key.value === node.property.value + '') {
417 | return property.value;
418 | }
419 | }
420 | }
421 |
422 | if (node.object.type === 'Literal' && typeof node.object.value === 'string') {
423 | if (node.property.value === 'length') {
424 | return {
425 | type: 'Literal',
426 | value: node.object.value.length
427 | };
428 | }
429 |
430 | if (typeof node.property.value === 'number') {
431 | // Check for a match inside the string literal
432 | var index = node.property.value >>> 0;
433 | if (index === +node.property.value && index < node.object.value.length) {
434 | return {
435 | type: 'Literal',
436 | value: node.object.value[index]
437 | };
438 | }
439 |
440 | // Optimize to an empty string literal (may still be a numeric property on String.prototype)
441 | return {
442 | type: 'MemberExpression',
443 | computed: true,
444 | object: {
445 | type: 'Literal',
446 | value: ''
447 | },
448 | property: node.property
449 | }
450 | }
451 | }
452 |
453 | if (node.object.type === 'ArrayExpression') {
454 | if (node.property.value === 'length') {
455 | return {
456 | type: 'Literal',
457 | value: node.object.elements.length
458 | };
459 | }
460 |
461 | if (typeof node.property.value === 'number') {
462 | // Check for a match inside the array literal
463 | var index = node.property.value >>> 0;
464 | if (index === +node.property.value && index < node.object.elements.length) {
465 | return node.object.elements[index];
466 | }
467 |
468 | // Optimize to an empty array literal (may still be a numeric property on Array.prototype)
469 | return {
470 | type: 'MemberExpression',
471 | computed: true,
472 | object: {
473 | type: 'ArrayExpression',
474 | elements: []
475 | },
476 | property: node.property
477 | }
478 | }
479 | }
480 | }
481 | }
482 | });
483 | }
484 |
485 | function filterDeadCode(nodes) {
486 | return nodes.filter(function(node) {
487 | if (node.type === 'EmptyStatement') {
488 | return false;
489 | }
490 |
491 | if (node.type === 'VariableDeclaration' && node.declarations.length === 0) {
492 | return false;
493 | }
494 |
495 | // Users won't like it if we remove 'use strict' directives
496 | if (node.type === 'ExpressionStatement' && !hasSideEffects(node.expression) &&
497 | (node.expression.type !== 'Literal' || node.expression.value !== 'use strict')) {
498 | return false;
499 | }
500 |
501 | return true;
502 | });
503 | }
504 |
505 | function flattenNodeList(nodes) {
506 | return Array.prototype.concat.apply([], nodes.map(function(node) {
507 | if (node.type === 'BlockStatement') {
508 | return node.body;
509 | }
510 |
511 | if (node.type === 'ExpressionStatement' && node.expression.type === 'SequenceExpression') {
512 | return flattenNodeList(node.expression.expressions).map(function(node) {
513 | return {
514 | type: 'ExpressionStatement',
515 | expression: node
516 | }
517 | });
518 | }
519 |
520 | if (node.type === 'SequenceExpression') {
521 | return flattenNodeList(node.expressions);
522 | }
523 |
524 | return node;
525 | }));
526 | }
527 |
528 | function hoistUseStrict(nodes) {
529 | var useStrict = false;
530 |
531 | nodes = nodes.filter(function(node) {
532 | if (node.type === 'ExpressionStatement' && node.expression.type === 'Literal' && node.expression.value === 'use strict') {
533 | useStrict = true;
534 | return false;
535 | }
536 | return true;
537 | });
538 |
539 | if (useStrict) {
540 | return [{
541 | type: 'ExpressionStatement',
542 | expression: {
543 | type: 'Literal',
544 | value: 'use strict'
545 | }
546 | }].concat(nodes);
547 | }
548 |
549 | return nodes;
550 | }
551 |
552 | function canRemoveVariable(name) {
553 | var variable = scope.variableForName(name);
554 | assert(variable !== null);
555 | return !variable.isGlobal() && !variable.isArgument() && (!variable.isReadFrom() || !variable.isWrittenTo());
556 | }
557 |
558 | function removeDeadCode(node) {
559 | return estraverse.replace(node, wrapVisitorScope(wrapVisitorParent({
560 | leave: function(node) {
561 | if (node.type === 'Program') {
562 | return {
563 | type: 'Program',
564 | body: hoistUseStrict(flattenNodeList(filterDeadCode(node.body)))
565 | };
566 | }
567 |
568 | if (node.type === 'BlockStatement') {
569 | var body = hoistUseStrict(flattenNodeList(filterDeadCode(node.body)));
570 |
571 | if (parent === null || (parent.type !== 'FunctionExpression' && parent.type !== 'FunctionDeclaration' &&
572 | parent.type !== 'TryStatement' && parent.type !== 'CatchClause')) {
573 | if (body.length === 0) {
574 | return {
575 | type: 'EmptyStatement'
576 | };
577 | }
578 |
579 | if (body.length === 1) {
580 | return body[0];
581 | }
582 | }
583 |
584 | return {
585 | type: 'BlockStatement',
586 | body: body
587 | };
588 | }
589 |
590 | if (node.type === 'ForStatement') {
591 | if (node.test !== null && node.test.type === 'Literal' && !node.test.value) {
592 | if (node.init === null) {
593 | return {
594 | type: 'EmptyStatement'
595 | };
596 | }
597 |
598 | if (node.init.type === 'VariableDeclaration') {
599 | return node.init;
600 | }
601 |
602 | return {
603 | type: 'ExpressionStatement',
604 | expression: node.init
605 | };
606 | }
607 | }
608 |
609 | if (node.type === 'TryStatement') {
610 | var handler = node.handlers.length === 1 ? node.handlers[0] : null;
611 | var finalizer = node.finalizer;
612 | assert(node.handlers.length < 2);
613 |
614 | if (node.block.body.length === 0) {
615 | return {
616 | type: 'EmptyStatement'
617 | };
618 | }
619 |
620 | if (handler !== null && handler.body.body.length === 0) {
621 | handler = null;
622 | }
623 |
624 | if (finalizer !== null && finalizer.body.length === 0) {
625 | finalizer = null;
626 | }
627 |
628 | if (handler === null && finalizer === null) {
629 | return node.block;
630 | }
631 |
632 | return {
633 | type: 'TryStatement',
634 | block: node.block,
635 | guardedHandlers: [],
636 | handlers: handler !== null ? [handler] : [],
637 | finalizer: finalizer
638 | };
639 | }
640 |
641 | if (node.type === 'SwitchStatement' && (!node.cases || node.cases.length === 0)) {
642 | return {
643 | type: 'ExpressionStatement',
644 | expression: node.discriminant
645 | };
646 | }
647 |
648 | if (node.type === 'ReturnStatement' && node.argument !== null && node.argument.type === 'Literal' && node.argument.value === void 0) {
649 | return {
650 | type: 'ReturnStatement',
651 | argument: null
652 | };
653 | }
654 |
655 | if (node.type === 'WhileStatement') {
656 | if (node.test.type === 'Literal' && !node.test.value) {
657 | return {
658 | type: 'EmptyStatement'
659 | };
660 | }
661 | }
662 |
663 | if (node.type === 'DoWhileStatement') {
664 | if (node.test.type === 'Literal' && !node.test.value) {
665 | return node.body;
666 | }
667 | }
668 |
669 | if (node.type === 'WithStatement') {
670 | if (node.body.type === 'EmptyStatement') {
671 | return {
672 | type: 'ExpressionStatement',
673 | expression: node.object
674 | };
675 | }
676 | }
677 |
678 | if (node.type === 'IfStatement') {
679 | if (node.test.type === 'Literal') {
680 | return node.test.value ? node.consequent : node.alternate || {
681 | type: 'EmptyStatement'
682 | };
683 | }
684 |
685 | if (node.consequent.type === 'EmptyStatement' && (node.alternate === null || node.alternate.type === 'EmptyStatement')) {
686 | return {
687 | type: 'ExpressionStatement',
688 | expression: node.test
689 | };
690 | }
691 |
692 | if (node.alternate !== null && node.alternate.type === 'EmptyStatement') {
693 | return {
694 | type: 'IfStatement',
695 | test: node.test,
696 | consequent: node.consequent,
697 | alternate: null
698 | };
699 | }
700 | }
701 |
702 | if (node.type === 'Identifier' && canRemoveVariable(node.name)) {
703 | var variable = scope.variableForName(node.name);
704 | assert(variable !== null);
705 | if (variable.referenceForNode(node).isRead) {
706 | return {
707 | type: 'Literal',
708 | value: void 0
709 | };
710 | }
711 | }
712 |
713 | if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && canRemoveVariable(node.left.name)) {
714 | return node.right;
715 | }
716 |
717 | if (node.type === 'FunctionDeclaration' && canRemoveVariable(node.id.name)) {
718 | return {
719 | type: 'EmptyStatement'
720 | };
721 | }
722 |
723 | if (node.type === 'VariableDeclaration') {
724 | return {
725 | type: 'VariableDeclaration',
726 | declarations: node.declarations.filter(function(node) {
727 | return !canRemoveVariable(node.id.name);
728 | }),
729 | kind: node.kind
730 | };
731 | }
732 | }
733 | })));
734 | }
735 |
736 | function wrapVisitorParent(visitor) {
737 | var parentStack = [];
738 | return {
739 | enter: function(node) {
740 | if (visitor.enter) node = visitor.enter(node) || node;
741 | parentStack.push(parent);
742 | parent = node;
743 | return node;
744 | },
745 |
746 | leave: function(node) {
747 | parent = parentStack.pop();
748 | if (visitor.leave) node = visitor.leave(node) || node;
749 | return node;
750 | }
751 | };
752 | }
753 |
754 | function wrapVisitorScope(visitor) {
755 | scope = null;
756 | return {
757 | enter: function(node) {
758 | if (esscope.nodeStartsNewScope(node)) {
759 | scope = scope === null ? esscope.analyze(node) : scope.childScopeForNode(node);
760 | assert(scope !== null);
761 | }
762 | if (visitor.enter) node = visitor.enter(node) || node;
763 | return node;
764 | },
765 |
766 | leave: function(node) {
767 | if (visitor.leave) node = visitor.leave(node) || node;
768 | if (esscope.nodeStartsNewScope(node)) scope = scope.parentScope;
769 | return node;
770 | }
771 | };
772 | }
773 |
774 | function optimize(node) {
775 | node = normalize(node);
776 | node = foldConstants(node);
777 | node = removeDeadCode(node);
778 | node = denormalize(node);
779 | return node;
780 | }
781 |
782 | esoptimize.optimize = optimize;
783 |
784 | }.call(this));
785 |
--------------------------------------------------------------------------------
/esscope.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | var estraverse = typeof window !== 'undefined' ? window.estraverse : require('estraverse');
5 | var esscope = typeof window !== 'undefined' ? (window.esscope = {}) : exports;
6 |
7 | function assert(truth) {
8 | if (!truth) {
9 | throw new Error('assertion failed');
10 | }
11 | }
12 |
13 | function Variable(name, scope) {
14 | this.name = name;
15 | this.scope = scope;
16 | this.references = [];
17 | }
18 |
19 | Variable.prototype.referenceForNode = function(node) {
20 | for (var i = 0; i < this.references.length; i++) {
21 | if (this.references[i].node === node) return this.references[i];
22 | }
23 | return null;
24 | };
25 |
26 | Variable.prototype.isGlobal = function() {
27 | return this.scope.parentScope === null;
28 | };
29 |
30 | Variable.prototype.isVariable = function() {
31 | return this.references.some(function(reference) {
32 | var decl = reference.declarationNode;
33 | return decl !== null && decl.type === 'VariableDeclarator';
34 | });
35 | };
36 |
37 | Variable.prototype.isArgument = function() {
38 | return this.references.some(function(reference) {
39 | var decl = reference.declarationNode;
40 | return decl !== null && (decl.type === 'FunctionExpression' ||
41 | decl.type === 'FunctionDeclaration') && decl.params.indexOf(reference.node) >= 0;
42 | });
43 | };
44 |
45 | Variable.prototype.isCaptured = function() {
46 | return this.references.some(function(reference) {
47 | return reference.scope !== this.scope;
48 | }, this);
49 | };
50 |
51 | Variable.prototype.isReadFrom = function() {
52 | return this.references.some(function(reference) {
53 | return reference.isRead;
54 | });
55 | };
56 |
57 | Variable.prototype.isWrittenTo = function() {
58 | return this.references.some(function(reference) {
59 | return reference.isWrite;
60 | });
61 | };
62 |
63 | function Reference(node, scope) {
64 | this.node = node;
65 | this.scope = scope;
66 | this.variable = null;
67 | this.isRead = false;
68 | this.isWrite = false;
69 | this.declarationNode = null;
70 | }
71 |
72 | function Scope(parentScope, node) {
73 | this.parentScope = parentScope;
74 | this.childScopes = [];
75 | this.node = node;
76 | this.variables = [];
77 | }
78 |
79 | function variableForName(scope, name) {
80 | for (var i = 0; i < scope.variables.length; i++) {
81 | if (scope.variables[i].name === name) return scope.variables[i];
82 | }
83 | return null;
84 | }
85 |
86 | Scope.prototype.childScopeForNode = function(node) {
87 | for (var i = 0; i < this.childScopes.length; i++) {
88 | if (this.childScopes[i].node === node) {
89 | return this.childScopes[i];
90 | }
91 | }
92 | return null;
93 | };
94 |
95 | Scope.prototype.variableForName = function(name) {
96 | for (var scope = this; scope !== null; scope = scope.parentScope) {
97 | var variable = variableForName(scope, name);
98 | if (variable !== null) return variable;
99 | }
100 | return null;
101 | };
102 |
103 | Scope.prototype.define = function(name) {
104 | var variable = variableForName(this, name);
105 | if (variable === null) {
106 | variable = new Variable(name, this);
107 | this.variables.push(variable);
108 | }
109 | return variable;
110 | };
111 |
112 | function Resolver(parentResolver, node) {
113 | var parentScope = parentResolver !== null ? parentResolver.scope : null;
114 | this.parentResolver = parentResolver;
115 | this.scope = new Scope(parentScope, node);
116 | this.references = [];
117 | if (parentScope !== null) parentScope.childScopes.push(this.scope);
118 | }
119 |
120 | Resolver.prototype.close = function() {
121 | var globalScope = this.scope;
122 | while (globalScope.parentScope !== null) {
123 | globalScope = globalScope.parentScope;
124 | }
125 | for (var i = 0; i < this.references.length; i++) {
126 | var reference = this.references[i];
127 | reference.variable = this.scope.variableForName(reference.node.name);
128 | if (reference.variable === null) reference.variable = globalScope.define(reference.node.name);
129 | reference.variable.references.push(reference);
130 | }
131 | };
132 |
133 | Resolver.prototype.recordReference = function(node) {
134 | var reference = new Reference(node, this.scope);
135 | this.references.push(reference);
136 | return reference;
137 | };
138 |
139 | function nodeStartsNewScope(node) {
140 | return node.type === 'Program' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
141 | }
142 |
143 | function analyze(node) {
144 | var currentResolver = null;
145 | var parentStack = [];
146 | var parent = null;
147 |
148 | function enter(node) {
149 | if (node.type === 'VariableDeclarator' || node.type === 'FunctionDeclaration') {
150 | currentResolver.scope.define(node.id.name);
151 | }
152 | }
153 |
154 | function leave(node) {
155 | if (node.type === 'Identifier') {
156 | var reference = currentResolver.recordReference(node);
157 |
158 | if (parent.type === 'VariableDeclarator') {
159 | reference.isWrite = parent.init !== null;
160 | reference.declarationNode = parent;
161 | }
162 |
163 | else if (parent.type === 'FunctionExpression' || parent.type === 'FunctionDeclaration') {
164 | reference.isWrite = true;
165 | reference.declarationNode = parent;
166 | }
167 |
168 | else if (parent.type === 'AssignmentExpression' && parent.left === node) {
169 | reference.isWrite = true;
170 |
171 | if (parent.operator !== '=') {
172 | reference.isRead = true;
173 | }
174 | }
175 |
176 | else {
177 | reference.isRead = true;
178 | }
179 | }
180 |
181 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
182 | for (var i = 0; i < node.params.length; i++) {
183 | currentResolver.scope.define(node.params[i].name);
184 | }
185 | }
186 | }
187 |
188 | estraverse.traverse(node, {
189 | enter: function(node) {
190 | enter(node);
191 | if (nodeStartsNewScope(node)) {
192 | currentResolver = new Resolver(currentResolver, node);
193 | }
194 | parentStack.push(parent);
195 | parent = node;
196 | },
197 |
198 | leave: function(node) {
199 | parent = parentStack.pop();
200 | leave(node);
201 | if (nodeStartsNewScope(node)) {
202 | currentResolver.close();
203 | if (currentResolver.parentResolver !== null) {
204 | currentResolver = currentResolver.parentResolver;
205 | }
206 | }
207 | }
208 | });
209 |
210 | return currentResolver.scope;
211 | }
212 |
213 | esscope.Variable = Variable;
214 | esscope.Reference = Reference;
215 | esscope.Scope = Scope;
216 | esscope.nodeStartsNewScope = nodeStartsNewScope;
217 | esscope.analyze = analyze;
218 |
219 | }.call(this));
220 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esoptimize",
3 | "description": "A JavaScript AST optimizer",
4 | "main": "esoptimize.js",
5 | "version": "0.0.1",
6 | "repository": {
7 | "type": "git",
8 | "url": "http://github.com/evanw/esoptimize.git"
9 | },
10 | "dependencies": {
11 | "estraverse": "1.1.1"
12 | },
13 | "devDependencies": {
14 | "mocha": "1.9.0",
15 | "esprima": "1.0.2",
16 | "escodegen": "0.0.21",
17 | "codemirror": "3.11.01"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var esprima = require('esprima');
3 | var escodegen = require('escodegen');
4 | var esoptimize = require('./esoptimize');
5 |
6 | function bodyOfFunction(f) {
7 | return f.toString().replace(/^[^\{]*\{((?:.|\n)*)\}[^\}]*$/, '$1');
8 | }
9 |
10 | function test(input, expected) {
11 | input = esprima.parse(bodyOfFunction(input));
12 | expected = esprima.parse(bodyOfFunction(expected));
13 | var output = esoptimize.optimize(input);
14 | var options = { format: { indent: { style: ' ' } } };
15 | assert.strictEqual(escodegen.generate(output, options), escodegen.generate(expected, options));
16 | assert.strictEqual(JSON.stringify(output, null, 2), JSON.stringify(expected, null, 2));
17 | }
18 |
19 | it('numeric constants', function() {
20 | test(function() {
21 | var a = 0 / 0 * 2;
22 | var b = 100 / 0;
23 | var c = 100 / 0 * -2;
24 | var d = -0;
25 | }, function() {
26 | var a, b, c, d;
27 | a = 0 / 0;
28 | b = 1 / 0;
29 | c = -1 / 0;
30 | d = 0;
31 | });
32 | });
33 |
34 | it('unary operators', function() {
35 | test(function() {
36 | a(
37 | !1,
38 | ~1,
39 | +1,
40 | -1,
41 | void 1,
42 | typeof 1,
43 | delete b
44 | );
45 | b++;
46 | b--;
47 | ++b;
48 | --b;
49 | }, function() {
50 | a(
51 | false,
52 | -2,
53 | 1,
54 | -1,
55 | void 0,
56 | 'number',
57 | delete b
58 | );
59 | b++;
60 | b--;
61 | ++b;
62 | --b;
63 | });
64 | });
65 |
66 | it('binary operators', function() {
67 | test(function() {
68 | a(
69 | 1 + 2,
70 | 1 - 2,
71 | 1 * 2,
72 | 1 / 2,
73 | 1 % 2,
74 | 1 & 2,
75 | 1 | 2,
76 | 1 ^ 2,
77 | 1 << 2,
78 | 1 >> 2,
79 | 1 >>> 2,
80 | 1 < 2,
81 | 1 > 2,
82 | 1 <= 2,
83 | 1 >= 2,
84 | 1 == 2,
85 | 1 != 2,
86 | 1 === 2,
87 | 1 !== 2,
88 | 0 && 1,
89 | 0 || 1,
90 | 1 instanceof b,
91 | 1 in b
92 | );
93 | b = 2;
94 | b += 2;
95 | b -= 2;
96 | b *= 2;
97 | b /= 2;
98 | b %= 2;
99 | b &= 2;
100 | b |= 2;
101 | b ^= 2;
102 | b <<= 2;
103 | b >>= 2;
104 | b >>>= 2;
105 | }, function() {
106 | a(
107 | 3,
108 | -1,
109 | 2,
110 | 0.5,
111 | 1,
112 | 0,
113 | 3,
114 | 3,
115 | 4,
116 | 0,
117 | 0,
118 | true,
119 | false,
120 | true,
121 | false,
122 | false,
123 | true,
124 | false,
125 | true,
126 | 0,
127 | 1,
128 | 1 instanceof b,
129 | 1 in b
130 | );
131 | b = 2;
132 | b += 2;
133 | b -= 2;
134 | b *= 2;
135 | b /= 2;
136 | b %= 2;
137 | b &= 2;
138 | b |= 2;
139 | b ^= 2;
140 | b <<= 2;
141 | b >>= 2;
142 | b >>>= 2;
143 | });
144 | });
145 |
146 | it('sequence folding', function() {
147 | test(function() {
148 | var a = (1, 2, 3);
149 | var b = (1, x(), 2, y(), 3);
150 | var c = (1, (x(), 2), 3);
151 | }, function() {
152 | var a, b, c;
153 | a = 3;
154 | b = (x(), y(), 3);
155 | c = (x(), 3);
156 | });
157 | });
158 |
159 | it('logical negation', function() {
160 | test(function() {
161 | a(!(b < c));
162 | a(!(b > c));
163 | a(!(b <= c));
164 | a(!(b >= c));
165 | a(!(b == c));
166 | a(!(b != c));
167 | a(!(b === c));
168 | a(!(b !== c));
169 | a(!(b && c));
170 | a(!(b || c));
171 | a(!(b < c && d || e > f));
172 | a(!(b < c || d && e > f));
173 | }, function() {
174 | a(b >= c);
175 | a(b <= c);
176 | a(b > c);
177 | a(b < c);
178 | a(b != c);
179 | a(b == c);
180 | a(b !== c);
181 | a(b === c);
182 | a(!b || !c);
183 | a(!b && !c);
184 | a((b >= c || !d) && e <= f);
185 | a(b >= c && (!d || e <= f));
186 | });
187 | });
188 |
189 | it('array folding', function() {
190 | test(function() {
191 | var a = [1, 2][0];
192 | var b = [1, c()][0];
193 | var c = [1, 2][-1];
194 | var d = [1, 2][0.5];
195 | var e = [1, 2, 3]['len' + 'gth'];
196 | }, function() {
197 | var a, b, c, d, e;
198 | a = 1;
199 | b = [1, c()][0];
200 | c = [][-1];
201 | d = [][0.5];
202 | e = 3;
203 | });
204 | });
205 |
206 | it('string folding', function() {
207 | test(function() {
208 | var a = '12'[0];
209 | var b = '12'[-1];
210 | var c = '12'[0.5];
211 | var d = '123'['len' + 'gth'];
212 | }, function() {
213 | var a, b, c, d;
214 | a = '1';
215 | b = ''[-1];
216 | c = ''[0.5];
217 | d = 3;
218 | });
219 | });
220 |
221 | it('object literal folding', function() {
222 | test(function() {
223 | var a = { 'x': 0, 'y': 1 }['x'];
224 | var b = { 'x': 0, 'y': 1 }.x;
225 | var c = { 1: 2, 3: 4 }[1];
226 | }, function() {
227 | var a, b, c;
228 | a = 0;
229 | b = 0;
230 | c = 2;
231 | });
232 | });
233 |
234 | it('property normalization', function() {
235 | test(function() {
236 | a(b['c']);
237 | a(b['c d']);
238 | a({ 1: 2, 'b': 'c' });
239 | }, function() {
240 | a(b.c);
241 | a(b['c d']);
242 | a({ '1': 2, b: 'c' });
243 | });
244 | });
245 |
246 | it('side-effect-free code removal', function() {
247 | test(function() {
248 | 'use strict';
249 | if (false) var x;
250 | 'not use strict';
251 | 1;
252 | x;
253 | x.y;
254 | (function() {});
255 | var foo = function() {};
256 | function foo() {}
257 | }, function() {
258 | 'use strict';
259 | var x, foo;
260 | foo = function() {};
261 | function foo() {}
262 | });
263 | });
264 |
265 | it('block flattening', function() {
266 | test(function() {
267 | a();
268 | ;
269 | { ; b(); { c(); } d(), e(); }
270 | f();
271 | }, function() {
272 | a();
273 | b();
274 | c();
275 | d();
276 | e();
277 | f();
278 | });
279 | });
280 |
281 | it('function block flattening', function() {
282 | test(function() {
283 | function foo(a, b) {
284 | a();
285 | ;
286 | { ; b(); { c(); } d(), e(); }
287 | f();
288 | }
289 | }, function() {
290 | function foo(a, b) {
291 | a();
292 | b();
293 | c();
294 | d();
295 | e();
296 | f();
297 | }
298 | });
299 | });
300 |
301 | it('unused variable removal', function() {
302 | test(function() {
303 | var a, b;
304 | function foo(a) {
305 | var a, b, c;
306 | a();
307 | b();
308 | }
309 | (function(a) {
310 | function foo() {
311 | var b, c;
312 | a();
313 | b();
314 | }
315 | function bar() {}
316 | foo();
317 | }());
318 | b = 0;
319 | }, function() {
320 | var a, b;
321 | function foo(a) {
322 | a();
323 | (void 0)();
324 | }
325 | (function(a) {
326 | function foo() {
327 | a();
328 | (void 0)();
329 | }
330 | foo();
331 | }());
332 | b = 0;
333 | });
334 | });
335 |
336 | it('ternary expression folding', function() {
337 | test(function() {
338 | a(true ? b() : c());
339 | }, function() {
340 | a(b());
341 | });
342 | });
343 |
344 | it('if statement dead code removal', function() {
345 | test(function() {
346 | if (0) a();
347 | if (0) a(); else b();
348 | if (1) a(); else b();
349 | if (a()) { b(); } else {}
350 | if (a()) { 1; }
351 | if (a()) { 1; } else { 2; }
352 | }, function() {
353 | b();
354 | a();
355 | if (a()) b();
356 | a();
357 | a();
358 | });
359 | });
360 |
361 | it('while statement dead code removal', function() {
362 | test(function() {
363 | while (false) foo();
364 | while (true) foo();
365 | }, function() {
366 | while (true) foo();
367 | });
368 | });
369 |
370 | it('do-while statement dead code removal', function() {
371 | test(function() {
372 | do foo(); while (false);
373 | do foo(); while (true);
374 | }, function() {
375 | foo();
376 | do foo(); while (true);
377 | });
378 | });
379 |
380 | it('for statement dead code removal', function() {
381 | test(function() {
382 | for (;0;) foo();
383 | for (foo();0;) foo();
384 | for (var bar;0;) foo();
385 | for (var bar = 0;0;) foo();
386 | for (;1;) foo();
387 | for (;;) foo();
388 | }, function() {
389 | var bar;
390 | foo();
391 | bar = 0;
392 | for (;1;) foo();
393 | for (;;) foo();
394 | });
395 | });
396 |
397 | it('with statement dead code removal', function() {
398 | test(function() {
399 | with (foo) {}
400 | with (foo) foo();
401 | }, function() {
402 | with (foo) foo();
403 | });
404 | });
405 |
406 | it('try statement dead code removal', function() {
407 | test(function() {
408 | try { foo(); } catch (e) { foo(); } finally { foo(); }
409 | try { foo(); } catch (e) { foo(); } finally {}
410 | try { foo(); } catch (e) { foo(); }
411 | try { foo(); } catch (e) {} finally { foo(); }
412 | try { foo(); } catch (e) {} finally {}
413 | try { foo(); } catch (e) {}
414 | try { foo(); } finally { foo(); }
415 | try { foo(); } finally {}
416 | try {} catch (e) { foo(); } finally { foo(); }
417 | try {} catch (e) { foo(); } finally {}
418 | try {} catch (e) { foo(); }
419 | try {} catch (e) {} finally { foo(); }
420 | try {} catch (e) {} finally {}
421 | try {} catch (e) {}
422 | try {} finally { foo(); }
423 | try {} finally {}
424 | }, function() {
425 | try { foo(); } catch (e) { foo(); } finally { foo(); }
426 | try { foo(); } catch (e) { foo(); }
427 | try { foo(); } catch (e) { foo(); }
428 | try { foo(); } finally { foo(); }
429 | foo();
430 | foo();
431 | try { foo(); } finally { foo(); }
432 | foo();
433 | });
434 | });
435 |
436 | it('return statement folding', function() {
437 | test(function() {
438 | function foo() { if (bar()) return void 0; bar(); }
439 | function foo() { var x; if (bar()) return x; bar(); }
440 | }, function() {
441 | function foo() { if (bar()) return; bar(); }
442 | function foo() { if (bar()) return; bar(); }
443 | });
444 | });
445 |
446 | it('switch statement dead code removal', function() {
447 | test(function() {
448 | switch (0) {}
449 | switch (foo()) {}
450 | }, function() {
451 | foo();
452 | });
453 | });
454 |
--------------------------------------------------------------------------------