From 5fec714b7bce9cf8155df415f6f290c0bd4f6034 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 17 Dec 2021 18:02:43 -0800 Subject: [PATCH] Allow certain ES6 constructs in internal JS code (#15763) With this change we allow these features by default. If a user explicitly opts into older browser support we trigger the running of closure compiler with `--language_out ES5 --compilation_level WHITESPACE_ONLY`. For most users this will be a code size win, but otherwise a no-op. For users who need older browser support they will now have their output run though closure by default. If they want to take a care of the transpilation process themselves rather than have emscripten to it auto-magically they can run with `--closure=0`. When we auto-magically run closure to do transpilation we generate a warning. This warning can be suppressed by add `--closure=1` to explicitly opt in, or `--closure=0` to explicitly opt out. This change in does not actually include any usage of these features and so don't include the code size benefits. Those will be part of followup PRs. Fixes: #11984 --- ChangeLog.md | 8 +++++ emcc.py | 31 ++++++++++++++----- src/settings_internal.js | 5 +++ src/shell.js | 2 +- tests/test_browser.py | 4 +-- tests/test_other.py | 66 ++++++++++++++++++++++++++++++++++++++++ tools/building.py | 16 ++++++++-- tools/shared.py | 1 + 8 files changed, 121 insertions(+), 12 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index cf60430b844b2..f6804726a9f9d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,14 @@ See docs/process.md for more on how version tagging works. 3.0.2 ----- +- Emscripten in starting to use ES6 features in its core libraries (at last!). + For most users targeting the default set of browsers this is a code size win. + For projects targeting older browsers (e.g. `-sMIN_CHROME_VERSION=10`), + emscripten will now run closure compiler in `WHITESPACE_ONLY` mode in order to + traspile any ES6 down to ES5. When this automatic transpilation is performed + we generate a warning which can disabled (using `-Wno-transpile`) or by + explicitly opting in-to or out-of closure using `--closure=1` or + `--closure=0`. (#15763). 3.0.1 - 12/17/2021 ------------------ diff --git a/emcc.py b/emcc.py index 4ffe4b105d632..d402507822f3a 100755 --- a/emcc.py +++ b/emcc.py @@ -1816,6 +1816,24 @@ def default_setting(name, new_default): setup_environment_settings() + if options.use_closure_compiler != 0: + # Emscripten requires certain ES6 constructs by default in library code + # - https://caniuse.com/let : EDGE:12 FF:44 CHROME:49 SAFARI:11 + # - https://caniuse.com/const : EDGE:12 FF:36 CHROME:49 SAFARI:11 + # - https://caniuse.com/arrow-functions: : EDGE:12 FF:22 CHROME:45 SAFARI:10 + # - https://caniuse.com/mdn-javascript_builtins_object_assign: + # EDGE:12 FF:34 CHROME:45 SAFARI:9 + # Taking the highest requirements gives is our minimum: + # Max Version: EDGE:12 FF:44 CHROME:49 SAFARI:11 + settings.TRANSPILE_TO_ES5 = (settings.MIN_EDGE_VERSION < 12 or + settings.MIN_FIREFOX_VERSION < 44 or + settings.MIN_CHROME_VERSION < 49 or + settings.MIN_SAFARI_VERSION < 110000 or + settings.MIN_IE_VERSION != 0x7FFFFFFF) + + if options.use_closure_compiler is None and settings.TRANSPILE_TO_ES5: + diagnostics.warning('transpile', 'enabling transpilation via closure due to browser version settings. This warning can be suppressed by passing `--closure=1` or `--closure=0` to opt into our explicitly.') + # Silently drop any individual backwards compatibility emulation flags that are known never to occur on browsers that support WebAssembly. if not settings.WASM2JS: settings.POLYFILL_OLD_MATH_FUNCTIONS = 0 @@ -3272,15 +3290,14 @@ def phase_binaryen(target, options, wasm_target): def preprocess_wasm2js_script(): return read_and_preprocess(utils.path_from_root('src/wasm2js.js'), expand_macros=True) - def run_closure_compiler(): - global final_js - final_js = building.closure_compiler(final_js, pretty=not minify_whitespace(), - extra_closure_args=options.closure_args) + if final_js and (options.use_closure_compiler or settings.TRANSPILE_TO_ES5): + if options.use_closure_compiler: + final_js = building.closure_compiler(final_js, pretty=not minify_whitespace(), + extra_closure_args=options.closure_args) + else: + final_js = building.closure_transpile(final_js, pretty=not minify_whitespace()) save_intermediate_with_wasm('closure', wasm_target) - if final_js and options.use_closure_compiler: - run_closure_compiler() - symbols_file = None if options.emit_symbol_map: symbols_file = shared.replace_or_append_suffix(target, '.symbols') diff --git a/src/settings_internal.js b/src/settings_internal.js index 178fd66f546de..15544a1643244 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -219,3 +219,8 @@ var HAS_MAIN = 0; // Set to true if we are linking as C++ and including C++ stdlibs var LINK_AS_CXX = 0; + +// Set when some minimum browser version triggers doesn't support the +// minimum set of ES6 featurs. This triggers transpilation to ES5 +// using closure compiler. +var TRANSPILE_TO_ES5 = 0; diff --git a/src/shell.js b/src/shell.js index 28924b7517d86..9b0a7a4882af7 100644 --- a/src/shell.js +++ b/src/shell.js @@ -46,7 +46,7 @@ var Module = typeof {{{ EXPORT_NAME }}} !== 'undefined' ? {{{ EXPORT_NAME }}} : // See https://caniuse.com/mdn-javascript_builtins_object_assign #if MIN_CHROME_VERSION < 45 || MIN_EDGE_VERSION < 12 || MIN_FIREFOX_VERSION < 34 || MIN_IE_VERSION != TARGET_NOT_SUPPORTED || MIN_SAFARI_VERSION < 90000 function objAssign(target, source) { - for (key in source) { + for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } diff --git a/tests/test_browser.py b/tests/test_browser.py index e87a796559c0a..99369d16c9f2a 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -2601,7 +2601,7 @@ def test_doublestart_bug(self): '': ([],), 'closure': (['-O2', '-g1', '--closure=1', '-s', 'HTML5_SUPPORT_DEFERRING_USER_SENSITIVE_REQUESTS=0'],), 'pthread': (['-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD'],), - 'legacy': (['-s', 'MIN_FIREFOX_VERSION=0', '-s', 'MIN_SAFARI_VERSION=0', '-s', 'MIN_IE_VERSION=0', '-s', 'MIN_EDGE_VERSION=0', '-s', 'MIN_CHROME_VERSION=0'],) + 'legacy': (['-s', 'MIN_FIREFOX_VERSION=0', '-s', 'MIN_SAFARI_VERSION=0', '-s', 'MIN_IE_VERSION=0', '-s', 'MIN_EDGE_VERSION=0', '-s', 'MIN_CHROME_VERSION=0', '-Wno-transpile'],) }) @requires_threads def test_html5_core(self, opts): @@ -2666,7 +2666,7 @@ def test_webgl_unmasked_vendor_webgl(self): @requires_graphics_hardware def test_webgl2(self): for opts in [ - ['-s', 'MIN_CHROME_VERSION=0'], + ['-s', 'MIN_CHROME_VERSION=0', '-Wno-transpile'], ['-O2', '-g1', '--closure=1', '-s', 'WORKAROUND_OLD_WEBGL_UNIFORM_UPLOAD_IGNORED_OFFSET_BUG'], ['-s', 'FULL_ES2=1'], ]: diff --git a/tests/test_other.py b/tests/test_other.py index f5a16d98b3507..249622d423ed2 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -11328,3 +11328,69 @@ def test_hello_function(self): # reference them in our docs. Should we move this file to somewhere else such # as `examples/`?) self.run_process([EMCC, test_file('hello_function.cpp'), '-o', 'function.html', '-sEXPORTED_FUNCTIONS=_int_sqrt', '-sEXPORTED_RUNTIME_METHODS=ccall,cwrap']) + + @parameterized({ + '': ([],), + 'O2': (['-O2'],), + }) + def test_es5_transpile(self, args): + self.emcc_args += args + + # Create a library file that uses the following ES6 features + # - let/const + # - arrow funcs + # - for..of + # - object.assign + create_file('es6_library.js', '''\ + mergeInto(LibraryManager.library, { + foo: function() { + // Object.assign + let + let obj = Object.assign({}, {prop:1}); + err('prop: ' + obj.prop); + + // arror funcs + const + const bar = () => 2; + err('bar: ' + bar()); + } + }); + ''') + create_file('test.c', 'extern void foo(); int main() { foo(); }') + self.emcc_args += ['--js-library', 'es6_library.js'] + self.uses_es6 = True + + def check_for_es6(filename, expect): + js = read_file(filename) + if expect: + self.assertContained(['() => 2', '()=>2'], js) + self.assertContained('const ', js) + self.assertContained('let ', js) + else: + self.assertNotContained('() => 2', js) + self.assertNotContained('()=>2', js) + self.assertNotContained('const ', js) + self.assertNotContained('let ', js) + + # Check that under normal circumstances none of these features get + # removed / transpiled. + print('base case') + self.do_runf('test.c', 'prop: 1\nbar: 2\n') + check_for_es6('test.js', True) + + # If we select and older browser than closure will kick in by default + # to traspile. + print('with old browser') + self.emcc_args.remove('-Werror') + self.set_setting('MIN_CHROME_VERSION', '10') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', output_basename='test2') + check_for_es6('test2.js', False) + + # If we add `--closure=0` that traspiler (closure) is not run at all + print('with old browser + --closure=0') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', emcc_args=['--closure=0'], output_basename='test3') + check_for_es6('test3.js', True) + + # If we use `--closure=1` closure will run in full optimization mode + # and also transpile to ES5 + print('with old browser + --closure=1') + self.do_runf('test.c', 'prop: 1\nbar: 2\n', emcc_args=['--closure=1'], output_basename='test4') + check_for_es6('test4.js', False) diff --git a/tools/building.py b/tools/building.py index bd1e6b919ff78..3d75a12f8dab8 100644 --- a/tools/building.py +++ b/tools/building.py @@ -732,6 +732,15 @@ def add_to_path(dirname): return closure_cmd, env +@ToolchainProfiler.profile_block('closure_transpile') +def closure_transpile(filename, pretty): + user_args = [] + closure_cmd, env = get_closure_compiler_and_env(user_args) + closure_cmd += ['--language_out', 'ES5'] + closure_cmd += ['--compilation_level', 'WHITESPACE_ONLY'] + return run_closure_cmd(closure_cmd, filename, env, pretty) + + @ToolchainProfiler.profile_block('closure_compiler') def closure_compiler(filename, pretty, advanced=True, extra_closure_args=None): user_args = [] @@ -794,7 +803,10 @@ def closure_compiler(filename, pretty, advanced=True, extra_closure_args=None): # Tell closure not to do any transpiling or inject any polyfills. # At some point we may want to look into using this as way to convert to ES5 but # babel is perhaps a better tool for that. - args += ['--language_out', 'NO_TRANSPILE'] + if settings.TRANSPILE_TO_ES5: + args += ['--language_out', 'ES5'] + else: + args += ['--language_out', 'NO_TRANSPILE'] # Tell closure never to inject the 'use strict' directive. args += ['--emit_use_strict=false'] @@ -834,7 +846,7 @@ def move_to_safe_7bit_ascii_filename(filename): if pretty: cmd += ['--formatting', 'PRETTY_PRINT'] - logger.debug(f'closure compiler: {shared.shlex_join(cmd)}') + shared.print_compiler_stage(cmd) # Closure compiler does not work if any of the input files contain characters outside the # 7-bit ASCII range. Therefore make sure the command line we pass does not contain any such diff --git a/tools/shared.py b/tools/shared.py index 043f0770abb33..f08c1ab8e792e 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -71,6 +71,7 @@ diagnostics.add_warning('map-unrecognized-libraries') diagnostics.add_warning('unused-command-line-argument', shared=True) diagnostics.add_warning('pthreads-mem-growth') +diagnostics.add_warning('transpile') # TODO(sbc): Investigate switching to shlex.quote