| // Copyright 2014 The Emscripten Authors. All rights reserved. |
| // Emscripten is available under two separate licenses, the MIT license and the |
| // University of Illinois/NCSA Open Source License. Both these licenses can be |
| // found in the LICENSE file. |
| |
| #include <cstdint> |
| #include <cstdio> |
| #include <cmath> |
| #include <string> |
| #include <algorithm> |
| #include <map> |
| |
| #include "simple_ast.h" |
| #include "optimizer.h" |
| |
| using namespace cashew; |
| |
| typedef std::vector<IString> StringVec; |
| |
| //================== |
| // Globals |
| //================== |
| |
| Ref extraInfo; |
| |
| //================== |
| // Infrastructure |
| //================== |
| |
| template<class T, class V> |
| int indexOf(T list, V value) { |
| for (size_t i = 0; i < list.size(); i++) { |
| if (list[i] == value) return i; |
| } |
| return -1; |
| } |
| |
| int jsD2I(double x) { |
| return (int)((int64_t)x); |
| } |
| |
| char *strdupe(const char *str) { |
| char *ret = (char *)malloc(strlen(str)+1); // leaked! |
| strcpy(ret, str); |
| return ret; |
| } |
| |
| IString getHeapStr(int x, bool unsign) { |
| switch (x) { |
| case 8: return unsign ? HEAPU8 : HEAP8; |
| case 16: return unsign ? HEAPU16 : HEAP16; |
| case 32: return unsign ? HEAPU32 : HEAP32; |
| } |
| assert(0); |
| return ":("; |
| } |
| |
| Ref deStat(Ref node) { |
| if (node[0] == STAT) return node[1]; |
| return node; |
| } |
| |
| Ref getStatements(Ref node) { |
| if (node[0] == DEFUN) { |
| return node[3]; |
| } else if (node[0] == BLOCK) { |
| return node->size() > 1 ? node[1] : nullptr; |
| } else { |
| return arena.alloc(); |
| } |
| } |
| |
| // Types |
| |
| AsmType intToAsmType(int type) { |
| if (type >= 0 && type <= ASM_NONE) return (AsmType)type; |
| else { |
| assert(0); |
| return ASM_NONE; |
| } |
| } |
| |
| // forward decls |
| Ref makeEmpty(); |
| bool isEmpty(Ref node); |
| Ref makeAsmCoercedZero(AsmType type); |
| Ref makeArray(int size_hint); |
| Ref makeBool(bool b); |
| Ref makeNum(double x); |
| Ref makeName(IString str); |
| Ref makeAsmCoercion(Ref node, AsmType type); |
| Ref make1(IString type, Ref a); |
| Ref make3(IString type, Ref a, Ref b, Ref c); |
| |
| AsmData::AsmData(Ref f) { |
| func = f; |
| |
| // process initial params |
| Ref stats = func[3]; |
| size_t i = 0; |
| while (i < stats->size()) { |
| Ref node = stats[i]; |
| if (node[0] != STAT || node[1][0] != ASSIGN || node[1][2][0] != NAME) break; |
| node = node[1]; |
| Ref name = node[2][1]; |
| int index = func[2]->indexOf(name); |
| if (index < 0) break; // not an assign into a parameter, but a global |
| IString& str = name->getIString(); |
| if (locals.count(str) > 0) break; // already done that param, must be starting function body |
| locals[str] = Local(detectType(node[3]), true); |
| params.push_back(str); |
| stats[i] = makeEmpty(); |
| i++; |
| } |
| // process initial variable definitions and remove '= 0' etc parts - these |
| // are not actually assignments in asm.js |
| while (i < stats->size()) { |
| Ref node = stats[i]; |
| if (node[0] != VAR) break; |
| for (size_t j = 0; j < node[1]->size(); j++) { |
| Ref v = node[1][j]; |
| IString& name = v[0]->getIString(); |
| Ref value = v[1]; |
| if (locals.count(name) == 0) { |
| locals[name] = Local(detectType(value, nullptr, true), false); |
| vars.push_back(name); |
| v->setSize(1); // make an un-assigning var |
| } else { |
| assert(j == 0); // cannot break in the middle |
| goto outside; |
| } |
| } |
| i++; |
| } |
| outside: |
| // look for other var definitions and collect them |
| while (i < stats->size()) { |
| traversePre(stats[i], [&](Ref node) { |
| Ref type = node[0]; |
| if (type == VAR) { |
| dump("bad, seeing a var in need of fixing", func); |
| abort(); //, 'should be no vars to fix! ' + func[1] + ' : ' + JSON.stringify(node)); |
| } |
| }); |
| i++; |
| } |
| // look for final RETURN statement to get return type. |
| Ref retStmt = stats->back(); |
| if (!!retStmt && retStmt[0] == RETURN && !!retStmt[1]) { |
| ret = detectType(retStmt[1]); |
| } else { |
| ret = ASM_NONE; |
| } |
| } |
| |
| void AsmData::denormalize() { |
| Ref stats = func[3]; |
| // Remove var definitions, if any |
| for (size_t i = 0; i < stats->size(); i++) { |
| if (stats[i][0] == VAR) { |
| stats[i] = makeEmpty(); |
| } else { |
| if (!isEmpty(stats[i])) break; |
| } |
| } |
| // calculate variable definitions |
| Ref varDefs = makeArray(vars.size()); |
| for (auto v : vars) { |
| varDefs->push_back(make1(v, makeAsmCoercedZero(locals[v].type))); |
| } |
| // each param needs a line; reuse emptyNodes as much as we can |
| size_t numParams = params.size(); |
| size_t emptyNodes = 0; |
| while (emptyNodes < stats->size()) { |
| if (!isEmpty(stats[emptyNodes])) break; |
| emptyNodes++; |
| } |
| size_t neededEmptyNodes = numParams + (varDefs->size() ? 1 : 0); // params plus one big var if there are vars |
| if (neededEmptyNodes > emptyNodes) { |
| stats->insert(0, neededEmptyNodes - emptyNodes); |
| } else if (neededEmptyNodes < emptyNodes) { |
| stats->splice(0, emptyNodes - neededEmptyNodes); |
| } |
| // add param coercions |
| int next = 0; |
| for (auto param : func[2]->getArray()) { |
| IString str = param->getIString(); |
| assert(locals.count(str) > 0); |
| stats[next++] = make1(STAT, make3(ASSIGN, makeBool(true), makeName(str.c_str()), makeAsmCoercion(makeName(str.c_str()), locals[str].type))); |
| } |
| if (varDefs->size()) { |
| stats[next] = make1(VAR, varDefs); |
| } |
| /* |
| if (inlines->size() > 0) { |
| var i = 0; |
| traverse(func, function(node, type) { |
| if (type == CALL && node[1][0] == NAME && node[1][1] == 'inlinejs') { |
| node[1] = inlines[i++]; // swap back in the body |
| } |
| }); |
| } |
| */ |
| // ensure that there's a final RETURN statement if needed. |
| if (ret != ASM_NONE) { |
| Ref retStmt = stats->back(); |
| if (!retStmt || retStmt[0] != RETURN) { |
| stats->push_back(make1(RETURN, makeAsmCoercedZero(ret))); |
| } |
| } |
| //printErr('denormalized \n\n' + astToSrc(func) + '\n\n'); |
| } |
| |
| // Constructions TODO: share common constructions, and assert they remain frozen |
| |
| Ref makeArray(int size_hint=0) { |
| return &arena.alloc()->setArray(size_hint); |
| } |
| |
| Ref makeBool(bool b) { |
| return &arena.alloc()->setBool(b); |
| } |
| |
| Ref makeString(const IString& s) { |
| return &arena.alloc()->setString(s); |
| } |
| |
| Ref makeEmpty() { |
| return ValueBuilder::makeToplevel(); |
| } |
| |
| Ref makeNum(double x) { |
| return ValueBuilder::makeDouble(x); |
| } |
| |
| Ref makeName(IString str) { |
| return ValueBuilder::makeName(str); |
| } |
| |
| Ref makeBlock() { |
| return ValueBuilder::makeBlock(); |
| } |
| |
| Ref make1(IString s1, Ref a) { |
| Ref ret(makeArray(2)); |
| ret->push_back(makeString(s1)); |
| ret->push_back(a); |
| return ret; |
| } |
| |
| Ref make2(IString s1, IString s2, Ref a) { |
| Ref ret(makeArray(2)); |
| ret->push_back(makeString(s1)); |
| ret->push_back(makeString(s2)); |
| ret->push_back(a); |
| return ret; |
| } |
| |
| Ref make2(IString s1, Ref a, Ref b) { |
| Ref ret(makeArray(3)); |
| ret->push_back(makeString(s1)); |
| ret->push_back(a); |
| ret->push_back(b); |
| return ret; |
| } |
| |
| Ref make3(IString type, IString a, Ref b, Ref c) { |
| Ref ret(makeArray(4)); |
| ret->push_back(makeString(type)); |
| ret->push_back(makeString(a)); |
| ret->push_back(b); |
| ret->push_back(c); |
| return ret; |
| } |
| |
| Ref make3(IString type, Ref a, Ref b, Ref c) { |
| Ref ret(makeArray(4)); |
| ret->push_back(makeString(type)); |
| ret->push_back(a); |
| ret->push_back(b); |
| ret->push_back(c); |
| return ret; |
| } |
| |
| Ref makeAsmCoercedZero(AsmType type) { |
| switch (type) { |
| case ASM_INT: return makeNum(0); break; |
| case ASM_DOUBLE: return make2(UNARY_PREFIX, PLUS, makeNum(0)); break; |
| case ASM_FLOAT: { |
| if (!ASM_FLOAT_ZERO.isNull()) { |
| return makeName(ASM_FLOAT_ZERO); |
| } else { |
| return make2(CALL, makeName(MATH_FROUND), &(makeArray(1))->push_back(makeNum(0))); |
| } |
| break; |
| } |
| case ASM_FLOAT32X4: { |
| return make2(CALL, makeName(SIMD_FLOAT32X4), &(makeArray(4))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_FLOAT64X2: { |
| return make2(CALL, makeName(SIMD_FLOAT64X2), &(makeArray(2))->push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_INT8X16: { |
| return make2(CALL, makeName(SIMD_INT8X16), &(makeArray(16))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_INT16X8: { |
| return make2(CALL, makeName(SIMD_INT16X8), &(makeArray(8))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_INT32X4: { |
| return make2(CALL, makeName(SIMD_INT32X4), &(makeArray(4))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_BOOL8X16: { |
| return make2(CALL, makeName(SIMD_BOOL8X16), &(makeArray(16))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_BOOL16X8: { |
| return make2(CALL, makeName(SIMD_BOOL16X8), &(makeArray(8))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_BOOL32X4: { |
| return make2(CALL, makeName(SIMD_BOOL32X4), &(makeArray(4))->push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| case ASM_BOOL64X2: { |
| return make2(CALL, makeName(SIMD_BOOL64X2), &(makeArray(2))->push_back(makeNum(0)).push_back(makeNum(0))); |
| break; |
| } |
| default: assert(0); |
| } |
| abort(); |
| } |
| |
| Ref makeAsmCoercion(Ref node, AsmType type) { |
| switch (type) { |
| case ASM_INT: return make3(BINARY, OR, node, makeNum(0)); |
| case ASM_DOUBLE: return make2(UNARY_PREFIX, PLUS, node); |
| case ASM_FLOAT: return make2(CALL, makeName(MATH_FROUND), &(makeArray(1))->push_back(node)); |
| case ASM_FLOAT32X4: return make2(CALL, makeName(SIMD_FLOAT32X4_CHECK), &(makeArray(1))->push_back(node)); |
| case ASM_FLOAT64X2: return make2(CALL, makeName(SIMD_FLOAT64X2_CHECK), &(makeArray(1))->push_back(node)); |
| case ASM_INT8X16: return make2(CALL, makeName(SIMD_INT8X16_CHECK), &(makeArray(1))->push_back(node)); |
| case ASM_INT16X8: return make2(CALL, makeName(SIMD_INT16X8_CHECK), &(makeArray(1))->push_back(node)); |
| case ASM_INT32X4: return make2(CALL, makeName(SIMD_INT32X4_CHECK), &(makeArray(1))->push_back(node)); |
| case ASM_NONE: |
| default: return node; // non-validating code, emit nothing XXX this is dangerous, we should only allow this when we know we are not validating |
| } |
| } |
| |
| // Checks |
| |
| bool isEmpty(Ref node) { |
| return (node->size() == 2 && node[0] == TOPLEVEL && node[1]->size() == 0) || |
| (node->size() > 0 && node[0] == BLOCK && (!node[1] || node[1]->size() == 0)); |
| } |
| |
| bool commable(Ref node) { // TODO: hashing |
| IString type = node[0]->getIString(); |
| if (type == ASSIGN || type == BINARY || type == UNARY_PREFIX || type == NAME || type == NUM || type == CALL || type == SEQ || type == CONDITIONAL || type == SUB) return true; |
| return false; |
| } |
| |
| bool isMathFunc(const char *name) { |
| static const char *Math_ = "Math_"; |
| static unsigned size = strlen(Math_); |
| return strncmp(name, Math_, size) == 0; |
| } |
| |
| bool isMathFunc(Ref value) { |
| return value->isString() && isMathFunc(value->getCString()); |
| } |
| |
| bool callHasSideEffects(Ref node) { // checks if the call itself (not the args) has side effects (or is not statically known) |
| return !(node[1][0] == NAME && isMathFunc(node[1][1])); |
| } |
| |
| bool hasSideEffects(Ref node) { // this is 99% incomplete! |
| IString type = node[0]->getIString(); |
| switch (type[0]) { |
| case 'n': |
| if (type == NUM || type == NAME) return false; |
| break; |
| case 's': |
| if (type == STRING) return false; |
| if (type == SUB) return hasSideEffects(node[1]) || hasSideEffects(node[2]); |
| break; |
| case 'u': |
| if (type == UNARY_PREFIX) return hasSideEffects(node[2]); |
| break; |
| case 'b': |
| if (type == BINARY) return hasSideEffects(node[2]) || hasSideEffects(node[3]); |
| break; |
| case 'c': |
| if (type == CALL) { |
| if (callHasSideEffects(node)) return true; |
| // This is a statically known call, with no side effects. only args can side effect us |
| for (auto arg : node[2]->getArray()) { |
| if (hasSideEffects(arg)) return true; |
| } |
| return false; |
| } else if (type == CONDITIONAL) return hasSideEffects(node[1]) || hasSideEffects(node[2]) || hasSideEffects(node[3]); |
| break; |
| } |
| return true; |
| } |
| |
| // checks if a node has just basic operations, nothing with side effects nor that can notice side effects, which |
| // implies we can move it around in the code |
| bool triviallySafeToMove(Ref node, AsmData& asmData) { |
| bool ok = true; |
| traversePre(node, [&](Ref node) { |
| Ref type = node[0]; |
| if (type == STAT || type == BINARY || type == UNARY_PREFIX || type == ASSIGN || type == NUM) return; |
| else if (type == NAME) { |
| if (!asmData.isLocal(node[1]->getIString())) ok = false; |
| } else if (type == CALL) { |
| if (callHasSideEffects(node)) ok = false; |
| } else { |
| ok = false; |
| } |
| }); |
| return ok; |
| } |
| |
| // Transforms |
| |
| // We often have branchings that are simplified so one end vanishes, and |
| // we then get |
| // if (!(x < 5)) |
| // or such. Simplifying these saves space and time. |
| Ref simplifyNotCompsDirect(Ref node) { |
| if (node[0] == UNARY_PREFIX && node[1] == L_NOT) { |
| // de-morgan's laws do not work on floats, due to nans >:( |
| if (node[2][0] == BINARY && (detectType(node[2][2]) == ASM_INT && detectType(node[2][3]) == ASM_INT)) { |
| Ref op = node[2][1]; |
| switch(op->getCString()[0]) { |
| case '<': { |
| if (op == LT) { op->setString(GE); break; } |
| if (op == LE) { op->setString(GT); break; } |
| return node; |
| } |
| case '>': { |
| if (op == GT) { op->setString(LE); break; } |
| if (op == GE) { op->setString(LT); break; } |
| return node; |
| } |
| case '=': { |
| if (op == EQ) { op->setString(NE); break; } |
| return node; |
| } |
| case '!': { |
| if (op == NE) { op->setString(EQ); break; } |
| return node; |
| } |
| default: return node; |
| } |
| return make3(BINARY, op, node[2][2], node[2][3]); |
| } else if (node[2][0] == UNARY_PREFIX && node[2][1] == L_NOT) { |
| return node[2][2]; |
| } |
| } |
| return node; |
| } |
| |
| Ref flipCondition(Ref cond) { |
| return simplifyNotCompsDirect(make2(UNARY_PREFIX, L_NOT, cond)); |
| } |
| |
| void safeCopy(Ref target, Ref source) { // safely copy source onto target, even if source is a subnode of target |
| Ref temp = source; // hold on to source |
| *target = *temp; |
| } |
| |
| void clearEmptyNodes(Ref arr) { |
| int skip = 0; |
| for (size_t i = 0; i < arr->size(); i++) { |
| if (skip) { |
| arr[i-skip] = arr[i]; |
| } |
| if (isEmpty(deStat(arr[i]))) { |
| skip++; |
| } |
| } |
| if (skip) arr->setSize(arr->size() - skip); |
| } |
| void clearUselessNodes(Ref arr) { |
| int skip = 0; |
| for (size_t i = 0; i < arr->size(); i++) { |
| Ref curr = arr[i]; |
| if (skip) { |
| arr[i-skip] = curr; |
| } |
| if (isEmpty(deStat(curr)) || (curr[0] == STAT && !hasSideEffects(curr[1]))) { |
| skip++; |
| } |
| } |
| if (skip) arr->setSize(arr->size() - skip); |
| } |
| |
| void removeAllEmptySubNodes(Ref ast) { |
| traversePre(ast, [](Ref node) { |
| if (node[0] == DEFUN) { |
| clearEmptyNodes(node[3]); |
| } else if (node[0] == BLOCK && node->size() > 1 && !!node[1]) { |
| clearEmptyNodes(node[1]); |
| } else if (node[0] == SEQ && isEmpty(node[1])) { |
| safeCopy(node, node[2]); |
| } |
| }); |
| } |
| void removeAllUselessSubNodes(Ref ast) { |
| traversePrePost(ast, [](Ref node) { |
| Ref type = node[0]; |
| if (type == DEFUN) { |
| clearUselessNodes(node[3]); |
| } else if (type == BLOCK && node->size() > 1 && !!node[1]) { |
| clearUselessNodes(node[1]); |
| } else if (type == SEQ && isEmpty(node[1])) { |
| safeCopy(node, node[2]); |
| } |
| }, [](Ref node) { |
| Ref type = node[0]; |
| if (type == IF) { |
| bool empty2 = isEmpty(node[2]), has3 = node->size() == 4 && !!node[3], empty3 = !has3 || isEmpty(node[3]); |
| if (!empty2 && empty3 && has3) { // empty else clauses |
| node->setSize(3); |
| } else if (empty2 && !empty3) { // empty if blocks |
| safeCopy(node, make2(IF, make2(UNARY_PREFIX, L_NOT, node[1]), node[3])); |
| } else if (empty2 && empty3) { |
| if (hasSideEffects(node[1])) { |
| safeCopy(node, make1(STAT, node[1])); |
| } else { |
| safeCopy(node, makeEmpty()); |
| } |
| } |
| } |
| }); |
| } |
| |
| Ref unVarify(Ref vars) { // transform var x=1, y=2 etc. into (x=1, y=2), i.e., the same assigns, but without a var definition |
| Ref ret = makeArray(1); |
| ret->push_back(makeString(STAT)); |
| if (vars->size() == 1) { |
| ret->push_back(make3(ASSIGN, makeBool(true), makeName(vars[0][0]->getIString()), vars[0][1])); |
| } else { |
| ret->push_back(makeArray(vars->size()-1)); |
| Ref curr = ret[1]; |
| for (size_t i = 0; i+1 < vars->size(); i++) { |
| curr->push_back(makeString(SEQ)); |
| curr->push_back(make3(ASSIGN, makeBool(true), makeName(vars[i][0]->getIString()), vars[i][1])); |
| if (i != vars->size()-2) { |
| curr->push_back(makeArray()); |
| curr = curr[2]; |
| } |
| } |
| curr->push_back(make3(ASSIGN, makeBool(true), makeName(vars->back()[0]->getIString()), vars->back()[1])); |
| } |
| return ret; |
| } |
| |
| // Calculations |
| |
| int measureCost(Ref ast) { |
| int size = 0; |
| traversePre(ast, [&size](Ref node) { |
| Ref type = node[0]; |
| if (type == NUM || type == UNARY_PREFIX) size--; |
| else if (type == BINARY) { |
| if (node[3][0] == NUM && node[3][1]->getNumber() == 0) size--; |
| else if (node[1] == DIV || node[1] == MOD) size += 2; |
| } |
| else if (type == CALL && !callHasSideEffects(node)) size -= 2; |
| else if (type == SUB) size++; |
| size++; |
| }); |
| return size; |
| } |
| |
| //================== |
| // Params |
| //================== |
| |
| bool preciseF32 = false, |
| receiveJSON = false, |
| emitJSON = false, |
| minifyWhitespace = false, |
| last = false; |
| |
| //===================== |
| // Optimization passes |
| //===================== |
| |
| #define HASES \ |
| bool has(const IString& str) { \ |
| return count(str) > 0; \ |
| } \ |
| bool has(Ref node) { \ |
| return node->isString() && count(node->getIString()) > 0; \ |
| } |
| |
| class StringSet : public cashew::IStringSet { |
| public: |
| StringSet() {} |
| StringSet(const char *str) : IStringSet(str) {} |
| |
| HASES |
| |
| void dump() { |
| err("==="); |
| for (auto str : *this) { |
| errv("%s", str.c_str()); |
| } |
| err("==="); |
| } |
| }; |
| |
| StringSet USEFUL_BINARY_OPS("<< >> | & ^"), |
| COMPARE_OPS("< <= > >= == == != !=="), |
| BITWISE("| & ^"), |
| SAFE_BINARY_OPS("+ -"), // division is unsafe as it creates non-ints in JS; mod is unsafe as signs matter so we can't remove |0's; mul does not nest with +,- in asm |
| COERCION_REQUIRING_OPS("sub unary-prefix"), // ops that in asm must be coerced right away |
| COERCION_REQUIRING_BINARIES("* / %"); // binary ops that in asm must be coerced |
| |
| StringSet ASSOCIATIVE_BINARIES("+ * | & ^"), |
| CONTROL_FLOW("do while for if switch"), |
| LOOP("do while for"), |
| NAME_OR_NUM("name num"), |
| CONDITION_CHECKERS("if do while switch"), |
| BOOLEAN_RECEIVERS("if do while conditional"), |
| SAFE_TO_DROP_COERCION("unary-prefix name num"); |
| |
| StringSet BREAK_CAPTURERS("do while for switch"), |
| CONTINUE_CAPTURERS("do while for"), |
| FUNCTIONS_THAT_ALWAYS_THROW("abort ___resumeException ___cxa_throw ___cxa_rethrow"); |
| |
| IString DCEABLE_TYPE_DECLS("__emscripten_dceable_type_decls"); |
| |
| |
| bool isFunctionTable(const char *name) { |
| static const char *functionTable = "FUNCTION_TABLE"; |
| static unsigned size = strlen(functionTable); |
| return strncmp(name, functionTable, size) == 0; |
| } |
| |
| bool isFunctionTable(Ref value) { |
| return value->isString() && isFunctionTable(value->getCString()); |
| } |
| |
| // Internal utilities |
| |
| bool canDropCoercion(Ref node) { |
| if (SAFE_TO_DROP_COERCION.has(node[0])) return true; |
| if (node[0] == BINARY) { |
| switch (node[1]->getCString()[0]) { |
| case '>': return node[1] == RSHIFT || node[1] == TRSHIFT; |
| case '<': return node[1] == LSHIFT; |
| case '|': case '^': case '&': return true; |
| } |
| } |
| return false; |
| } |
| |
| Ref simplifyCondition(Ref node) { |
| node = simplifyNotCompsDirect(node); |
| // on integers, if (x == 0) is the same as if (x), and if (x != 0) as if (!x) |
| if (node[0] == BINARY && (node[1] == EQ || node[1] == NE)) { |
| Ref target; |
| if (detectType(node[2]) == ASM_INT && node[3][0] == NUM && node[3][1]->getNumber() == 0) { |
| target = node[2]; |
| } else if (detectType(node[3]) == ASM_INT && node[2][0] == NUM && node[2][1]->getNumber() == 0) { |
| target = node[3]; |
| } |
| if (!!target) { |
| if (target[0] == BINARY && (target[1] == OR || target[1] == TRSHIFT) && target[3][0] == NUM && target[3][1]->getNumber() == 0 && |
| canDropCoercion(target[2])) { |
| target = target[2]; // drop the coercion, in a condition it is ok to do if (x) |
| } |
| if (node[1] == EQ) { |
| return make2(UNARY_PREFIX, L_NOT, target); |
| } else { |
| return target; |
| } |
| } |
| } |
| return node; |
| } |
| |
| // Passes |
| |
| // Eliminator aka Expressionizer |
| // |
| // The goal of this pass is to eliminate unneeded variables (which represent one of the infinite registers in the LLVM |
| // model) and thus to generate complex expressions where possible, for example |
| // |
| // var x = a(10); |
| // var y = HEAP[20]; |
| // print(x+y); |
| // |
| // can be transformed into |
| // |
| // print(a(10)+HEAP[20]); |
| // |
| // The basic principle is to scan along the code in the order of parsing/execution, and keep a list of tracked |
| // variables that are current contenders for elimination. We must untrack when we see something that we cannot |
| // cross, for example, a write to memory means we must invalidate variables that depend on reading from |
| // memory, since if we change the order then we do not preserve the computation. |
| // |
| // We rely on some assumptions about emscripten-generated code here, which means we can do a lot more than |
| // a general JS optimization can. For example, we assume that SUB nodes (indexing like HEAP[..]) are |
| // memory accesses or FUNCTION_TABLE accesses, and in both cases that the symbol cannot be replaced although |
| // the contents can. So we assume FUNCTION_TABLE might have its contents changed but not be pointed to |
| // a different object, which allows |
| // |
| // var x = f(); |
| // FUNCTION_TABLE[x](); |
| // |
| // to be optimized (f could replace FUNCTION_TABLE, so in general JS eliminating x is not valid). |
| // |
| // In memSafe mode, we are more careful and assume functions can replace HEAP and FUNCTION_TABLE, which |
| // can happen in ALLOW_MEMORY_GROWTH mode |
| |
| StringSet ELIMINATION_SAFE_NODES("assign call if toplevel do return label switch binary unary-prefix"); // do is checked carefully, however |
| StringSet IGNORABLE_ELIMINATOR_SCAN_NODES("num toplevel string break continue dot"); // dot can only be STRING_TABLE.* |
| StringSet ABORTING_ELIMINATOR_SCAN_NODES("new object function defun for while array throw"); // we could handle some of these, TODO, but nontrivial (e.g. for while, the condition is hit multiple times after the body) |
| StringSet HEAP_NAMES("HEAP8 HEAP16 HEAP32 HEAPU8 HEAPU16 HEAPU32 HEAPF32 HEAPF64"); |
| |
| bool isTempDoublePtrAccess(Ref node) { // these are used in bitcasts; they are not really affecting memory, and should cause no invalidation |
| assert(node[0] == SUB); |
| return (node[2][0] == NAME && node[2][1] == TEMP_DOUBLE_PTR) || |
| (node[2][0] == BINARY && ((node[2][2][0] == NAME && node[2][2][1] == TEMP_DOUBLE_PTR) || |
| (node[2][3][0] == NAME && node[2][3][1] == TEMP_DOUBLE_PTR))); |
| } |
| |
| class StringIntMap : public std::unordered_map<IString, int> { |
| public: |
| HASES |
| }; |
| |
| class StringStringMap : public std::unordered_map<IString, IString> { |
| public: |
| HASES |
| }; |
| |
| class StringRefMap : public std::unordered_map<IString, Ref> { |
| public: |
| HASES |
| }; |
| |
| class StringTypeMap : public std::unordered_map<IString, AsmType> { |
| public: |
| HASES |
| }; |
| |
| void eliminate(Ref ast, bool memSafe) { |
| #ifdef PROFILING |
| clock_t tasmdata = 0; |
| clock_t tfnexamine = 0; |
| clock_t tvarcheck = 0; |
| clock_t tstmtelim = 0; |
| clock_t tstmtscan = 0; |
| clock_t tcleanvars = 0; |
| clock_t treconstruct = 0; |
| #endif |
| |
| // Find variables that have a single use, and if they can be eliminated, do so |
| traverseFunctions(ast, [&](Ref func) { |
| |
| #ifdef PROFILING |
| clock_t start = clock(); |
| #endif |
| |
| AsmData asmData(func); |
| |
| #ifdef PROFILING |
| tasmdata += clock() - start; |
| start = clock(); |
| #endif |
| |
| // First, find the potentially eliminatable functions: that have one definition and one use |
| |
| StringIntMap definitions; |
| StringIntMap uses; |
| StringIntMap namings; |
| StringRefMap values; |
| StringIntMap varsToRemove; // variables being removed, that we can eliminate all 'var x;' of (this refers to VAR nodes we should remove) |
| // 1 means we should remove it, 2 means we successfully removed it |
| StringSet varsToTryToRemove; // variables that have 0 uses, but have side effects - when we scan we can try to remove them |
| |
| // examine body and note locals |
| traversePre(func, [&](Ref node) { |
| Ref type = node[0]; |
| if (type == NAME) { |
| IString& name = node[1]->getIString(); |
| uses[name]++; |
| namings[name]++; |
| } else if (type == ASSIGN) { |
| Ref target = node[2]; |
| if (target[0] == NAME) { |
| IString& name = target[1]->getIString(); |
| // values is only used if definitions is 1 |
| if (definitions[name]++ == 0) { |
| values[name] = node[3]; |
| } |
| assert(node[1]->isBool(true)); // not +=, -= etc., just = |
| uses[name]--; // because the name node will show up by itself in the previous case |
| } |
| } |
| }); |
| |
| #ifdef PROFILING |
| tfnexamine += clock() - start; |
| start = clock(); |
| #endif |
| |
| StringSet potentials; // local variables with 1 definition and 1 use |
| StringSet sideEffectFree; // whether a local variable has no side effects in its definition. Only relevant when there are no uses |
| |
| auto unprocessVariable = [&](IString name) { |
| potentials.erase(name); |
| varsToRemove.erase(name); |
| sideEffectFree.erase(name); |
| varsToTryToRemove.erase(name); |
| }; |
| std::function<void (IString)> processVariable = [&](IString name) { |
| if (definitions[name] == 1 && uses[name] == 1) { |
| potentials.insert(name); |
| } else if (uses[name] == 0 && definitions[name] <= 1) { // no uses, no def or 1 def (cannot operate on phis, and the llvm optimizer will remove unneeded phis anyhow) (no definition means it is a function parameter, or a local with just |var x;| but no defining assignment) |
| bool sideEffects = false; |
| auto val = values.find(name); |
| Ref value; |
| if (val != values.end()) { |
| value = val->second; |
| // TODO: merge with other side effect code |
| // First, pattern-match |
| // (HEAP32[((tempDoublePtr)>>2)]=((HEAP32[(($_sroa_0_0__idx1)>>2)])|0),HEAP32[(((tempDoublePtr)+(4))>>2)]=((HEAP32[((($_sroa_0_0__idx1)+(4))>>2)])|0),(+(HEAPF64[(tempDoublePtr)>>3]))) |
| // which has no side effects and is the special form of converting double to i64. |
| if (!(value[0] == SEQ && value[1][0] == ASSIGN && value[1][2][0] == SUB && value[1][2][2][0] == BINARY && value[1][2][2][1] == RSHIFT && |
| value[1][2][2][2][0] == NAME && value[1][2][2][2][1] == TEMP_DOUBLE_PTR)) { |
| // If not that, then traverse and scan normally. |
| sideEffects = hasSideEffects(value); |
| } |
| } |
| if (!sideEffects) { |
| varsToRemove[name] = !definitions[name] ? 2 : 1; // remove it normally |
| sideEffectFree.insert(name); |
| // Each time we remove a variable with 0 uses, if its value has no |
| // side effects and vanishes too, then we can remove a use from variables |
| // appearing in it, and possibly eliminate again |
| if (!!value) { |
| traversePre(value, [&](Ref node) { |
| if (node[0] == NAME) { |
| IString name = node[1]->getIString(); |
| node[1]->setString(EMPTY); // we can remove this - it will never be shown, and should not be left to confuse us as we traverse |
| if (asmData.isLocal(name)) { |
| uses[name]--; // cannot be infinite recursion since we descend an energy function |
| assert(uses[name] >= 0); |
| unprocessVariable(name); |
| processVariable(name); |
| } |
| } else if (node[0] == CALL) { |
| // no side effects, so this must be a Math.* call or such. We can just ignore it and all children |
| node[0]->setString(NAME); |
| node[1]->setString(EMPTY); |
| } |
| }); |
| } |
| } else { |
| varsToTryToRemove.insert(name); // try to remove it later during scanning |
| } |
| } |
| }; |
| for (auto name : asmData.locals) { |
| processVariable(name.first); |
| } |
| |
| #ifdef PROFILING |
| tvarcheck += clock() - start; |
| start = clock(); |
| #endif |
| |
| //printErr('defs: ' + JSON.stringify(definitions)); |
| //printErr('uses: ' + JSON.stringify(uses)); |
| //printErr('values: ' + JSON.stringify(values)); |
| //printErr('locals: ' + JSON.stringify(locals)); |
| //printErr('varsToRemove: ' + JSON.stringify(varsToRemove)); |
| //printErr('varsToTryToRemove: ' + JSON.stringify(varsToTryToRemove)); |
| values.clear(); |
| //printErr('potentials: ' + JSON.stringify(potentials)); |
| // We can now proceed through the function. In each list of statements, we try to eliminate |
| struct Tracking { |
| bool usesGlobals, usesMemory, hasDeps; |
| Ref defNode; |
| bool doesCall; |
| }; |
| class Tracked : public std::unordered_map<IString, Tracking> { |
| public: |
| HASES |
| }; |
| Tracked tracked; |
| #define dumpTracked() { errv("tracking %d", tracked.size()); for (auto t : tracked) errv("... %s", t.first.c_str()); } |
| // Although a set would be more appropriate, it would also be slower |
| std::unordered_map<IString, StringVec> depMap; |
| |
| bool globalsInvalidated = false; // do not repeat invalidations, until we track something new |
| bool memoryInvalidated = false; |
| bool callsInvalidated = false; |
| auto track = [&](IString name, Ref value, Ref defNode) { // add a potential that has just been defined to the tracked list, we hope to eliminate it |
| Tracking& track = tracked[name]; |
| track.usesGlobals = false; |
| track.usesMemory = false; |
| track.hasDeps = false; |
| track.defNode = defNode; |
| track.doesCall = false; |
| bool ignoreName = false; // one-time ignorings of names, as first op in sub and call |
| traversePre(value, [&](Ref node) { |
| Ref type = node[0]; |
| if (type == NAME) { |
| if (!ignoreName) { |
| IString depName = node[1]->getIString(); |
| if (!asmData.isLocal(depName)) { |
| track.usesGlobals = true; |
| } |
| if (!potentials.has(depName)) { // deps do not matter for potentials - they are defined once, so no complexity |
| depMap[depName].push_back(name); |
| track.hasDeps = true; |
| } |
| } else { |
| ignoreName = false; |
| } |
| } else if (type == SUB) { |
| track.usesMemory = true; |
| ignoreName = true; |
| } else if (type == CALL) { |
| track.usesGlobals = true; |
| track.usesMemory = true; |
| track.doesCall = true; |
| ignoreName = true; |
| } else { |
| ignoreName = false; |
| } |
| }); |
| if (track.usesGlobals) globalsInvalidated = false; |
| if (track.usesMemory) memoryInvalidated = false; |
| if (track.doesCall) callsInvalidated = false; |
| }; |
| |
| // TODO: invalidate using a sequence number for each type (if you were tracked before the last invalidation, you are cancelled). remove for.in loops |
| #define INVALIDATE(what, check) \ |
| auto invalidate##what = [&]() { \ |
| std::vector<IString> temp; \ |
| for (auto t : tracked) { \ |
| IString name = t.first; \ |
| Tracking& info = tracked[name]; \ |
| if (check) { \ |
| temp.push_back(name); \ |
| } \ |
| } \ |
| for (size_t i = 0; i < temp.size(); i++) { \ |
| tracked.erase(temp[i]); \ |
| } \ |
| }; |
| INVALIDATE(Globals, info.usesGlobals); |
| INVALIDATE(Memory, info.usesMemory); |
| INVALIDATE(Calls, info.doesCall); |
| |
| auto invalidateByDep = [&](IString dep) { |
| for (auto name : depMap[dep]) { |
| tracked.erase(name); |
| } |
| depMap.erase(dep); |
| }; |
| |
| std::function<void (IString name, Ref node)> doEliminate; |
| |
| // Generate the sequence of execution. This determines what is executed before what, so we know what can be reordered. Using |
| // that, performs invalidations and eliminations |
| auto scan = [&](Ref node) { |
| bool abort = false; |
| bool allowTracking = true; // false inside an if; also prevents recursing in an if |
| std::function<void (Ref, bool)> traverseInOrder = [&](Ref node, bool ignoreSub) { |
| if (abort) return; |
| Ref type = node[0]; |
| if (type == ASSIGN) { |
| Ref target = node[2]; |
| Ref value = node[3]; |
| bool nameTarget = target[0] == NAME; |
| // If this is an assign to a name, handle it below rather than |
| // traversing and treating as a read |
| if (!nameTarget) { |
| traverseInOrder(target, true); // evaluate left |
| } |
| traverseInOrder(value, false); // evaluate right |
| // do the actual assignment |
| if (nameTarget) { |
| IString name = target[1]->getIString(); |
| if (potentials.has(name) && allowTracking) { |
| track(name, node[3], node); |
| } else if (varsToTryToRemove.has(name)) { |
| // replace it in-place |
| safeCopy(node, value); |
| varsToRemove[name] = 2; |
| } else { |
| // expensive check for invalidating specific tracked vars. This list is generally quite short though, because of |
| // how we just eliminate in short spans and abort when control flow happens TODO: history numbers instead |
| invalidateByDep(name); // can happen more than once per dep.. |
| if (!asmData.isLocal(name) && !globalsInvalidated) { |
| invalidateGlobals(); |
| globalsInvalidated = true; |
| } |
| // if we can track this name (that we assign into), and it has 0 uses and we want to remove its VAR |
| // definition - then remove it right now, there is no later chance |
| if (allowTracking && varsToRemove.has(name) && uses[name] == 0) { |
| track(name, node[3], node); |
| doEliminate(name, node); |
| } |
| } |
| } else if (target[0] == SUB) { |
| if (isTempDoublePtrAccess(target)) { |
| if (!globalsInvalidated) { |
| invalidateGlobals(); |
| globalsInvalidated = true; |
| } |
| } else if (!memoryInvalidated) { |
| invalidateMemory(); |
| memoryInvalidated = true; |
| } |
| } |
| } else if (type == SUB) { |
| // Only keep track of the global array names in memsafe mode i.e. |
| // when they may change underneath us due to resizing |
| if (node[1][0] != NAME || memSafe) { |
| traverseInOrder(node[1], false); // evaluate inner |
| } |
| traverseInOrder(node[2], false); // evaluate outer |
| // ignoreSub means we are a write (happening later), not a read |
| if (!ignoreSub && !isTempDoublePtrAccess(node)) { |
| // do the memory access |
| if (!callsInvalidated) { |
| invalidateCalls(); |
| callsInvalidated = true; |
| } |
| } |
| } else if (type == BINARY) { |
| bool flipped = false; |
| if (ASSOCIATIVE_BINARIES.has(node[1]) && !NAME_OR_NUM.has(node[2][0]) && NAME_OR_NUM.has(node[3][0])) { // TODO recurse here? |
| // associatives like + and * can be reordered in the simple case of one of the sides being a name, since we assume they are all just numbers |
| Ref temp = node[2]; |
| node[2] = node[3]; |
| node[3] = temp; |
| flipped = true; |
| } |
| traverseInOrder(node[2], false); |
| traverseInOrder(node[3], false); |
| if (flipped && NAME_OR_NUM.has(node[2][0])) { // dunno if we optimized, but safe to flip back - and keeps the code closer to the original and more readable |
| Ref temp = node[2]; |
| node[2] = node[3]; |
| node[3] = temp; |
| } |
| } else if (type == NAME) { |
| IString name = node[1]->getIString(); |
| if (tracked.has(name)) { |
| doEliminate(name, node); |
| } else if (!asmData.isLocal(name) && !callsInvalidated && (memSafe || !HEAP_NAMES.has(name))) { // ignore HEAP8 etc when not memory safe, these are ok to |
| // access, e.g. SIMD_Int32x4_load(HEAP8, ...) |
| invalidateCalls(); |
| callsInvalidated = true; |
| } |
| } else if (type == UNARY_PREFIX) { |
| traverseInOrder(node[2], false); |
| } else if (IGNORABLE_ELIMINATOR_SCAN_NODES.has(type)) { |
| } else if (type == CALL) { |
| // Named functions never change and are therefore safe to not track |
| if (node[1][0] != NAME) { |
| traverseInOrder(node[1], false); |
| } |
| Ref args = node[2]; |
| for (size_t i = 0; i < args->size(); i++) { |
| traverseInOrder(args[i], false); |
| } |
| if (callHasSideEffects(node)) { |
| // these two invalidations will also invalidate calls |
| if (!globalsInvalidated) { |
| invalidateGlobals(); |
| globalsInvalidated = true; |
| } |
| if (!memoryInvalidated) { |
| invalidateMemory(); |
| memoryInvalidated = true; |
| } |
| } |
| } else if (type == IF) { |
| if (allowTracking) { |
| traverseInOrder(node[1], false); // can eliminate into condition, but nowhere else |
| if (!callsInvalidated) { // invalidate calls, since we cannot eliminate them into an if that may not execute! |
| invalidateCalls(); |
| callsInvalidated = true; |
| } |
| allowTracking = false; |
| traverseInOrder(node[2], false); // 2 and 3 could be 'parallel', really.. |
| if (!!node[3]) traverseInOrder(node[3], false); |
| allowTracking = true; |
| } else { |
| tracked.clear(); |
| } |
| } else if (type == BLOCK) { |
| Ref stats = getStatements(node); |
| if (!!stats) { |
| for (size_t i = 0; i < stats->size(); i++) { |
| traverseInOrder(stats[i], false); |
| } |
| } |
| } else if (type == STAT) { |
| traverseInOrder(node[1], false); |
| } else if (type == LABEL) { |
| traverseInOrder(node[2], false); |
| } else if (type == SEQ) { |
| traverseInOrder(node[1], false); |
| traverseInOrder(node[2], false); |
| } else if (type == DO) { |
| if (node[1][0] == NUM && node[1][1]->getNumber() == 0) { // one-time loop |
| traverseInOrder(node[2], false); |
| } else { |
| tracked.clear(); |
| } |
| } else if (type == RETURN) { |
| if (!!node[1]) traverseInOrder(node[1], false); |
| } else if (type == CONDITIONAL) { |
| if (!callsInvalidated) { // invalidate calls, since we cannot eliminate them into a branch of an LLVM select/JS conditional that does not execute |
| invalidateCalls(); |
| callsInvalidated = true; |
| } |
| traverseInOrder(node[1], false); |
| traverseInOrder(node[2], false); |
| traverseInOrder(node[3], false); |
| } else if (type == SWITCH) { |
| traverseInOrder(node[1], false); |
| Tracked originalTracked = tracked; |
| Ref cases = node[2]; |
| for (size_t i = 0; i < cases->size(); i++) { |
| Ref c = cases[i]; |
| assert(c[0]->isNull() || c[0][0] == NUM || (c[0][0] == UNARY_PREFIX && c[0][2][0] == NUM)); |
| Ref stats = c[1]; |
| for (size_t j = 0; j < stats->size(); j++) { |
| traverseInOrder(stats[j], false); |
| } |
| // We cannot track from one switch case into another if there are external dependencies, undo all new trackings |
| // Otherwise we can track, e.g. a var used in a case before assignment in another case is UB in asm.js, so no need for the assignment |
| // TODO: general framework here, use in if-else as well |
| std::vector<IString> toDelete; |
| for (auto t : tracked) { |
| if (!originalTracked.has(t.first)) { |
| Tracking& info = tracked[t.first]; |
| if (info.usesGlobals || info.usesMemory || info.hasDeps) { |
| toDelete.push_back(t.first); |
| } |
| } |
| } |
| for (auto t : toDelete) { |
| tracked.erase(t); |
| } |
| } |
| tracked.clear(); // do not track from inside the switch to outside |
| } else { |
| assert(ABORTING_ELIMINATOR_SCAN_NODES.has(type)); |
| tracked.clear(); |
| abort = true; |
| } |
| }; |
| traverseInOrder(node, false); |
| }; |
| //var eliminationLimit = 0; // used to debugging purposes |
| doEliminate = [&](IString name, Ref node) { |
| //if (eliminationLimit == 0) return; |
| //eliminationLimit--; |
| //printErr('elim!!!!! ' + name); |
| // yes, eliminate! |
| varsToRemove[name] = 2; // both assign and var definitions can have other vars we must clean up |
| assert(tracked.has(name)); |
| Tracking& info = tracked[name]; |
| Ref defNode = info.defNode; |
| assert(!!defNode); |
| if (!sideEffectFree.has(name)) { |
| assert(defNode[0] != VAR); |
| // assign |
| Ref value = defNode[3]; |
| // wipe out the assign |
| safeCopy(defNode, makeEmpty()); |
| // replace this node in-place |
| safeCopy(node, value); |
| } else { |
| // This has no side effects and no uses, empty it out in-place |
| safeCopy(node, makeEmpty()); |
| } |
| tracked.erase(name); |
| }; |
| traversePre(func, [&](Ref block) { |
| // Look for statements, including while-switch pattern |
| Ref stats = getStatements(block); |
| if (!stats && (block[0] == WHILE && block[2][0] == SWITCH)) { |
| stats = &(makeArray(1)->push_back(block[2])); |
| } |
| if (!stats) return; |
| tracked.clear(); |
| for (size_t i = 0; i < stats->size(); i++) { |
| Ref node = deStat(stats[i]); |
| Ref type = node[0]; |
| if (type == RETURN && i+1 < stats->size()) { |
| stats->setSize(i+1); // remove any code after a return |
| } |
| // Check for things that affect elimination |
| if (ELIMINATION_SAFE_NODES.has(type)) { |
| #ifdef PROFILING |
| tstmtelim += clock() - start; |
| start = clock(); |
| #endif |
| scan(node); |
| #ifdef PROFILING |
| tstmtscan += clock() - start; |
| start = clock(); |
| #endif |
| } else if (type == VAR) { |
| continue; // asm normalisation has reduced 'var' to just the names |
| } else { |
| tracked.clear(); // not a var or assign, break all potential elimination so far |
| } |
| } |
| }); |
| |
| #ifdef PROFILING |
| tstmtelim += clock() - start; |
| start = clock(); |
| #endif |
| |
| StringIntMap seenUses; |
| StringStringMap helperReplacements; // for looper-helper optimization |
| |
| // clean up vars, and loop variable elimination |
| traversePrePost(func, [&](Ref node) { |
| // pre |
| Ref type = node[0]; |
| /*if (type == VAR) { |
| node[1] = node[1].filter(function(pair) { return !varsToRemove[pair[0]] }); |
| if (node[1]->size() == 0) { |
| // wipe out an empty |var;| |
| node[0] = TOPLEVEL; |
| node[1] = []; |
| } |
| } else */ |
| if (type == ASSIGN && node[1]->isBool(true) && node[2][0] == NAME && node[3][0] == NAME && node[2][1] == node[3][1]) { |
| // elimination led to X = X, which we can just remove |
| safeCopy(node, makeEmpty()); |
| } |
| }, [&](Ref node) { |
| // post |
| Ref type = node[0]; |
| if (type == NAME) { |
| IString name = node[1]->getIString(); |
| if (helperReplacements.has(name)) { |
| node[1]->setString(helperReplacements[name]); |
| return; // no need to track this anymore, we can't loop-optimize more than once |
| } |
| // track how many uses we saw. we need to know when a variable is no longer used (hence we run this in the post) |
| seenUses[name]++; |
| } else if (type == WHILE) { |
| // try to remove loop helper variables specifically |
| Ref stats = node[2][1]; |
| Ref last = stats->back(); |
| if (!!last && last[0] == IF && last[2][0] == BLOCK && !!last[3] && last[3][0] == BLOCK) { |
| Ref ifTrue = last[2]; |
| Ref ifFalse = last[3]; |
| clearEmptyNodes(ifTrue[1]); |
| clearEmptyNodes(ifFalse[1]); |
| bool flip = false; |
| if (ifFalse[1]->size() > 0 && !!ifFalse[1][0] && !!ifFalse[1]->back() && ifFalse[1]->back()[0] == BREAK) { // canonicalize break in the if-true |
| Ref temp = ifFalse; |
| ifFalse = ifTrue; |
| ifTrue = temp; |
| flip = true; |
| } |
| if (ifTrue[1]->size() > 0 && !!ifTrue[1][0] && !!ifTrue[1]->back() && ifTrue[1]->back()[0] == BREAK) { |
| Ref assigns = ifFalse[1]; |
| clearEmptyNodes(assigns); |
| std::vector<IString> loopers, helpers; |
| for (size_t i = 0; i < assigns->size(); i++) { |
| if (assigns[i][0] == STAT && assigns[i][1][0] == ASSIGN) { |
| Ref assign = assigns[i][1]; |
| if (assign[1]->isBool(true) && assign[2][0] == NAME && assign[3][0] == NAME) { |
| IString looper = assign[2][1]->getIString(); |
| IString helper = assign[3][1]->getIString(); |
| if (definitions[helper] == 1 && seenUses[looper] == namings[looper] && |
| !helperReplacements.has(helper) && !helperReplacements.has(looper)) { |
| loopers.push_back(looper); |
| helpers.push_back(helper); |
| } |
| } |
| } |
| } |
| // remove loop vars that are used in the rest of the else |
| for (size_t i = 0; i < assigns->size(); i++) { |
| if (assigns[i][0] == STAT && assigns[i][1][0] == ASSIGN) { |
| Ref assign = assigns[i][1]; |
| if (!(assign[1]->isBool(true) && assign[2][0] == NAME && assign[3][0] == NAME) || indexOf(loopers, assign[2][1]->getIString()) < 0) { |
| // this is not one of the loop assigns |
| traversePre(assign, [&](Ref node) { |
| if (node[0] == NAME) { |
| int index = indexOf(loopers, node[1]->getIString()); |
| if (index < 0) index = indexOf(helpers, node[1]->getIString()); |
| if (index >= 0) { |
| loopers.erase(loopers.begin() + index); |
| helpers.erase(helpers.begin() + index); |
| } |
| } |
| }); |
| } |
| } |
| } |
| // remove loop vars that are used in the if |
| traversePre(ifTrue, [&](Ref node) { |
| if (node[0] == NAME) { |
| int index = indexOf(loopers, node[1]->getIString()); |
| if (index < 0) index = indexOf(helpers, node[1]->getIString()); |
| if (index >= 0) { |
| loopers.erase(loopers.begin() + index); |
| helpers.erase(helpers.begin() + index); |
| } |
| } |
| }); |
| if (loopers.size() == 0) return; |
| for (size_t l = 0; l < loopers.size(); l++) { |
| IString looper = loopers[l]; |
| IString helper = helpers[l]; |
| // the remaining issue is whether loopers are used after the assignment to helper and before the last line (where we assign to it) |
| int found = -1; |
| for (int i = (int)stats->size()-2; i >= 0; i--) { |
| Ref curr = stats[i]; |
| if (curr[0] == STAT && curr[1][0] == ASSIGN) { |
| Ref currAssign = curr[1]; |
| if (currAssign[1]->isBool(true) && currAssign[2][0] == NAME) { |
| IString to = currAssign[2][1]->getIString(); |
| if (to == helper) { |
| found = i; |
| break; |
| } |
| } |
| } |
| } |
| if (found < 0) return; |
| // if a loop variable is used after we assigned to the helper, we must save its value and use that. |
| // (note that this can happen due to elimination, if we eliminate an expression containing the |
| // loop var far down, past the assignment!) |
| // first, see if the looper and helpers overlap. Note that we check for this looper, compared to |
| // *ALL* the helpers. Helpers will be replaced by loopers as we eliminate them, potentially |
| // causing conflicts, so any helper is a concern. |
| int firstLooperUsage = -1; |
| int lastLooperUsage = -1; |
| int firstHelperUsage = -1; |
| for (int i = found+1; i < (int)stats->size(); i++) { |
| Ref curr = i < (int)stats->size()-1 ? stats[i] : last[1]; // on the last line, just look in the condition |
| traversePre(curr, [&](Ref node) { |
| if (node[0] == NAME) { |
| if (node[1] == looper) { |
| if (firstLooperUsage < 0) firstLooperUsage = i; |
| lastLooperUsage = i; |
| } else if (indexOf(helpers, node[1]->getIString()) >= 0) { |
| if (firstHelperUsage < 0) firstHelperUsage = i; |
| } |
| } |
| }); |
| } |
| if (firstLooperUsage >= 0) { |
| // the looper is used, we cannot simply merge the two variables |
| if ((firstHelperUsage < 0 || firstHelperUsage > lastLooperUsage) && lastLooperUsage+1 < (int)stats->size() && triviallySafeToMove(stats[found], asmData) && |
| seenUses[helper] == namings[helper]) { |
| // the helper is not used, or it is used after the last use of the looper, so they do not overlap, |
| // and the last looper usage is not on the last line (where we could not append after it), and the |
| // helper is not used outside of the loop. |
| // just move the looper definition to after the looper's last use |
| stats->insert(lastLooperUsage+1, stats[found]); |
| stats->splice(found, 1); |
| } else { |
| // they overlap, we can still proceed with the loop optimization, but we must introduce a |
| // loop temp helper variable |
| IString temp(strdupe((std::string(looper.c_str()) + "$looptemp").c_str())); |
| assert(!asmData.isLocal(temp)); |
| for (int i = firstLooperUsage; i <= lastLooperUsage; i++) { |
| Ref curr = i < (int)stats->size()-1 ? stats[i] : last[1]; // on the last line, just look in the condition |
| |
| std::function<bool (Ref)> looperToLooptemp = [&](Ref node) { |
| if (node[0] == NAME) { |
| if (node[1] == looper) { |
| node[1]->setString(temp); |
| } |
| } else if (node[0] == ASSIGN && node[2][0] == NAME) { |
| // do not traverse the assignment target, phi assignments to the loop variable must remain |
| traversePrePostConditional(node[3], looperToLooptemp, [](Ref node){}); |
| return false; |
| } |
| return true; |
| }; |
| traversePrePostConditional(curr, looperToLooptemp, [](Ref node){}); |
| } |
| asmData.addVar(temp, asmData.getType(looper)); |
| stats->insert(found, make1(STAT, make3(ASSIGN, makeBool(true), makeName(temp), makeName(looper)))); |
| } |
| } |
| } |
| for (size_t l = 0; l < helpers.size(); l++) { |
| for (size_t k = 0; k < helpers.size(); k++) { |
| if (l != k && helpers[l] == helpers[k]) return; // it is complicated to handle a shared helper, abort |
| } |
| } |
| // hurrah! this is safe to do |
| for (size_t l = 0; l < loopers.size(); l++) { |
| IString looper = loopers[l]; |
| IString helper = helpers[l]; |
| varsToRemove[helper] = 2; |
| traversePre(node, [&](Ref node) { // replace all appearances of helper with looper |
| if (node[0] == NAME && node[1] == helper) node[1]->setString(looper); |
| }); |
| helperReplacements[helper] = looper; // replace all future appearances of helper with looper |
| helperReplacements[looper] = looper; // avoid any further attempts to optimize looper in this manner (seenUses is wrong anyhow, too) |
| } |
| // simplify the if. we remove the if branch, leaving only the else |
| if (flip) { |
| last[1] = simplifyNotCompsDirect(make2(UNARY_PREFIX, L_NOT, last[1])); |
| Ref temp = last[2]; |
| last[2] = last[3]; |
| last[3] = temp; |
| } |
| if (loopers.size() == assigns->size()) { |
| last->pop_back(); |
| } else { |
| Ref elseStats = getStatements(last[3]); |
| for (size_t i = 0; i < elseStats->size(); i++) { |
| Ref stat = deStat(elseStats[i]); |
| if (stat[0] == ASSIGN && stat[2][0] == NAME) { |
| if (indexOf(loopers, stat[2][1]->getIString()) >= 0) { |
| elseStats[i] = makeEmpty(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| }); |
| |
| #ifdef PROFILING |
| tcleanvars += clock() - start; |
| start = clock(); |
| #endif |
| |
| for (auto v : varsToRemove) { |
| if (v.second == 2 && asmData.isVar(v.first)) asmData.deleteVar(v.first); |
| } |
| |
| asmData.denormalize(); |
| |
| #ifdef PROFILING |
| treconstruct += clock() - start; |
| start = clock(); |
| #endif |
| |
| }); |
| |
| removeAllEmptySubNodes(ast); |
| |
| #ifdef PROFILING |
| errv(" EL stages: a:%li fe:%li vc:%li se:%li (ss:%li) cv:%li r:%li", |
| tasmdata, tfnexamine, tvarcheck, tstmtelim, tstmtscan, tcleanvars, treconstruct); |
| #endif |
| } |
| |
| void eliminateMemSafe(Ref ast) { |
| eliminate(ast, true); |
| } |
| |
| void simplifyExpressions(Ref ast) { |
| // Simplify common expressions used to perform integer conversion operations |
| // in cases where no conversion is needed. |
| auto simplifyIntegerConversions = [](Ref ast) { |
| traversePre(ast, [](Ref node) { |
| Ref type = node[0]; |
| if (type == BINARY && node[1] == RSHIFT && node[3][0] == NUM && |
| node[2][0] == BINARY && node[2][1] == LSHIFT && node[2][3][0] == NUM && node[3][1]->getNumber() == node[2][3][1]->getNumber()) { |
| // Transform (x&A)<<B>>B to X&A. |
| Ref innerNode = node[2][2]; |
| double shifts = node[3][1]->getNumber(); |
| if (innerNode[0] == BINARY && innerNode[1] == AND && innerNode[3][0] == NUM) { |
| double mask = innerNode[3][1]->getNumber(); |
| if (isInteger32(mask) && isInteger32(shifts) && ((jsD2I(mask) << jsD2I(shifts)) >> jsD2I(shifts)) == jsD2I(mask)) { |
| safeCopy(node, innerNode); |
| return; |
| } |
| } |
| } else if (type == BINARY && BITWISE.has(node[1])) { |
| for (int i = 2; i <= 3; i++) { |
| Ref subNode = node[i]; |
| if (subNode[0] == BINARY && subNode[1] == AND && subNode[3][0] == NUM && subNode[3][1]->getNumber() == 1) { |
| // Rewrite (X < Y) & 1 to X < Y , when it is going into a bitwise operator. We could |
| // remove even more (just replace &1 with |0, then subsequent passes could remove the |0) |
| // but v8 issue #2513 means the code would then run very slowly in chrome. |
| Ref input = subNode[2]; |
| if (input[0] == BINARY && COMPARE_OPS.has(input[1])) { |
| safeCopy(node[i], input); |
| } |
| } |
| } |
| } |
| }); |
| }; |
| |
| // When there is a bunch of math like (((8+5)|0)+12)|0, only the external |0 is needed, one correction is enough. |
| // At each node, ((X|0)+Y)|0 can be transformed into (X+Y): The inner corrections are not needed |
| // TODO: Is the same is true for 0xff, 0xffff? |
| // Likewise, if we have |0 inside a block that will be >>'d, then the |0 is unnecessary because some |
| // 'useful' mathops already |0 anyhow. |
| |
| auto simplifyOps = [](Ref ast) { |
| auto removeMultipleOrZero = [&ast] { |
| bool rerun = true; |
| while (rerun) { |
| rerun = false; |
| std::vector<int> stack; |
| std::function<void (Ref)> process = [&stack, &rerun, &process, &ast](Ref node) { |
| Ref type = node[0]; |
| if (type == BINARY && node[1] == OR) { |
| if (node[2][0] == NUM && node[3][0] == NUM) { |
| node[2][1]->setNumber(jsD2I(node[2][1]->getNumber()) | jsD2I(node[3][1]->getNumber())); |
| stack.push_back(0); |
| safeCopy(node, node[2]); |
| return; |
| } |
| bool go = false; |
| if (node[2][0] == NUM && node[2][1]->getNumber() == 0) { |
| // canonicalize order |
| Ref temp = node[3]; |
| node[3] = node[2]; |
| node[2] = temp; |
| go = true; |
| } else if (node[3][0] == NUM && node[3][1]->getNumber() == 0) { |
| go = true; |
| } |
| if (!go) { |
| stack.push_back(1); |
| return; |
| } |
| // We might be able to remove this correction |
| for (int i = stack.size()-1; i >= 0; i--) { |
| if (stack[i] >= 1) { |
| if (stack.back() < 2 && node[2][0] == CALL) break; // we can only remove multiple |0s on these |
| if (stack.back() < 1 && (COERCION_REQUIRING_OPS.has(node[2][0]) || |
| (node[2][0] == BINARY && COERCION_REQUIRING_BINARIES.has(node[2][1])))) break; // we can remove |0 or >>2 |
| // we will replace ourselves with the non-zero side. Recursively process that node. |
| Ref result = node[2][0] == NUM && node[2][1]->getNumber() == 0 ? node[3] : node[2], other; |
| // replace node in-place |
| safeCopy(node, result); |
| rerun = true; |
| process(result); |
| return; |
| } else if (stack[i] == -1) { |
| break; // Too bad, we can't |
| } |
| } |
| stack.push_back(2); // From here on up, no need for this kind of correction, it's done at the top |
| // (Add this at the end, so it is only added if we did not remove it) |
| } else if (type == BINARY && USEFUL_BINARY_OPS.has(node[1])) { |
| stack.push_back(1); |
| } else if ((type == BINARY && SAFE_BINARY_OPS.has(node[1])) || type == NUM || type == NAME) { |
| stack.push_back(0); // This node is safe in that it does not interfere with this optimization |
| } else if (type == UNARY_PREFIX && node[1] == B_NOT) { |
| stack.push_back(1); |
| } else { |
| stack.push_back(-1); // This node is dangerous! Give up if you see this before you see '1' |
| } |
| }; |
| |
| traversePrePost(ast, process, [&stack](Ref node) { |
| assert(!stack.empty()); |
| stack.pop_back(); |
| }); |
| } |
| }; |
| |
| removeMultipleOrZero(); |
| |
| // & and heap-related optimizations |
| |
| bool hasTempDoublePtr = false, rerunOrZeroPass = false; |
| |
| traversePrePostConditional(ast, [](Ref node) { |
| // Detect trees which should not |
| // be simplified. |
| if (node[0] == SUB && node[1][0] == NAME && isFunctionTable(node[1][1])) { |
| return false; // do not traverse subchildren here, we should not collapse 55 & 126. |
| } |
| return true; |
| }, [&hasTempDoublePtr, &rerunOrZeroPass](Ref node) { |
| // Simplifications are done now so |
| // that we simplify a node's operands before the node itself. This allows |
| // optimizations to cascade. |
| Ref type = node[0]; |
| if (type == NAME) { |
| if (node[1] == TEMP_DOUBLE_PTR) hasTempDoublePtr = true; |
| } else if (type == BINARY && node[1] == AND && node[3][0] == NUM) { |
| if (node[2][0] == NUM) { |
| safeCopy(node, makeNum(jsD2I(node[2][1]->getNumber()) & jsD2I(node[3][1]->getNumber()))); |
| return; |
| } |
| Ref input = node[2]; |
| double amount = node[3][1]->getNumber(); |
| if (input[0] == BINARY && input[1] == AND && input[3][0] == NUM) { |
| // Collapse X & 255 & 1 |
| node[3][1]->setNumber(jsD2I(amount) & jsD2I(input[3][1]->getNumber())); |
| node[2] = input[2]; |
| } else if (input[0] == SUB && input[1][0] == NAME) { |
| // HEAP8[..] & 255 => HEAPU8[..] |
| HeapInfo hi = parseHeap(input[1][1]->getCString()); |
| if (hi.valid) { |
| if (isInteger32(amount) && amount == powl(2, hi.bits)-1) { |
| if (!hi.unsign) { |
| input[1][1]->setString(getHeapStr(hi.bits, true)); // make unsigned |
| } |
| // we cannot return HEAPU8 without a coercion, but at least we do HEAP8 & 255 => HEAPU8 | 0 |
| node[1]->setString(OR); |
| node[3][1]->setNumber(0); |
| return; |
| } |
| } |
| } else if (input[0] == BINARY && input[1] == RSHIFT && |
| input[2][0] == BINARY && input[2][1] == LSHIFT && |
| input[2][3][0] == NUM && input[3][0] == NUM && |
| input[2][3][1]->getInteger() == input[3][1]->getInteger() && |
| (~(0xFFFFFFFFu >> input[3][1]->getInteger()) & jsD2I(amount)) == 0) { |
| // x << 24 >> 24 & 255 => x & 255 |
| return safeCopy(node, make3(BINARY, AND, input[2][2], node[3])); |
| } |
| } else if (type == BINARY && node[1] == XOR) { |
| // LLVM represents bitwise not as xor with -1. Translate it back to an actual bitwise not. |
| if (node[3][0] == UNARY_PREFIX && node[3][1] == MINUS && node[3][2][0] == NUM && |
| node[3][2][1]->getNumber() == 1 && |
| !(node[2][0] == UNARY_PREFIX && node[2][1] == B_NOT)) { // avoid creating ~~~ which is confusing for asm given the role of ~~ |
| safeCopy(node, make2(UNARY_PREFIX, B_NOT, node[2])); |
| return; |
| } |
| } else if (type == BINARY && node[1] == RSHIFT && node[3][0] == NUM && |
| node[2][0] == BINARY && node[2][1] == LSHIFT && node[2][3][0] == NUM && |
| node[2][2][0] == SUB && node[2][2][1][0] == NAME) { |
| // collapse HEAPU?8[..] << 24 >> 24 etc. into HEAP8[..] | 0 |
| double amount = node[3][1]->getNumber(); |
| if (amount == node[2][3][1]->getNumber()) { |
| HeapInfo hi = parseHeap(node[2][2][1][1]->getCString()); |
| if (hi.valid && hi.bits == 32 - amount) { |
| node[2][2][1][1]->setString(getHeapStr(hi.bits, false)); |
| node[1]->setString(OR); |
| node[2] = node[2][2]; |
| node[3][1]->setNumber(0); |
| rerunOrZeroPass = true; |
| return; |
| } |
| } |
| } else if (type == ASSIGN) { |
| // optimizations for assigning into HEAP32 specifically |
| if (node[1]->isBool(true) && node[2][0] == SUB && node[2][1][0] == NAME) { |
| if (node[2][1][1] == HEAP32) { |
| // HEAP32[..] = x | 0 does not need the | 0 (unless it is a mandatory |0 of a call) |
| if (node[3][0] == BINARY && node[3][1] == OR) { |
| if (node[3][2][0] == NUM && node[3][2][1]->getNumber() == 0 && node[3][3][0] != CALL) { |
| node[3] = node[3][3]; |
| } else if (node[3][3][0] == NUM && node[3][3][1]->getNumber() == 0 && node[3][2][0] != CALL) { |
| node[3] = node[3][2]; |
| } |
| } |
| } else if (node[2][1][1] == HEAP8) { |
| // HEAP8[..] = x & 0xff does not need the & 0xff |
| if (node[3][0] == BINARY && node[3][1] == AND && node[3][3][0] == NUM && node[3][3][1]->getNumber() == 0xff) { |
| node[3] = node[3][2]; |
| } |
| } else if (node[2][1][1] == HEAP16) { |
| // HEAP16[..] = x & 0xffff does not need the & 0xffff |
| if (node[3][0] == BINARY && node[3][1] == AND && node[3][3][0] == NUM && node[3][3][1]->getNumber() == 0xffff) { |
| node[3] = node[3][2]; |
| } |
| } |
| } |
| Ref value = node[3]; |
| if (value[0] == BINARY && value[1] == OR) { |
| // canonicalize order of |0 to end |
| if (value[2][0] == NUM && value[2][1]->getNumber() == 0) { |
| Ref temp = value[2]; |
| value[2] = value[3]; |
| value[3] = temp; |
| } |
| // if a seq ends in an |0, remove an external |0 |
| // note that it is only safe to do this in assigns, like we are doing here (return (x, y|0); is not valid) |
| if (value[2][0] == SEQ && value[2][2][0] == BINARY && USEFUL_BINARY_OPS.has(value[2][2][1])) { |
| node[3] = value[2]; |
| } |
| } |
| } else if (type == BINARY && node[1] == RSHIFT && node[2][0] == NUM && node[3][0] == NUM) { |
| // optimize num >> num, in asm we need this since we do not optimize shifts in asm.js |
| node[0]->setString(NUM); |
| node[1]->setNumber(jsD2I(node[2][1]->getNumber()) >> jsD2I(node[3][1]->getNumber())); |
| node->setSize(2); |
| return; |
| } else if (type == BINARY && node[1] == PLUS) { |
| // The most common mathop is addition, e.g. in getelementptr done repeatedly. We can join all of those, |
| // by doing (num+num) ==> newnum. |
| if (node[2][0] == NUM && node[3][0] == NUM) { |
| node[2][1]->setNumber(jsD2I(node[2][1]->getNumber()) + jsD2I(node[3][1]->getNumber())); |
| safeCopy(node, node[2]); |
| return; |
| } |
| } |
| }); |
| |
| if (rerunOrZeroPass) removeMultipleOrZero(); |
| |
| if (hasTempDoublePtr) { |
| AsmData asmData(ast); |
| traversePre(ast, [](Ref node) { |
| Ref type = node[0]; |
| if (type == ASSIGN) { |
| if (node[1]->isBool(true) && node[2][0] == SUB && node[2][1][0] == NAME && node[2][1][1] == HEAP32) { |
| // remove bitcasts that are now obviously pointless, e.g. |
| // HEAP32[$45 >> 2] = HEAPF32[tempDoublePtr >> 2] = ($14 < $28 ? $14 : $28) - $42, HEAP32[tempDoublePtr >> 2] | 0; |
| Ref value = node[3]; |
| if (value[0] == SEQ && value[1][0] == ASSIGN && value[1][2][0] == SUB && value[1][2][1][0] == NAME && value[1][2][1][1] == HEAPF32 && |
| value[1][2][2][0] == BINARY && value[1][2][2][2][0] == NAME && value[1][2][2][2][1] == TEMP_DOUBLE_PTR) { |
| // transform to HEAPF32[$45 >> 2] = ($14 < $28 ? $14 : $28) - $42; |
| node[2][1][1]->setString(HEAPF32); |
| node[3] = value[1][3]; |
| } |
| } |
| } else if (type == SEQ) { |
| // (HEAP32[tempDoublePtr >> 2] = HEAP32[$37 >> 2], +HEAPF32[tempDoublePtr >> 2]) |
| // ==> |
| // +HEAPF32[$37 >> 2] |
| if (node[0] == SEQ && node[1][0] == ASSIGN && node[1][2][0] == SUB && node[1][2][1][0] == NAME && |
| (node[1][2][1][1] == HEAP32 || node[1][2][1][1] == HEAPF32) && |
| node[1][2][2][0] == BINARY && node[1][2][2][2][0] == NAME && node[1][2][2][2][1] == TEMP_DOUBLE_PTR && |
| node[1][3][0] == SUB && node[1][3][1][0] == NAME && (node[1][3][1][1] == HEAP32 || node[1][3][1][1] == HEAPF32) && |
| node[2][0] != SEQ) { // avoid (x, y, z) which can be used for tempDoublePtr on doubles for alignment fixes |
| if (node[1][2][1][1] == HEAP32) { |
| node[1][3][1][1]->setString(HEAPF32); |
| safeCopy(node, makeAsmCoercion(node[1][3], detectType(node[2]))); |
| return; |
| } else { |
| node[1][3][1][1]->setString(HEAP32); |
| safeCopy(node, make3(BINARY, OR, node[1][3], makeNum(0))); |
| return; |
| } |
| } |
| } |
| }); |
| |
| // finally, wipe out remaining ones by finding cases where all assignments to X are bitcasts, and all uses are writes to |
| // the other heap type, then eliminate the bitcast |
| struct BitcastData { |
| int define_HEAP32, define_HEAPF32, use_HEAP32, use_HEAPF32, namings; |
| bool ok; |
| std::vector<Ref> defines, uses; |
| |
| BitcastData() : define_HEAP32(0), define_HEAPF32(0), use_HEAP32(0), use_HEAPF32(0), namings(0), ok(false) {} |
| }; |
| std::unordered_map<IString, BitcastData> bitcastVars; |
| traversePre(ast, [&bitcastVars](Ref node) { |
| if (node[0] == ASSIGN && node[1]->isBool(true) && node[2][0] == NAME) { |
| Ref value = node[3]; |
| if (value[0] == SEQ && value[1][0] == ASSIGN && value[1][2][0] == SUB && value[1][2][1][0] == NAME && |
| (value[1][2][1][1] == HEAP32 || value[1][2][1][1] == HEAPF32) && |
| value[1][2][2][0] == BINARY && value[1][2][2][2][0] == NAME && value[1][2][2][2][1] == TEMP_DOUBLE_PTR) { |
| IString name = node[2][1]->getIString(); |
| IString heap = value[1][2][1][1]->getIString(); |
| if (heap == HEAP32) { |
| bitcastVars[name].define_HEAP32++; |
| } else { |
| assert(heap == HEAPF32); |
| bitcastVars[name].define_HEAPF32++; |
| } |
| bitcastVars[name].defines.push_back(node); |
| bitcastVars[name].ok = true; |
| } |
| } |
| }); |
| traversePre(ast, [&bitcastVars](Ref node) { |
| Ref type = node[0]; |
| if (type == NAME && bitcastVars[node[1]->getCString()].ok) { |
| bitcastVars[node[1]->getCString()].namings++; |
| } else if (type == ASSIGN && node[1]->isBool(true)) { |
| Ref value = node[3]; |
| if (value[0] == NAME) { |
| IString name = value[1]->getIString(); |
| if (bitcastVars[name].ok) { |
| Ref target = node[2]; |
| if (target[0] == SUB && target[1][0] == NAME && (target[1][1] == HEAP32 || target[1][1] == HEAPF32)) { |
| if (target[1][1] == HEAP32) { |
| bitcastVars[name].use_HEAP32++; |
| } else { |
| bitcastVars[name].use_HEAPF32++; |
| } |
| bitcastVars[name].uses.push_back(node); |
| } |
| } |
| } |
| } |
| }); |
| for (auto iter : bitcastVars) { |
| const IString& v = iter.first; |
| BitcastData& info = iter.second; |
| // good variables define only one type, use only one type, have definitions and uses, and define as a different type than they use |
| if (info.define_HEAP32*info.define_HEAPF32 == 0 && info.use_HEAP32*info.use_HEAPF32 == 0 && |
| info.define_HEAP32+info.define_HEAPF32 > 0 && info.use_HEAP32+info.use_HEAPF32 > 0 && |
| info.define_HEAP32*info.use_HEAP32 == 0 && info.define_HEAPF32*info.use_HEAPF32 == 0 && |
| asmData.isLocal(v.c_str()) && info.namings == info.define_HEAP32+info.define_HEAPF32+info.use_HEAP32+info.use_HEAPF32) { |
| IString& correct = info.use_HEAP32 ? HEAPF32 : HEAP32; |
| for (auto define : info.defines) { |
| define[3] = define[3][1][3]; |
| if (correct == HEAP32) { |
| define[3] = make3(BINARY, OR, define[3], makeNum(0)); |
| } else { |
| assert(correct == HEAPF32); |
| define[3] = makeAsmCoercion(define[3], preciseF32 ? ASM_FLOAT : ASM_DOUBLE); |
| } |
| // do we want a simplifybitops on the new values here? |
| } |
| for (auto use : info.uses) { |
| use[2][1][1]->setString(correct.c_str()); |
| } |
| AsmType correctType; |
| switch(asmData.getType(v.c_str())) { |
| case ASM_INT: correctType = preciseF32 ? ASM_FLOAT : ASM_DOUBLE; break; |
| case ASM_FLOAT: case ASM_DOUBLE: correctType = ASM_INT; break; |
| default: assert(0); |
| } |
| asmData.setType(v.c_str(), correctType); |
| } |
| } |
| asmData.denormalize(); |
| } |
| }; |
| |
| std::function<bool (Ref)> emitsBoolean = [&emitsBoolean](Ref node) { |
| Ref type = node[0]; |
| if (type == NUM) { |
| return node[1]->getNumber() == 0 || node[1]->getNumber() == 1; |
| } |
| if (type == BINARY) return COMPARE_OPS.has(node[1]); |
| if (type == UNARY_PREFIX) return node[1] == L_NOT; |
| if (type == CONDITIONAL) return emitsBoolean(node[2]) && emitsBoolean(node[3]); |
| return false; |
| }; |
| |
| // expensive | expensive can be turned into expensive ? 1 : expensive, and |
| // expensive | cheap can be turned into cheap ? 1 : expensive, |
| // so that we can avoid the expensive computation, if it has no side effects. |
| auto conditionalize = [&emitsBoolean](Ref ast) { |
| traversePre(ast, [&emitsBoolean](Ref node) { |
| const int MIN_COST = 7; |
| if (node[0] == BINARY && (node[1] == OR || node[1] == AND) && node[3][0] != NUM && node[2][0] != NUM) { |
| // logical operator on two non-numerical values |
| Ref left = node[2]; |
| Ref right = node[3]; |
| if (!emitsBoolean(left) || !emitsBoolean(right)) return; |
| bool leftEffects = hasSideEffects(left); |
| bool rightEffects = hasSideEffects(right); |
| if (leftEffects && rightEffects) return; // both must execute |
| // canonicalize with side effects, if any, happening on the left |
| if (rightEffects) { |
| if (measureCost(left) < MIN_COST) return; // avoidable code is too cheap |
| Ref temp = left; |
| left = right; |
| right = temp; |
| } else if (leftEffects) { |
| if (measureCost(right) < MIN_COST) return; // avoidable code is too cheap |
| } else { |
| // no side effects, reorder based on cost estimation |
| int leftCost = measureCost(left); |
| int rightCost = measureCost(right); |
| if (std::max(leftCost, rightCost) < MIN_COST) return; // avoidable code is too cheap |
| // canonicalize with expensive code on the right |
| if (leftCost > rightCost) { |
| Ref temp = left; |
| left = right; |
| right = temp; |
| } |
| } |
| // worth it, perform conditionalization |
| Ref ret; |
| if (node[1] == OR) { |
| ret = make3(CONDITIONAL, left, makeNum(1), right); |
| } else { // & |
| ret = make3(CONDITIONAL, left, right, makeNum(0)); |
| } |
| if (left[0] == UNARY_PREFIX && left[1] == L_NOT) { |
| ret[1] = flipCondition(left); |
| Ref temp = ret[2]; |
| ret[2] = ret[3]; |
| ret[3] = temp; |
| } |
| safeCopy(node, ret); |
| return; |
| } |
| }); |
| }; |
| |
| auto simplifyNotZero = [](Ref ast) { |
| traversePre(ast, [](Ref node) { |
| if (BOOLEAN_RECEIVERS.has(node[0])) { |
| auto boolean = node[1]; |
| if (boolean[0] == BINARY && boolean[1] == NE && boolean[3][0] == NUM && boolean[3][1]->getNumber() == 0) { |
| node[1] = boolean[2]; |
| } |
| } |
| }); |
| }; |
| |
| traverseFunctions(ast, [&](Ref func) { |
| simplifyIntegerConversions(func); |
| simplifyOps(func); |
| traversePre(func, [](Ref node) { |
| Ref ret = simplifyNotCompsDirect(node); |
| if (ret.get() != node.get()) { // if we received a different pointer in return, then we need to copy the new value |
| safeCopy(node, ret); |
| } |
| }); |
| conditionalize(func); |
| simplifyNotZero(func); |
| }); |
| } |
| |
| void simplifyIfs(Ref ast) { |
| traverseFunctions(ast, [](Ref func) { |
| bool simplifiedAnElse = false; |
| |
| traversePre(func, [&simplifiedAnElse](Ref node) { |
| // simplify if (x) { if (y) { .. } } to if (x ? y : 0) { .. } |
| if (node[0] == IF) { |
| Ref body = node[2]; |
| // recurse to handle chains |
| while (body[0] == BLOCK) { |
| Ref stats = body[1]; |
| if (stats->size() == 0) break; |
| Ref other = stats->back(); |
| if (other[0] != IF) { |
| // our if block does not end with an if. perhaps if have an else we can flip |
| if (node->size() > 3 && !!node[3] && node[3][0] == BLOCK) { |
| stats = node[3][1]; |
| if (stats->size() == 0) break; |
| other = stats->back(); |
| if (other[0] == IF) { |
| // flip node |
| node[1] = flipCondition(node[1]); |
| node[2] = node[3]; |
| node[3] = body; |
| body = node[2]; |
| } else break; |
| } else break; |
| } |
| // we can handle elses, but must be fully identical |
| if (!!node[3] || !!other[3]) { |
| if (!node[3]) break; |
| if (!node[3]->deepCompare(other[3])) { |
| // the elses are different, but perhaps if we flipped a condition we can do better |
| if (node[3]->deepCompare(other[2])) { |
| // flip other. note that other may not have had an else! add one if so; we will eliminate such things later |
| if (!other[3]) other[3] = makeBlock(); |
| other[1] = flipCondition(other[1]); |
| Ref temp = other[2]; |
| other[2] = other[3]; |
| other[3] = temp; |
| } else break; |
| } |
| } |
| if (stats->size() > 1) { |
| // try to commaify - turn everything between the ifs into a comma operator inside the second if |
| bool ok = true; |
| for (size_t i = 0; i+1 < stats->size(); i++) { |
| Ref curr = deStat(stats[i]); |
| if (!commable(curr)) ok = false; |
| } |
| if (!ok) break; |
| for (int i = stats->size()-2; i >= 0; i--) { |
| Ref curr = deStat(stats[i]); |
| other[1] = make2(SEQ, curr, other[1]); |
| } |
| Ref temp = makeArray(1); |
| temp->push_back(other); |
| stats = body[1] = temp; |
| } |
| if (stats->size() != 1) break; |
| if (!!node[3]) simplifiedAnElse = true; |
| node[1] = make3(CONDITIONAL, node[1], other[1], makeNum(0)); |
| body = node[2] = other[2]; |
| } |
| } |
| }); |
| |
| if (simplifiedAnElse) { |
| // there may be fusing opportunities |
| |
| // we can only fuse if we remove all uses of the label. if there are |
| // other ones - if the label check can be reached from elsewhere - |
| // we must leave it |
| bool abort = false; |
| |
| std::unordered_map<int, int> labelAssigns; |
| |
| traversePre(func, [&labelAssigns, &abort](Ref node) { |
| if (node[0] == ASSIGN && node[2][0] == NAME && node[2][1] == LABEL) { |
| if (node[3][0] == NUM) { |
| int value = node[3][1]->getInteger(); |
| labelAssigns[value] = labelAssigns[value] + 1; |
| } else { |
| // label is assigned a dynamic value (like from indirectbr), we cannot do anything |
| abort = true; |
| } |
| } |
| }); |
| if (abort) return; |
| |
| std::unordered_map<int, int> labelChecks; |
| |
| traversePre(func, [&labelChecks, &abort](Ref node) { |
| if (node[0] == BINARY && node[1] == EQ && node[2][0] == BINARY && node[2][1] == OR && |
| node[2][2][0] == NAME && node[2][2][1] == LABEL) { |
| if (node[3][0] == NUM) { |
| int value = node[3][1]->getInteger(); |
| labelChecks[value] = labelChecks[value] + 1; |
| } else { |
| // label is checked vs a dynamic value (like from indirectbr), we cannot do anything |
| abort = true; |
| } |
| } |
| }); |
| if (abort) return; |
| |
| int inLoop = 0; // when in a loop, we do not emit label = 0; in the relooper as there is no need |
| traversePrePost(func, [&inLoop, &labelAssigns, &labelChecks](Ref node) { |
| if (node[0] == WHILE) inLoop++; |
| Ref stats = getStatements(node); |
| if (!!stats && stats->size() > 0) { |
| for (int i = 0; i < (int)stats->size()-1; i++) { |
| Ref pre = stats[i]; |
| Ref post = stats[i+1]; |
| if (pre[0] == IF && pre->size() > 3 && !!pre[3] && post[0] == IF && (post->size() <= 3 || !post[3])) { |
| Ref postCond = post[1]; |
| if (postCond[0] == BINARY && postCond[1] == EQ && |
| postCond[2][0] == BINARY && postCond[2][1] == OR && |
| postCond[2][2][0] == NAME && postCond[2][2][1] == LABEL && |
| postCond[2][3][0] == NUM && postCond[2][3][1]->getNumber() == 0 && |
| postCond[3][0] == NUM) { |
| int postValue = postCond[3][1]->getInteger(); |
| Ref preElse = pre[3]; |
| if (labelAssigns[postValue] == 1 && labelChecks[postValue] == 1 && preElse[0] == BLOCK && preElse->size() >= 2 && preElse[1]->size() == 1) { |
| Ref preStat = preElse[1][0]; |
| if (preStat[0] == STAT && preStat[1][0] == ASSIGN && |
| preStat[1][1]->isBool(true) && preStat[1][2][0] == NAME && preStat[1][2][1] == LABEL && |
| preStat[1][3][0] == NUM && preStat[1][3][1]->getNumber() == postValue) { |
| // Conditions match, just need to make sure the post clears label |
| if (post[2][0] == BLOCK && post[2]->size() >= 2 && post[2][1]->size() > 0) { |
| Ref postStat = post[2][1][0]; |
| bool haveClear = |
| postStat[0] == STAT && postStat[1][0] == ASSIGN && |
| postStat[1][1]->isBool(true) && postStat[1][2][0] == NAME && postStat[1][2][1] == LABEL && |
| postStat[1][3][0] == NUM && postStat[1][3][1]->getNumber() == 0; |
| if (!inLoop || haveClear) { |
| // Everything lines up, do it |
| pre[3] = post[2]; |
| if (haveClear) pre[3][1]->splice(0, 1); // remove the label clearing |
| stats->splice(i+1, 1); // remove the post entirely |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| }, [&inLoop](Ref node) { |
| if (node[0] == WHILE) inLoop--; |
| }); |
| assert(inLoop == 0); |
| } |
| }); |
| } |
| |
| void optimizeFrounds(Ref ast) { |
| // collapse fround(fround(..)), which can happen due to elimination |
| // also emit f0 instead of fround(0) (except in returns) |
| int inReturn = 0; |
| traversePrePost(ast, [&](Ref node) { |
| if (node[0] == RETURN) { |
| inReturn++; |
| } |
| }, [&](Ref node) { |
| if (node[0] == RETURN) { |
| inReturn--; |
| } |
| if (node[0] == CALL && node[1][0] == NAME && node[1][1] == MATH_FROUND) { |
| Ref arg = node[2][0]; |
| if (arg[0] == NUM) { |
| if (!inReturn && arg[1]->getInteger() == 0) { |
| safeCopy(node, makeName(F0)); |
| } |
| } else if (arg[0] == CALL && arg[1][0] == NAME && arg[1][1] == MATH_FROUND) { |
| safeCopy(node, arg); |
| } |
| } |
| }); |
| } |
| |
| // Very simple 'registerization', coalescing of variables into a smaller number. |
| |
| const char* getRegPrefix(AsmType type) { |
| switch (type) { |
| case ASM_INT: return "i"; break; |
| case ASM_DOUBLE: return "d"; break; |
| case ASM_FLOAT: return "f"; break; |
| case ASM_FLOAT32X4: return "F4"; break; |
| case ASM_FLOAT64X2: return "F2"; break; |
| case ASM_INT8X16: return "I16"; break; |
| case ASM_INT16X8: return "I8"; break; |
| case ASM_INT32X4: return "I4"; break; |
| case ASM_BOOL8X16: return "B16"; break; |
| case ASM_BOOL16X8: return "B8"; break; |
| case ASM_BOOL32X4: return "B4"; break; |
| case ASM_BOOL64X2: return "B2"; break; |
| case ASM_NONE: return "Z"; break; |
| default: assert(0); // type doesn't have a name yet |
| } |
| return nullptr; |
| } |
| |
| IString getRegName(AsmType type, int num) { |
| const char* str = getRegPrefix(type); |
| const int size = 256; |
| char temp[size]; |
| int written = sprintf(temp, "%s%d", str, num); |
| assert(written < size); |
| temp[written] = 0; |
| IString ret; |
| ret.set(temp, false); |
| return ret; |
| } |
| |
| void registerize(Ref ast) { |
| traverseFunctions(ast, [](Ref fun) { |
| AsmData asmData(fun); |
| // Add parameters as a first (fake) var (with assignment), so they get taken into consideration |
| // note: params are special, they can never share a register between them (see later) |
| Ref fake; |
| if (!!fun[2] && fun[2]->size()) { |
| Ref assign = makeNum(0); |
| // TODO: will be an isEmpty here, can reuse it. |
| fun[3]->insert(0, make1(VAR, fun[2]->map([&assign](Ref param) { |
| return &(makeArray(2)->push_back(param).push_back(assign)); |
| }))); |
| } |
| // Replace all var definitions with assignments; we will add var definitions at the top after we registerize |
| StringSet allVars; |
| traversePre(fun, [&](Ref node) { |
| Ref type = node[0]; |
| if (type == VAR) { |
| Ref vars = node[1]->filter([](Ref varr) { return varr->size() > 1; }); |
| if (vars->size() >= 1) { |
| safeCopy(node, unVarify(vars)); |
| } else { |
| safeCopy(node, makeEmpty()); |
| } |
| } else if (type == NAME) { |
| allVars.insert(node[1]->getIString()); |
| } |
| }); |
| removeAllUselessSubNodes(fun); // vacuum? |
| StringTypeMap regTypes; // reg name -> type |
| auto getNewRegName = [&](int num, IString name) { |
| AsmType type = asmData.getType(name); |
| IString ret = getRegName(type, num); |
| assert(!allVars.has(ret) || asmData.isLocal(ret)); // register must not shadow non-local name |
| regTypes[ret] = type; |
| return ret; |
| }; |
| // Find the # of uses of each variable. |
| // While doing so, check if all a variable's uses are dominated in a simple |
| // way by a simple assign, if so, then we can assign its register to it |
| // just for its definition to its last use, and not to the entire toplevel loop, |
| // we call such variables "optimizable" |
| StringIntMap varUses; |
| int level = 1; |
| std::unordered_map<int, StringSet> levelDominations; // level => set of dominated variables XXX vector? |
| StringIntMap varLevels; |
| StringSet possibles; |
| StringSet unoptimizables; |
| auto purgeLevel = [&]() { |
| // Invalidate all dominating on this level, further users make it unoptimizable |
| for (auto name : levelDominations[level]) { |
| varLevels[name] = 0; |
| } |
| levelDominations[level].clear(); |
| level--; |
| }; |
| std::function<bool (Ref node)> possibilifier = [&](Ref node) { |
| Ref type = node[0]; |
| if (type == NAME) { |
| IString name = node[1]->getIString(); |
| if (asmData.isLocal(name)) { |
| varUses[name]++; |
| if (possibles.has(name) && !varLevels[name]) unoptimizables.insert(name); // used outside of simple domination |
| } |
| } else if (type == ASSIGN && node[1]->isBool(true)) { |
| if (!!node[2] && node[2][0] == NAME) { |
| IString name = node[2][1]->getIString(); |
| // if local and not yet used, this might be optimizable if we dominate |
| // all other uses |
| if (asmData.isLocal(name) && !varUses[name] && !varLevels[name]) { |
| possibles.insert(name); |
| varLevels[name] = level; |
| levelDominations[level].insert(name); |
| } |
| } |
| } else if (CONTROL_FLOW.has(type)) { |
| // recurse children, in the context of a loop |
| if (type == WHILE || type == DO) { |
| traversePrePostConditional(node[1], possibilifier, [](Ref node){}); |
| level++; |
| traversePrePostConditional(node[2], possibilifier, [](Ref node){}); |
| purgeLevel(); |
| } else if (type == FOR) { |
| traversePrePostConditional(node[1], possibilifier, [](Ref node){}); |
| for (int i = 2; i <= 4; i++) { |
| level++; |
| traversePrePostConditional(node[i], possibilifier, [](Ref node){}); |
| purgeLevel(); |
| } |
| } else if (type == IF) { |
| traversePrePostConditional(node[1], possibilifier, [](Ref node){}); |
| level++; |
| traversePrePostConditional(node[2], possibilifier, [](Ref node){}); |
| purgeLevel(); |
| if (node->size() > 3 && !!node[3]) { |
| level++; |
| traversePrePostConditional(node[3], possibilifier, [](Ref node){}); |
| purgeLevel(); |
| } |
| } else if (type == SWITCH) { |
| traversePrePostConditional(node[1], possibilifier, [](Ref node){}); |
| Ref cases = node[2]; |
| for (size_t i = 0; i < cases->size(); i++) { |
| level++; |
| traversePrePostConditional(cases[i][1], possibilifier, [](Ref node){}); |
| purgeLevel(); |
| } |
| } else assert(0);; |
| return false; // prevent recursion into children, which we already did |
| } |
| return true; |
| }; |
| traversePrePostConditional(fun, possibilifier, [](Ref node){}); |
| |
| StringSet optimizables; |
| for (auto possible : possibles) { |
| if (!unoptimizables.has(possible)) optimizables.insert(possible); |
| } |
| |
| // Go through the function's code, assigning 'registers'. |
| // The only tricky bit is to keep variables locked on a register through loops, |
| // since they can potentially be returned to. Optimizable variables lock onto |
| // loops that they enter, unoptimizable variables lock in a conservative way |
| // into the topmost loop. |
| // Note that we cannot lock onto a variable in a loop if it was used and free'd |
| // before! (then they could overwrite us in the early part of the loop). For now |
| // we just use a fresh register to make sure we avoid this, but it could be |
| // optimized to check for safe registers (free, and not used in this loop level). |
| StringStringMap varRegs; // maps variables to the register they will use all their life |
| std::vector<StringVec> freeRegsClasses; |
| freeRegsClasses.resize(ASM_NONE); |
| int nextReg = 1; |
| StringVec fullNames; |
| fullNames.push_back(EMPTY); // names start at 1 |
| std::vector<StringVec> loopRegs; // for each loop nesting level, the list of bound variables |
| int loops = 0; // 0 is toplevel, 1 is first loop, etc |
| StringSet activeOptimizables; |
| StringIntMap optimizableLoops; |
| StringSet paramRegs; // true if the register is used by a parameter (and so needs no def at start of function; also cannot |
| // be shared with another param, each needs its own) |
| auto decUse = [&](IString name) { |
| if (!varUses[name]) return false; // no uses left, or not a relevant variable |
| if (optimizables.has(name)) activeOptimizables.insert(name); |
| IString reg = varRegs[name]; |
| assert(asmData.isLocal(name)); |
| StringVec& freeRegs = freeRegsClasses[asmData.getType(name)]; |
| if (!reg) { |
| // acquire register |
| if (optimizables.has(name) && freeRegs.size() > 0 && |
| !(asmData.isParam(name) && paramRegs.has(freeRegs.back()))) { // do not share registers between parameters |
| reg = freeRegs.back(); |
| freeRegs.pop_back(); |
| } else { |
| assert(fullNames.size() == nextReg); |
| reg = getNewRegName(nextReg++, name); |
| fullNames.push_back(reg); |
| if (asmData.isParam(name)) paramRegs.insert(reg); |
| } |
| varRegs[name] = reg; |
| } |
| varUses[name]--; |
| assert(varUses[name] >= 0); |
| if (varUses[name] == 0) { |
| if (optimizables.has(name)) activeOptimizables.erase(name); |
| // If we are not in a loop, or we are optimizable and not bound to a loop |
| // (we might have been in one but left it), we can free the register now. |
| if (loops == 0 || (optimizables.has(name) && !optimizableLoops.has(name))) { |
| // free register |
| freeRegs.push_back(reg); |
| } else { |
| // when the relevant loop is exited, we will free the register |
| int relevantLoop = optimizables.has(name) ? (optimizableLoops[name] ? optimizableLoops[name] : 1) : 1; |
| if ((int)loopRegs.size() <= relevantLoop+1) loopRegs.resize(relevantLoop+1); |
| loopRegs[relevantLoop].push_back(reg); |
| } |
| } |
| return true; |
| }; |
| traversePrePost(fun, [&](Ref node) { // XXX we rely on traversal order being the same as execution order here |
| Ref type = node[0]; |
| if (type == NAME) { |
| IString name = node[1]->getIString(); |
| if (decUse(name)) { |
| node[1]->setString(varRegs[name]); |
| } |
| } else if (LOOP.has(type)) { |
| loops++; |
| // Active optimizables lock onto this loop, if not locked onto one that encloses this one |
| for (auto name : activeOptimizables) { |
| if (!optimizableLoops[name]) { |
| optimizableLoops[name] = loops; |
| } |
| } |
| } |
| }, [&](Ref node) { |
| Ref type = node[0]; |
| if (LOOP.has(type)) { |
| // Free registers that were locked to this loop |
| if ((int)loopRegs.size() > loops && loopRegs[loops].size() > 0) { |
| for (auto loopReg : loopRegs[loops]) { |
| freeRegsClasses[regTypes[loopReg]].push_back(loopReg); |
| } |
| loopRegs[loops].clear(); |
| } |
| loops--; |
| } |
| }); |
| if (!!fun[2] && fun[2]->size()) { |
| fun[2]->setSize(0); // clear params, we will fill with registers |
| fun[3]->splice(0, 1); // remove fake initial var |
| } |
| |
| asmData.locals.clear(); |
| asmData.params.clear(); |
| asmData.vars.clear(); |
| for (int i = 1; i < nextReg; i++) { |
| IString reg = fullNames[i]; |
| AsmType type = regTypes[reg]; |
| if (!paramRegs.has(reg)) { |
| asmData.addVar(reg, type); |
| } else { |
| asmData.addParam(reg, type); |
| fun[2]->push_back(makeString(reg)); |
| } |
| } |
| asmData.denormalize(); |
| }); |
| } |
| |
| // Assign variables to 'registers', coalescing them onto a smaller number of shared |
| // variables. |
| // |
| // This does the same job as 'registerize' above, but burns a lot more cycles trying |
| // to reduce the total number of register variables. Key points about the operation: |
| // |
| // * we decompose the AST into a flow graph and perform a full liveness |
| // analysis, to determine which variables are live at each point. |
| // |
| // * variables that are live concurrently are assigned to different registers. |
| // |
| // * variables that are linked via 'x=y' style statements are assigned the same |
| // register if possible, so that the redundant assignment can be removed. |
| // (e.g. assignments used to pass state around through loops). |
| // |
| // * any code that cannot be reached through the flow-graph is removed. |
| // (e.g. redundant break statements like 'break L123; break;'). |
| // |
| // * any assignments that we can prove are not subsequently used are removed. |
| // (e.g. unnecessary assignments to the 'label' variable). |
| // |
| void registerizeHarder(Ref ast) { |
| #ifdef PROFILING |
| clock_t tasmdata = 0; |
| clock_t tflowgraph = 0; |
| clock_t tlabelfix = 0; |
| clock_t tbackflow = 0; |
| clock_t tjuncvaruniqassign = 0; |
| clock_t tjuncvarsort = 0; |
| clock_t tregassign = 0; |
| clock_t tblockproc = 0; |
| clock_t treconstruct = 0; |
| #endif |
| |
| traverseFunctions(ast, [&](Ref fun) { |
| |
| #ifdef PROFILING |
| clock_t start = clock(); |
| #endif |
| |
| // Do not try to process non-validating methods, like the heap replacer |
| bool abort = false; |
| traversePre(fun, [&abort](Ref node) { |
| if (node[0] == NEW) abort = true; |
| }); |
| if (abort) return; |
| |
| // Do not process the dceable helper function for wasm, which declares |
| // types, we need to alive for asm2wasm |
| if (fun[1] == DCEABLE_TYPE_DECLS) return; |
| |
| AsmData asmData(fun); |
| |
| #ifdef PROFILING |
| tasmdata += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Utilities for allocating register variables. |
| // We need distinct register pools for each type of variable. |
| |
| typedef std::map<int, IString> IntStringMap; |
| std::vector<IntStringMap> allRegsByType; |
| allRegsByType.resize(ASM_NONE+1); |
| int nextReg = 1; |
| |
| auto createReg = [&](IString forName) { |
| // Create a new register of type suitable for the given variable name. |
| AsmType type = asmData.getType(forName); |
| IntStringMap& allRegs = allRegsByType[type]; |
| int reg = nextReg++; |
| allRegs[reg] = getRegName(type, reg); |
| return reg; |
| }; |
| |
| // Traverse the tree in execution order and synthesize a basic flow-graph. |
| // It's convenient to build a kind of "dual" graph where the nodes identify |
| // the junctions between blocks at which control-flow may branch, and each |
| // basic block is an edge connecting two such junctions. |
| // For each junction we store: |
| // * set of blocks that originate at the junction |
| // * set of blocks that terminate at the junction |
| // For each block we store: |
| // * a single entry junction |
| // * a single exit junction |
| // * a 'use' and 'kill' set of names for the block |
| // * full sequence of NAME and ASSIGN nodes in the block |
| // * whether each such node appears as part of a larger expression |
| // (and therefore cannot be safely eliminated) |
| // * set of labels that can be used to jump to this block |
| |
| struct Junction { |
| int id; |
| std::set<int> inblocks, outblocks; |
| IOrderedStringSet live; |
| Junction(int id_) : id(id_) {} |
| }; |
| struct Node { |
| }; |
| struct Block { |
| int id, entry, exit; |
| std::set<int> labels; |
| std::vector<Ref> nodes; |
| std::vector<bool> isexpr; |
| StringIntMap use; |
| StringSet kill; |
| StringStringMap link; |
| StringIntMap lastUseLoc; |
| StringIntMap firstDeadLoc; |
| StringIntMap firstKillLoc; |
| StringIntMap lastKillLoc; |
| |
| Block() : id(-1), entry(-1), exit(-1) {} |
| }; |
| struct ContinueBreak { |
| int co, br; |
| ContinueBreak() : co(-1), br(-1) {} |
| ContinueBreak(int co_, int br_) : co(co_), br(br_) {} |
| }; |
| typedef std::unordered_map<IString, ContinueBreak> LabelState; |
| |
| std::vector<Junction> junctions; |
| std::vector<Block*> blocks; |
| int currEntryJunction = -1; |
| Block* nextBasicBlock = nullptr; |
| int isInExpr = 0; |
| std::vector<LabelState> activeLabels; |
| activeLabels.resize(1); |
| IString nextLoopLabel; |
| |
| const int ENTRY_JUNCTION = 0; |
| const int EXIT_JUNCTION = 1; |
| const int ENTRY_BLOCK = 0; |
| |
| auto addJunction = [&]() { |
| // Create a new junction, without inserting it into the graph. |
| // This is useful for e.g. pre-allocating an exit node. |
| int id = junctions.size(); |
| junctions.push_back(Junction(id)); |
| return id; |
| }; |
| |
| std::function<int (int, bool)> joinJunction; |
| |
| auto markJunction = [&](int id) { |
| // Mark current traversal location as a junction. |
| // This makes a new basic block exiting at this position. |
| if (id < 0) { |
| id = addJunction(); |
| } |
| joinJunction(id, true); |
| return id; |
| }; |
| |
| auto setJunction = [&](int id, bool force) { |
| // Set the next entry junction to the given id. |
| // This can be used to enter at a previously-declared point. |
| // You can't return to a junction with no incoming blocks |
| // unless the 'force' parameter is specified. |
| assert(nextBasicBlock->nodes.size() == 0); // refusing to abandon an in-progress basic block |
| if (force || junctions[id].inblocks.size() > 0) { |
| currEntryJunction = id; |
| } else { |
| currEntryJunction = -1; |
| } |
| }; |
| |
| joinJunction = [&](int id, bool force) { |
| // Complete the pending basic block by exiting at this position. |
| // This can be used to exit at a previously-declared point. |
| if (currEntryJunction >= 0) { |
| assert(nextBasicBlock); |
| nextBasicBlock->id = blocks.size(); |
| nextBasicBlock->entry = currEntryJunction; |
| nextBasicBlock->exit = id; |
| junctions[currEntryJunction].outblocks.insert(nextBasicBlock->id); |
| junctions[id].inblocks.insert(nextBasicBlock->id); |
| blocks.push_back(nextBasicBlock); |
| } |
| nextBasicBlock = new Block(); |
| setJunction(id, force); |
| return id; |
| }; |
| |
| auto pushActiveLabels = [&](int onContinue, int onBreak) { |
| // Push the target junctions for continuing/breaking a loop. |
| // This should be called before traversing into a loop. |
| assert(activeLabels.size() > 0); |
| LabelState& prevLabels = activeLabels.back(); |
| LabelState newLabels = prevLabels; |
| newLabels[EMPTY] = ContinueBreak(onContinue, onBreak); |
| if (!!nextLoopLabel) { |
| newLabels[nextLoopLabel] = ContinueBreak(onContinue, onBreak); |
| nextLoopLabel = EMPTY; |
| } |
| // An unlabelled CONTINUE should jump to innermost loop, |
| // ignoring any nested SWITCH statements. |
| if (onContinue < 0 && prevLabels.count(EMPTY) > 0) { |
| newLabels[EMPTY].co = prevLabels[EMPTY].co; |
| } |
| activeLabels.push_back(newLabels); |
| }; |
| |
| auto popActiveLabels = [&]() { |
| // Pop the target junctions for continuing/breaking a loop. |
| // This should be called after traversing into a loop. |
| activeLabels.pop_back(); |
| }; |
| |
| auto markNonLocalJump = [&](IString type, IString label) { |
| // Complete a block via RETURN, BREAK or CONTINUE. |
| // This joins the targetted junction and then sets the current junction to null. |
| // Any code traversed before we get back to an existing junction is dead code. |
| if (type == RETURN) { |
| joinJunction(EXIT_JUNCTION, false); |
| } else { |
| assert(activeLabels.size() > 0); |
| assert(activeLabels.back().count(label) > 0); // 'jump to unknown label'); |
| auto targets = activeLabels.back()[label]; |
| if (type == CONTINUE) { |
| joinJunction(targets.co, false); |
| } else if (type == BREAK) { |
| joinJunction(targets.br, false); |
| } else { |
| assert(0); // 'unknown jump node type'); |
| } |
| } |
| currEntryJunction = -1; |
| }; |
| |
| auto addUseNode = [&](Ref node) { |
| // Mark a use of the given name node in the current basic block. |
| assert(node[0] == NAME); // 'not a use node'); |
| IString name = node[1]->getIString(); |
| if (asmData.isLocal(name)) { |
| nextBasicBlock->nodes.push_back(node); |
| nextBasicBlock->isexpr.push_back(isInExpr != 0); |
| if (nextBasicBlock->kill.count(name) == 0) { |
| nextBasicBlock->use[name] = 1; |
| } |
| } |
| }; |
| |
| auto addKillNode = [&](Ref node) { |
| // Mark an assignment to the given name node in the current basic block. |
| assert(node[0] == ASSIGN); //, 'not a kill node'); |
| assert(node[1]->isBool(true)); // 'not a kill node'); |
| assert(node[2][0] == NAME); //, 'not a kill node'); |
| IString name = node[2][1]->getIString(); |
| if (asmData.isLocal(name)) { |
| nextBasicBlock->nodes.push_back(node); |
| nextBasicBlock->isexpr.push_back(isInExpr != 0); |
| nextBasicBlock->kill.insert(name); |
| } |
| }; |
| |
| std::function<Ref (Ref)> lookThroughCasts = [&](Ref node) { |
| // Look through value-preserving casts, like "x | 0" => "x" |
| if (node[0] == BINARY && node[1] == OR) { |
| if (node[3][0] == NUM && node[3][1]->getNumber() == 0) { |
| return lookThroughCasts(node[2]); |
| } |
| } |
| return node; |
| }; |
| |
| auto addBlockLabel = [&](Ref node) { |
| assert(nextBasicBlock->nodes.size() == 0); // 'cant add label to an in-progress basic block') |
| if (node[0] == NUM) { |
| nextBasicBlock->labels.insert(node[1]->getInteger()); |
| } |
| }; |
| |
| auto isTrueNode = [&](Ref node) { |
| // Check if the given node is statically truthy. |
| return (node[0] == NUM && node[1]->getNumber() != 0); |
| }; |
| |
| auto isFalseNode = [&](Ref node) { |
| // Check if the given node is statically falsy. |
| return (node[0] == NUM && node[1]->getNumber() == 0); |
| }; |
| |
| std::function<void (Ref)> buildFlowGraph = [&](Ref node) { |
| // Recursive function to build up the flow-graph. |
| // It walks the tree in execution order, calling the above state-management |
| // functions at appropriate points in the traversal. |
| Ref type = node[0]; |
| |
| // Any code traversed without an active entry junction must be dead, |
| // as the resulting block could never be entered. Let's remove it. |
| if (currEntryJunction < 0 && junctions.size() > 0) { |
| safeCopy(node, makeEmpty()); |
| return; |
| } |
| |
| // Traverse each node type according to its particular control-flow semantics. |
| // TODO: switchify this |
| if (type == DEFUN) { |
| int jEntry = markJunction(-1); |
| assert(jEntry == ENTRY_JUNCTION); |
| int jExit = addJunction(); |
| assert(jExit == EXIT_JUNCTION); |
| for (size_t i = 0; i < node[3]->size(); i++) { |
| buildFlowGraph(node[3][i]); |
| } |
| joinJunction(jExit, false); |
| } else if (type == IF) { |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| int jEnter = markJunction(-1); |
| int jExit = addJunction(); |
| if (!!node[2]) { |
| // Detect and mark "if (label == N) { <labelled block> }". |
| if (node[1][0] == BINARY && node[1][1] == EQ) { |
| Ref lhs = lookThroughCasts(node[1][2]); |
| if (lhs[0] == NAME && lhs[1] == LABEL) { |
| addBlockLabel(lookThroughCasts(node[1][3])); |
| } |
| } |
| buildFlowGraph(node[2]); |
| } |
| joinJunction(jExit, false); |
| setJunction(jEnter, false); |
| if (node->size() > 3 && !!node[3]) { |
| buildFlowGraph(node[3]); |
| } |
| joinJunction(jExit, false); |
| } else if (type == CONDITIONAL) { |
| isInExpr++; |
| // If the conditional has no side-effects, we can treat it as a single |
| // block, which might open up opportunities to remove it entirely. |
| if (!hasSideEffects(node)) { |
| buildFlowGraph(node[1]); |
| if (!!node[2]) { |
| buildFlowGraph(node[2]); |
| } |
| if (!!node[3]) { |
| buildFlowGraph(node[3]); |
| } |
| } else { |
| buildFlowGraph(node[1]); |
| int jEnter = markJunction(-1); |
| int jExit = addJunction(); |
| if (!!node[2]) { |
| buildFlowGraph(node[2]); |
| } |
| joinJunction(jExit, false); |
| setJunction(jEnter, false); |
| if (!!node[3]) { |
| buildFlowGraph(node[3]); |
| } |
| joinJunction(jExit, false); |
| } |
| isInExpr--; |
| } else if (type == WHILE) { |
| // Special-case "while (1) {}" to use fewer junctions, |
| // since emscripten generates a lot of these. |
| if (isTrueNode(node[1])) { |
| int jLoop = markJunction(-1); |
| int jExit = addJunction(); |
| pushActiveLabels(jLoop, jExit); |
| buildFlowGraph(node[2]); |
| popActiveLabels(); |
| joinJunction(jLoop, false); |
| setJunction(jExit, false); |
| } else { |
| int jCond = markJunction(-1); |
| int jLoop = addJunction(); |
| int jExit = addJunction(); |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| joinJunction(jLoop, false); |
| pushActiveLabels(jCond, jExit); |
| buildFlowGraph(node[2]); |
| popActiveLabels(); |
| joinJunction(jCond, false); |
| // An empty basic-block linking condition exit to loop exit. |
| setJunction(jLoop, false); |
| joinJunction(jExit, false); |
| } |
| } else if (type == DO) { |
| // Special-case "do {} while (1)" and "do {} while (0)" to use |
| // fewer junctions, since emscripten generates a lot of these. |
| if (isFalseNode(node[1])) { |
| int jExit = addJunction(); |
| pushActiveLabels(jExit, jExit); |
| buildFlowGraph(node[2]); |
| popActiveLabels(); |
| joinJunction(jExit, false); |
| } else if (isTrueNode(node[1])) { |
| int jLoop = markJunction(-1); |
| int jExit = addJunction(); |
| pushActiveLabels(jLoop, jExit); |
| buildFlowGraph(node[2]); |
| popActiveLabels(); |
| joinJunction(jLoop, false); |
| setJunction(jExit, false); |
| } else { |
| int jLoop = markJunction(-1); |
| int jCond = addJunction(); |
| int jCondExit = addJunction(); |
| int jExit = addJunction(); |
| pushActiveLabels(jCond, jExit); |
| buildFlowGraph(node[2]); |
| popActiveLabels(); |
| joinJunction(jCond, false); |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| joinJunction(jCondExit, false); |
| joinJunction(jLoop, false); |
| setJunction(jCondExit, false); |
| joinJunction(jExit, false); |
| } |
| } else if (type == FOR) { |
| int jTest = addJunction(); |
| int jBody = addJunction(); |
| int jStep = addJunction(); |
| int jExit = addJunction(); |
| buildFlowGraph(node[1]); |
| joinJunction(jTest, false); |
| isInExpr++; |
| buildFlowGraph(node[2]); |
| isInExpr--; |
| joinJunction(jBody, false); |
| pushActiveLabels(jStep, jExit); |
| buildFlowGraph(node[4]); |
| popActiveLabels(); |
| joinJunction(jStep, false); |
| buildFlowGraph(node[3]); |
| joinJunction(jTest, false); |
| setJunction(jBody, false); |
| joinJunction(jExit, false); |
| } else if (type == LABEL) { |
| assert(BREAK_CAPTURERS.has(node[2][0])); // 'label on non-loop, non-switch statement') |
| nextLoopLabel = node[1]->getIString(); |
| buildFlowGraph(node[2]); |
| } else if (type == SWITCH) { |
| // Emscripten generates switch statements of a very limited |
| // form: all case clauses are numeric literals, and all |
| // case bodies end with a (maybe implicit) break. So it's |
| // basically equivalent to a multi-way IF statement. |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| Ref condition = lookThroughCasts(node[1]); |
| int jCheckExit = markJunction(-1); |
| int jExit = addJunction(); |
| pushActiveLabels(-1, jExit); |
| bool hasDefault = false; |
| for (size_t i = 0; i < node[2]->size(); i++) { |
| setJunction(jCheckExit, false); |
| // All case clauses are either 'default' or a numeric literal. |
| if (!node[2][i][0]) { |
| hasDefault = true; |
| } else { |
| // Detect switches dispatching to labelled blocks. |
| if (condition[0] == NAME && condition[1] == LABEL) { |
| addBlockLabel(lookThroughCasts(node[2][i][0])); |
| } |
| } |
| for (size_t j = 0; j < node[2][i][1]->size(); j++) { |
| buildFlowGraph(node[2][i][1][j]); |
| } |
| // Control flow will never actually reach the end of the case body. |
| // If there's live code here, assume it jumps to case exit. |
| if (currEntryJunction >= 0 && nextBasicBlock->nodes.size() > 0) { |
| if (!!node[2][i][0]) { |
| markNonLocalJump(RETURN, EMPTY); |
| } else { |
| joinJunction(jExit, false); |
| } |
| } |
| } |
| // If there was no default case, we also need an empty block |
| // linking straight from the test evaluation to the exit. |
| if (!hasDefault) { |
| setJunction(jCheckExit, false); |
| } |
| joinJunction(jExit, false); |
| popActiveLabels(); |
| } else if (type == RETURN) { |
| if (!!node[1]) { |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| } |
| markNonLocalJump(type->getIString(), EMPTY); |
| } else if (type == BREAK || type == CONTINUE) { |
| markNonLocalJump(type->getIString(), !!node[1] ? node[1]->getIString() : EMPTY); |
| } else if (type == ASSIGN) { |
| isInExpr++; |
| buildFlowGraph(node[3]); |
| isInExpr--; |
| if (node[1]->isBool(true) && node[2][0] == NAME) { |
| addKillNode(node); |
| } else { |
| buildFlowGraph(node[2]); |
| } |
| } else if (type == NAME) { |
| addUseNode(node); |
| } else if (type == BLOCK || type == TOPLEVEL) { |
| if (!!node[1]) { |
| for (size_t i = 0; i < node[1]->size(); i++) { |
| buildFlowGraph(node[1][i]); |
| } |
| } |
| } else if (type == STAT) { |
| buildFlowGraph(node[1]); |
| } else if (type == UNARY_PREFIX) { |
| isInExpr++; |
| buildFlowGraph(node[2]); |
| isInExpr--; |
| } else if (type == BINARY) { |
| isInExpr++; |
| buildFlowGraph(node[2]); |
| buildFlowGraph(node[3]); |
| isInExpr--; |
| } else if (type == CALL) { |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| if (!!node[2]) { |
| for (size_t i = 0; i < node[2]->size(); i++) { |
| buildFlowGraph(node[2][i]); |
| } |
| } |
| isInExpr--; |
| // If the call is statically known to throw, |
| // treat it as a jump to function exit. |
| if (!isInExpr && node[1][0] == NAME) { |
| if (FUNCTIONS_THAT_ALWAYS_THROW.has(node[1][1])) { |
| markNonLocalJump(RETURN, EMPTY); |
| } |
| } |
| } else if (type == SEQ || type == SUB) { |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| buildFlowGraph(node[2]); |
| isInExpr--; |
| } else if (type == DOT || type == THROW) { |
| isInExpr++; |
| buildFlowGraph(node[1]); |
| isInExpr--; |
| } else if (type == NUM || type == STRING || type == VAR) { |
| // nada |
| } else { |
| assert(0); // 'unsupported node type: ' + type); |
| } |
| }; |
| |
| buildFlowGraph(fun); |
| |
| #ifdef PROFILING |
| tflowgraph += clock() - start; |
| start = clock(); |
| #endif |
| |
| assert(junctions[ENTRY_JUNCTION].inblocks.size() == 0); // 'function entry must have no incoming blocks'); |
| assert(junctions[EXIT_JUNCTION].outblocks.size() == 0); // 'function exit must have no outgoing blocks'); |
| assert(blocks[ENTRY_BLOCK]->entry == ENTRY_JUNCTION); //, 'block zero must be the initial block'); |
| |
| // Fix up implicit jumps done by assigning to the LABEL variable. |
| // If a block ends with an assignment to LABEL and there's another block |
| // with that value of LABEL as precondition, we tweak the flow graph so |
| // that the former jumps straight to the later. |
| |
| std::map<int, Block*> labelledBlocks; |
| typedef std::pair<Ref, Block*> Jump; |
| std::vector<Jump> labelledJumps; |
| |
| for (size_t i = 0; i < blocks.size(); i++) { |
| Block* block = blocks[i]; |
| // Does it have any labels as preconditions to its entry? |
| for (auto labelVal : block->labels) { |
| // If there are multiple blocks with the same label, all bets are off. |
| // This seems to happen sometimes for short blocks that end with a return. |
| // TODO: it should be safe to merge the duplicates if they're identical. |
| if (labelledBlocks.count(labelVal) > 0) { |
| labelledBlocks.clear(); |
| labelledJumps.clear(); |
| goto AFTER_FINDLABELLEDBLOCKS; |
| } |
| labelledBlocks[labelVal] = block; |
| } |
| // Does it assign a specific label value at exit? |
| if (block->kill.has(LABEL)) { |
| Ref finalNode = block->nodes.back(); |
| if (finalNode[0] == ASSIGN && finalNode[2][1] == LABEL) { |
| // If labels are computed dynamically then all bets are off. |
| // This can happen due to indirect branching in llvm output. |
| if (finalNode[3][0] != NUM) { |
| labelledBlocks.clear(); |
| labelledJumps.clear(); |
| goto AFTER_FINDLABELLEDBLOCKS; |
| } |
| labelledJumps.push_back(Jump(finalNode[3][1], block)); |
| } else { |
| // If label is assigned a non-zero value elsewhere in the block |
| // then all bets are off. This can happen e.g. due to outlining |
| // saving/restoring label to the stack. |
| for (size_t j = 0; j < block->nodes.size() - 1; j++) { |
| if (block->nodes[j][0] == ASSIGN && block->nodes[j][2][1] == LABEL) { |
| if (block->nodes[j][3][0] != NUM || block->nodes[j][3][1]->getNumber() != 0) { |
| labelledBlocks.clear(); |
| labelledJumps.clear(); |
| goto AFTER_FINDLABELLEDBLOCKS; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| AFTER_FINDLABELLEDBLOCKS: |
| |
| for (auto labelVal : labelledBlocks) { |
| Block* block = labelVal.second; |
| // Disconnect it from the graph, and create a |
| // new junction for jumps targetting this label. |
| junctions[block->entry].outblocks.erase(block->id); |
| block->entry = addJunction(); |
| junctions[block->entry].outblocks.insert(block->id); |
| // Add a fake use of LABEL to keep it alive in predecessor. |
| block->use[LABEL] = 1; |
| block->nodes.insert(block->nodes.begin(), makeName(LABEL)); |
| block->isexpr.insert(block->isexpr.begin(), 1); |
| } |
| for (size_t i = 0; i < labelledJumps.size(); i++) { |
| auto labelVal = labelledJumps[i].first; |
| auto block = labelledJumps[i].second; |
| Block* targetBlock = labelledBlocks[labelVal->getInteger()]; |
| if (targetBlock) { |
| // Redirect its exit to entry of the target block. |
| junctions[block->exit].inblocks.erase(block->id); |
| block->exit = targetBlock->entry; |
| junctions[block->exit].inblocks.insert(block->id); |
| } |
| } |
| |
| #ifdef PROFILING |
| tlabelfix += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Do a backwards data-flow analysis to determine the set of live |
| // variables at each junction, and to use this information to eliminate |
| // any unused assignments. |
| // We run two nested phases. The inner phase builds the live set for each |
| // junction. The outer phase uses this to try to eliminate redundant |
| // stores in each basic block, which might in turn affect liveness info. |
| |
| auto analyzeJunction = [&](Junction& junc) { |
| // Update the live set for this junction. |
| IOrderedStringSet live; |
| for (auto b : junc.outblocks) { |
| Block* block = blocks[b]; |
| IOrderedStringSet& liveSucc = junctions[block->exit].live; |
| for (auto name : liveSucc) { |
| if (!block->kill.has(name)) { |
| live.insert(name); |
| } |
| } |
| for (auto name : block->use) { |
| live.insert(name.first); |
| } |
| } |
| junc.live = live; |
| }; |
| |
| auto analyzeBlock = [&](Block* block) { |
| // Update information about the behaviour of the block. |
| // This includes the standard 'use' and 'kill' information, |
| // plus a 'link' set naming values that flow through from entry |
| // to exit, possibly changing names via simple 'x=y' assignments. |
| // As we go, we eliminate assignments if the variable is not |
| // subsequently used. |
| auto live = junctions[block->exit].live; |
| StringIntMap use; |
| StringSet kill; |
| StringStringMap link; |
| StringIntMap lastUseLoc; |
| StringIntMap firstDeadLoc; |
| StringIntMap firstKillLoc; |
| StringIntMap lastKillLoc; |
| for (auto name : live) { |
| link[name] = name; |
| lastUseLoc[name] = block->nodes.size(); |
| firstDeadLoc[name] = block->nodes.size(); |
| } |
| for (int j = block->nodes.size() - 1; j >= 0 ; j--) { |
| Ref node = block->nodes[j]; |
| if (node[0] == NAME) { |
| IString name = node[1]->getIString(); |
| live.insert(name); |
| use[name] = j; |
| if (lastUseLoc.count(name) == 0) { |
| lastUseLoc[name] = j; |
| firstDeadLoc[name] = j; |
| } |
| } else { |
| IString name = node[2][1]->getIString(); |
| // We only keep assignments if they will be subsequently used. |
| if (live.has(name)) { |
| kill.insert(name); |
| use.erase(name); |
| live.erase(name); |
| firstDeadLoc[name] = j; |
| firstKillLoc[name] = j; |
| if (lastUseLoc.count(name) == 0) { |
| lastUseLoc[name] = j; |
| } |
| if (lastKillLoc.count(name) == 0) { |
| lastKillLoc[name] = j; |
| } |
| // If it's an "x=y" and "y" is not live, then we can create a |
| // flow-through link from "y" to "x". If not then there's no |
| // flow-through link for "x". |
| if (link.has(name)) { |
| IString oldLink = link[name]; |
| link.erase(name); |
| if (node[3][0] == NAME) { |
| if (asmData.isLocal(node[3][1]->getIString())) { |
| link[node[3][1]->getIString()] = oldLink; |
| } |
| } |
| } |
| } else { |
| // The result of this assignment is never used, so delete it. |
| // We may need to keep the RHS for its value or its side-effects. |
| auto removeUnusedNodes = [&](int j, int n) { |
| for (auto pair : lastUseLoc) { |
| pair.second -= n; |
| } |
| for (auto pair : firstKillLoc) { |
| pair.second -= n; |
| } |
| for (auto pair : lastKillLoc) { |
| pair.second -= n; |
| } |
| for (auto pair : firstDeadLoc) { |
| pair.second -= n; |
| } |
| block->nodes.erase(block->nodes.begin() + j, block->nodes.begin() + j + n); |
| block->isexpr.erase(block->isexpr.begin() + j, block->isexpr.begin() + j + n); |
| }; |
| if (block->isexpr[j] || hasSideEffects(node[3])) { |
| safeCopy(node, node[3]); |
| removeUnusedNodes(j, 1); |
| } else { |
| int numUsesInExpr = 0; |
| traversePre(node[3], [&](Ref node) { |
| if (node[0] == NAME && asmData.isLocal(node[1]->getIString())) { |
| numUsesInExpr++; |
| } |
| }); |
| safeCopy(node, makeEmpty()); |
| j = j - numUsesInExpr; |
| removeUnusedNodes(j, 1 + numUsesInExpr); |
| } |
| } |
| } |
| } |
| // XXX efficiency |
| block->use = use; |
| block->kill = kill; |
| block->link = link; |
| block->lastUseLoc = lastUseLoc; |
| block->firstDeadLoc = firstDeadLoc; |
| block->firstKillLoc = firstKillLoc; |
| block->lastKillLoc = lastKillLoc; |
| }; |
| |
| // Ordered map to work in approximate reverse order of junction appearance |
| std::set<int> jWorkSet; |
| std::set<int> bWorkSet; |
| |
| // Be sure to visit every junction at least once. |
| // This avoids missing some vars because we disconnected them |
| // when processing the labelled jumps. |
| for (size_t i = EXIT_JUNCTION; i < junctions.size(); i++) { |
| jWorkSet.insert(i); |
| for (auto b : junctions[i].inblocks) { |
| bWorkSet.insert(b); |
| } |
| } |
| // Exit junction never has any live variable changes to propagate |
| jWorkSet.erase(EXIT_JUNCTION); |
| |
| do { |
| // Iterate on just the junctions until we get stable live sets. |
| // The first run of this loop will grow the live sets to their maximal size. |
| // Subsequent runs will shrink them based on eliminated in-block uses. |
| while (jWorkSet.size() > 0) { |
| auto last = jWorkSet.end(); |
| --last; |
| Junction& junc = junctions[*last]; |
| jWorkSet.erase(last); |
| IOrderedStringSet oldLive = junc.live; // copy it here, to check for changes later |
| analyzeJunction(junc); |
| if (oldLive != junc.live) { |
| // Live set changed, updated predecessor blocks and junctions. |
| for (auto b : junc.inblocks) { |
| bWorkSet.insert(b); |
| jWorkSet.insert(blocks[b]->entry); |
| } |
| } |
| } |
| // Now update the blocks based on the calculated live sets. |
| while (bWorkSet.size() > 0) { |
| auto last = bWorkSet.end(); |
| --last; |
| Block* block = blocks[*last]; |
| bWorkSet.erase(last); |
| auto oldUse = block->use; |
| analyzeBlock(block); |
| if (oldUse != block->use) { |
| // The use set changed, re-process the entry junction. |
| jWorkSet.insert(block->entry); |
| } |
| } |
| } while (jWorkSet.size() > 0); |
| |
| #ifdef PROFILING |
| tbackflow += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Insist that all function parameters are alive at function entry. |
| // This ensures they will be assigned independent registers, even |
| // if they happen to be unused. |
| |
| for (auto name : asmData.params) { |
| junctions[ENTRY_JUNCTION].live.insert(name); |
| } |
| |
| // For variables that are live at one or more junctions, we assign them |
| // a consistent register for the entire scope of the function. Find pairs |
| // of variable that cannot use the same register (the "conflicts") as well |
| // as pairs of variables that we'd like to have share the same register |
| // (the "links"). |
| |
| struct JuncVar { |
| std::vector<bool> conf; |
| IOrderedStringSet link; |
| std::unordered_set<int> excl; |
| int reg; |
| bool used; |
| JuncVar() : reg(-1), used(false) {} |
| }; |
| size_t numLocals = asmData.locals.size(); |
| std::unordered_map<IString, size_t> nameToNum; |
| std::vector<IString> numToName; |
| nameToNum.reserve(numLocals); |
| numToName.reserve(numLocals); |
| for (auto kv : asmData.locals) { |
| nameToNum[kv.first] = numToName.size(); |
| numToName.push_back(kv.first); |
| } |
| |
| std::vector<JuncVar> juncVars(numLocals); |
| for (Junction& junc : junctions) { |
| for (IString name : junc.live) { |
| JuncVar& jVar = juncVars[nameToNum[name]]; |
| jVar.used = true; |
| jVar.conf.assign(numLocals, false); |
| } |
| } |
| std::map<IString, std::vector<Block*>> possibleBlockConflictsMap; |
| std::vector<std::pair<size_t, std::vector<Block*>>> possibleBlockConflicts; |
| std::unordered_map<IString, std::vector<Block*>> possibleBlockLinks; |
| possibleBlockConflicts.reserve(numLocals); |
| possibleBlockLinks.reserve(numLocals); |
| |
| for (Junction& junc : junctions) { |
| // Pre-compute the possible conflicts and links for each block rather |
| // than checking potentially impossible options for each var |
| possibleBlockConflictsMap.clear(); |
| possibleBlockConflicts.clear(); |
| possibleBlockLinks.clear(); |
| for (auto b : junc.outblocks) { |
| Block* block = blocks[b]; |
| Junction& jSucc = junctions[block->exit]; |
| for (auto name : jSucc.live) { |
| possibleBlockConflictsMap[name].push_back(block); |
| } |
| for (auto name_linkname : block->link) { |
| if (name_linkname.first != name_linkname.second) { |
| possibleBlockLinks[name_linkname.first].push_back(block); |
| } |
| } |
| } |
| // Find the live variables in this block, mark them as unnecessary to |
| // check for conflicts (we mark all live vars as conflicting later) |
| std::vector<size_t> liveJVarNums; |
| liveJVarNums.reserve(junc.live.size()); |
| for (auto name : junc.live) { |
| size_t jVarNum = nameToNum[name]; |
| liveJVarNums.push_back(jVarNum); |
| possibleBlockConflictsMap.erase(name); |
| } |
| // Extract just the variables we might want to check for conflicts |
| for (auto kv : possibleBlockConflictsMap) { |
| possibleBlockConflicts.push_back(std::make_pair(nameToNum[kv.first], kv.second)); |
| } |
| |
| for (size_t jVarNum : liveJVarNums) { |
| JuncVar& jvar = juncVars[jVarNum]; |
| IString name = numToName[jVarNum]; |
| // It conflicts with all other names live at this junction. |
| for (size_t liveJVarNum : liveJVarNums) { |
| jvar.conf[liveJVarNum] = true; |
| } |
| jvar.conf[jVarNum] = false; // except for itself, of course |
| |
| // It conflicts with any output vars of successor blocks, |
| // if they're assigned before it goes dead in that block. |
| for (auto jvarnum_blocks : possibleBlockConflicts) { |
| size_t otherJVarNum = jvarnum_blocks.first; |
| IString otherName = numToName[otherJVarNum]; |
| for (auto block : jvarnum_blocks.second) { |
| if (block->lastKillLoc[otherName] < block->firstDeadLoc[name]) { |
| jvar.conf[otherJVarNum] = true; |
| juncVars[otherJVarNum].conf[jVarNum] = true; |
| break; |
| } |
| } |
| } |
| |
| // It links with any linkages in the outgoing blocks. |
| for (auto block: possibleBlockLinks[name]) { |
| IString linkName = block->link[name]; |
| jvar.link.insert(linkName); |
| juncVars[nameToNum[linkName]].link.insert(name); |
| } |
| } |
| } |
| |
| #ifdef PROFILING |
| tjuncvaruniqassign += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Attempt to sort the junction variables to heuristically reduce conflicts. |
| // Simple starting point: handle the most-conflicted variables first. |
| // This seems to work pretty well. |
| |
| std::vector<size_t> sortedJVarNums; |
| sortedJVarNums.reserve(juncVars.size()); |
| std::vector<size_t> jVarConfCounts(numLocals); |
| for (size_t jVarNum = 0; jVarNum < juncVars.size(); jVarNum++) { |
| JuncVar& jVar = juncVars[jVarNum]; |
| if (!jVar.used) continue; |
| jVarConfCounts[jVarNum] = std::count(jVar.conf.begin(), jVar.conf.end(), true); |
| sortedJVarNums.push_back(jVarNum); |
| } |
| std::sort(sortedJVarNums.begin(), sortedJVarNums.end(), [&](const size_t vi1, const size_t vi2) { |
| // sort by # of conflicts |
| if (jVarConfCounts[vi1] < jVarConfCounts[vi2]) return true; |
| if (jVarConfCounts[vi1] == jVarConfCounts[vi2]) return numToName[vi1] < numToName[vi2]; |
| return false; |
| }); |
| |
| #ifdef PROFILING |
| tjuncvarsort += clock() - start; |
| start = clock(); |
| #endif |
| |
| // We can now assign a register to each junction variable. |
| // Process them in order, trying available registers until we find |
| // one that works, and propagating the choice to linked/conflicted |
| // variables as we go. |
| |
| std::function<bool (IString, int)> tryAssignRegister = [&](IString name, int reg) { |
| // Try to assign the given register to the given variable, |
| // and propagate that choice throughout the graph. |
| // Returns true if successful, false if there was a conflict. |
| JuncVar& jv = juncVars[nameToNum[name]]; |
| if (jv.reg > 0) { |
| return jv.reg == reg; |
| } |
| if (jv.excl.count(reg) > 0) { |
| return false; |
| } |
| jv.reg = reg; |
| // Exclude use of this register at all conflicting variables. |
| for (size_t confNameNum = 0; confNameNum < jv.conf.size(); confNameNum++) { |
| if (jv.conf[confNameNum]) { |
| juncVars[confNameNum].excl.insert(reg); |
| } |
| } |
| // Try to propagate it into linked variables. |
| // It's not an error if we can't. |
| for (auto linkName : jv.link) { |
| tryAssignRegister(linkName, reg); |
| } |
| return true; |
| }; |
| for (size_t jVarNum : sortedJVarNums) { |
| // It may already be assigned due to linked-variable propagation. |
| if (juncVars[jVarNum].reg > 0) { |
| continue; |
| } |
| IString name = numToName[jVarNum]; |
| // Try to use existing registers first. |
| auto& allRegs = allRegsByType[asmData.getType(name)]; |
| bool moar = false; |
| for (auto reg : allRegs) { |
| if (tryAssignRegister(name, reg.first)) { |
| moar = true; |
| break; |
| } |
| } |
| if (moar) continue; |
| // They're all taken, create a new one. |
| tryAssignRegister(name, createReg(name)); |
| } |
| |
| #ifdef PROFILING |
| tregassign += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Each basic block can now be processed in turn. |
| // There may be internal-use-only variables that still need a register |
| // assigned, but they can be treated just for this block. We know |
| // that all inter-block variables are in a good state thanks to |
| // junction variable consistency. |
| |
| for (size_t i = 0; i < blocks.size(); i++) { |
| Block* block = blocks[i]; |
| if (block->nodes.size() == 0) continue; |
| Junction& jEnter = junctions[block->entry]; |
| Junction& jExit = junctions[block->exit]; |
| // Mark the point at which each input reg becomes dead. |
| // Variables alive before this point must not be assigned |
| // to that register. |
| StringSet inputVars; |
| std::unordered_map<int, int> inputDeadLoc; |
| std::unordered_map<int, IString> inputVarsByReg; |
| for (auto name : jExit.live) { |
| if (!block->kill.has(name)) { |
| inputVars.insert(name); |
| int reg = juncVars[nameToNum[name]].reg; |
| assert(reg > 0); // 'input variable doesnt have a register'); |
| inputDeadLoc[reg] = block->firstDeadLoc[name]; |
| inputVarsByReg[reg] = name; |
| } |
| } |
| for (auto pair : block->use) { |
| IString name = pair.first; |
| if (!inputVars.has(name)) { |
| inputVars.insert(name); |
| int reg = juncVars[nameToNum[name]].reg; |
| assert(reg > 0); // 'input variable doesnt have a register'); |
| inputDeadLoc[reg] = block->firstDeadLoc[name]; |
| inputVarsByReg[reg] = name; |
| } |
| } |
| // TODO assert(setSize(setSub(inputVars, jEnter.live)) == 0); |
| // Scan through backwards, allocating registers on demand. |
| // Be careful to avoid conflicts with the input registers. |
| // We consume free registers in last-used order, which helps to |
| // eliminate "x=y" assignments that are the last use of "y". |
| StringIntMap assignedRegs; |
| auto freeRegsByTypePre = allRegsByType; // XXX copy |
| // Begin with all live vars assigned per the exit junction. |
| for (auto name : jExit.live) { |
| int reg = juncVars[nameToNum[name]].reg; |
| assert(reg > 0); // 'output variable doesnt have a register'); |
| assignedRegs[name] = reg; |
| freeRegsByTypePre[asmData.getType(name)].erase(reg); // XXX assert? |
| } |
| std::vector<std::vector<int>> freeRegsByType; |
| freeRegsByType.resize(freeRegsByTypePre.size()); |
| for (size_t j = 0; j < freeRegsByTypePre.size(); j++) { |
| for (auto pair : freeRegsByTypePre[j]) { |
| freeRegsByType[j].push_back(pair.first); |
| } |
| } |
| // Scan through the nodes in sequence, modifying each node in-place |
| // and grabbing/freeing registers as needed. |
| std::vector<std::pair<int, Ref>> maybeRemoveNodes; |
| for (int j = block->nodes.size() - 1; j >= 0; j--) { |
| Ref node = block->nodes[j]; |
| IString name = (node[0] == ASSIGN ? node[2][1] : node[1])->getIString(); |
| IntStringMap& allRegs = allRegsByType[asmData.getType(name)]; |
| std::vector<int>& freeRegs = freeRegsByType[asmData.getType(name)]; |
| int reg = assignedRegs[name]; // XXX may insert a zero |
| if (node[0] == NAME) { |
| // A use. Grab a register if it doesn't have one. |
| if (reg <= 0) { |
| if (inputVars.has(name) && j <= block->firstDeadLoc[name]) { |
| // Assignment to an input variable, must use pre-assigned reg. |
| reg = juncVars[nameToNum[name]].reg; |
| assignedRegs[name] = reg; |
| for (int k = freeRegs.size() - 1; k >= 0; k--) { |
| if (freeRegs[k] == reg) { |
| freeRegs.erase(freeRegs.begin() + k); |
| break; |
| } |
| } |
| } else { |
| // Try to use one of the existing free registers. |
| // It must not conflict with an input register. |
| for (int k = freeRegs.size() - 1; k >= 0; k--) { |
| reg = freeRegs[k]; |
| // Check for conflict with input registers. |
| if (inputDeadLoc.count(reg) > 0) { |
| if (block->firstKillLoc[name] <= inputDeadLoc[reg]) { |
| if (name != inputVarsByReg[reg]) { |
| continue; |
| } |
| } |
| } |
| // Found one! |
| assignedRegs[name] = reg; |
| assert(reg > 0); |
| freeRegs.erase(freeRegs.begin() + k); |
| break; |
| } |
| // If we didn't find a suitable register, create a new one. |
| if (assignedRegs[name] <= 0) { |
| reg = createReg(name); |
| assignedRegs[name] = reg; |
| } |
| } |
| } |
| node[1]->setString(allRegs[reg]); |
| } else { |
| // A kill. This frees the assigned register. |
| assert(reg > 0); //, 'live variable doesnt have a reg?') |
| node[2][1]->setString(allRegs[reg]); |
| freeRegs.push_back(reg); |
| assignedRegs.erase(name); |
| if (node[3][0] == NAME && asmData.isLocal(node[3][1]->getIString())) { |
| maybeRemoveNodes.push_back(std::pair<int, Ref>(j, node)); |
| } |
| } |
| } |
| // If we managed to create any "x=x" assignments, remove them. |
| for (size_t j = 0; j < maybeRemoveNodes.size(); j++) { |
| Ref node = maybeRemoveNodes[j].second; |
| if (node[2][1] == node[3][1]) { |
| if (block->isexpr[maybeRemoveNodes[j].first]) { |
| safeCopy(node, node[2]); |
| } else { |
| safeCopy(node, makeEmpty()); |
| } |
| } |
| } |
| } |
| |
| #ifdef PROFILING |
| tblockproc += clock() - start; |
| start = clock(); |
| #endif |
| |
| // Assign registers to function params based on entry junction |
| |
| StringSet paramRegs; |
| if (!!fun[2]) { |
| for (size_t i = 0; i < fun[2]->size(); i++) { |
| auto& allRegs = allRegsByType[asmData.getType(fun[2][i]->getIString())]; |
| fun[2][i]->setString(allRegs[juncVars[nameToNum[fun[2][i]->getIString()]].reg]); |
| paramRegs.insert(fun[2][i]->getIString()); |
| } |
| } |
| |
| // That's it! |
| // Re-construct the function with appropriate variable definitions. |
| |
| asmData.locals.clear(); |
| asmData.params.clear(); |
| asmData.vars.clear(); |
| for (int i = 1; i < nextReg; i++) { |
| for (size_t type = 0; type < allRegsByType.size(); type++) { |
| if (allRegsByType[type].count(i) > 0) { |
| IString reg = allRegsByType[type][i]; |
| if (!paramRegs.has(reg)) { |
| asmData.addVar(reg, intToAsmType(type)); |
| } else { |
| asmData.addParam(reg, intToAsmType(type)); |
| } |
| break; |
| } |
| } |
| } |
| asmData.denormalize(); |
| |
| removeAllUselessSubNodes(fun); // XXX vacuum? vacuum(fun); |
| |
| #ifdef PROFILING |
| treconstruct += clock() - start; |
| start = clock(); |
| #endif |
| |
| }); |
| #ifdef PROFILING |
| errv(" RH stages: a:%li fl:%li lf:%li bf:%li jvua:%li jvs:%li jra:%li bp:%li r:%li", |
| tasmdata, tflowgraph, tlabelfix, tbackflow, tjuncvaruniqassign, tjuncvarsort, tregassign, tblockproc, treconstruct); |
| #endif |
| } |
| // end registerizeHarder |
| |
| // minified names generation |
| StringSet RESERVED("do if in for new try var env let case else enum this void with"); |
| const char *VALID_MIN_INITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"; |
| const char *VALID_MIN_LATERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789"; |
| |
| StringVec minifiedNames; |
| std::vector<int> minifiedState; |
| |
| void ensureMinifiedNames(int n) { // make sure the nth index in minifiedNames exists. done 100% deterministically |
| static int VALID_MIN_INITS_LEN = strlen(VALID_MIN_INITS); |
| static int VALID_MIN_LATERS_LEN = strlen(VALID_MIN_LATERS); |
| |
| while ((int)minifiedNames.size() < n+1) { |
| // generate the current name |
| std::string name; |
| name += VALID_MIN_INITS[minifiedState[0]]; |
| for (size_t i = 1; i < minifiedState.size(); i++) { |
| name += VALID_MIN_LATERS[minifiedState[i]]; |
| } |
| IString str(strdupe(name.c_str())); // leaked! |
| if (!RESERVED.has(str)) minifiedNames.push_back(str); |
| // increment the state |
| size_t i = 0; |
| while (1) { |
| minifiedState[i]++; |
| if (minifiedState[i] < (i == 0 ? VALID_MIN_INITS_LEN : VALID_MIN_LATERS_LEN)) break; |
| // overflow |
| minifiedState[i] = 0; |
| i++; |
| if (i == minifiedState.size()) minifiedState.push_back(-1); // will become 0 after increment in next loop head |
| } |
| } |
| } |
| |
| void minifyLocals(Ref ast) { |
| assert(!!extraInfo); |
| IString GLOBALS("globals"); |
| assert(extraInfo->has(GLOBALS)); |
| Ref globals = extraInfo[GLOBALS]; |
| |
| if (minifiedState.size() == 0) minifiedState.push_back(0); |
| |
| traverseFunctions(ast, [&globals](Ref fun) { |
| // Analyse the asmjs to figure out local variable names, |
| // but operate on the original source tree so that we don't |
| // miss any global names in e.g. variable initializers. |
| AsmData asmData(fun); |
| asmData.denormalize(); // TODO: we can avoid modifying at all here - we just need a list of local vars+params |
| |
| StringStringMap newNames; |
| StringSet usedNames; |
| |
| // Find all the globals that we need to minify using |
| // pre-assigned names. Don't actually minify them yet |
| // as that might interfere with local variable names. |
| traversePre(fun, [&](Ref node) { |
| if (node[0] == NAME) { |
| IString name = node[1]->getIString(); |
| if (!asmData.isLocal(name)) { |
| if (globals->has(name)) { |
| IString minified = globals[name]->getIString(); |
| assert(!!minified); |
| newNames[name] = minified; |
| usedNames.insert(minified); |
| } |
| } |
| } |
| }); |
| |
| // The first time we encounter a local name, we assign it a |
| // minified name that's not currently in use. Allocating on |
| // demand means they're processed in a predictable order, |
| // which is very handy for testing/debugging purposes. |
| int nextMinifiedName = 0; |
| auto getNextMinifiedName = [&]() { |
| IString minified; |
| while (1) { |
| ensureMinifiedNames(nextMinifiedName); |
| minified = minifiedNames[nextMinifiedName++]; |
| // TODO: we can probably remove !isLocalName here |
| if (!usedNames.has(minified) && !asmData.isLocal(minified)) { |
| return minified; |
| } |
| } |
| }; |
| |
| // We can also minify loop labels, using a separate namespace |
| // to the variable declarations. |
| StringStringMap newLabels; |
| int nextMinifiedLabel = 0; |
| auto getNextMinifiedLabel = [&]() { |
| ensureMinifiedNames(nextMinifiedLabel); |
| return minifiedNames[nextMinifiedLabel++]; |
| }; |
| |
| // Traverse and minify all names. |
| if (globals->has(fun[1]->getIString())) { |
| fun[1]->setString(globals[fun[1]->getIString()]->getIString()); |
| assert(!!fun[1]); |
| } |
| if (!!fun[2]) { |
| for (size_t i = 0; i < fun[2]->size(); i++) { |
| IString minified = getNextMinifiedName(); |
| newNames[fun[2][i]->getIString()] = minified; |
| fun[2][i]->setString(minified); |
| } |
| } |
| traversePre(fun[3], [&](Ref node) { |
| Ref type = node[0]; |
| if (type == NAME) { |
| IString name = node[1]->getIString(); |
| IString minified = newNames[name]; |
| if (!!minified) { |
| node[1]->setString(minified); |
| } else if (asmData.isLocal(name)) { |
| minified = getNextMinifiedName(); |
| newNames[name] = minified; |
| node[1]->setString(minified); |
| } |
| } else if (type == VAR) { |
| for (size_t i = 0; i < node[1]->size(); i++) { |
| Ref defn = node[1][i]; |
| IString name = defn[0]->getIString(); |
| if (!(newNames.has(name))) { |
| newNames[name] = getNextMinifiedName(); |
| } |
| defn[0]->setString(newNames[name]); |
| } |
| } else if (type == LABEL) { |
| IString name = node[1]->getIString(); |
| if (!newLabels.has(name)) { |
| newLabels[name] = getNextMinifiedLabel(); |
| } |
| node[1]->setString(newLabels[name]); |
| } else if (type == BREAK || type == CONTINUE) { |
| if (node->size() > 1 && !!node[1]) { |
| node[1]->setString(newLabels[node[1]->getIString()]); |
| } |
| } |
| }); |
| }); |
| } |
| |
| void asmLastOpts(Ref ast) { |
| std::vector<Ref> statsStack; |
| traverseFunctions(ast, [&](Ref fun) { |
| traversePrePost(fun, [&](Ref node) { |
| Ref type = node[0]; |
| Ref stats = getStatements(node); |
| if (!!stats) statsStack.push_back(stats); |
| if (CONDITION_CHECKERS.has(type)) { |
| node[1] = simplifyCondition(node[1]); |
| } |
| if (type == WHILE && node[1][0] == NUM && node[1][1]->getNumber() == 1 && node[2][0] == BLOCK && node[2]->size() == 2) { |
| // This is at the end of the pipeline, we can assume all other optimizations are done, and we modify loops |
| // into shapes that might confuse other passes |
| |
| // while (1) { .. if (..) { break } } ==> do { .. } while(..) |
| Ref stats = node[2][1]; |
| Ref last = stats->back(); |
| if (!!last && last[0] == IF && (last->size() < 4 || !last[3]) && last[2][0] == BLOCK && !!last[2][1][0]) { |
| Ref lastStats = last[2][1]; |
| int lastNum = lastStats->size(); |
| Ref lastLast = lastStats[lastNum-1]; |
| if (!(lastLast[0] == BREAK && !lastLast[1])) return;// if not a simple break, dangerous |
| for (int i = 0; i < lastNum; i++) { |
| if (lastStats[i][0] != STAT && lastStats[i][0] != BREAK) return; // something dangerous |
| } |
| // ok, a bunch of statements ending in a break |
| bool abort = false; |
| int stack = 0; |
| int breaks = 0; |
| traversePrePost(stats, [&](Ref node) { |
| Ref type = node[0]; |
| if (type == CONTINUE) { |
| if (stack == 0 || !!node[1]) { // abort if labeled (we do not analyze labels here yet), or a continue directly on us |
| abort = true; |
| } |
| } else if (type == BREAK) { |
| if (stack == 0 || !!node[1]) { // relevant if labeled (we do not analyze labels here yet), or a break directly on us |
| breaks++; |
| } |
| } else if (LOOP.has(type)) { |
| stack++; |
| } |
| }, [&](Ref node) { |
| if (LOOP.has(node[0])) { |
| stack--; |
| } |
| }); |
| if (abort) return; |
| assert(breaks > 0); |
| if (lastStats->size() > 1 && breaks != 1) return; // if we have code aside from the break, we can only move it out if there is just one break |
| if (statsStack.size() < 1) return; // no chance we have this stats on hand |
| // start to optimize |
| if (lastStats->size() > 1) { |
| Ref parent = statsStack.back(); |
| int me = parent->indexOf(node); |
| if (me < 0) return; // not always directly on a stats, could be in a label for example |
| parent->insert(me+1, lastStats->size()-1); |
| for (size_t i = 0; i+1 < lastStats->size(); i++) { |
| parent[me+1+i] = lastStats[i]; |
| } |
| } |
| Ref conditionToBreak = last[1]; |
| stats->pop_back(); |
| node[0]->setString(DO); |
| node[1] = simplifyNotCompsDirect(make2(UNARY_PREFIX, L_NOT, conditionToBreak)); |
| } |
| } else if (type == BINARY) { |
| if (node[1] == AND) { |
| if (node[3][0] == UNARY_PREFIX && node[3][1] == MINUS && node[3][2][0] == NUM && node[3][2][1]->getNumber() == 1) { |
| // Change &-1 into |0, at this point the hint is no longer needed |
| node[1]->setString(OR); |
| node[3] = node[3][2]; |
| node[3][1]->setNumber(0); |
| } |
| } else if (node[1] == MINUS && node[3][0] == UNARY_PREFIX) { |
| // avoid X - (-Y) because some minifiers buggily emit X--Y which is invalid as -- can be a unary. Transform to |
| // X + Y |
| if (node[3][1] == MINUS) { // integer |
| node[1]->setString(PLUS); |
| node[3] = node[3][2]; |
| } else if (node[3][1] == PLUS) { // float |
| if (node[3][2][0] == UNARY_PREFIX && node[3][2][1] == MINUS) { |
| node[1]->setString(PLUS); |
| node[3][2] = node[3][2][2]; |
| } |
| } |
| } |
| } |
| }, [&](Ref node) { |
| if (statsStack.size() > 0) { |
| Ref stats = getStatements(node); |
| if (!!stats) statsStack.pop_back(); |
| } |
| }); |
| // convert { singleton } into singleton |
| traversePre(fun, [](Ref node) { |
| if (node[0] == BLOCK && !!getStatements(node) && node[1]->size() == 1) { |
| safeCopy(node, node[1][0]); |
| } |
| }); |
| // convert L: do { .. } while(0) into L: { .. } |
| traversePre(fun, [](Ref node) { |
| if (node[0] == LABEL && node[1]->isString() /* careful of var label = 5 */ && |
| node[2][0] == DO && node[2][1][0] == NUM && node[2][1][1]->getNumber() == 0 && node[2][2][0] == BLOCK) { |
| // there shouldn't be any continues on this, not direct break or continue |
| IString label = node[1]->getIString(); |
| bool abort = false; |
| int breakCaptured = 0, continueCaptured = 0; |
| traversePrePost(node[2][2], [&](Ref node) { |
| if (node[0] == CONTINUE) { |
| if (!node[1] && !continueCaptured) { |
| abort = true; |
| } else if (node[1]->isString() && node[1]->getIString() == label) { |
| abort = true; |
| } |
| } |
| if (node[0] == BREAK && !node[1] && !breakCaptured) { |
| abort = true; |
| } |
| if (BREAK_CAPTURERS.has(node[0])) { |
| breakCaptured++; |
| } |
| if (CONTINUE_CAPTURERS.has(node[0])) { |
| continueCaptured++; |
| } |
| }, [&](Ref node) { |
| if (BREAK_CAPTURERS.has(node[0])) { |
| breakCaptured--; |
| } |
| if (CONTINUE_CAPTURERS.has(node[0])) { |
| continueCaptured--; |
| } |
| }); |
| if (abort) return; |
| safeCopy(node[2], node[2][2]); |
| } |
| }); |
| }); |
| } |
| |
| // Contrary to the name this does not eliminate actual dead functions, only |
| // those marked as such with DEAD_FUNCTIONS |
| void eliminateDeadFuncs(Ref ast) { |
| assert(!!extraInfo); |
| IString DEAD_FUNCTIONS("dead_functions"); |
| IString ABORT("abort"); |
| assert(extraInfo->has(DEAD_FUNCTIONS)); |
| StringSet deadFunctions; |
| for (size_t i = 0; i < extraInfo[DEAD_FUNCTIONS]->size(); i++) { |
| deadFunctions.insert(extraInfo[DEAD_FUNCTIONS][i]->getIString()); |
| } |
| traverseFunctions(ast, [&](Ref fun) { |
| if (!deadFunctions.has(fun[1].get()->getIString())) { |
| return; |
| } |
| AsmData asmData(fun); |
| fun[3]->setSize(1); |
| fun[3][0] = make1(STAT, make2(CALL, makeName(ABORT), &(makeArray(1))->push_back(makeNum(-1)))); |
| asmData.vars.clear(); |
| asmData.denormalize(); |
| }); |
| } |
| |