Skip to content

Commit

Permalink
Allow certain ES6 constructs in internal JS code (emscripten-core#15763)
Browse files Browse the repository at this point in the history
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: emscripten-core#11984
  • Loading branch information
sbc100 authored and mmarczell-graphisoft committed Jan 5, 2022
1 parent d89b22f commit 5fec714
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 12 deletions.
8 changes: 8 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------
Expand Down
31 changes: 24 additions & 7 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
5 changes: 5 additions & 0 deletions src/settings_internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion src/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
4 changes: 2 additions & 2 deletions tests/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'],
]:
Expand Down
66 changes: 66 additions & 0 deletions tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
16 changes: 14 additions & 2 deletions tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tools/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5fec714

Please sign in to comment.