mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +00:00
* added .mjs extension to JavaScript * add missing newline at end of file * add example from https://github.com/bmeck/composable-ast-walker/blob/master/example/constant_fold.mjs
956 lines
27 KiB
JavaScript
956 lines
27 KiB
JavaScript
// consumes <stdin> and performs constant folding
|
|
// echo '"use strict";"_"[0],1+2;' | node constant_fold.js
|
|
import _NodePath from '../NodePath';
|
|
const {NodePath} = _NodePath;
|
|
import _WalkCombinator from '../WalkCombinator';
|
|
const {WalkCombinator} = _WalkCombinator;
|
|
|
|
const $CONSTEXPR = Symbol.for('$CONSTEXTR');
|
|
const $CONSTVALUE = Symbol.for('$CONSTVALUE');
|
|
const IS_EMPTY = path => {
|
|
return (path.node.type === 'BlockStatement' && path.node.body.length === 0) ||
|
|
path.node.type === 'EmptyStatement';
|
|
};
|
|
const IN_PRAGMA_POS = path => {
|
|
if (path.parent && Array.isArray(path.parent.node)) {
|
|
const siblings = path.parent.node;
|
|
for (let i = 0; i < path.key; i++) {
|
|
// preceded by non-pragma
|
|
if (
|
|
siblings[i].type !== 'ExpressionStatement' ||
|
|
!IS_CONSTEXPR(siblings[i].expression) ||
|
|
typeof CONSTVALUE(siblings[i].expression) !== 'string'
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
const IS_PRAGMA = path => {
|
|
if (path.parent && Array.isArray(path.parent.node)) {
|
|
const siblings = path.parent.node;
|
|
for (let i = 0; i < path.key + 1; i++) {
|
|
// preceded by non-pragma
|
|
if (
|
|
siblings[i].type !== 'ExpressionStatement' ||
|
|
!IS_CONSTEXPR(siblings[i].expression) ||
|
|
typeof CONSTVALUE(siblings[i].expression) !== 'string'
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
// worst case is the completion value
|
|
const IS_NOT_COMPLETION = path => {
|
|
while (true) {
|
|
if (!path.parent) {
|
|
return true;
|
|
}
|
|
if (
|
|
Array.isArray(path.parent.node) &&
|
|
path.key !== path.parent.node.length - 1
|
|
) {
|
|
return true;
|
|
}
|
|
path = path.parent;
|
|
while (Array.isArray(path.node)) {
|
|
path = path.parent;
|
|
}
|
|
if (/Function/.test(path.node.type)) {
|
|
return true;
|
|
} else if (path.node.type === 'Program') {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
const REMOVE_IF_EMPTY = path => {
|
|
if (IS_EMPTY(path)) REMOVE(path);
|
|
return null;
|
|
};
|
|
const REPLACE_IF_EMPTY = (path, folded) => {
|
|
if (IS_EMPTY(path)) return REPLACE(path, folded);
|
|
return path;
|
|
};
|
|
const REMOVE = path => {
|
|
if (Array.isArray(path.parent.node)) {
|
|
path.parent.node.splice(path.key, 1);
|
|
} else {
|
|
path.parent.node[path.key] = null;
|
|
}
|
|
return null;
|
|
};
|
|
const REPLACE = (path, folded) => {
|
|
const replacement = new NodePath(path.parent, folded, path.key);
|
|
path.parent.node[path.key] = folded;
|
|
return replacement;
|
|
};
|
|
// no mutation, this is an atomic value
|
|
const NEG_ZERO = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'UnaryExpression',
|
|
operator: '-',
|
|
argument: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 0,
|
|
}),
|
|
});
|
|
const INFINITY = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'BinaryExpression',
|
|
operator: '/',
|
|
left: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 1,
|
|
}),
|
|
right: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 0,
|
|
}),
|
|
});
|
|
const NEG_INFINITY = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'BinaryExpression',
|
|
operator: '/',
|
|
left: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 1,
|
|
}),
|
|
right: NEG_ZERO,
|
|
});
|
|
const EMPTY = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'EmptyStatement',
|
|
});
|
|
const NULL = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: null,
|
|
});
|
|
const NAN = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'BinaryExpression',
|
|
operator: '/',
|
|
left: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 0,
|
|
}),
|
|
right: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 0,
|
|
}),
|
|
});
|
|
const UNDEFINED = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'UnaryExpression',
|
|
operator: 'void',
|
|
argument: Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'Literal',
|
|
value: 0,
|
|
}),
|
|
});
|
|
// ESTree doesn't like negative numeric literals
|
|
// this also preserves -0
|
|
const IS_UNARY_NEGATIVE = node => {
|
|
if (
|
|
node.type === 'UnaryExpression' &&
|
|
node.operator === '-' &&
|
|
typeof node.argument.value === 'number' &&
|
|
node.argument.value === node.argument.value &&
|
|
node.argument.type === 'Literal'
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const IS_CONSTEXPR = node => {
|
|
if (typeof node !== 'object' || node === null) {
|
|
return false;
|
|
}
|
|
// DONT CALCULATE THINGS MULTIPLE TIMES!!@!@#
|
|
if (node[$CONSTEXPR]) return true;
|
|
if (node.type === 'ArrayExpression') {
|
|
for (let i = 0; i < node.elements.length; i++) {
|
|
const element = node.elements[i];
|
|
// hole == null
|
|
if (element !== null && !IS_CONSTEXPR(element)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (node.type === 'ObjectExpression') {
|
|
for (let i = 0; i < node.properties.length; i++) {
|
|
const element = node.properties[i];
|
|
if (element.kind !== 'init') return false;
|
|
if (element.method) return false;
|
|
let key;
|
|
if (element.computed) {
|
|
// be sure {["y"]:1} works
|
|
if (!IS_CONSTEXPR(element.key)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!IS_CONSTEXPR(element.value)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (node.type === 'Literal' || IS_UNDEFINED(node) || IS_NAN(node)) {
|
|
return true;
|
|
}
|
|
if (IS_UNARY_NEGATIVE(node)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const IS_NAN = node => {
|
|
return node === NAN;
|
|
};
|
|
const IS_UNDEFINED = node => {
|
|
return node === UNDEFINED;
|
|
};
|
|
const CONSTVALUE = node => {
|
|
if (node[$CONSTVALUE]) {
|
|
return node[$CONSTVALUE];
|
|
}
|
|
if (IS_UNDEFINED(node)) return void 0;
|
|
if (IS_NAN(node)) return +'_';
|
|
if (!IS_CONSTEXPR(node)) throw new Error('Not a CONSTEXPR');
|
|
if (node.type === 'ArrayExpression') {
|
|
let ret = [];
|
|
ret.length = node.elements.length;
|
|
for (let i = 0; i < node.elements.length; i++) {
|
|
if (node.elements[i] !== null) {
|
|
ret[i] = CONSTVALUE(node.elements[i]);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
if (node.type === 'ObjectExpression') {
|
|
let ret = Object.create(null);
|
|
for (let i = 0; i < node.properties.length; i++) {
|
|
const element = node.properties[i];
|
|
let key;
|
|
if (element.computed) {
|
|
key = `${CONSTVALUE(element.key)}`;
|
|
}
|
|
else {
|
|
key = element.key.name;
|
|
}
|
|
Object.defineProperty(ret, key, {
|
|
// duplicate keys...
|
|
configurable: true,
|
|
writable: true,
|
|
value: CONSTVALUE(element.value),
|
|
enumerable: true
|
|
});
|
|
}
|
|
Object.freeze(ret);
|
|
return ret;
|
|
}
|
|
if (IS_UNARY_NEGATIVE(node)) {
|
|
return -node.argument.value;
|
|
}
|
|
if (node.regex !== void 0) {
|
|
return new RegExp(node.regex.pattern, node.regex.flags);
|
|
}
|
|
return node.value;
|
|
};
|
|
const CONSTEXPRS = new Map();
|
|
CONSTEXPRS.set(void 0, UNDEFINED);
|
|
CONSTEXPRS.set(+'_', NAN);
|
|
CONSTEXPRS.set(null, NULL);
|
|
const TO_CONSTEXPR = value => {
|
|
if (value === -Infinity) {
|
|
return NEG_INFINITY;
|
|
}
|
|
if (value === Infinity) {
|
|
return INFINITY;
|
|
}
|
|
let is_neg_zero = 1 / value === -Infinity;
|
|
if (is_neg_zero) return NEG_ZERO;
|
|
if (CONSTEXPRS.has(value)) {
|
|
return CONSTEXPRS.get(value);
|
|
}
|
|
if (typeof value === 'number') {
|
|
if (value < 0) {
|
|
const CONSTEXPR = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
[$CONSTVALUE]: value,
|
|
type: 'UnaryExpression',
|
|
operator: '-',
|
|
argument: Object.freeze({ type: 'Literal', value: -value }),
|
|
});
|
|
CONSTEXPRS.set(value, CONSTEXPR);
|
|
return CONSTEXPR;
|
|
}
|
|
}
|
|
if (
|
|
value === null ||
|
|
typeof value === 'number' ||
|
|
typeof value === 'boolean' ||
|
|
typeof value === 'string'
|
|
) {
|
|
const CONSTEXPR = Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
[$CONSTVALUE]: value,
|
|
type: 'Literal',
|
|
value,
|
|
});
|
|
CONSTEXPRS.set(value, CONSTEXPR);
|
|
return CONSTEXPR;
|
|
}
|
|
// have to generate new one every time :-/
|
|
if (Array.isArray(value)) {
|
|
return Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'ArrayExpression',
|
|
elements: Object.freeze(value.map(TO_CONSTEXPR)),
|
|
});
|
|
}
|
|
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.getPrototypeOf({}) && [...Object.getOwnPropertySymbols(value)].length === 0) {
|
|
return Object.freeze({
|
|
[$CONSTEXPR]: true,
|
|
type: 'ObjectExpression',
|
|
properties: Object.freeze(
|
|
[...Object.getOwnPropertyKeys(value)].map(key => {
|
|
if (!('value' in Object.getOwnProperty(value, key))) {
|
|
throw Error('Not a CONSTVALUE (found a setter or getter?)');
|
|
}
|
|
return {
|
|
type: 'Property',
|
|
kind: 'init',
|
|
method: false,
|
|
shorthand: false,
|
|
computed: true,
|
|
key: {
|
|
type: 'Literal',
|
|
value: key
|
|
},
|
|
value: TO_CONSTEXPR(value[key])
|
|
}
|
|
})),
|
|
});
|
|
}
|
|
throw Error('Not a CONSTVALUE (did you pass a RegExp?)');
|
|
};
|
|
|
|
// THIS DOES NOT HANDLE NODE SPECIFIC CASES LIKE IfStatement
|
|
const FOLD_EMPTY = function*(path) {
|
|
if (
|
|
path &&
|
|
path.node &&
|
|
path.parent &&
|
|
Array.isArray(path.parent.node) &&
|
|
IS_EMPTY(path)
|
|
) {
|
|
REMOVE(path);
|
|
return yield;
|
|
}
|
|
return yield path;
|
|
};
|
|
|
|
// THIS DOES NOT HANDLE NODE SPECIFIC CASES LIKE IfStatement
|
|
const FOLD_TEMPLATE = function*(path) {
|
|
if (
|
|
path &&
|
|
path.node &&
|
|
path.type === 'TemplateLiteral'
|
|
) {
|
|
let updated = false;
|
|
for (let i = 0; i < path.node.exressions.length; i++) {
|
|
if (IS_CONSTEXPR(path.node.expressions[i])) {
|
|
//let
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_EXPR_STMT = function*(path) {
|
|
// TODO: enforce completion value checking
|
|
if (path && path.node && path.node.type === 'ExpressionStatement') {
|
|
// merge all the adjacent expression statements into sequences
|
|
if (Array.isArray(path.parent.node)) {
|
|
// could have nodes after it
|
|
const siblings = path.parent.node;
|
|
if (!IS_PRAGMA(path)) {
|
|
if (path.key < siblings.length - 1) {
|
|
const mergeable = [path.node];
|
|
for (let needle = path.key + 1; needle < siblings.length; needle++) {
|
|
if (siblings[needle].type !== 'ExpressionStatement') {
|
|
break;
|
|
}
|
|
mergeable.push(siblings[needle]);
|
|
}
|
|
if (mergeable.length > 1) {
|
|
siblings.splice(path.key, mergeable.length, {
|
|
type: 'ExpressionStatement',
|
|
expression: {
|
|
type: 'SequenceExpression',
|
|
expressions: mergeable.reduce(
|
|
(acc, es) => {
|
|
if (es.expression.type == 'SequenceExpression') {
|
|
return [...acc, ...es.expression.expressions];
|
|
} else {
|
|
return [...acc, es.expression];
|
|
}
|
|
},
|
|
[]
|
|
),
|
|
},
|
|
});
|
|
return path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (IS_NOT_COMPLETION(path) && IS_CONSTEXPR(path.node.expression)) {
|
|
return REPLACE(path, EMPTY);
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_WHILE = function*(path) {
|
|
if (path && path.node) {
|
|
if (path.node.type === 'DoWhileStatement') {
|
|
console.error('FOLD_DOWHILE');
|
|
REPLACE_IF_EMPTY(path.get(['body']), EMPTY);
|
|
}
|
|
if (path.node.type === 'WhileStatement') {
|
|
console.error('FOLD_WHILE');
|
|
let { test, consequent, alternate } = path.node;
|
|
if (IS_CONSTEXPR(test)) {
|
|
test = CONSTVALUE(test);
|
|
if (!test) {
|
|
return REPLACE(path, EMPTY);
|
|
}
|
|
}
|
|
REPLACE_IF_EMPTY(path.get(['body']), EMPTY);
|
|
}
|
|
if (path.node.type === 'ForStatement') {
|
|
console.error('FOLD_FOR');
|
|
REPLACE_IF_EMPTY(path.get(['body']), EMPTY);
|
|
let { init, test, update } = path.node;
|
|
let updated = false;
|
|
if (init && IS_CONSTEXPR(init)) {
|
|
updated = true;
|
|
REPLACE(path.get(['init']), null);
|
|
}
|
|
if (test && IS_CONSTEXPR(test)) {
|
|
let current = CONSTVALUE(test);
|
|
let coerced = Boolean(current);
|
|
// remove the test if it is always true
|
|
if (coerced === true) {
|
|
updated = true;
|
|
REPLACE(path.get(['test']), null);
|
|
} else if (coerced !== current) {
|
|
updated = true;
|
|
REPLACE(path.get(['test']), TO_CONSTEXPR(coerced));
|
|
}
|
|
}
|
|
if (update && IS_CONSTEXPR(update)) {
|
|
updated = true;
|
|
REPLACE(path.get(['update']), null);
|
|
}
|
|
if (updated) {
|
|
return path;
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_IF = function*(path) {
|
|
if (path && path.node && path.node.type === 'IfStatement') {
|
|
let { test, consequent, alternate } = path.node;
|
|
const is_not_completion = IS_NOT_COMPLETION(path);
|
|
if (is_not_completion && !alternate) {
|
|
if (IS_EMPTY(path.get(['consequent']))) {
|
|
console.error('FOLD_IF_EMPTY_CONSEQUENT');
|
|
REPLACE(path, {
|
|
type: 'ExpressionStatement',
|
|
expression: test,
|
|
});
|
|
return path.parent;
|
|
}
|
|
}
|
|
if (alternate) {
|
|
if (alternate.type === consequent.type) {
|
|
if (consequent.type === 'ExpressionStatement') {
|
|
console.error('FOLD_IF_BOTH_EXPRSTMT');
|
|
REPLACE(path, {
|
|
type: 'ExpressionStatement', expression:
|
|
{
|
|
type: 'ConditionalExpression',
|
|
test: test,
|
|
consequent: consequent.expression,
|
|
alternate: alternate.expression,
|
|
}});
|
|
return path.parent;
|
|
}
|
|
else if (consequent.type === 'ReturnStatement' ||
|
|
consequent.type === 'ThrowStatement') {
|
|
console.error('FOLD_IF_BOTH_COMPLETIONS');
|
|
REPLACE(path, {
|
|
type: 'ExpressionStatement', expression:{
|
|
type: consequent.type,
|
|
argument: {
|
|
type: 'ConditionalExpression',
|
|
test: test,
|
|
consequent: consequent.argument,
|
|
alternate: alternate.argument,
|
|
}}
|
|
});
|
|
return path.parent;
|
|
}
|
|
}
|
|
}
|
|
else if (is_not_completion && consequent.type === 'ExpressionStatement') {
|
|
console.error('FOLD_IF_NON_COMPLETION_TO_&&');
|
|
REPLACE(path, {
|
|
type: 'ExpressionStatement',
|
|
expression: {
|
|
type: 'BinaryExpression',
|
|
operator: '&&',
|
|
left: test,
|
|
right: consequent.expression,
|
|
}
|
|
});
|
|
return path.parent;
|
|
}
|
|
if (IS_CONSTEXPR(test)) {
|
|
test = CONSTVALUE(test);
|
|
if (test) {
|
|
return REPLACE(path, consequent);
|
|
}
|
|
if (alternate) {
|
|
return REPLACE(path, alternate);
|
|
}
|
|
return REPLACE(path, EMPTY);
|
|
}
|
|
consequent = path.get(['consequent']);
|
|
let updated;
|
|
if (consequent.node !== EMPTY) {
|
|
REPLACE_IF_EMPTY(consequent, EMPTY);
|
|
if (consequent.parent.node[consequent.key] === EMPTY) {
|
|
updated = true;
|
|
}
|
|
}
|
|
if (alternate) {
|
|
alternate = path.get(['alternate']);
|
|
REMOVE_IF_EMPTY(alternate);
|
|
if (path.node.alternate === null) {
|
|
updated = true;
|
|
}
|
|
}
|
|
if (updated) {
|
|
return path;
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_SEQUENCE = function*(path) {
|
|
if (path && path.node && path.node.type === 'SequenceExpression') {
|
|
console.error('FOLD_SEQUENCE');
|
|
// never delete the last value
|
|
for (let i = 0; i < path.node.expressions.length - 1; i++) {
|
|
if (IS_CONSTEXPR(path.node.expressions[i])) {
|
|
path.node.expressions.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
if (path.node.expressions.length === 1) {
|
|
return REPLACE(path, path.node.expressions[0]);
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_LOGICAL = function*(path) {
|
|
if (path && path.node && path.node.type === 'LogicalExpression') {
|
|
console.error('FOLD_LOGICAL');
|
|
let { left, right, operator } = path.node;
|
|
if (IS_CONSTEXPR(left)) {
|
|
left = CONSTVALUE(left);
|
|
if (operator === '||') {
|
|
if (left) {
|
|
return REPLACE(path, TO_CONSTEXPR(left));
|
|
}
|
|
return REPLACE(path, right);
|
|
} else if (operator === '&&') {
|
|
if (!left) {
|
|
return REPLACE(path, TO_CONSTEXPR(left));
|
|
}
|
|
return REPLACE(path, right);
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_SWITCH = function*(path) {
|
|
if (path && path.node && path.node.type === 'SwitchStatement') {
|
|
let { discriminant, cases } = path.node;
|
|
// if there are no cases, just become an expression
|
|
if (cases.length === 0 && IS_NOT_COMPLETION(path)) {
|
|
return REPLACE(path, {
|
|
type: 'ExpressionStatement',
|
|
expression: discriminant
|
|
});
|
|
}
|
|
// if the discriminant is static
|
|
// remove any preceding non-matching static cases
|
|
// fold any trailing cases into the matching case
|
|
if (cases.length > 1 && IS_CONSTEXPR(discriminant)) {
|
|
const discriminant_value = CONSTVALUE(discriminant);
|
|
for (var i = 0; i < cases.length; i++) {
|
|
const test = cases[i].test;
|
|
if (IS_CONSTEXPR(test)) {
|
|
let test_value = CONSTVALUE(test);
|
|
if (discriminant_value === test_value) {
|
|
let new_consequent = cases[i].consequent;
|
|
if (i < cases.length - 1) {
|
|
for (let fallthrough of cases.slice(i+1)) {
|
|
new_consequent.push(...fallthrough.consequent);
|
|
}
|
|
}
|
|
cases[i].consequent = new_consequent;
|
|
REPLACE(path.get(['cases']), [cases[i]]);
|
|
return path;
|
|
}
|
|
}
|
|
else {
|
|
// we had a dynamic case need to bail
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_UNREACHABLE = function*(path) {
|
|
if (path && path.node && path.parent && Array.isArray(path.parent.node)) {
|
|
if (path.node.type === 'ReturnStatement' ||
|
|
path.node.type === 'ContinueStatement' ||
|
|
path.node.type === 'BreakStatement' ||
|
|
path.node.type === 'ThrowStatement') {
|
|
const next_key = path.key + 1;
|
|
path.parent.node.splice(next_key, path.parent.node.length - next_key);
|
|
}
|
|
}
|
|
return yield path;
|
|
}
|
|
const FOLD_CONDITIONAL = function*(path) {
|
|
if (path && path.node && path.node.type === 'ConditionalExpression') {
|
|
console.error('FOLD_CONDITIONAL');
|
|
let { test, consequent, alternate } = path.node;
|
|
if (IS_CONSTEXPR(test)) {
|
|
test = CONSTVALUE(test);
|
|
if (test) {
|
|
return REPLACE(path, consequent);
|
|
}
|
|
return REPLACE(path, alternate);
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_BINARY = function*(path) {
|
|
if (
|
|
path &&
|
|
path.node &&
|
|
path.node.type === 'BinaryExpression' &&
|
|
!IS_NAN(path.node)
|
|
) {
|
|
console.error('FOLD_BINARY');
|
|
let { left, right, operator } = path.node;
|
|
if (operator === '==' || operator === '!=') {
|
|
let updated = false;
|
|
if (IS_UNDEFINED(left)) {
|
|
updated = true;
|
|
REPLACE(path.get(['left']), NULL);
|
|
}
|
|
if (IS_UNDEFINED(right)) {
|
|
updated = true;
|
|
REPLACE(path.get(['right']), NULL);
|
|
}
|
|
if (updated) {
|
|
return path;
|
|
}
|
|
}
|
|
if (path.node !== INFINITY && path.node !== NEG_INFINITY && IS_CONSTEXPR(left) && IS_CONSTEXPR(right)) {
|
|
left = CONSTVALUE(left);
|
|
right = CONSTVALUE(right);
|
|
let value;
|
|
if ((!left || typeof left !== 'object') && (!right || typeof right !== 'object')) {
|
|
if (operator === '+') {
|
|
value = left + right;
|
|
} else if (operator === '-') {
|
|
value = left - right;
|
|
} else if (operator === '*') {
|
|
value = left * right;
|
|
} else if (operator === '/') {
|
|
value = left / right;
|
|
} else if (operator === '%') {
|
|
value = left % right;
|
|
} else if (operator === '==') {
|
|
value = left == right;
|
|
} else if (operator === '!=') {
|
|
value = left != right;
|
|
} else if (operator === '===') {
|
|
value = left === right;
|
|
} else if (operator === '!==') {
|
|
value = left !== right;
|
|
} else if (operator === '<') {
|
|
value = left < right;
|
|
} else if (operator === '<=') {
|
|
value = left <= right;
|
|
} else if (operator === '>') {
|
|
value = left > right;
|
|
} else if (operator === '>=') {
|
|
value = left >= right;
|
|
} else if (operator === '<<') {
|
|
value = left << right;
|
|
} else if (operator === '>>') {
|
|
value = left >> right;
|
|
} else if (operator === '>>>') {
|
|
value = left >>> right;
|
|
} else if (operator === '|') {
|
|
value = left | right;
|
|
} else if (operator === '&') {
|
|
value = left & right;
|
|
} else if (operator === '^') {
|
|
value = left ^ right;
|
|
}
|
|
}
|
|
else {
|
|
if (operator === '==') value = false;
|
|
if (operator === '===') value = false;
|
|
if (operator === '!=') value = true;
|
|
if (operator === '!==') value = true;
|
|
if (operator === 'in' && typeof right === 'object' && right) {
|
|
value = Boolean(Object.getOwnPropertyDescriptor(right, left));
|
|
}
|
|
}
|
|
if (value !== void 0) {
|
|
if (typeof value === 'string' || typeof value === 'boolean' || value === null) {
|
|
return REPLACE(path, TO_CONSTEXPR(value));
|
|
}
|
|
if (typeof value === 'number') {
|
|
return REPLACE(path, TO_CONSTEXPR(value));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_UNARY = function*(path) {
|
|
if (path && path.node && path.node.type === 'UnaryExpression') {
|
|
console.error('FOLD_UNARY');
|
|
if (IS_CONSTEXPR(path.node)) {
|
|
return yield path;
|
|
}
|
|
let { argument, operator } = path.node;
|
|
if (IS_CONSTEXPR(argument)) {
|
|
if (operator === 'void') {
|
|
return REPLACE(path, UNDEFINED);
|
|
}
|
|
let value = CONSTVALUE(argument);
|
|
if (operator === '-') {
|
|
value = -value;
|
|
} else if (operator === '+') {
|
|
value = +value;
|
|
} else if (operator === '~') {
|
|
value = ~value;
|
|
} else if (operator === '!') {
|
|
value = !value;
|
|
} else if (operator === 'typeof') {
|
|
value = typeof value;
|
|
} else if (operator === 'delete') {
|
|
value = true;
|
|
}
|
|
return REPLACE(path, TO_CONSTEXPR(value));
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
const FOLD_EVAL = function*(path) {
|
|
if (path && path.node && path.node.type === 'CallExpression' &&
|
|
path.node.callee.type === 'Identifier' && path.node.callee.name === 'eval') {
|
|
console.error('FOLD_EVAL');
|
|
if (path.node.arguments.length === 1 && path.node.arguments[0].type === 'Literal') {
|
|
let result = esprima.parse(`${
|
|
CONSTVALUE(path.node.arguments[0])
|
|
}`);
|
|
if (result.body.length === 1 && result.body[0].type === 'ExpressionStatement') {
|
|
return REPLACE(path, result.body[0].expression);
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
}
|
|
const FOLD_MEMBER = function*(path) {
|
|
if (path && path.node && path.node.type === 'MemberExpression') {
|
|
console.error('FOLD_MEMBER');
|
|
if (path.node.computed && path.node.property.type === 'Literal') {
|
|
const current = `${CONSTVALUE(path.node.property)}`;
|
|
if (typeof current === 'string' && /^[$_a-z][$_a-z\d]*$/i.test(current)) {
|
|
path.node.computed = false;
|
|
path.node.property = {
|
|
type: 'Identifier',
|
|
name: current,
|
|
};
|
|
return path;
|
|
}
|
|
}
|
|
if (IS_CONSTEXPR(path.node.object)) {
|
|
const value = CONSTVALUE(path.node.object);
|
|
if (typeof value === 'string' || Array.isArray(value) || (value && typeof value === 'object')) {
|
|
let key;
|
|
if (IS_CONSTEXPR(path.node.property)) {
|
|
key = `${CONSTVALUE(path.node.property)}`;
|
|
}
|
|
else if (!path.node.computed) {
|
|
key = path.node.property.name;
|
|
}
|
|
if (key !== void 0) {
|
|
const desc = Object.getOwnPropertyDescriptor(value, key);
|
|
if (desc) {
|
|
const folded = value[key];
|
|
console.error('FOLDING', JSON.stringify(folded));
|
|
if (IN_PRAGMA_POS(path) && typeof folded === 'string') {
|
|
if (value.length > 1) {
|
|
REPLACE(
|
|
path.get(['object']),
|
|
TO_CONSTEXPR(value.slice(key, key + 1))
|
|
);
|
|
REPLACE(path.get(['property']), TO_CONSTEXPR(0));
|
|
return path;
|
|
}
|
|
} else {
|
|
return REPLACE(path, TO_CONSTEXPR(value[key]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return yield path;
|
|
};
|
|
|
|
const $MIN = Symbol();
|
|
const MIN_TRUE = Object.freeze({
|
|
[$MIN]: true,
|
|
type: 'UnaryExpression',
|
|
operator: '!',
|
|
argument: Object.freeze({
|
|
[$MIN]: true,
|
|
type: 'Literal',
|
|
value: 0
|
|
})
|
|
});
|
|
const MIN_FALSE = Object.freeze({
|
|
[$MIN]: true,
|
|
type: 'UnaryExpression',
|
|
operator: '!',
|
|
argument: Object.freeze({
|
|
[$MIN]: true,
|
|
type: 'Literal',
|
|
value: 1
|
|
})
|
|
});
|
|
const MIN_REPLACEMENTS = new Map;
|
|
MIN_REPLACEMENTS.set(true, MIN_TRUE);
|
|
MIN_REPLACEMENTS.set(false, MIN_FALSE);
|
|
const MIN_VALUES = function*(path) {
|
|
if (path && path.node && !path.node[$MIN] && IS_CONSTEXPR(path.node)) {
|
|
let value = CONSTVALUE(path.node);
|
|
if (MIN_REPLACEMENTS.has(value)) {
|
|
console.error('MIN_VALUE', value)
|
|
return REPLACE(path, MIN_REPLACEMENTS.get(value));
|
|
}
|
|
}
|
|
return yield path;
|
|
}
|
|
|
|
import esprima from 'esprima';
|
|
import util from 'util';
|
|
import escodegen from 'escodegen';
|
|
const optimize = (src) => {
|
|
const ROOT = new NodePath(
|
|
null,
|
|
esprima.parse(
|
|
src,
|
|
{
|
|
// loc: true,
|
|
// source: '<stdin>',
|
|
}
|
|
),
|
|
null
|
|
);
|
|
// all of these are things that could affect completion value positions
|
|
const walk_expressions = WalkCombinator.pipe(
|
|
...[
|
|
WalkCombinator.DEPTH_FIRST,
|
|
{
|
|
// We never work on Arrays
|
|
*inputs(path) {
|
|
if (Array.isArray(path)) return;
|
|
return yield path;
|
|
},
|
|
},
|
|
{ inputs: FOLD_UNREACHABLE },
|
|
{ inputs: FOLD_IF },
|
|
{ inputs: FOLD_SWITCH },
|
|
{ inputs: FOLD_EXPR_STMT },
|
|
{ inputs: FOLD_CONDITIONAL },
|
|
{ inputs: FOLD_LOGICAL },
|
|
{ inputs: FOLD_BINARY },
|
|
{ inputs: FOLD_UNARY },
|
|
{ inputs: FOLD_SEQUENCE },
|
|
{ inputs: FOLD_MEMBER },
|
|
{ inputs: FOLD_EMPTY },
|
|
{ inputs: FOLD_WHILE },
|
|
{ inputs: FOLD_EVAL },
|
|
]
|
|
).walk(ROOT);
|
|
for (const _ of walk_expressions) {
|
|
}
|
|
const minify = WalkCombinator.pipe(
|
|
...[
|
|
WalkCombinator.DEPTH_FIRST,
|
|
{
|
|
// We never work on Arrays
|
|
*inputs(path) {
|
|
if (Array.isArray(path)) return;
|
|
return yield path;
|
|
},
|
|
},
|
|
{ inputs: MIN_VALUES },
|
|
]
|
|
).walk(ROOT);
|
|
for (const _ of minify) {
|
|
}
|
|
return ROOT;
|
|
}
|
|
import mississippi from 'mississippi';
|
|
process.stdin.pipe(
|
|
mississippi.concat(buff => {
|
|
const ROOT = optimize(`${buff}`)
|
|
console.error(
|
|
'%s',
|
|
util.inspect(ROOT.node, {
|
|
depth: null,
|
|
colors: true,
|
|
})
|
|
);
|
|
const out = escodegen.generate(ROOT.node);
|
|
console.log(out);
|
|
})
|
|
);
|