// consumes 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: '', } ), 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); }) );