diff --git a/emcc b/emcc index a78fa890131c2..fd4e4c75ff6cd 100755 --- a/emcc +++ b/emcc @@ -860,6 +860,11 @@ try: # Apply optimization level settings shared.Settings.apply_opt_level(opt_level, noisy=True) + fastcomp = os.environ.get('EMCC_FAST_COMPILER') != '0' + if fastcomp: + # Set ASM_JS default here so that we can override it from the command line. + shared.Settings.ASM_JS = 1 if opt_level > 0 else 2 + # Apply -s settings in newargs here (after optimization levels, so they can override them) for change in settings_changes: key, value = change.split('=') @@ -871,11 +876,9 @@ try: if key == 'EXPORTED_FUNCTIONS': shared.Settings.ORIGINAL_EXPORTED_FUNCTIONS = shared.Settings.EXPORTED_FUNCTIONS[:] # used for warnings in emscripten.py - fastcomp = os.environ.get('EMCC_FAST_COMPILER') != '0' - if fastcomp: - shared.Settings.ASM_JS = 1 if opt_level > 0 else 2 try: + assert shared.Settings.ASM_JS > 0, 'ASM_JS must be enabled in fastcomp' assert shared.Settings.UNALIGNED_MEMORY == 0, 'forced unaligned memory not supported in fastcomp' assert shared.Settings.CHECK_HEAP_ALIGN == 0, 'check heap align not supported in fastcomp - use SAFE_HEAP instead' assert shared.Settings.SAFE_DYNCALLS == 0, 'safe dyncalls not supported in fastcomp' @@ -932,7 +935,7 @@ try: js_opts = True logging.warning('enabling js opts to allow SAFE_HEAP to work properly') - if shared.Settings.ALLOW_MEMORY_GROWTH: + if shared.Settings.ALLOW_MEMORY_GROWTH and shared.Settings.ASM_JS != 2: logging.warning('Disabling asm.js validation for memory growth (memory can grow, but you lose some amount of speed)'); shared.Settings.ASM_JS = 2 diff --git a/emscripten-version.txt b/emscripten-version.txt index 362c239d66da5..b1a41036cde93 100644 --- a/emscripten-version.txt +++ b/emscripten-version.txt @@ -1,2 +1,2 @@ -1.27.2 +1.28.0 diff --git a/src/library.js b/src/library.js index 432b4880f002b..711cc7296a4a6 100644 --- a/src/library.js +++ b/src/library.js @@ -1878,7 +1878,7 @@ LibraryManager.library = { } unget(); } - if (buffer.length === 0) return 0; // Failure. + if (buffer.length === 0) return fields; // Stop here. if (suppressAssignment) continue; var text = buffer.join(''); diff --git a/src/library_glfw.js b/src/library_glfw.js index 7998022485c9b..25da42bab5bff 100644 --- a/src/library_glfw.js +++ b/src/library_glfw.js @@ -527,6 +527,11 @@ var LibraryGLFW = { }, getCursorPos: function(winid, x, y) { + setValue(x, Browser.mouseX, 'double'); + setValue(y, Browser.mouseY, 'double'); + }, + + getMousePos: function(winid, x, y) { setValue(x, Browser.mouseX, 'i32'); setValue(y, Browser.mouseY, 'i32'); }, @@ -1145,7 +1150,7 @@ var LibraryGLFW = { }, glfwGetMousePos: function(x, y) { - GLFW.getCursorPos(GLFW.active.id, x, y); + GLFW.getMousePos(GLFW.active.id, x, y); }, glfwSetMousePos: function(x, y) { @@ -1188,12 +1193,12 @@ var LibraryGLFW = { _sleep(time); }, - glfwEnable: function(token) { + glfwEnable: function(target) { target = GLFW.GLFW2ParamToGLFW3Param(target); GLFW.hints[target] = false; }, - glfwDisable: function(token) { + glfwDisable: function(target) { target = GLFW.GLFW2ParamToGLFW3Param(target); GLFW.hints[target] = true; }, diff --git a/src/library_html5.js b/src/library_html5.js index 57f37ae5aca60..b2409328f8ee9 100644 --- a/src/library_html5.js +++ b/src/library_html5.js @@ -207,6 +207,10 @@ var LibraryJSEvents = { JSEvents.registerOrRemoveHandler(eventHandler); }, + getBoundingClientRectOrZeros: function(target) { + return target.getBoundingClientRect ? target.getBoundingClientRect() : { left: 0, top: 0 }; + }, + // Copies mouse event data from the given JS mouse event 'e' to the specified Emscripten mouse event structure in the HEAP. // eventStruct: the structure to populate. // e: The JS mouse event to read data from. @@ -235,7 +239,7 @@ var LibraryJSEvents = { {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.canvasY, '0', 'i32') }}}; } if (target) { - var rect = (target === window) ? { left: 0, top: 0 } : target.getBoundingClientRect(); + var rect = JSEvents.getBoundingClientRectOrZeros(target); {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.targetX, 'e.clientX - rect.left', 'i32') }}}; {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.targetY, 'e.clientY - rect.top', 'i32') }}}; } else { // No specific target passed, return 0. @@ -810,7 +814,7 @@ var LibraryJSEvents = { {{{ makeSetValue('ptr', C_STRUCTS.EmscriptenTouchEvent.metaKey, 'e.metaKey', 'i32') }}}; ptr += {{{ C_STRUCTS.EmscriptenTouchEvent.touches }}}; // Advance to the start of the touch array. var canvasRect = Module['canvas'] ? Module['canvas'].getBoundingClientRect() : undefined; - var targetRect = (target === window) ? { left: 0, top: 0 } : target.getBoundingClientRect(); + var targetRect = JSEvents.getBoundingClientRectOrZeros(target); var numTouches = 0; for(var i in touches) { var t = touches[i]; diff --git a/src/library_sdl.js b/src/library_sdl.js index befa4e4b32c9d..839f7f1b2f6a4 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -491,7 +491,7 @@ var LibrarySDL = { SDL.updateRect(dstrect, dr); } } - var blitw, blitr; + var blitw, blith; if (scale) { blitw = dr.w; blith = dr.h; } else { diff --git a/system/include/emscripten/bind.h b/system/include/emscripten/bind.h index 1ec344927b106..7f9de9ab8404c 100644 --- a/system/include/emscripten/bind.h +++ b/system/include/emscripten/bind.h @@ -1379,7 +1379,7 @@ namespace emscripten { typedef std::vector<T> VecType; void (VecType::*push_back)(const T&) = &VecType::push_back; - void (VecType::*resize)(const size_t) = &VecType::resize; + void (VecType::*resize)(const size_t, const T&) = &VecType::resize; return class_<std::vector<T>>(name) .template constructor<>() .function("push_back", push_back) diff --git a/tests/embind/embind.test.js b/tests/embind/embind.test.js index 1b29c5ad79755..141f5bc747fd0 100644 --- a/tests/embind/embind.test.js +++ b/tests/embind/embind.test.js @@ -918,23 +918,23 @@ module({ vec.delete(); }); - test("resize appends the default value", function() { + test("resize appends the given value", function() { var vec = cm.emval_test_return_vector(); - vec.resize(5); + vec.resize(5, 42); assert.equal(5, vec.size()); assert.equal(10, vec.get(0)); assert.equal(20, vec.get(1)); assert.equal(30, vec.get(2)); - assert.equal(0, vec.get(3)); - assert.equal(0, vec.get(4)); + assert.equal(42, vec.get(3)); + assert.equal(42, vec.get(4)); vec.delete(); }); test("resize preserves content when shrinking", function() { var vec = cm.emval_test_return_vector(); - vec.resize(2); + vec.resize(2, 42); assert.equal(2, vec.size()); assert.equal(10, vec.get(0)); assert.equal(20, vec.get(1)); diff --git a/tests/float_tex.cpp b/tests/float_tex.cpp index c40ff78668dbb..698b058e88671 100644 --- a/tests/float_tex.cpp +++ b/tests/float_tex.cpp @@ -61,7 +61,13 @@ static void updateFloatTexture() { ++count; } glBindTexture(GL_TEXTURE_2D, nodeTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, nbNodes, 1, 0, GL_RGBA, GL_FLOAT, data); +#ifdef __EMSCRIPTEN__ // In GLES2 and WebGL1, we must use unsized texture internal formats. + const GLenum internalFormat = GL_RGBA; +#else + // In desktop GL, we can also use sized internal formats. + const GLenum internalFormat = GL_RGBA32F; +#endif + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, nbNodes, 1, 0, GL_RGBA, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glBindTexture(GL_TEXTURE_2D, NULL); @@ -77,7 +83,7 @@ static void glut_draw_callback(void) { glActiveTexture(GL_TEXTURE0); updateFloatTexture(); //we change the texture each time to create the effect (it is just for the test) glBindTexture(GL_TEXTURE_2D, nodeTexture); - glUniform1i(nodeSamplerLocation, GL_TEXTURE0); + glUniform1i(nodeSamplerLocation, 0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, indicesVBO); glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, NULL); @@ -117,9 +123,11 @@ static void gl_init(void) { /* Get the locations of the uniforms so we can access them */ nodeSamplerLocation = glGetUniformLocation(program, "nodeInfo"); glBindAttribLocation(program, 0, "indices"); +#ifndef __EMSCRIPTEN__ // GLES2 & WebGL do not have these, only pre 3.0 desktop GL and compatibility mode GL3.0+ GL do. //Enable glPoint size in shader, always enable in Open Gl ES 2. glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); glEnable(GL_POINT_SPRITE); +#endif } int main(int argc, char *argv[]) { glutInit(&argc, argv); diff --git a/tests/gl_subdata.cpp b/tests/gl_subdata.cpp index 5c6a992686f96..52508a4a3ec35 100644 --- a/tests/gl_subdata.cpp +++ b/tests/gl_subdata.cpp @@ -62,6 +62,13 @@ static void updateFloatTexture() { } glBindTexture(GL_TEXTURE_2D, nodeTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, nbNodes, 1, 0, GL_RGBA, GL_FLOAT, data); +#ifdef __EMSCRIPTEN__ // In GLES2 and WebGL1, we must use unsized texture internal formats. + const GLenum internalFormat = GL_RGBA; +#else + // In desktop GL, we can also use sized internal formats. + const GLenum internalFormat = GL_RGBA32F; +#endif + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, nbNodes, 1, 0, GL_RGBA, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glBindTexture(GL_TEXTURE_2D, NULL); @@ -77,7 +84,7 @@ static void glut_draw_callback(void) { glActiveTexture(GL_TEXTURE0); updateFloatTexture(); //we change the texture each time to create the effect (it is just for the test) glBindTexture(GL_TEXTURE_2D, nodeTexture); - glUniform1i(nodeSamplerLocation, GL_TEXTURE0); + glUniform1i(nodeSamplerLocation, 0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, indicesVBO); glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, NULL); @@ -122,9 +129,11 @@ static void gl_init(void) { /* Get the locations of the uniforms so we can access them */ nodeSamplerLocation = glGetUniformLocation(program, "nodeInfo"); glBindAttribLocation(program, 0, "indices"); +#ifndef __EMSCRIPTEN__ // GLES2 & WebGL do not have these, only pre 3.0 desktop GL and compatibility mode GL3.0+ GL do. //Enable glPoint size in shader, always enable in Open Gl ES 2. glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); glEnable(GL_POINT_SPRITE); +#endif } int main(int argc, char *argv[]) { glutInit(&argc, argv); diff --git a/tests/glfw.c b/tests/glfw.c index 0a7a358b28fe8..2a247603ae573 100644 --- a/tests/glfw.c +++ b/tests/glfw.c @@ -49,6 +49,8 @@ void Init() if (glfwInit() != GL_TRUE) Shut_Down(1); + glfwEnable(GLFW_KEY_REPEAT); // test for issue #3059 + int red_bits = glfwGetWindowParam(GLFW_RED_BITS); glfwOpenWindowHint(GLFW_RED_BITS, 8); assert(glfwGetWindowParam(GLFW_RED_BITS) == 8); diff --git a/tests/hello_world_gles_proxy.c b/tests/hello_world_gles_proxy.c index 1a8e5f61da278..7e977fed64d0b 100644 --- a/tests/hello_world_gles_proxy.c +++ b/tests/hello_world_gles_proxy.c @@ -40,6 +40,7 @@ #define _GNU_SOURCE +#include <assert.h> #include <math.h> #include <stdlib.h> #include <stdio.h> @@ -613,11 +614,19 @@ gears_idle(void) static double tRot0 = -1.0, tRate0 = -1.0; double dt, t = glutGet(GLUT_ELAPSED_TIME) / 1000.0; +#ifdef STATIC_GEARS + assert(t - tRot0 > 0); // Test that time does advance even though we are rendering a static image. +#endif + if (tRot0 < 0.0) tRot0 = t; dt = t - tRot0; tRot0 = t; +#ifdef STATIC_GEARS + dt = t = 0.0; // Render the gears in a static position. +#endif + /* advance rotation for next frame */ angle += 70.0 * dt; /* 70 degrees per second */ if (angle > 3600.0) diff --git a/tests/test_browser.py b/tests/test_browser.py index 51f257412a6a4..ddd4849ac6dbb 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -758,7 +758,7 @@ def test_sdl_canvas_proxy(self): self.btest('sdl_canvas_proxy.c', reference='sdl_canvas_proxy.png', args=['--proxy-to-worker', '--preload-file', 'data.txt'], manual_reference=True, post_build=self.post_manual_reftest) def test_glgears_proxy(self): - self.btest('hello_world_gles_proxy.c', reference='gears.png', args=['--proxy-to-worker', '-s', 'GL_TESTING=1'], manual_reference=True, post_build=self.post_manual_reftest) + self.btest('hello_world_gles_proxy.c', reference='gears.png', args=['--proxy-to-worker', '-s', 'GL_TESTING=1', '-DSTATIC_GEARS=1'], manual_reference=True, post_build=self.post_manual_reftest) # test noProxy option applied at runtime diff --git a/tests/test_core.py b/tests/test_core.py index 7e297b434094f..e49c6a737b10c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4347,6 +4347,35 @@ def test_fscanf(self): self.emcc_args += ['--embed-file', 'three_numbers.txt'] self.do_run(src, 'match = 3\nx = -1.0, y = 0.1, z = -0.1\n') + def test_fscanf_2(self): + if self.emcc_args is None: return self.skip('requires emcc') + + open('a.txt', 'w').write('''1/2/3 4/5/6 7/8/9 +''') + self.emcc_args += ['--embed-file', 'a.txt'] + self.do_run(r'''#include <cstdio> +#include <iostream> + +using namespace std; + +int +main( int argv, char ** argc ) { + cout << "fscanf test" << endl; + + FILE * file; + file = fopen("a.txt", "rb"); + int vertexIndex[4]; + int normalIndex[4]; + int uvIndex[4]; + + int matches = fscanf(file, "%d/%d/%d %d/%d/%d %d/%d/%d %d/%d/%d\n", &vertexIndex[0], &uvIndex[0], &normalIndex[0], &vertexIndex [1], &uvIndex[1], &normalIndex[1], &vertexIndex[2], &uvIndex[2], &normalIndex[2], &vertexIndex[3], &uvIndex[3], &normalIndex[3]); + + cout << matches << endl; + + return 0; +} +''', 'fscanf test\n9\n') + def test_fileno(self): if self.emcc_args is None: return self.skip('requires emcc') open(os.path.join(self.get_dir(), 'empty.txt'), 'w').write('') @@ -7232,7 +7261,7 @@ def setUp(self): return TT # Main test modes -default = make_run("default", compiler=CLANG, emcc_args=[]) +default = make_run("default", compiler=CLANG, emcc_args=["-s", "ASM_JS=2"]) asm1 = make_run("asm1", compiler=CLANG, emcc_args=["-O1"]) asm2 = make_run("asm2", compiler=CLANG, emcc_args=["-O2"]) asm3 = make_run("asm3", compiler=CLANG, emcc_args=["-O3"]) diff --git a/tests/test_other.py b/tests/test_other.py index 6b5807f05ee9e..e88ff62376f10 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -118,13 +118,13 @@ def test_emcc(self): (['-o', 'something.js', '-O2'], 2, None, 0, 1), (['-o', 'something.js', '-O2', '-g'], 2, None, 0, 0), (['-o', 'something.js', '-Os'], 2, None, 0, 1), - (['-o', 'something.js', '-O3', '-s', 'ASM_JS=0'], 3, None, 0, 1), + (['-o', 'something.js', '-O3'], 3, None, 0, 1), # and, test compiling to bitcode first (['-o', 'something.bc'], 0, [], 0, 0), (['-o', 'something.bc', '-O0'], 0, [], 0, 0), (['-o', 'something.bc', '-O1'], 1, ['-O1'], 0, 0), (['-o', 'something.bc', '-O2'], 2, ['-O2'], 0, 0), - (['-o', 'something.bc', '-O3'], 3, ['-O3', '-s', 'ASM_JS=0'], 0, 0), + (['-o', 'something.bc', '-O3'], 3, ['-O3'], 0, 0), (['-O1', '-o', 'something.bc'], 1, [], 0, 0), ]: print params, opt_level, bc_params, closure, has_malloc diff --git a/tests/test_sanity.py b/tests/test_sanity.py index 24b352be3778f..02c0954bed90d 100644 --- a/tests/test_sanity.py +++ b/tests/test_sanity.py @@ -149,12 +149,12 @@ def test_closure_compiler(self): f = open(CONFIG_FILE, 'a') f.write('CLOSURE_COMPILER = "/tmp/nowhere/nothingtoseehere/kjadsfkjwelkjsdfkqgas/nonexistent.txt"\n') f.close() - output = self.check_working([EMCC, '-O2', '-s', 'ASM_JS=0', '--closure', '1', 'tests/hello_world.cpp'], CLOSURE_FATAL) + output = self.check_working([EMCC, '-O2', '-s', '--closure', '1', 'tests/hello_world.cpp'], CLOSURE_FATAL) # With a working path, all is well restore() try_delete('a.out.js') - output = self.check_working([EMCC, '-O2', '-s', 'ASM_JS=0', '--closure', '1', 'tests/hello_world.cpp'], '') + output = self.check_working([EMCC, '-O2', '-s', '--closure', '1', 'tests/hello_world.cpp'], '') assert os.path.exists('a.out.js'), output def test_llvm(self): diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js index a7ba7e9f39fe7..b94c2beaa0340 100644 --- a/tools/js-optimizer.js +++ b/tools/js-optimizer.js @@ -2872,7 +2872,7 @@ function registerizeHarder(ast) { function createReg(forName) { // Create a new register of type suitable for the given variable name. var allRegs = allRegsByType[localVars[forName]]; - reg = nextReg++; + var reg = nextReg++; allRegs[reg] = regPrefixByType[localVars[forName]] + reg; return reg; } @@ -3027,7 +3027,7 @@ function registerizeHarder(ast) { // Look through value-preserving casts, like "x | 0" => "x" if (node[0] === 'binary' && node[1] === '|') { if (node[3][0] === 'num' && node[3][1] === 0) { - return lookThroughCasts(node[2]); + return lookThroughCasts(node[2]); } } return node; @@ -3202,7 +3202,7 @@ function registerizeHarder(ast) { joinJunction(jCondExit); joinJunction(jLoop); setJunction(jCondExit); - joinJunction(jExit) + joinJunction(jExit); } break; case 'for': @@ -3273,7 +3273,7 @@ function registerizeHarder(ast) { setJunction(jCheckExit); } joinJunction(jExit); - popActiveLabels() + popActiveLabels(); break; case 'return': if (node[1]) { @@ -3654,9 +3654,9 @@ function registerizeHarder(ast) { junctionVariables[name].conf[otherName] = 1; } for (var b in junc.outblocks) { - // It conflits with any output vars of successor blocks, + // It conflicts with any output vars of successor blocks, // if they're assigned before it goes dead in that block. - block = blocks[b]; + var block = blocks[b]; var jSucc = junctions[block.exit]; for (var otherName in jSucc.live) { if (junc.live[otherName]) continue; @@ -3755,7 +3755,7 @@ function registerizeHarder(ast) { // Mark the point at which each input reg becomes dead. // Variables alive before this point must not be assigned // to that register. - var inputVars = {} + var inputVars = {}; var inputDeadLoc = {}; var inputVarsByReg = {}; for (var name in jExit.live) { @@ -3865,7 +3865,7 @@ function registerizeHarder(ast) { // Assign registers to function params based on entry junction - var paramRegs = {} + var paramRegs = {}; if (fun[2]) { for (var i = 0; i < fun[2].length; i++) { var allRegs = allRegsByType[localVars[fun[2][i]]]; diff --git a/tools/optimizer/optimizer.cpp b/tools/optimizer/optimizer.cpp index b5f7ddfff71e0..beca2b08c4475 100644 --- a/tools/optimizer/optimizer.cpp +++ b/tools/optimizer/optimizer.cpp @@ -2,6 +2,7 @@ #include <cstdio> #include <cmath> #include <string> +#include <algorithm> #include "simple_ast.h" @@ -80,6 +81,18 @@ enum AsmType { ASM_NONE = 5 // number of types }; +AsmType intToAsmType(int type) { + switch (type) { + case 0: return ASM_INT; + case 1: return ASM_DOUBLE; + case 2: return ASM_FLOAT; + case 3: return ASM_FLOAT32X4; + case 4: return ASM_INT32X4; + case 5: return ASM_NONE; + default: assert(0); return ASM_NONE; + } +} + // forward decls struct AsmData; AsmType detectType(Ref node, AsmData *asmData=nullptr, bool inVarDef=false); @@ -765,6 +778,8 @@ StringSet ASSOCIATIVE_BINARIES("+ * | & ^"), CONDITION_CHECKERS("if do while switch"), SAFE_TO_DROP_COERCION("unary-prefix name num"); +StringSet BREAK_CAPTURERS("do while for switch"), + FUNCTIONS_THAT_ALWAYS_THROW("abort ___resumeException ___cxa_throw ___cxa_rethrow"); bool isFunctionTable(const char *name) { static const char *functionTable = "FUNCTION_TABLE"; @@ -1351,7 +1366,7 @@ void eliminate(Ref ast, bool memSafe=false) { 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[0] = TOPLEVEL; node[1] = []; } } else */ @@ -2189,6 +2204,32 @@ void optimizeFrounds(Ref ast) { } // 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_INT32X4: return "I4"; 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); + int size = strlen(str) + int(ceil(log10(num))) + 3; + 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); @@ -2222,23 +2263,9 @@ void registerize(Ref ast) { auto getNewRegName = [&](int num, IString name) { const char *str; AsmType type = asmData.getType(name); - switch (type) { - case ASM_INT: str = "i"; break; - case ASM_DOUBLE: str = "d"; break; - case ASM_FLOAT: str = "f"; break; - case ASM_FLOAT32X4: str = "F4"; break; - case ASM_INT32X4: str = "I4"; break; - case ASM_NONE: str = "Z"; break; - default: assert(0); // type doesn't have a name yet - } - int size = strlen(str) + int(ceil(log10(num))) + 3; - char *temp = (char*)malloc(size); - int written = sprintf(temp, "%s%d", str, num); - assert(written < size); - temp[written] = 0; - IString ret(temp); // likely interns a new string; leaks if not XXX FIXME - regTypes[ret] = type; + 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. @@ -2433,6 +2460,1086 @@ void registerize(Ref ast) { }); } +// 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) { + traverseFunctions(ast, [](Ref fun) { + + // 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; + + AsmData asmData(fun); + + // Utilities for allocating register variables. + // We need distinct register pools for each type of variable. + + typedef std::unordered_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::unordered_set<int> inblocks, outblocks; + StringSet live; + Junction(int id_) : id(id_) {} + }; + struct Node { + }; + struct Block { + int id, entry, exit; + StringSet 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; + 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=-1) { + // 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; + }; + + IString NULL_STR; + + auto pushActiveLabels = [&](int onContinue, int onBreak) { + // Push the target junctions for continuing/breaking a loop. + // This should be called before traversing into a loop. + LabelState& prevLabels = activeLabels.back(); + LabelState newLabels = prevLabels; + newLabels[NULL_STR] = ContinueBreak(onContinue, onBreak); + if (!!nextLoopLabel) { + newLabels[nextLoopLabel] = ContinueBreak(onContinue, onBreak); + nextLoopLabel = NULL_STR; + } + // An unlabelled CONTINUE should jump to innermost loop, + // ignoring any nested SWITCH statements. + if (onContinue < 0 && prevLabels.count(NULL_STR) > 0) { + newLabels[NULL_STR].co = prevLabels[NULL_STR].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 an existing junction is dead code. + if (type == RETURN) { + joinJunction(EXIT_JUNCTION, false); + } else { + 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); + 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); + 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]->getIString()); // XXX? + } + }; + + 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, makeBlock()); + return; + } + + // Traverse each node type according to its particular control-flow semantics. + // TODO: switchify this + if (type == DEFUN) { + int jEntry = markJunction(); + assert(jEntry == ENTRY_JUNCTION); + int jExit = addJunction(); + assert(jExit == EXIT_JUNCTION); + for (int 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(); + 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[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(); + 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(); + int jExit = addJunction(); + pushActiveLabels(jLoop, jExit); + buildFlowGraph(node[2]); + popActiveLabels(); + joinJunction(jLoop, false); + setJunction(jExit, false); + } else { + int jCond = markJunction(); + 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(); + int jExit = addJunction(); + pushActiveLabels(jLoop, jExit); + buildFlowGraph(node[2]); + popActiveLabels(); + joinJunction(jLoop, false); + setJunction(jExit, false); + } else { + int jLoop = markJunction(); + 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(); + int jExit = addJunction(); + pushActiveLabels(-1, jExit); + bool hasDefault = false; + for (int 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 (int 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, IString()); + } 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(), IString()); + } else if (type == BREAK || type == CONTINUE) { + markNonLocalJump(type->getIString(), node[1]->getIString()); + } 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 (int i = 0; i < node[1]->size(); i++) { + buildFlowGraph(node[1][i]); + } + } + } else if (type == STAT) { + buildFlowGraph(node[1]); + } else if (type == UNARY_PREFIX || type == UNARY_POSTFIX) { + 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 (int 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, IString()); + } + } + } 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); + + 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::unordered_map<IString, Block*> labelledBlocks; + typedef std::pair<Ref, Block*> Jump; + std::vector<Jump> labelledJumps; + + for (int 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 (int 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 (int i = 0; i < labelledJumps.size(); i++) { + auto labelVal = labelledJumps[i].first; + auto block = labelledJumps[i].second; + Block* targetBlock = labelledBlocks[labelVal->getIString()]; + 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); + } + } + + // 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. + StringSet live; + for (auto b : junc.outblocks) { + Block* block = blocks[b]; + StringSet& 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, makeBlock()); + 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; + }; + + std::unordered_set<int> jWorklistMap; + jWorklistMap.insert(EXIT_JUNCTION); + std::vector<int> jWorklist; + jWorklist.push_back(EXIT_JUNCTION); + std::unordered_set<int> bWorklistMap; + std::vector<int> bWorklist; + + // Be sure to visit every junction at least once. + // This avoids missing some vars because we disconnected them + // when processing the labelled jumps. + for (int i = junctions.size() - 1; i >= EXIT_JUNCTION; i--) { + jWorklistMap.insert(i); + jWorklist.push_back(i); + } + + while (jWorklist.size() > 0) { + // 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 (jWorklist.size() > 0) { + Junction& junc = junctions[jWorklist.back()]; + jWorklist.pop_back(); + jWorklistMap.erase(junc.id); + StringSet 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) { + if (bWorklistMap.count(b) == 0) { + bWorklistMap.insert(b); + bWorklist.push_back(b); + } + int jPred = blocks[b]->entry; + if (jWorklistMap.count(jPred) == 0) { + jWorklistMap.insert(jPred); + jWorklist.push_back(jPred); + } + } + } + } + // Now update the blocks based on the calculated live sets. + while (bWorklist.size() > 0) { + Block* block = blocks[bWorklist.back()]; + bWorklist.pop_back(); + bWorklistMap.erase(block->id); + auto oldUse = block->use; + analyzeBlock(block); + if (oldUse != block->use) { + // The use set changed, re-process the entry junction. + if (jWorklistMap.count(block->entry) == 0) { + jWorklistMap.insert(block->entry); + jWorklist.push_back(block->entry); + } + } + } + } + + // 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 { + StringSet conf, link; + std::unordered_set<int> excl; + int reg; + JuncVar() : reg(-1) {} + }; + std::unordered_map<IString, JuncVar> junctionVariables; + + auto initializeJunctionVariable = [&](IString name) { + junctionVariables[name] = JuncVar(); // XXX + }; + + for (int i = 0; i < junctions.size(); i++) { + Junction& junc = junctions[i]; + for (auto name : junc.live) { + if (junctionVariables.count(name) == 0) initializeJunctionVariable(name); + // It conflicts with all other names live at this junction. + for (auto otherName : junc.live) { + if (otherName == name) continue; + junctionVariables[name].conf.insert(otherName); + } + for (auto b : junc.outblocks) { + // It conflicts with any output vars of successor blocks, + // if they're assigned before it goes dead in that block. + Block* block = blocks[b]; + Junction& jSucc = junctions[block->exit]; + for (auto otherName : jSucc.live) { + if (junc.live.has(otherName)) continue; + if (block->lastKillLoc[otherName] < block->firstDeadLoc[name]) { + if (junctionVariables.count(otherName) == 0) initializeJunctionVariable(otherName); + junctionVariables[name].conf.insert(otherName); + junctionVariables[otherName].conf.insert(name); + } + } + // It links with any linkages in the outgoing blocks. + IString linkName = block->link[name]; + if (!!linkName && linkName != name) { + if (junctionVariables.count(linkName) == 0) initializeJunctionVariable(linkName); + junctionVariables[name].link.insert(linkName); + junctionVariables[linkName].link.insert(name); + } + } + } + } + + // 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. + + StringVec sortedJunctionVariables; + for (auto pair : junctionVariables) { + sortedJunctionVariables.push_back(pair.first); + } + std::sort(sortedJunctionVariables.begin(), sortedJunctionVariables.end(), [&](const IString name1, const IString name2) { + return junctionVariables[name2].conf.size() < junctionVariables[name1].conf.size(); //XXX + }); + + // 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 = junctionVariables[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 (auto confName : jv.conf) { + junctionVariables[confName].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 (int i = 0; i < sortedJunctionVariables.size(); i++) { + IString name = sortedJunctionVariables[i]; + // It may already be assigned due to linked-variable propagation. + if (!!junctionVariables[name].reg) { + continue; + } + // 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)); + } + + // 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 (int 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 = junctionVariables[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 = junctionVariables[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 = junctionVariables[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 (int 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) { + if (inputVars.has(name) && j <= block->firstDeadLoc[name]) { + // Assignment to an input variable, must use pre-assigned reg. + reg = junctionVariables[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 (block->firstKillLoc[name] <= inputDeadLoc[reg]) { + if (name != inputVarsByReg[reg]) { + continue; + } + } + // Found one! + assignedRegs[name] = reg; + freeRegs.erase(freeRegs.begin() + k); + break; + } + // If we didn't find a suitable register, create a new one. + if (!assignedRegs.has(name)) { + reg = createReg(name); + assignedRegs[name] = reg; + } + } + } + node[1]->setString(allRegs[reg]); + } else { + // A kill. This frees the assigned register. + assert(!!reg); //, '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 an "x=x" assignments, remove them. + for (int 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, makeBlock()); + } + } + } + } + + // Assign registers to function params based on entry junction + + StringSet paramRegs; + if (!!fun[2]) { + for (int i = 0; i < fun[2]->size(); i++) { + auto& allRegs = allRegsByType[asmData.getType(fun[2][i]->getIString())]; + fun[2][i]->setString(allRegs[junctionVariables[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 (int 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); + }); +} +// end registerizeHarder + // minified names generation StringSet RESERVED("do if in for new try var env let"); const char *VALID_MIN_INITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"; @@ -2738,6 +3845,7 @@ int main(int argc, char **argv) { else if (str == "optimizeFrounds") optimizeFrounds(doc); else if (str == "simplifyIfs") simplifyIfs(doc); else if (str == "registerize") registerize(doc); + //else if (str == "registerizeHarder") registerizeHarder(doc); else if (str == "minifyLocals") minifyLocals(doc); else if (str == "minifyWhitespace") {} else if (str == "asmLastOpts") asmLastOpts(doc); diff --git a/tools/optimizer/parser.cpp b/tools/optimizer/parser.cpp index 4a642a7103b5b..a650f4bd7bb1e 100644 --- a/tools/optimizer/parser.cpp +++ b/tools/optimizer/parser.cpp @@ -82,6 +82,7 @@ IString TOPLEVEL("toplevel"), NEW("new"), ARRAY("array"), OBJECT("object"), + THROW("throw"), SET("="); IStringSet keywords("var function if else do while for break continue return switch case default throw try catch finally true false null new"), @@ -115,7 +116,7 @@ struct Init { precedences.resize(OperatorClass::Tertiary + 1); - for (int prec = 0; prec < operatorClasses.size(); prec++) { + for (size_t prec = 0; prec < operatorClasses.size(); prec++) { for (auto curr : operatorClasses[prec].ops) { precedences[operatorClasses[prec].type][curr] = prec; } diff --git a/tools/optimizer/parser.h b/tools/optimizer/parser.h index 1e822e908d6c4..16fc92df54e16 100644 --- a/tools/optimizer/parser.h +++ b/tools/optimizer/parser.h @@ -91,6 +91,7 @@ extern IString TOPLEVEL, NEW, ARRAY, OBJECT, + THROW, SET; extern IStringSet keywords, allOperators; @@ -203,7 +204,7 @@ class Parser { type = NUMBER; } else if (hasChar(OPERATOR_INITS, *src)) { switch (*src) { - case '!': str = src[1] == '=' ? str = NE : str = L_NOT; break; + case '!': str = src[1] == '=' ? NE : L_NOT; break; case '%': str = MOD; break; case '&': str = AND; break; case '*': str = MUL; break; diff --git a/tools/test-js-optimizer-asm-regs-harder-output.js b/tools/test-js-optimizer-asm-regs-harder-output.js index c3b326f61feef..25e4e2db085df 100644 --- a/tools/test-js-optimizer-asm-regs-harder-output.js +++ b/tools/test-js-optimizer-asm-regs-harder-output.js @@ -116,7 +116,7 @@ function linkedVars() { while (1) { i2 = 9; i1 = 5; - while (i2 > 0 || i1 > 0) { + while (i2 > 0 | i1 > 0) { if (i2 < i1) { i2 = i2 - 1; } else { diff --git a/tools/test-js-optimizer-asm-regs-harder.js b/tools/test-js-optimizer-asm-regs-harder.js index fa72aab8135a2..faa2845920d62 100644 --- a/tools/test-js-optimizer-asm-regs-harder.js +++ b/tools/test-js-optimizer-asm-regs-harder.js @@ -108,7 +108,7 @@ function iffey() { } function labelledJump(x) { x = x | 0; - var label = 0 + var label = 0; // y and z don't conflict, but only if you know about labelled jumps. var y = 0, z = 0; y = 2; @@ -129,7 +129,7 @@ function linkedVars() { while (1) { outer1 = 9; outer2 = 5; - while (outer1 > 0 || outer2 > 0) { + while ((outer1 > 0) | (outer2 > 0)) { // All these copy assignment should be eliminated by var sharing. inner1_0 = outer1; inner2_0 = outer2;