@ 261106.15
current
# Copyright 2022 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.

"""Utilities for mapping browser versions to webassembly features."""

import logging
from enum import IntEnum, auto

from . import diagnostics
from .settings import default_setting, settings, user_settings

logger = logging.getLogger('feature_matrix')

UNSUPPORTED = 0x7FFFFFFF

# Oldest support browser versions.
# Emscripten unconditionally requires support for:
# - DedicatedWorkerGlobalScope.name parameter for multithreading support, which
#   landed first in Chrome 70, Firefox 55 and Safari 12.2.

# N.b. when modifying these values, update comments in src/settings.js on
# MIN_x_VERSION fields to match accordingly.
OLDEST_SUPPORTED_CHROME = 85  # Released on 2020-08-25
OLDEST_SUPPORTED_FIREFOX = 79  # Released on 2020-07-28
OLDEST_SUPPORTED_SAFARI = 150000  # Released on 2021-09-20
# This is the oldest version of node that we do any testing with.
# Keep this in sync with the test-node-compat in .circleci/config.yml.
OLDEST_SUPPORTED_NODE = 180300


class Feature(IntEnum):
  MEMORY64 = auto()
  WORKER_ES6_MODULES = auto()
  OFFSCREENCANVAS_SUPPORT = auto()
  WASM_LEGACY_EXCEPTIONS = auto()
  WASM_EXCEPTIONS = auto()
  WEBGL2 = auto()
  WEBGPU = auto()
  GROWABLE_ARRAYBUFFERS = auto()
  EXTENDED_CONST = auto()


disable_override_features: set[Feature] = set()
enable_override_features: set[Feature] = set()

min_browser_versions = {
  # https://caniuse.com/wf-wasm-memory64
  Feature.MEMORY64: {
    'chrome': 133,
    'firefox': 134,
    'safari': UNSUPPORTED,
    'node': 240000,
  },
  # Emscripten itself does not use this feature but we use it in our browser
  # tests.
  # https://caniuse.com/webgl2
  Feature.WEBGL2: {
    'chrome': 56,
    'firefox': 51,
    'safari': 150000,
    'node': UNSUPPORTED,
  },
  # Emscripten itself does not use this feature but we use it in our browser
  # tests.
  # https://caniuse.com/webgpu
  Feature.WEBGPU: {
    'chrome': 113,
    'firefox': 141,
    'safari': 260000,
    'node': UNSUPPORTED,
  },
  # Emscripten itself does not use this feature but we use it in our browser
  # tests.
  # https://caniuse.com/mdn-api_worker_worker_ecmascript_modules: The ability to
  # call new Worker(url, { type: 'module' });
  Feature.WORKER_ES6_MODULES: {
    'chrome': 80,
    'firefox': 114,
    'safari': 150000,
    'node': 0, # This is a browser only feature, no requirements on Node.js
  },
  # OffscreenCanvas feature allows creating canvases that are not connected to
  # a visible DOM element, e.g. in a Worker.
  # https://caniuse.com/offscreencanvas
  Feature.OFFSCREENCANVAS_SUPPORT: {
    'chrome': 69,
    'firefox': 105,
    'safari': 170000,
    'node': 0, # This is a browser only feature, no requirements on Node.js
  },
  # Legacy Wasm exceptions was the first (now legacy) format for native
  # exception handling in WebAssembly.
  # https://caniuse.com/wf-wasm-exception-handling
  Feature.WASM_LEGACY_EXCEPTIONS: {
    'chrome': 95,
    'firefox': 100,
    'safari': 150200,
    'node': 170000,
  },
  # Wasm exceptions is a newer format for native exception handling in
  # WebAssembly.
  # https://caniuse.com/wf-wasm-exnref-exceptions
  Feature.WASM_EXCEPTIONS: {
    'chrome': 137,
    'firefox': 131,
    'safari': 180400,
    'node': 241500,
  },
  # Growable SharedArrayBuffers improves memory growth feature in multithreaded
  # builds by avoiding need to poll resizes to ArrayBuffer views in Workers.
  # https://caniuse.com/mdn-webassembly_api_memory_toresizablebuffer
  Feature.GROWABLE_ARRAYBUFFERS: {
    'chrome': 144,
    'firefox': 145,
    'safari': 260200,
    'node': 260000,
  },
  # https://caniuse.com/wasm-extended-const
  Feature.EXTENDED_CONST: {
    'chrome': 114,
    'firefox': 112,
    'safari': 170400,
    'node': 210000,
  },

# The following features we now support unconditionally, but keeping them around
# in code comments for future reference

#  Feature.NON_TRAPPING_FPTOINT: {
#    'chrome': 75,
#    'firefox': 64,
#    'safari': 150000,
#    'node': 130000,
#  },

#  Feature.BULK_MEMORY: {
#    'chrome': 75,
#    'firefox': 79,
#    'safari': 150000,
#    'node': 130000,
#  },

#  Feature.JS_BIGINT_INTEGRATION: {
#    'chrome': 67,
#    'firefox': 78,
#    'safari': 150000,
#    'node': 130000,
#  },

#  Feature.MUTABLE_GLOBALS: {
#    'chrome': 74,
#    'firefox': 61,
#    'safari': 130100,
#    'node': 120000,
#  },

#  Feature.SIGN_EXT: {
#    'chrome': 74,
#    'firefox': 62,
#    'safari': 140100,
#    'node': 120000,
#  },

#  Feature.THREADS: {
#    'chrome': 74,
#    'firefox': 79,
#    'safari': 140100,
#    'node': 160400,
#  },

#  Feature.PROMISE_ANY: {
#    'chrome': 85,
#    'firefox': 79,
#    'safari': 140000,
#    'node': 150000,
#  },

}

# Static assertion to check that we actually need each of the above feature flags
# Once the OLDEST_SUPPORTED_XX versions are high enough they can/should be removed.
for feature, reqs in min_browser_versions.items():
  always_present = (reqs['chrome'] <= OLDEST_SUPPORTED_CHROME and
                    reqs['firefox'] <= OLDEST_SUPPORTED_FIREFOX and
                    reqs['safari'] <= OLDEST_SUPPORTED_SAFARI and
                    reqs['node'] <= OLDEST_SUPPORTED_NODE)
  assert not always_present, f'{feature.name} is no longer needed'


def caniuse(feature):
  if feature in disable_override_features:
    return False
  if feature in enable_override_features:
    return True

  # Certain features are incompatible with certain settings.
  # TODO(sbc): Make this more generate, perhaps based on INCOMPATIBLE_SETTINGS
  if feature == Feature.GROWABLE_ARRAYBUFFERS and settings.WASM2JS:
    logger.debug(f'cannot use {feature.name} because WASM2JS is enabled')
    return False

  min_versions = min_browser_versions[feature]

  def report_missing(setting_name):
    setting_value = getattr(settings, setting_name)
    logger.debug(f'cannot use {feature.name} because {setting_name} is too old: {setting_value}')

  if settings.MIN_CHROME_VERSION < min_versions['chrome']:
    report_missing('MIN_CHROME_VERSION')
    return False
  if settings.MIN_FIREFOX_VERSION < min_versions['firefox']:
    report_missing('MIN_FIREFOX_VERSION')
    return False
  if settings.MIN_SAFARI_VERSION < min_versions['safari']:
    report_missing('MIN_SAFARI_VERSION')
    return False
  if 'node' in min_versions and settings.MIN_NODE_VERSION < min_versions['node']:
    report_missing('MIN_NODE_VERSION')
    return False
  return True


def enable_feature(feature, reason, override=False):
  """Update default settings for browser versions such that the given feature is available everywhere."""
  if override:
    enable_override_features.add(feature)
  for name, min_version in min_browser_versions[feature].items():
    name = f'MIN_{name.upper()}_VERSION'
    if settings[name] < min_version:
      if name in user_settings:
        # If the user explicitly chose an older version we issue a warning.
        diagnostics.warning(
            'compatibility',
            f'{name}={user_settings[name]} is not compatible with {reason} '
            f'({name}={min_version} or above required)')
      else:
        # If no conflict, bump the minimum version to accommodate the feature.
        logger.debug(f'Enabling {name}={min_version} to accommodate {reason}')
        setattr(settings, name, min_version)


def disable_feature(feature):
  """Allow the user to disable a feature that would otherwise be on by default."""
  disable_override_features.add(feature)


def apply_min_browser_versions():
  """Update minimum browser version defaults based on user settings.

  If a user requests a feature that we know is only supported in browsers
  from a specific version and above, we can assume that browser version.
  """
  if settings.MEMORY64 == 1:
    enable_feature(Feature.MEMORY64, 'MEMORY64')
  if settings.EXPORT_ES6 and settings.PTHREADS:
    enable_feature(Feature.WORKER_ES6_MODULES, 'EXPORT_ES6 with -pthread')
  if settings.EXPORT_ES6 and settings.WASM_WORKERS:
    enable_feature(Feature.WORKER_ES6_MODULES, 'EXPORT_ES6 with -sWASM_WORKERS')
  if settings.OFFSCREENCANVAS_SUPPORT:
    enable_feature(Feature.OFFSCREENCANVAS_SUPPORT, 'OFFSCREENCANVAS_SUPPORT')
  if settings.WASM_EXCEPTIONS or settings.SUPPORT_LONGJMP == 'wasm': # Wasm longjmp support will lean on Wasm (Legacy) EH
    if settings.WASM_LEGACY_EXCEPTIONS:
      enable_feature(Feature.WASM_LEGACY_EXCEPTIONS, 'Wasm Legacy exceptions (-fwasm-exceptions with -sWASM_LEGACY_EXCEPTIONS=1)')
    else:
      enable_feature(Feature.WASM_EXCEPTIONS, 'Wasm exceptions (-fwasm-exceptions with -sWASM_LEGACY_EXCEPTIONS=0)')
  if settings.GROWABLE_ARRAYBUFFERS == 2:
    enable_feature(Feature.GROWABLE_ARRAYBUFFERS, 'GrowableSharedArrayBuffer')


def auto_enable_features():
  """Enable settings based on usable features."""
  # TODO(sbc): Find make a generic way to expose the feature matrix to JS
  # compiler rather then adding them all ad-hoc as internal settings
  if caniuse(Feature.GROWABLE_ARRAYBUFFERS):
    default_setting('GROWABLE_ARRAYBUFFERS', 2)