diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e315e3ff1a2..00000000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -ignore = - ; line too long - E501, - ; space after comma (ignored for list in gen-s-parser.py) - E241, - ; line break after binary operator - W504 -exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit,./_deps,./build,./emcc-build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05e9b5be3c4..c3513897b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,6 @@ jobs: sudo chmod +x llvm.sh sudo ./llvm.sh ${LLVM_VERSION} sudo apt-get install clang-format clang-format-${LLVM_VERSION} clang-tidy-${LLVM_VERSION} - - run: flake8 - run: ruff check - run: ./scripts/clang-format-diff.sh - name: clang-tidy @@ -53,7 +52,10 @@ jobs: strategy: matrix: # Test on the oldest support Ubuntu version in addition to `latest`. - os: [ubuntu-22.04, ubuntu-latest, macos-latest, windows-latest, windows-11-arm] + # Build using the oldest support macOS version. For emsdk this is + # currently macos-13, but unfortunately that doesn't exist in github + # actions so we settle for macos-14. + os: [ubuntu-22.04, ubuntu-latest, macos-14, windows-latest, windows-11-arm] steps: - uses: actions/setup-python@v5 with: @@ -75,7 +77,7 @@ jobs: - name: install ninja (macos) run: brew install ninja - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: install ninja (win) run: choco install ninja @@ -95,7 +97,7 @@ jobs: - name: cmake (macos) run: cmake -S . -B out -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out/install '-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64' - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: cmake (win) # -G "Visual Studio 15 2017" @@ -221,7 +223,7 @@ jobs: # Run tests on Alpine Linux, which we use to make our release builds. # Note: Alpine uses musl libc. - # Keep in sync with build_release.yml. The only difference is that here we + # Keep in sync with create_release.yml. The only difference is that here we # do not have the "archive" and "upload tarball" jobs. build-alpine: name: alpine @@ -259,8 +261,11 @@ jobs: run: ./alpine.sh pip3 install --break-system-packages -r requirements-dev.txt - name: cmake + # Build with an 8MB stack size, as otherwise Alpine/musl's default stack + # size for pthreads is tiny, + # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | @@ -357,23 +362,20 @@ jobs: - name: install ninja run: sudo apt-get install ninja-build - name: emsdk install - run: | - mkdir $HOME/emsdk - git clone --depth 1 https://github.com/emscripten-core/emsdk.git $HOME/emsdk - $HOME/emsdk/emsdk update-tags - $HOME/emsdk/emsdk install tot - $HOME/emsdk/emsdk activate tot + uses: emscripten-core/setup-emsdk@v15 + with: + version: tot - name: override emscripten repository if: ${{ env.EMSCRIPTEN_REPO != '' }} run: | - $HOME/emsdk/emsdk install emscripten-main-64bit \ + $EMSDK/emsdk install emscripten-main-64bit \ --override-repository emscripten-main-64bit@$EMSCRIPTEN_REPO - $HOME/emsdk/emsdk activate emscripten-main-64bit + $EMSDK/emsdk activate emscripten-main-64bit - name: update path - run: echo "PATH=$PATH:$HOME/emsdk" >> $GITHUB_ENV + run: echo "PATH=$PATH:$EMSDK" >> $GITHUB_ENV - name: emcc-tests run: | - source $HOME/emsdk/emsdk_env.sh + source $EMSDK/emsdk_env.sh ./scripts/emcc-tests.sh # Windows + gcc needs work before the tests will run, so just test the compile diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index ed8bb2fb6c4..723257fc47d 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-latest, windows-latest, windows-11-arm] + os: [macos-14, windows-latest, windows-11-arm] defaults: run: shell: bash @@ -27,7 +27,7 @@ jobs: - name: install ninja (macos) run: brew install ninja - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: install ninja (win) run: choco install ninja @@ -40,7 +40,7 @@ jobs: run: | cmake -S . -B out -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out/install -DCMAKE_OSX_ARCHITECTURES=x86_64 cmake -S . -B out-arm64 -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out-arm64/install -DCMAKE_OSX_ARCHITECTURES=arm64 - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: cmake (win) # -G "Visual Studio 15 2017" @@ -57,7 +57,7 @@ jobs: - name: build-arm64 run: cmake --build out-arm64 -v --config Release --target install - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: strip run: find out*/install/ -type f -perm -u=x -exec strip -x {} + @@ -83,7 +83,7 @@ jobs: - name: archive-arm64 id: archive-arm64 run: | - OSNAME=$(echo ${{ matrix.os }} | sed 's/-latest//' | sed 's/-11-arm//') + OSNAME=$(echo ${{ matrix.os }} | sed 's/-11-arm//' | sed 's/-14//') VERSION=$GITHUB_REF_NAME PKGNAME="binaryen-$VERSION-arm64-$OSNAME" TARBALL=$PKGNAME.tar.gz @@ -95,7 +95,7 @@ jobs: cmake -E sha256sum $TARBALL > $SHASUM echo "TARBALL=$TARBALL" >> $GITHUB_OUTPUT echo "SHASUM=$SHASUM" >> $GITHUB_OUTPUT - if: ${{ matrix.os == 'macos-latest' || matrix.os == 'windows-11-arm' }} + if: ${{ matrix.os == 'macos-14' || matrix.os == 'windows-11-arm' }} - name: upload tarball uses: softprops/action-gh-release@v1 @@ -108,6 +108,7 @@ jobs: ${{ steps.archive-arm64.outputs.SHASUM }} # Build with gcc 6.3 and run tests on Alpine Linux (inside chroot). + # Keep in sync with ci.yml. # Note: Alpine uses musl libc. build-alpine: name: alpine @@ -145,8 +146,11 @@ jobs: run: ./alpine.sh pip3 install --break-system-packages -r requirements-dev.txt - name: cmake + # Build with an 8MB stack size, as otherwise Alpine/musl's default stack + # size for pthreads is tiny, + # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | diff --git a/.gitignore b/.gitignore index 95af1a39719..b88109b38e0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ CMakeUserPresets.json # files related to clangd cache .cache/* + +.venv/ diff --git a/.ruff.toml b/.ruff.toml index e6fd850fd43..e48daa90671 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,5 @@ target-version = "py310" +preview = true exclude = [ 'third_party', @@ -6,11 +7,19 @@ exclude = [ 'test/spec/testsuite', ] +[lint.pylint] +max-locals = 30 +max-positional-args = 7 + +[lint.per-file-ignores] +"test/**.py" = ["PLR6301", "PLW1514", "PLR0914"] + [lint] select = [ "ARG", "ASYNC", "B", + "D", "C4", "C90", "COM", @@ -27,17 +36,29 @@ select = [ ignore = [ "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ + "D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/ + "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ + "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ + "D103", # https://docs.astral.sh/ruff/rules/undocumented-public-function/ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ + "D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/ + "D203", # https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class/ + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ "B006", # https://docs.astral.sh/ruff/rules/mutable-argument-default/ "B011", # https://docs.astral.sh/ruff/rules/assert-false/ "B023", # https://docs.astral.sh/ruff/rules/function-uses-loop-variable/ "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ "E741", # https://docs.astral.sh/ruff/rules/ambiguous-variable-name/ "PERF401", # https://docs.astral.sh/ruff/rules/manual-list-comprehension/ + "PLR0904", # https://docs.astral.sh/ruff/rules/too-many-public-methods/ "PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/ "PLR0913", # https://docs.astral.sh/ruff/rules/too-many-arguments/ "PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/ + "PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/ "PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/ "PLW0603", # https://docs.astral.sh/ruff/rules/global-statement/ "PLW1510", # https://docs.astral.sh/ruff/rules/subprocess-run-without-check/ "PLW2901", # https://docs.astral.sh/ruff/rules/redefined-loop-name/ + "PLW1514", # https://docs.astral.sh/ruff/rules/unspecified-encoding/ ] diff --git a/CHANGELOG.md b/CHANGELOG.md index f39a79773e4..12a2e838529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,32 @@ full changeset diff at the end of each section. Current Trunk ------------- +v130 +---- + + - MarkJSCalled pass, to help configureAll users. (#8733) + - RemoveExports pass, to allow easy export removal (e.g. after merge) (#8670) + - Wide Arithmetic support (#8544) + - New fuzzer mode: PreserveImportsExportsJS (#8592) + - New fuzzer mode: Fuzz against JavaScript (#8655) + - Increase Alpine stack size to 8MB for release builds (#8595) + - Rename relaxed SIMD instructions to prepend the `relaxed_` prefix. (#8673) + - Rename C and JS API operations to prepend the `Relaxed` prefix: + - `LaneselectI8x16` to `RelaxedLaneselectI8x16` + - `LaneselectI16x8` to `RelaxedLaneselectI16x8` + - `LaneselectI32x4` to `RelaxedLaneselectI32x4` + - `LaneselectI64x2` to `RelaxedLaneselectI64x2` + - `DotI8x16I7x16AddSToVecI32x4` to `RelaxedDotI8x16I7x16AddSToVecI32x4` + - `DotI8x16I7x16SToVecI16x8` to `RelaxedDotI8x16I7x16SToVecI16x8` + - [JS & C API] Rename MemorySegment functions to DataSegment (#8576) + - Rename `BinaryenGetNumMemorySegments` to `BinaryenGetNumDataSegments` in c api. + - Rename `BinaryenGetMemorySegmentByteOffset` to `BinaryenGetDataSegmentByteOffset` in c api. + - Rename `BinaryenGetMemorySegmentByteLength` to `BinaryenGetDataSegmentByteLength` in c api. + - Rename `BinaryenGetMemorySegmentPassive` to `BinaryenGetDataSegmentPassive` in c api. + - Rename `BinaryenCopyMemorySegmentData` to `BinaryenCopyDataSegmentData` in c api. + - Rename `module.getNumMemorySegments` to `module.getNumDataSegments` in js api. + - Rename `module.getMemorySegmentInfo` to `module.getDataSegmentInfo` in js api. + v129 ---- diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cf32bc6481..fb4bb241a77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.16.3) # Needed for C++17 (std::path) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") -project(binaryen LANGUAGES C CXX VERSION 129) +project(binaryen LANGUAGES C CXX VERSION 130) include(GNUInstallDirs) # The C++ standard whose features are required to build Binaryen. @@ -52,10 +52,11 @@ if(EMSCRIPTEN) set(BUILD_LLVM_DWARF OFF) endif() -option(BUILD_STATIC_LIB "Build as a static library" OFF) -if(MSVC) +option(BUILD_SHARED_LIBS "Build using shared libraries" ON) +if(MSVC OR EMSCRIPTEN) # We don't have dllexport declarations set up for Windows yet. - set(BUILD_STATIC_LIB ON) + # With emscripten we require a static library to create binaryen_js correctly. + set(BUILD_SHARED_LIBS OFF) endif() # Advised to turn on when statically linking against musl libc (e.g., in the @@ -418,6 +419,7 @@ else() # MSVC add_compile_flag("-Wswitch") # we explicitly expect this in the code add_compile_flag("-Wimplicit-fallthrough") add_compile_flag("-Wnon-virtual-dtor") + add_compile_flag("-Wsuggest-override") if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") # Google style requires this, so make sure we compile cleanly with it. @@ -450,18 +452,18 @@ else() # MSVC endif() # Declare libbinaryen +# This will be either be STATIC or SHARED depending on BUILD_SHARED_LIBS +add_library(binaryen) -if(BUILD_STATIC_LIB) - message(STATUS "Building libbinaryen as statically linked library.") - add_library(binaryen STATIC) - add_definitions(-DBUILD_STATIC_LIBRARY) -else() +if(BUILD_SHARED_LIBS) + add_definitions(-DBUILD_SHARED_LIBS) message(STATUS "Building libbinaryen as shared library.") - add_library(binaryen SHARED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Disable interposition and resolve Binaryen symbols locally. add_link_flag("-Bsymbolic") endif() +else() + message(STATUS "Building libbinaryen as statically linked library.") endif() target_link_libraries(binaryen PUBLIC Threads::Threads) binaryen_setup_rpath(binaryen) @@ -474,13 +476,13 @@ if(BUILD_MIMALLOC) message(FATAL_ERROR "Linking mimalloc is only supported on Linux.") endif() message(STATUS "Building with mimalloc allocator.") - if(BUILD_STATIC_LIB) + if(BUILD_SHARED_LIBS) + target_link_options(mimalloc PRIVATE "-Wl,--as-needed") + target_link_libraries(binaryen PRIVATE mimalloc) + else() target_link_libraries(binaryen PRIVATE "-Wl,--push-state,--as-needed") target_link_libraries(binaryen PRIVATE mimalloc-static) target_link_libraries(binaryen PRIVATE "-Wl,--pop-state") - else() - target_link_options(mimalloc PRIVATE "-Wl,--as-needed") - target_link_libraries(binaryen PRIVATE mimalloc) endif() endif() @@ -517,7 +519,7 @@ set(binaryen_SOURCES ) target_sources(binaryen PRIVATE ${binaryen_SOURCES}) -if(INSTALL_LIBS OR NOT BUILD_STATIC_LIB) +if(INSTALL_LIBS OR BUILD_SHARED_LIBS) install(TARGETS binaryen RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -550,8 +552,8 @@ if(EMSCRIPTEN) if(JS_OF_OCAML) # js_of_ocaml needs a specified variable with special comment to provide the library to consumer target_link_libraries(binaryen_js PRIVATE "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js") - # Currently, js_of_ocaml can only process ES5 code - target_link_libraries(binaryen_js PRIVATE optimized "--closure-args=\"--language_out=ECMASCRIPT5\"") + # js_of_ocaml does not support top level await + target_link_libraries(binaryen_js PRIVATE "-sWASM_ASYNC_COMPILATION=0") else() target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6") endif() diff --git a/Contributing.md b/Contributing.md index 98f9c7bedce..2d8a5db109c 100644 --- a/Contributing.md +++ b/Contributing.md @@ -30,3 +30,7 @@ Use this handy checklist to make sure your new instructions are fully supported: - [ ] JS API tested in test/binaryen.js/kitchen-sink.js - [ ] Tests added in test/spec - [ ] Tests added in test/lit + - [ ] Tests are used as seeds for the fuzzer. If V8 doesn't support the + test, or if the new instruction isn't guarded by a wasm-validator check, + then either add the corresponding feature to DISALLOWED_FEATURES_IN_V8 or + mark the test as unfuzzable. diff --git a/check.py b/check.py index 0618c59c5e6..d1e1f49103a 100755 --- a/check.py +++ b/check.py @@ -25,7 +25,8 @@ from multiprocessing.pool import ThreadPool from pathlib import Path -from scripts.test import binaryenjs, lld, shared, support, wasm2js, wasm_opt +from scripts.test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt +from scripts.test.shared import print_heading assert sys.version_info >= (3, 10), 'requires Python 3.10' @@ -41,7 +42,7 @@ def get_changelog_version(): def run_version_tests(): - print('[ checking --version ... ]\n') + print_heading('checking --version ...') not_executable_suffix = ['.DS_Store', '.txt', '.js', '.ilk', '.pdb', '.dll', '.wasm', '.manifest'] executable_prefix = ['wasm'] @@ -67,7 +68,7 @@ def run_version_tests(): def run_wasm_dis_tests(): - print('\n[ checking wasm-dis on provided binaries... ]\n') + print_heading('checking wasm-dis on provided binaries...') for t in shared.get_tests(shared.options.binaryen_test, ['.wasm']): print('..', os.path.basename(t)) @@ -85,7 +86,7 @@ def run_wasm_dis_tests(): def run_crash_tests(): - print("\n[ checking we don't crash on tricky inputs... ]\n") + print_heading("checking we don't crash on tricky inputs...") for t in shared.get_tests(shared.get_test_dir('crash'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -95,7 +96,7 @@ def run_crash_tests(): def run_dylink_tests(): - print("\n[ we emit dylink sections properly... ]\n") + print_heading('we emit dylink sections properly...') dylink_tests = glob.glob(os.path.join(shared.options.binaryen_test, 'dylib*.wasm')) for t in sorted(dylink_tests): @@ -109,7 +110,7 @@ def run_dylink_tests(): def run_ctor_eval_tests(): - print('\n[ checking wasm-ctor-eval... ]\n') + print_heading('checking wasm-ctor-eval...') for t in shared.get_tests(shared.get_test_dir('ctor-eval'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -126,7 +127,7 @@ def run_ctor_eval_tests(): def run_wasm_metadce_tests(): - print('\n[ checking wasm-metadce ]\n') + print_heading('checking wasm-metadce') for t in shared.get_tests(shared.get_test_dir('metadce'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -141,10 +142,10 @@ def run_wasm_metadce_tests(): def run_wasm_reduce_tests(): if not shared.has_shell_timeout(): - print('\n[ skipping wasm-reduce testcases]\n') + print_heading('skipping wasm-reduce testcases') return - print('\n[ checking wasm-reduce testcases]\n') + print_heading('checking wasm-reduce testcases') # fixed testcases for t in shared.get_tests(shared.get_test_dir('reduce'), ['.wast']): @@ -161,7 +162,7 @@ def run_wasm_reduce_tests(): # run on a nontrivial fuzz testcase, for general coverage # this is very slow in ThreadSanitizer, so avoid it there if 'fsanitize=thread' not in str(os.environ): - print('\n[ checking wasm-reduce fuzz testcase ]\n') + print_heading('checking wasm-reduce fuzz testcase') # TODO: re-enable multivalue once it is better optimized support.run_command(shared.WASM_OPT + [os.path.join(shared.options.binaryen_test, 'lit/basic/signext.wast'), '-ttf', '-Os', '-o', 'a.wasm', '--detect-features', '--disable-multivalue']) before = os.stat('a.wasm').st_size @@ -191,7 +192,7 @@ def run_opt_test(wast, stdout=None): def check_expected(actual, expected, stdout=None): if expected and os.path.exists(expected): expected = open(expected).read() - print(' (using expected output)', file=stdout) + shared.verbose_log(' (using expected output)', file=stdout) actual = actual.strip() expected = expected.strip() if actual != expected: @@ -228,7 +229,7 @@ def run_one_spec_test(wast: Path, stdout=None): actual = run_spec_test(str(wast), stdout=stdout) except Exception as e: if ('wasm-validator error' in str(e) or 'error: ' in str(e)) and '.fail.' in test_name: - print('<< test failed as expected >>', file=stdout) + shared.verbose_log('<< test failed as expected >>', file=stdout) return # don't try all the binary format stuff TODO else: shared.fail_with_error(str(e)) @@ -248,7 +249,7 @@ def run_one_spec_test(wast: Path, stdout=None): if not module: # Skip any initial assertions that don't have a module continue - print(f' testing split module {i}', file=stdout) + shared.verbose_log(f' testing split module {i}', file=stdout) split_name = base_name + f'_split{i}.wast' support.write_wast(split_name, module) run_opt_test(split_name, stdout=stdout) # also that our optimizer doesn't break on it @@ -265,7 +266,9 @@ def run_one_spec_test(wast: Path, stdout=None): def run_spec_test_with_wrapped_stdout(wast: Path): - """Return (bool, str) where the first element is whether the test was + """Run a single spec test while capturing stdout. + + Return (bool, str) where the first element is whether the test was successful and the second is the combined stdout and stderr of the test. """ out = io.StringIO() @@ -294,7 +297,7 @@ def red_stderr(): def run_spec_tests(): - print('\n[ checking wasm-shell spec testcases... ]\n') + print_heading('checking wasm-shell spec testcases...') worker_count = os.cpu_count() print("Running with", worker_count, "workers") @@ -326,7 +329,7 @@ def run_spec_tests(): def run_validator_tests(): - print('\n[ running validation tests... ]\n') + print_heading('running validation tests...') # Ensure the tests validate by default cmd = shared.WASM_AS + [os.path.join(shared.get_test_dir('validator'), 'invalid_export.wast'), '-o', 'a.wasm'] support.run_command(cmd) @@ -343,7 +346,7 @@ def run_validator_tests(): def run_example_tests(): - print('\n[ checking native example testcases...]\n') + print_heading('checking native example testcases...') if not shared.NATIVECC or not shared.NATIVEXX: shared.fail_with_error('Native compiler (e.g. gcc/g++) was not found in PATH!') return @@ -385,7 +388,7 @@ def run_example_tests(): def run_unittest(): - print('\n[ checking unit tests...]\n') + print_heading('checking unit tests...') # equivalent to `python -m unittest discover -s ./test -v` suite = unittest.defaultTestLoader.discover(os.path.dirname(shared.options.binaryen_test)) @@ -436,7 +439,7 @@ def wrapper(*args, **kwargs): 'wasm-metadce': run_wasm_metadce_tests, 'wasm-reduce': run_wasm_reduce_tests, 'spec': run_spec_tests, - 'lld': lld.test_wasm_emscripten_finalize, + 'finalize': finalize.test_wasm_emscripten_finalize, 'wasm2js': wasm2js.test_wasm2js, 'validator': run_validator_tests, 'example': run_example_tests, @@ -469,16 +472,17 @@ def main(): for test in shared.requested: TEST_SUITES[test]() + print() # Check/display the results if shared.num_failures == 0: - print('\n[ success! ]') + print_heading('success!') if shared.warnings: print('\n' + '\n'.join(shared.warnings)) if shared.num_failures > 0: - print('\n[ ' + str(shared.num_failures) + ' failures! ]') + print_heading(f'{shared.num_failures} failures!') return 1 return 0 diff --git a/requirements-dev.txt b/requirements-dev.txt index 48eeb74ef5c..d64712fe2e8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,8 @@ # These requirements are only needed for developers who want to run the test -# suite or flake8, not for end users. +# suite, or CI checks, not for end users. # Install with `pip3 install -r requirements-dev.txt` -flake8==7.3.0 ruff==0.14.1 filecheck==0.0.22 lit==0.11.0.post1 diff --git a/scripts/auto_update_tests.py b/scripts/auto_update_tests.py index f6ce43cd41f..43f318055be 100755 --- a/scripts/auto_update_tests.py +++ b/scripts/auto_update_tests.py @@ -18,11 +18,13 @@ import subprocess import sys -from test import binaryenjs, lld, shared, support, wasm2js, wasm_opt +from test.shared import print_heading + +from test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt def update_example_tests(): - print('\n[ checking example testcases... ]\n') + print_heading('checking example testcases...') for src in shared.get_tests(shared.get_test_dir('example')): basename = os.path.basename(src) output_file = os.path.join(shared.options.binaryen_bin, 'example') @@ -64,7 +66,7 @@ def update_example_tests(): def update_wasm_dis_tests(): - print('\n[ checking wasm-dis on provided binaries... ]\n') + print_heading('checking wasm-dis on provided binaries...') for t in shared.get_tests(shared.options.binaryen_test, ['.wasm']): print('..', os.path.basename(t)) cmd = shared.WASM_DIS + [t] @@ -76,7 +78,7 @@ def update_wasm_dis_tests(): def update_ctor_eval_tests(): - print('\n[ checking wasm-ctor-eval... ]\n') + print_heading('checking wasm-ctor-eval...') for t in shared.get_tests(shared.get_test_dir('ctor-eval'), ['.wast', '.wasm']): print('..', os.path.basename(t)) ctors = open(t + '.ctors').read().strip() @@ -93,7 +95,7 @@ def update_ctor_eval_tests(): def update_metadce_tests(): - print('\n[ checking wasm-metadce... ]\n') + print_heading('checking wasm-metadce...') for t in shared.get_tests(shared.get_test_dir('metadce'), ['.wast', '.wasm']): print('..', os.path.basename(t)) graph = t + '.graph.txt' @@ -108,7 +110,7 @@ def update_metadce_tests(): def update_reduce_tests(): - print('\n[ checking wasm-reduce ]\n') + print_heading('checking wasm-reduce') for t in shared.get_tests(shared.get_test_dir('reduce'), ['.wast']): print('..', os.path.basename(t)) # convert to wasm @@ -120,7 +122,7 @@ def update_reduce_tests(): def update_spec_tests(): - print('\n[ updating wasm-shell spec testcases... ]\n') + print_heading('updating wasm-shell spec testcases...') for t in shared.options.spec_tests: print('..', os.path.basename(t)) @@ -138,7 +140,7 @@ def update_spec_tests(): def update_lit_tests(): - print('\n[ updating lit testcases... ]\n') + print_heading('updating lit testcases...') script = os.path.join(shared.options.binaryen_root, 'scripts', 'update_lit_checks.py') @@ -166,7 +168,7 @@ def update_lit_tests(): 'wasm-metadce': update_metadce_tests, 'wasm-reduce': update_reduce_tests, 'spec': update_spec_tests, - 'lld': lld.update_lld_tests, + 'finalize': finalize.update_finalize_tests, 'wasm2js': wasm2js.update_wasm2js_tests, 'binaryenjs': binaryenjs.update_binaryen_js_tests, 'lit': update_lit_tests, @@ -187,8 +189,9 @@ def main(): for test in shared.requested: TEST_SUITES[test]() + print() - print('\n[ success! ]') + print_heading('success!') if __name__ == '__main__': diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py index 60aebd78b7b..648c3f33a8e 100755 --- a/scripts/bundle_clusterfuzz.py +++ b/scripts/bundle_clusterfuzz.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 -''' -Bundle files for uploading to ClusterFuzz. +"""Bundle files for uploading to ClusterFuzz. Usage: @@ -68,7 +67,7 @@ 3. Check the stats and crashes page (known crashes should at least be showing up). Note that these may take longer to show up than 1 and 2. -''' +""" import glob import os @@ -110,7 +109,6 @@ '--disable-fp16', '--disable-strings', '--disable-stack-switching', - '--disable-relaxed-atomics', '--disable-multibyte', ] diff --git a/scripts/clusterfuzz/embed_wasms.py b/scripts/clusterfuzz/embed_wasms.py index 84ce6805370..17b4bcaf06d 100644 --- a/scripts/clusterfuzz/embed_wasms.py +++ b/scripts/clusterfuzz/embed_wasms.py @@ -13,11 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -Reverse script for extract_wasms.py: That one extracts wasm files from a -JavaScript testcase (which has wasm files embedded as arrays of numbers), and -this one re-embeds them back. To do so, we use the magic comments that the -extractor uses: it replaces each wasm array with +"""Reverse of extract_wasms.py. + +extract_wasms.py extracts wasm files from a JavaScript testcase (which has wasm +files embedded as arrays of numbers), and this script re-embeds them back. To +do so, we use the magic comments that the extractor uses: it replaces each +wasm array with 'undefined /* extracted wasm */' @@ -39,7 +40,7 @@ The first argument is the input JS, then the wasm files, then the last argument is the output JS. -''' +""" import re import sys diff --git a/scripts/clusterfuzz/extract_wasms.py b/scripts/clusterfuzz/extract_wasms.py index c1cc429eeb6..2833305b92d 100644 --- a/scripts/clusterfuzz/extract_wasms.py +++ b/scripts/clusterfuzz/extract_wasms.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -Wasm extractor for testcases generated by the ClusterFuzz run.py script. This is -general enough to also handle Fuzzilli output. +"""Wasm extractor for testcases generated by the ClusterFuzz run.py script. + +This is general enough to also handle Fuzzilli output. Usage: @@ -32,7 +32,7 @@ d8 OUTFILE.js -- OUTFILE.0.wasm That is, the embedded file can now be provided as a filename argument. -''' +""" import re import sys diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py index abd1d5ad5ba..b98c5658c50 100755 --- a/scripts/clusterfuzz/run.py +++ b/scripts/clusterfuzz/run.py @@ -13,15 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -ClusterFuzz run.py script: when run by ClusterFuzz, it uses wasm-opt to generate -a fixed number of testcases. This is a "blackbox fuzzer", see +"""ClusterFuzz run.py script. + +When run by ClusterFuzz, it uses wasm-opt to generate a fixed number of +testcases. This is a "blackbox fuzzer", see https://google.github.io/clusterfuzz/setting-up-fuzzing/blackbox-fuzzing/ This file should be bundled up together with the other files it needs, see bundle_clusterfuzz.py. -''' +""" import getopt import math @@ -32,7 +33,7 @@ # The V8 flags we put in the "fuzzer flags" files, which tell ClusterFuzz how to # run V8. By default we apply all staging flags. -FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop' +FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop --experimental-wasm-acquire-release --experimental-wasm-wide-arithmetic' # Optional V8 flags to add to FUZZER_FLAGS, some of the time. OPTIONAL_FUZZER_FLAGS = [ @@ -87,13 +88,12 @@ # Enable all features but disable ones not yet ready for fuzzing. This may # be a smaller set than fuzz_opt.py, as that enables a few experimental # flags, while here we just fuzz with d8's --wasm-staging. This should be - # synchonized with bundle_clusterfuzz. + # synchronized with bundle_clusterfuzz. '-all', '--disable-shared-everything', '--disable-fp16', '--disable-strings', '--disable-stack-switching', - '--disable-relaxed-atomics', ] diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 89ae3ff0007..8bc451b962c 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 -"""Run various fuzzing operations on random inputs, using wasm-opt. See -"testcase_handlers" below for the list of fuzzing operations. +"""Run various fuzzing operations on random inputs, using wasm-opt. + +See "testcase_handlers" below for the list of fuzzing operations. Usage: @@ -31,6 +32,7 @@ import json import math import os +import pathlib import random import re import shutil @@ -41,6 +43,7 @@ import traceback from datetime import datetime, timedelta, timezone from os.path import abspath +from typing import override from test import fuzzing, shared, support @@ -72,7 +75,6 @@ 'fp16', 'strings', 'stack-switching', - 'relaxed-atomics', 'multibyte', ] @@ -458,7 +460,7 @@ def get_export_from_export_line(export_line): # compare two strings, strictly def compare(x, y, context, verbose=True): - if x != y and IGNORE not in (x, y): + if x != y and IGNORE not in {x, y}: message = ''.join([a + '\n' for a in difflib.unified_diff(x.splitlines(), y.splitlines(), fromfile='expected', tofile='actual')]) if verbose: raise Exception(f"{context} comparison error, expected to have '{x}' == '{y}', diff:\n\n{message}") @@ -606,7 +608,7 @@ def note_ignored_vm_run(reason, extra_text='', amount=1): # Run a VM command, and filter out known issues. -def run_vm(cmd): +def run_vm(cmd, checked=True): def filter_known_issues(output): known_issues = [ # can be caused by flatten, ssa, etc. passes @@ -649,7 +651,11 @@ def filter_known_issues(output): try: # some known issues do not cause the entire process to fail - return filter_known_issues(run(cmd)) + if checked: + ret = run(cmd) + else: + ret = run_unchecked(cmd) + return filter_known_issues(ret) except subprocess.CalledProcessError: # other known issues do make it fail, so re-run without checking for # success and see if we should ignore it @@ -688,10 +694,15 @@ def get_v8_extra_flags(): if random.random() < 0.5: flags += ['--wasm-assert-types'] + # Some other options make sense to use sometimes. + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return flags V8_LIFTOFF_ARGS = ['--liftoff'] +V8_NO_LIFTOFF_ARGS = ['--no-liftoff'] # Default to running with liftoff enabled, because we need to pick either @@ -759,12 +770,14 @@ def __init__(self): # If the core handle_pair() method is not overridden, it calls handle() on # each of the items. That is useful if you just want the two wasms and don't # care about their relationship. + @override def handle_pair(self, input, before_wasm, after_wasm, opts): self.handle(before_wasm) # Add some visual space between the independent parts. print('\n') self.handle(after_wasm) + @override def can_run_on_wasm(self, wasm): return True @@ -779,9 +792,201 @@ def count_runs(self): class FuzzExec(TestCaseHandler): frequency = 1 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) +# VMs + + +class BinaryenInterpreter: + name = 'binaryen interpreter' + + @override + def run(self, wasm): + output = run_bynterp(wasm, ['--fuzz-exec-before']) + if output != IGNORE: + calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) + errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) + if errors > calls / 2: + # A significant amount of execution on this testcase + # simply trapped, and was not very useful, so mark it + # as ignored. Ideally the fuzzer testcases would be + # improved to reduce this number. + # + # Note that we don't change output=IGNORE as there may + # still be useful testing here (up to 50%), so we only + # note that this is a mostly-ignored run, but we do not + # ignore the parts that are useful. + # + # Note that we set amount to 0.5 because we are run both + # on the before wasm and the after wasm. Those will be + # in sync (because the optimizer does not remove traps) + # and so by setting 0.5 we only increment by 1 for the + # entire iteration. + note_ignored_vm_run('too many errors vs calls', + extra_text=f' ({calls} calls, {errors} errors)', + amount=0.5) + return output + + @override + def can_run(self, wasm): + return True + + @override + def can_compare_to_self(self): + return True + + @override + def can_compare_to_other(self, other): + return True + + +class D8: + name = 'd8' + + extra_d8_flags = [] + + def run_js(self, js, wasm, checked=True): + return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm], checked=checked) + + @override + def run(self, wasm): + return self.run_js(js=get_fuzz_shell_js(), wasm=wasm) + + @override + def can_run(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + @override + def can_compare_to_self(self): + # With nans, VM differences can confuse us, so only very simple VMs + # can compare to themselves after opts in that case. + return not NANS + + @override + def can_compare_to_other(self, other): + # Relaxed SIMD allows different behavior between VMs, so only + # allow comparisons to other d8 variants if it is enabled. + if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): + return False + + # If not legalized, the JS will fail immediately, so no point to + # compare to others. + return self.can_compare_to_self() and LEGALIZE + + +class D8Liftoff(D8): + name = 'd8_liftoff' + + extra_d8_flags = V8_LIFTOFF_ARGS + + +class D8Turboshaft(D8): + name = 'd8_turboshaft' + + extra_d8_flags = V8_NO_LIFTOFF_ARGS + + +class Wasm2C: + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + if not os.path.isdir(self.wasm2c_dir): + print('wabt found, but not wasm2c support dir') + self.wasm2c_dir = None + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + @override + def can_run(self, wasm): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # relatively slow, so run it less frequently + if random.random() < 0.5: + return False + # wasm2c doesn't support most features + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics', 'wide-arithmetic']) + + @override + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + @override + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + @override + def can_compare_to_other(self, other): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + + +class Wasm2C2Wasm(Wasm2C): + name = 'wasm2c2wasm' + + def __init__(self): + super().__init__() + + self.has_emcc = shared.which('emcc') is not None + + @override + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['emcc', 'main.c', 'wasm.c', + os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), + '-I' + self.wasm2c_dir, + '-lm', + '-s', 'ENVIRONMENT=shell', + '-s', 'ALLOW_MEMORY_GROWTH'] + # disable the signal handler: emcc looks like unix, but wasm has + # no signals + compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] + if random.random() < 0.5: + compile_cmd += ['-O' + str(random.randint(1, 3))] + elif random.random() < 0.5: + if random.random() < 0.5: + compile_cmd += ['-Os'] + else: + compile_cmd += ['-Oz'] + # avoid pass-debug on the emcc invocation itself (which runs + # binaryen to optimize the wasm), as the wasm here can be very + # large and it isn't what we are focused on testing here + with no_pass_debug(): + run(compile_cmd) + return run_d8_js(abspath('a.out.js')) + + @override + def can_run(self, wasm): + # quite slow (more steps), so run it less frequently + if random.random() < 0.8: + return False + # prefer not to run if the wasm is very large, as it can OOM + # the JS engine. + return super().can_run(wasm) and self.has_emcc and \ + os.path.getsize(wasm) <= INPUT_SIZE_MEAN + + @override + def can_compare_to_other(self, other): + # NaNs can differ from wasm VMs + return not NANS + class CompareVMs(TestCaseHandler): frequency = 1 @@ -789,174 +994,6 @@ class CompareVMs(TestCaseHandler): def __init__(self): super().__init__() - class BinaryenInterpreter: - name = 'binaryen interpreter' - - def run(self, wasm): - output = run_bynterp(wasm, ['--fuzz-exec-before']) - if output != IGNORE: - calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) - errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) - if errors > calls / 2: - # A significant amount of execution on this testcase - # simply trapped, and was not very useful, so mark it - # as ignored. Ideally the fuzzer testcases would be - # improved to reduce this number. - # - # Note that we don't change output=IGNORE as there may - # still be useful testing here (up to 50%), so we only - # note that this is a mostly-ignored run, but we do not - # ignore the parts that are useful. - # - # Note that we set amount to 0.5 because we are run both - # on the before wasm and the after wasm. Those will be - # in sync (because the optimizer does not remove traps) - # and so by setting 0.5 we only increment by 1 for the - # entire iteration. - note_ignored_vm_run('too many errors vs calls', - extra_text=f' ({calls} calls, {errors} errors)', - amount=0.5) - return output - - def can_run(self, wasm): - return True - - def can_compare_to_self(self): - return True - - def can_compare_to_other(self, other): - return True - - class D8: - name = 'd8' - - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) - - def can_run(self, wasm): - return all_disallowed(DISALLOWED_FEATURES_IN_V8) - - def can_compare_to_self(self): - # With nans, VM differences can confuse us, so only very simple VMs - # can compare to themselves after opts in that case. - return not NANS - - def can_compare_to_other(self, other): - # Relaxed SIMD allows different behavior between VMs, so only - # allow comparisons to other d8 variants if it is enabled. - if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): - return False - - # If not legalized, the JS will fail immediately, so no point to - # compare to others. - return self.can_compare_to_self() and LEGALIZE - - class D8Liftoff(D8): - name = 'd8_liftoff' - - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) - - class D8Turboshaft(D8): - name = 'd8_turboshaft' - - def run(self, wasm): - flags = ['--no-liftoff'] - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] - return super().run(wasm, extra_d8_flags=flags) - - class Wasm2C: - name = 'wasm2c' - - def __init__(self): - # look for wabt in the path. if it's not here, don't run wasm2c - try: - wabt_bin = shared.which('wasm2c') - wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) - self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') - if not os.path.isdir(self.wasm2c_dir): - print('wabt found, but not wasm2c support dir') - self.wasm2c_dir = None - except Exception as e: - print('warning: no wabt found:', e) - self.wasm2c_dir = None - - def can_run(self, wasm): - if self.wasm2c_dir is None: - return False - # if we legalize for JS, the ABI is not what C wants - if LEGALIZE: - return False - # relatively slow, so run it less frequently - if random.random() < 0.5: - return False - # wasm2c doesn't support most features - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] - run(compile_cmd) - return run_vm(['./a.out']) - - def can_compare_to_self(self): - # The binaryen optimizer changes NaNs in the ways that wasm - # expects, but that's not quite what C has - return not NANS - - def can_compare_to_other(self, other): - # C won't trap on OOB, and NaNs can differ from wasm VMs - return not OOB and not NANS - - class Wasm2C2Wasm(Wasm2C): - name = 'wasm2c2wasm' - - def __init__(self): - super().__init__() - - self.has_emcc = shared.which('emcc') is not None - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['emcc', 'main.c', 'wasm.c', - os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), - '-I' + self.wasm2c_dir, - '-lm', - '-s', 'ENVIRONMENT=shell', - '-s', 'ALLOW_MEMORY_GROWTH'] - # disable the signal handler: emcc looks like unix, but wasm has - # no signals - compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] - if random.random() < 0.5: - compile_cmd += ['-O' + str(random.randint(1, 3))] - elif random.random() < 0.5: - if random.random() < 0.5: - compile_cmd += ['-Os'] - else: - compile_cmd += ['-Oz'] - # avoid pass-debug on the emcc invocation itself (which runs - # binaryen to optimize the wasm), as the wasm here can be very - # large and it isn't what we are focused on testing here - with no_pass_debug(): - run(compile_cmd) - return run_d8_js(abspath('a.out.js')) - - def can_run(self, wasm): - # quite slow (more steps), so run it less frequently - if random.random() < 0.8: - return False - # prefer not to run if the wasm is very large, as it can OOM - # the JS engine. - return super().can_run(wasm) and self.has_emcc and \ - os.path.getsize(wasm) <= INPUT_SIZE_MEAN - - def can_compare_to_other(self, other): - # NaNs can differ from wasm VMs - return not NANS - # the binaryen interpreter is specifically useful for various things self.bynterpreter = BinaryenInterpreter() @@ -969,6 +1006,7 @@ def can_compare_to_other(self, other): # Wasm2C2Wasm() ] + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before = self.run_vms(before_wasm) @@ -1015,7 +1053,8 @@ def run_vms(self, wasm): return vm_results - def compare_before_and_after(self, before, after): + @staticmethod + def compare_before_and_after(before, after): # compare each VM to itself on the before and after inputs for vm in before.keys(): if vm in after and vm.can_compare_to_self(): @@ -1026,6 +1065,7 @@ def compare_before_and_after(self, before, after): class CheckDeterminism(TestCaseHandler): frequency = 0.2 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): # check for determinism run([in_bin('wasm-opt'), before_wasm, '-o', abspath('b1.wasm')] + opts) @@ -1043,6 +1083,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): class Wasm2JS(TestCaseHandler): frequency = 0.1 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before_wasm_temp = before_wasm + '.temp.wasm' after_wasm_temp = after_wasm + '.temp.wasm' @@ -1155,6 +1196,7 @@ def fix_number(x): interpreter = fix_output_for_js(interpreter) compare_between_vms(before, interpreter, 'Wasm2JS (vs interpreter)') + @override def run(self, wasm): with open(get_fuzz_shell_js()) as f: wrapper = f.read() @@ -1181,6 +1223,7 @@ def run(self, wasm): f.write(wrapper) return run_vm([shared.NODEJS, js_file, abspath('a.wasm')]) + @override def can_run_on_wasm(self, wasm): # TODO: properly handle memory growth. right now the wasm2js handler # uses --emscripten which assumes the Memory is created before, and @@ -1193,7 +1236,7 @@ def can_run_on_wasm(self, wasm): # implement wasm suspending using JS async/await. if JSPI: return False - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'multimemory', 'memory64', 'custom-descriptors', 'relaxed-atomics']) + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'multimemory', 'memory64', 'custom-descriptors', 'relaxed-atomics', 'wide-arithmetic']) # Returns the wat for a wasm file. If it is already wat, it just returns that @@ -1281,6 +1324,7 @@ def wasm_notices_export_changes(wasm): class TrapsNeverHappen(TestCaseHandler): frequency = 0.25 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before = run_bynterp(before_wasm, ['--fuzz-exec-before']) @@ -1367,6 +1411,7 @@ def ignore_references(out): compare_between_vms(before, after, 'TrapsNeverHappen') + @override def can_run_on_wasm(self, wasm): # If the wasm is sensitive to changes in exports then we cannot alter # them, but we must remove trapping exports (see above), so we cannot @@ -1378,6 +1423,7 @@ def can_run_on_wasm(self, wasm): class CtorEval(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # Get the list of func exports, so we can tell ctor-eval what to eval. func_exports = get_exports(wasm, ['func']) @@ -1418,6 +1464,7 @@ def handle(self, wasm): compare_between_vms(fix_output(wasm_exec), fix_output(evalled_wasm_exec), 'CtorEval') + @override def can_run_on_wasm(self, wasm): # ctor-eval modifies exports, because it assumes they are ctors and so # are only called once (so if it evals them away, they can be @@ -1455,6 +1502,7 @@ def traps_in_instantiation(output): class Merge(TestCaseHandler): frequency = 0.15 + @override def handle(self, wasm): # generate a second wasm file to merge. note that we intentionally pick # a smaller size than the main wasm file, so that reduction is @@ -1551,6 +1599,7 @@ def handle(self, wasm): compare_between_vms(output, merged_output, 'Merge') + @override def can_run_on_wasm(self, wasm): # wasm-merge combines exports, which can alter their indexes and lead to # noticeable differences if the wasm is sensitive to such things, which @@ -1567,6 +1616,7 @@ def can_run_on_wasm(self, wasm): class Split(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # get the list of function names, some of which we will decide to split # out @@ -1642,9 +1692,9 @@ def optimize(name): # prepare the list of exports to call. the format is # - # exports:A,B,C + # exports:["A","B","C"] # - exports_to_call = 'exports:' + ','.join(exports) + exports_to_call = 'exports:' + json.dumps(exports) # get the output from the split modules, linking them using JS # TODO run liftoff/turboshaft/etc. @@ -1662,6 +1712,7 @@ def optimize(name): if not (NANS and optimized): compare_between_vms(output, linked_output, 'Split') + @override def can_run_on_wasm(self, wasm): # to run the split wasm we use JS, that is, JS links the exports of one # to the imports of the other, etc. since we run in JS, the wasm must be @@ -1677,6 +1728,7 @@ def can_run_on_wasm(self, wasm): class RoundtripText(TestCaseHandler): frequency = 0.05 + @override def handle(self, wasm): # use name-types because in wasm GC we can end up truncating the default # names which are very long, causing names to collide and the wast to be @@ -1706,6 +1758,7 @@ class ClusterFuzz(TestCaseHandler): # we generate our own using run.py. If we used handle, we'd be called twice # for each iteration (once for each of the wasm files we ignore), which is # confusing. + @override def handle_pair(self, input, before_wasm, after_wasm, opts): # Do not run ClusterFuzz in the first seconds of fuzzing: the first time # it runs is very slow (to build the bundle), which is annoying when you @@ -1844,6 +1897,7 @@ class Two(TestCaseHandler): # module interactions. frequency = 1 # TODO: We may want even higher priority here + @override def handle(self, wasm): # Generate a second wasm file. (For fuzzing, we may be given one, but we # still do the work to prepare to generate it, as that consumes random @@ -1980,7 +2034,8 @@ def handle(self, wasm): compare(output, optimized_output, 'Two-V8') - def compare_to_merged_output(self, output, merged_output): + @staticmethod + def compare_to_merged_output(output, merged_output): # Comparing the original output from two files to the output after # merging them is not trivial. First, remove the extra logging that # --fuzz-exec-second adds. @@ -2032,10 +2087,12 @@ def compare_to_merged_output(self, output, merged_output): compare(output, merged_output, 'Two-Merged') -# Test --fuzz-preserve-imports-exports, which never modifies imports or exports. -class PreserveImportsExports(TestCaseHandler): +# Test --fuzz-preserve-imports-exports on random inputs. This should never +# modify imports or exports. +class PreserveImportsExportsRandom(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # We will later verify that no imports or exports changed, by comparing # to the unprocessed original text. @@ -2078,6 +2135,178 @@ def get_relevant_lines(wat): compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve') +# Test --fuzz-preserve-imports-exports on a realistic js+wasm input. Unlike +# PreserveImportsExportsRandom which starts with a random file and modifies it, +# this starts with a fixed js+wasm testcase, known to work and to have +# interesting operations on the js/wasm boundary, and then randomly modifies +# the wasm. This simulates how an external fuzzer could use binaryen to modify +# its known-working testcases (parallel to how we test ClusterFuzz here). +# +# This reads wasm+js combinations from the test/js_wasm directory, so as new +# testcases are added there, this will fuzz them. +# +# Note that bugs found by this fuzzer require BINARYEN_TRUST_GIVEN_WASM=1 in the +# env for reduction. TODO: simplify this +class PreserveImportsExportsJS(TestCaseHandler): + frequency = 1 + + @override + def handle_pair(self, input, before_wasm, after_wasm, opts): + try: + self.do_handle_pair(input, before_wasm, after_wasm, opts) + except Exception as e: + if not os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): + # We errored, and we were not given a wasm file to trust as we + # reduce, so this is the first time we hit an error. Save the + # pre wasm file, the one we began with, as `before_wasm`, so + # that the reducer will make us proceed exactly from there. + shutil.copyfile(self.pre_wasm, before_wasm) + raise e + + def do_handle_pair(self, input, before_wasm, after_wasm, opts): + # Some of the time use a custom input. The normal inputs the fuzzer + # generates are in range INPUT_SIZE_MIN-INPUT_SIZE_MAX, which is good + # for new testcases, but the more changes we make to js+wasm testcases, + # the more chance we have to break things entirely (the js/wasm boundary + # is fragile). It is useful to also fuzz smaller sizes. + if random.random() < 0.25: + size = random.randint(0, INPUT_SIZE_MIN * 2) + make_random_input(size, input) + + # Pick a js+wasm pair. + js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs')) + js_file = str(random.choice(js_files)) + print(f'js file: {js_file}') + wat_file = str(pathlib.Path(js_file).with_suffix('.wat')) + + # Verify the wat works with our features + try: + run([in_bin('wasm-opt'), wat_file] + FEATURE_OPTS, + stderr=subprocess.PIPE, + silent=True) + except Exception: + note_ignored_vm_run('PreserveImportsExportsJS: features not compatible with js+wasm') + return + + # Make sure the testcase runs by itself - there should be no invalid + # testcases. + original_wasm = 'orig.wasm' + run([in_bin('wasm-opt'), wat_file, '-o', original_wasm] + FEATURE_OPTS) + D8().run_js(js_file, original_wasm) + + # Modify the initial wat to get the pre-optimizations wasm. + pre_wasm = abspath('pre.wasm') + gen_args = [ + input, + '-ttf', + '--fuzz-preserve-imports-exports', + '--fuzz-against-js', + '--initial-fuzz=' + wat_file, + '-o', pre_wasm, + '-g', + ] + # We do not copy all of GEN_ARGS, as we don't need e.g. legalization. + if not NANS: + # TODO: do we also need this in each reduction step? + gen_args += ['--denan'] + run([in_bin('wasm-opt')] + gen_args + FEATURE_OPTS) + + # We successfully generated pre_wasm; stash it for possible reduction + # purposes later. + self.pre_wasm = pre_wasm + + # If we were given a wasm file, use that instead of all the above. We + # do this now, after creating pre_wasm, because we still need to consume + # all the randomness normally. + if os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): + print('using given wasm', before_wasm) + pre_wasm = before_wasm + + # Pick a vm and run before we optimize the wasm. + vms = [ + D8(), + D8Liftoff(), + D8Turboshaft(), + ] + pre_vm = random.choice(vms) + pre = self.do_run(pre_vm, js_file, pre_wasm) + + # We are about to optimize, and do not trust the given wasm file to + # have marked all js-called methods properly. In particular, it could + # have a configureAll that is not in the start function. + full_opts = [ + '--mark-js-called', + ] + opts + + # Optimize. + post_wasm = abspath('post.wasm') + cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + full_opts + FEATURE_OPTS + print(' '.join(cmd)) + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode: + if 'Invalid configureAll' in proc.stderr: + # We have a hard error on unfamiliar configureAll patterns atm. + # Mutation of configureAll will easily break that pattern, so we + # must ignore such cases. + note_ignored_vm_run('PreserveImportsExportsJS: bad configureAll') + return + + # Anything else is a problem. + print(proc.stderr) + raise Exception('opts failed') + + # Run after opts, in a random vm. + post_vm = random.choice(vms) + post = self.do_run(post_vm, js_file, post_wasm) + + # Compare, if we can. + if pre_vm.can_compare_to_other(post_vm): + compare(pre, post, 'PreserveImportsExportsJS') + + @staticmethod + def do_run(vm, js, wasm): + out = vm.run_js(js, wasm, checked=False) + + # VM crashes are actual issues we want to find. + if '(core dumped)' in out or 'Received signal' in out or '== C stack trace ==' in out or '== JS stack trace ==' in out: + raise Exception(f"VM crash:\n\n{out}") + + # Clean up stack traces. + cleaned = [] + for line in out.splitlines(): + if 'RuntimeError:' in line or 'TypeError:' in line: + # This is part of an error like + # + # wasm-function[2]:0x273: RuntimeError: unreachable + # + # We must ignore the binary location, which opts can change. We + # must also remove the specific trap, as Binaryen can change + # that. + line = 'TRAP' + elif line.startswith(' at '): + # This is part of a stack trace like + # + # at wasm://wasm/12345678:wasm-function[42]:0x123 + # at () + # at file.js + # + # Ignore it, as details of traces differ based on optimizations. + continue + cleaned.append(line) + cleaned = '\n'.join(cleaned) + + # Clean up function references, which can differ after opts, things like + # + # function 77() { [native code] } + # + cleaned = re.sub(r'function \d+\(\) ', 'function () ', cleaned) + return cleaned + + @override + def can_run_on_wasm(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + # Test that we preserve branch hints properly. The invariant that we test here # is that, given correct branch hints (that is, the input wasm's branch hints # are always correct: a branch is taken iff the hint is that it is taken), then @@ -2092,6 +2321,7 @@ def get_relevant_lines(wat): class BranchHintPreservation(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # Generate an instrumented wasm. instrumented = wasm + '.inst.wasm' @@ -2120,7 +2350,7 @@ def handle(self, wasm): for line in out.splitlines(): if line.startswith(LOG_BRANCH_PREFIX): # (1:-1 strips away the '[', ']' at the edges) - _, _, id_, hint, actual = line[1:-1].split(' ') + _, _, actual, hint, id_ = line[1:-1].split(' ') all_ids.add(id_) if hint != actual: # This hint was misleading. @@ -2282,10 +2512,10 @@ def handle(self, wasm): continue for line in group: if line.startswith(LOG_BRANCH_PREFIX): - _, _, id_, hint, actual = line[1:-1].split(' ') + _, _, actual, hint, id_ = line[1:-1].split(' ') hint = int(hint) actual = int(actual) - assert hint in (0, 1) + assert hint in {0, 1} # We do not care about the integer value of the condition, # only if it was 0 or non-zero. actual = (actual != 0) @@ -2301,11 +2531,12 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), - Split(), +# Split(), # Will reenable after stabilized RoundtripText(), ClusterFuzz(), Two(), - PreserveImportsExports(), + PreserveImportsExportsRandom(), + PreserveImportsExportsJS(), BranchHintPreservation(), ] @@ -2376,24 +2607,19 @@ def test_one(random_input, given_wasm): if len(filtered_handlers) == 0: # pick at least one, to not waste the effort we put into making the wasm filtered_handlers = [random.choice(relevant_handlers)] - # run only some of the pair handling handlers. if we ran them all all the - # time that would mean we have less variety in wasm files and passes run - # on them in the same amount of time. - NUM_PAIR_HANDLERS = 3 - used_handlers = set() - for _ in range(NUM_PAIR_HANDLERS): - testcase_handler = random.choice(filtered_handlers) - if testcase_handler in used_handlers: - continue - used_handlers.add(testcase_handler) - assert testcase_handler.can_run_on_wasm('a.wasm') - print('running testcase handler:', testcase_handler.__class__.__name__) - testcase_handler.increment_runs() - - # let the testcase handler handle this testcase however it wants. in this case we give it - # the input and both wasms. - testcase_handler.handle_pair(input=random_input, before_wasm=abspath('a.wasm'), after_wasm=abspath('b.wasm'), opts=opts + FEATURE_OPTS) - print('') + # run only one of the handlers. this is less efficient in terms of how many + # handlers we run, but more varied in the wasms we see. it also avoids the + # annoyance of running two testcase handlers on the same testcase during + # reduction (it is much simpler to reduce when only the failing thing is + # being run). + testcase_handler = random.choice(filtered_handlers) + assert testcase_handler.can_run_on_wasm('a.wasm') + print('running testcase handler:', testcase_handler.__class__.__name__) + testcase_handler.increment_runs() + + # let the testcase handler handle this testcase however it wants. in this case we give it + # the input and both wasms. + testcase_handler.handle_pair(input=random_input, before_wasm=abspath('a.wasm'), after_wasm=abspath('b.wasm'), opts=opts + FEATURE_OPTS) return bytes diff --git a/scripts/fuzz_passes.py b/scripts/fuzz_passes.py index 528de33e0b6..c2c7910a8dd 100755 --- a/scripts/fuzz_passes.py +++ b/scripts/fuzz_passes.py @@ -14,8 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes passes, by starting with a working program, then running +"""Script for fuzzing passes. + +Fuzzes passes by starting with a working program, then running random passes on the wast, and seeing if they break something Usage: Provide a base filename for a runnable program, e.g. a.out.js. @@ -23,7 +24,7 @@ be built to run using that wast (BINARYEN_METHOD=interpret-s-expr) Other parameters after the first are used when calling the program. -''' +""" import os diff --git a/scripts/fuzz_passes_wast.py b/scripts/fuzz_passes_wast.py index 19c76d33410..97f29a7b415 100755 --- a/scripts/fuzz_passes_wast.py +++ b/scripts/fuzz_passes_wast.py @@ -14,13 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes passes, by starting with a wast, then running -random passes on the wast, and seeing if they break optimization -or validation +"""Script for fuzzing passes. + +Starting with a wast, then running random passes on the wast, and seeing +if they break optimization or validation Usage: Provide the filename of the wast. -''' +""" import os diff --git a/scripts/fuzz_relooper.py b/scripts/fuzz_relooper.py index 7ee6525b160..4fd65e40c8e 100755 --- a/scripts/fuzz_relooper.py +++ b/scripts/fuzz_relooper.py @@ -14,9 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes the relooper using the C API. -''' +"""Script that fuzzes the relooper using the C API.""" # ruff: noqa: UP031 diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 28f5e2f44fb..06a1030a215 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -57,7 +57,12 @@ var fuzzSplit = false; for (var i = 0; i < argv.length; i++) { var curr = argv[i]; if (curr.startsWith('exports:')) { - exportsToCall = curr.substr('exports:'.length).split(','); + var payload = curr.substr('exports:'.length); + if (payload.startsWith('[')) { + exportsToCall = JSON.parse(payload); + } else { + exportsToCall = payload ? payload.split(',') : []; + } argv.splice(i, 1); i--; } else if (curr == '--fuzz-split') { @@ -400,8 +405,9 @@ var baseImports = { }); }, - 'log-branch': (id, expected, actual) => { - console.log(`[LoggingExternalInterface log-branch ${id} ${expected} ${actual}]`); + 'log-branch': (actual, expected, id) => { + console.log(`[LoggingExternalInterface log-branch ${actual} ${expected} ${id}]`); + return actual; }, }, // Emscripten support. diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 4b73fee9f89..3c0a39a7bb2 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -18,6 +18,8 @@ assert sys.version_info >= (3, 10), 'requires Python 3.10' +# ruff: noqa: E241 + instructions = [ ("unreachable", "makeUnreachable()"), ("nop", "makeNop()"), @@ -146,6 +148,10 @@ ("i64.shr_u", "makeBinary(BinaryOp::ShrUInt64)"), ("i64.rotl", "makeBinary(BinaryOp::RotLInt64)"), ("i64.rotr", "makeBinary(BinaryOp::RotRInt64)"), + ("i64.add128", "makeWideIntAddSub(WideIntAddSubOp::AddInt128)"), + ("i64.sub128", "makeWideIntAddSub(WideIntAddSubOp::SubInt128)"), + ("i64.mul_wide_s", "makeWideIntMul(WideIntMulOp::MulWideSInt64)"), + ("i64.mul_wide_u", "makeWideIntMul(WideIntMulOp::MulWideUInt64)"), ("f32.abs", "makeUnary(UnaryOp::AbsFloat32)"), ("f32.neg", "makeUnary(UnaryOp::NegFloat32)"), ("f32.ceil", "makeUnary(UnaryOp::CeilFloat32)"), @@ -549,6 +555,9 @@ ("i16x8.trunc_sat_f16x8_u", "makeUnary(UnaryOp::TruncSatUVecF16x8ToVecI16x8)"), ("f16x8.convert_i16x8_s", "makeUnary(UnaryOp::ConvertSVecI16x8ToVecF16x8)"), ("f16x8.convert_i16x8_u", "makeUnary(UnaryOp::ConvertUVecI16x8ToVecF16x8)"), + ("f32x4.promote_low_f16x8", "makeUnary(UnaryOp::PromoteLowVecF16x8ToVecF32x4)"), + ("f16x8.demote_f32x4_zero", "makeUnary(UnaryOp::DemoteZeroVecF32x4ToVecF16x8)"), + ("f16x8.demote_f64x2_zero", "makeUnary(UnaryOp::DemoteZeroVecF64x2ToVecF16x8)"), ("f16x8.madd", "makeSIMDTernary(SIMDTernaryOp::MaddVecF16x8)"), ("f16x8.nmadd", "makeSIMDTernary(SIMDTernaryOp::NmaddVecF16x8)"), @@ -562,17 +571,17 @@ ("f32x4.relaxed_nmadd", "makeSIMDTernary(SIMDTernaryOp::RelaxedNmaddVecF32x4)"), ("f64x2.relaxed_madd", "makeSIMDTernary(SIMDTernaryOp::RelaxedMaddVecF64x2)"), ("f64x2.relaxed_nmadd", "makeSIMDTernary(SIMDTernaryOp::RelaxedNmaddVecF64x2)"), - ("i8x16.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI8x16)"), - ("i16x8.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI16x8)"), - ("i32x4.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI32x4)"), - ("i64x2.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI64x2)"), + ("i8x16.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI8x16)"), + ("i16x8.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI16x8)"), + ("i32x4.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI32x4)"), + ("i64x2.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI64x2)"), ("f32x4.relaxed_min", "makeBinary(BinaryOp::RelaxedMinVecF32x4)"), ("f32x4.relaxed_max", "makeBinary(BinaryOp::RelaxedMaxVecF32x4)"), ("f64x2.relaxed_min", "makeBinary(BinaryOp::RelaxedMinVecF64x2)"), ("f64x2.relaxed_max", "makeBinary(BinaryOp::RelaxedMaxVecF64x2)"), ("i16x8.relaxed_q15mulr_s", "makeBinary(BinaryOp::RelaxedQ15MulrSVecI16x8)"), - ("i16x8.dot_i8x16_i7x16_s", "makeBinary(BinaryOp::DotI8x16I7x16SToVecI16x8)"), - ("i32x4.dot_i8x16_i7x16_add_s", "makeSIMDTernary(SIMDTernaryOp::DotI8x16I7x16AddSToVecI32x4)"), + ("i16x8.relaxed_dot_i8x16_i7x16_s", "makeBinary(BinaryOp::RelaxedDotI8x16I7x16SToVecI16x8)"), + ("i32x4.relaxed_dot_i8x16_i7x16_add_s", "makeSIMDTernary(SIMDTernaryOp::RelaxedDotI8x16I7x16AddSToVecI32x4)"), # reference types instructions ("ref.null", "makeRefNull()"), @@ -710,7 +719,8 @@ def indent(self): # call in a 'with' statement return self - def print_line(self, line): + @staticmethod + def print_line(line): print(" " * CodePrinter.indents + line) diff --git a/scripts/port_passes_tests_to_lit.py b/scripts/port_passes_tests_to_lit.py index ea1dabeebbc..19d5ce6bfc3 100755 --- a/scripts/port_passes_tests_to_lit.py +++ b/scripts/port_passes_tests_to_lit.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Automatically port legacy passes tests to be lit tests -""" +"""Automatically port legacy passes tests to be lit tests.""" import argparse import glob diff --git a/scripts/strip_local_names.py b/scripts/strip_local_names.py index 959e56dd8c1..038fe7cbec5 100644 --- a/scripts/strip_local_names.py +++ b/scripts/strip_local_names.py @@ -1,6 +1,8 @@ -"""Removes local names. When you don't care about local names but do want -to diff for structural changes, this can help. +"""Removes local names. + +When you don't care about local names but do want to diff for structural +changes, this can help. """ import sys diff --git a/scripts/test/binaryenjs.py b/scripts/test/binaryenjs.py index 97d84bb4f0f..aa9542be594 100644 --- a/scripts/test/binaryenjs.py +++ b/scripts/test/binaryenjs.py @@ -16,6 +16,7 @@ import subprocess from . import shared, support +from .shared import print_heading def make_js_test_header(binaryen_js): @@ -52,7 +53,7 @@ def test_binaryen_js(): if not os.path.exists(shared.BINARYEN_JS): shared.fail_with_error('no ' + shared.BINARYEN_JS + ' build to test') - print('\n[ checking binaryen.js testcases (' + shared.BINARYEN_JS + ')... ]\n') + print_heading(f'checking binaryen.js testcases ({shared.BINARYEN_JS})...') for s in shared.get_tests(shared.get_test_dir('binaryen.js'), ['.js']): outname = make_js_test(s, shared.BINARYEN_JS) @@ -87,7 +88,7 @@ def update_binaryen_js_tests(): print('no binaryen.js build to test') return - print('\n[ checking binaryen.js testcases... ]\n') + print_heading('checking binaryen.js testcases...') node_has_wasm = shared.NODEJS and support.node_has_webassembly(shared.NODEJS) for s in shared.get_tests(shared.get_test_dir('binaryen.js'), ['.js']): outname = make_js_test(s, shared.BINARYEN_JS) diff --git a/scripts/test/lld.py b/scripts/test/finalize.py similarity index 85% rename from scripts/test/lld.py rename to scripts/test/finalize.py index 6f4b2be803b..d5f3c2ebd0d 100644 --- a/scripts/test/lld.py +++ b/scripts/test/finalize.py @@ -15,6 +15,7 @@ import os from . import shared, support +from .shared import print_heading def args_for_finalize(filename): @@ -46,16 +47,16 @@ def run_test(input_path): def test_wasm_emscripten_finalize(): - print('\n[ checking wasm-emscripten-finalize testcases... ]\n') + print_heading('checking wasm-emscripten-finalize testcases...') - for input_path in shared.get_tests(shared.get_test_dir('lld'), ['.wat', '.wasm']): + for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): run_test(input_path) -def update_lld_tests(): - print('\n[ updating wasm-emscripten-finalize testcases... ]\n') +def update_finalize_tests(): + print_heading('updating wasm-emscripten-finalize testcases...') - for input_path in shared.get_tests(shared.get_test_dir('lld'), ['.wat', '.wasm']): + for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): print('..', input_path) extension_arg_map = { '.out': [], diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 29e4cf0e4ac..8a23262280d 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -117,6 +117,10 @@ 'waitqueue.wast', # TODO: fix handling of the non-utf8 names here 'name-high-bytes.wast', + # JS interop testcases have complex js-wasm interactions + 'js_interop_counter.wat', + 'js_interop_cases.wat', + 'js_interop_corners.wat', ] diff --git a/scripts/test/gen-cast-test.py b/scripts/test/gen-cast-test.py index 21e469b9858..994ddf40ebd 100755 --- a/scripts/test/gen-cast-test.py +++ b/scripts/test/gen-cast-test.py @@ -1,8 +1,6 @@ #! /usr/bin/python3 -''' -Generate test modules with all interesting casts -''' +"""Generate test modules with all interesting casts.""" import argparse import itertools diff --git a/scripts/test/generate_atomic_spec_test.py b/scripts/test/generate_atomic_spec_test.py index 0b98b98579c..6ce2f751efe 100644 --- a/scripts/test/generate_atomic_spec_test.py +++ b/scripts/test/generate_atomic_spec_test.py @@ -107,12 +107,12 @@ class Template: def all_combinations() -> Iterator[(Template, (int, ValueType), Ordering)]: """Yield tuples covering all possible combinations of atomic memory operations. + (template, (idx, memory_ptr_type), ordering) where idx is a memory index or None representing an implicit 0 index and memory_ptr_type is i32 or i64 based on the memory being indexed and ordering is an `Ordering` enum or None representing an implicit seqcst ordering. """ - # See the memory section defined in `binary_test` memories = [(None, ValueType.i32), (0, ValueType.i32), (1, ValueType.i64)] @@ -120,7 +120,7 @@ def all_combinations() -> Iterator[(Template, (int, ValueType), Ordering)]: def statement(template, mem_idx: int | None, mem_ptr_type: ValueType, ordering: Ordering | None): - """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42))""" + """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42)).""" memargs = [] if mem_idx is not None: memargs.append(str(mem_idx)) @@ -139,15 +139,17 @@ def statement(template, mem_idx: int | None, mem_ptr_type: ValueType, ordering: def func(): - """Return a func exercising all ops in `templates` e.g. - (func $test-all-ops - (drop (i32.atomic.load (i32.const 42))) - (drop (i32.atomic.load acqrel (i32.const 42))) - ... - ) + """Return a func exercising all ops in `templates`. + + e.g. + (func $test-all-ops + (drop (i32.atomic.load (i32.const 42))) + (drop (i32.atomic.load acqrel (i32.const 42))) + ... + ) """ return f''';; Memory index must come before memory ordering if present. -;; Both immediates are optional; an ommitted memory ordering will be treated as seqcst. +;; Both immediates are optional; an omitted memory ordering will be treated as seqcst. (func $test-all-ops {indent(newline.join(statement(template, mem_idx, mem_ptr_type, ordering) for template, (mem_idx, mem_ptr_type), ordering in all_combinations()))} )''' @@ -172,7 +174,7 @@ def invalid_text_test(): def bin_to_str(bin: bytes) -> str: - """Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00""" + r"""Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00.""" return ''.join(f'{backslash}{byte:02x}' for byte in bin) @@ -185,7 +187,9 @@ def bin_to_str(bin: bytes) -> str: def bin_statement_lines(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> Iterator[(bytes, str)]: - """Yield (b, comment) where `b` is a part of the statement using `template`, and `comment` explains that part, e.g. + r"""Yield (b, comment) where `b` is a part of the statement using `template`, and `comment` explains that part. + + e.g. (b"\xfe\x11", "i64.atomic.load") The entire iterator represents a complete expression using the `template`. e.g. (drop (i32.atomic.load (i32.const 42))) @@ -226,17 +230,18 @@ def bin_statement_lines(template: Template, mem_idx: int, mem_ptr_type: ValueTyp def bin_statement(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> (bytes, str): - """Return (b, s) where `b` is the binary exercising an instruction, e.g. - (drop (i32.atomic.load (i32.const 42))) - and `s` is a str containing the binary along with comments explaining it, e.g. - "\41\33" ;; (i32.const 51) - "\fe\10" ;; i32.atomic.load - "\42" ;; Alignment of 2 with bit 6 set indicating that a memory index immediate follows - "\00" ;; memory index - "\00" ;; offset - "\1a" ;; drop + r"""Return (b, s) where `b` is the binary exercising an instruction. + + e.g: + (drop (i32.atomic.load (i32.const 42))) + and `s` is a str containing the binary along with comments explaining it, e.g. + "\41\33" ;; (i32.const 51) + "\fe\10" ;; i32.atomic.load + "\42" ;; Alignment of 2 with bit 6 set indicating that a memory index immediate follows + "\00" ;; memory index + "\00" ;; offset + "\1a" ;; drop """ - bins = [] strs = [] diff --git a/scripts/test/generate_lld_tests.py b/scripts/test/generate_finalize_tests.py similarity index 86% rename from scripts/test/generate_lld_tests.py rename to scripts/test/generate_finalize_tests.py index 237b9b9cb72..aebe14382ed 100755 --- a/scripts/test/generate_lld_tests.py +++ b/scripts/test/generate_finalize_tests.py @@ -33,20 +33,20 @@ def files_with_extensions(path, extensions): def generate_wat_files(llvm_bin, emscripten_sysroot): print('\n[ building wat files from C sources... ]\n') - lld_path = os.path.join(shared.options.binaryen_test, 'lld') - for src_file, ext in files_with_extensions(lld_path, ['.c', '.cpp', '.s']): + test_path = os.path.join(shared.options.binaryen_test, 'finalize') + for src_file, ext in files_with_extensions(test_path, ['.c', '.cpp', '.s']): print('..', src_file) obj_file = src_file.replace(ext, '.o') - src_path = os.path.join(lld_path, src_file) - obj_path = os.path.join(lld_path, obj_file) + src_path = os.path.join(test_path, src_file) + obj_path = os.path.join(test_path, obj_file) wasm_file = src_file.replace(ext, '.wasm') wat_file = src_file.replace(ext, '.wat') - obj_path = os.path.join(lld_path, obj_file) - wasm_path = os.path.join(lld_path, wasm_file) - wat_path = os.path.join(lld_path, wat_file) + obj_path = os.path.join(test_path, obj_file) + wasm_path = os.path.join(test_path, wasm_file) + wat_path = os.path.join(test_path, wat_file) is_shared = 'shared' in src_file is_64 = '64' in src_file @@ -104,6 +104,6 @@ def generate_wat_files(llvm_bin, emscripten_sysroot): if __name__ == '__main__': if len(shared.options.positional_args) != 2: - print('Usage: generate_lld_tests.py [llvm/bin/dir] [path/to/emscripten]') + print('Usage: generate_finalize_tests.py [llvm/bin/dir] [path/to/emscripten]') sys.exit(1) generate_wat_files(*shared.options.positional_args) diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 72b64c762ad..5e3cd208270 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -95,8 +95,11 @@ def parse_args(args): parser.add_argument( '--no-auto-initial-contents', dest='auto_initial_contents', action='store_false', default=True, - help='Select important initial contents automaticaly in fuzzer. ' - 'Default: disabled.') + help='Disables the automatic selection of important initial contents ' + 'in fuzzer.') + parser.add_argument( + '--verbose', action='store_true', default=False, + help='Enables verbose logging.') return parser.parse_args(args) @@ -114,6 +117,15 @@ def warn(text): print('warning:', text, file=sys.stderr) +def print_heading(msg): + print(f'[ {msg} ]') + + +def verbose_log(*args, **kwargs): + if options.verbose: + print(*args, **kwargs) + + # setup # Locate Binaryen build artifacts directory (bin/ by default) @@ -130,7 +142,7 @@ def warn(text): options.binaryen_bin = os.path.normpath(os.path.abspath(options.binaryen_bin)) if not options.binaryen_lib: - options.binaryen_lib = os.path.join(os.path.dirname(options.binaryen_bin), 'lib') + options.binaryen_lib = os.path.join(os.path.dirname(options.binaryen_bin), 'lib') options.binaryen_lib = os.path.normpath(os.path.abspath(options.binaryen_lib)) @@ -254,6 +266,8 @@ def has_shell_timeout(): '--experimental-wasm-fp16', '--experimental-wasm-custom-descriptors', '--experimental-wasm-js-interop', + '--experimental-wasm-acquire-release', + '--experimental-wasm-wide-arithmetic', ] # external tools @@ -344,13 +358,14 @@ def fail_if_not_identical_to_file(actual, expected_file): def get_test_dir(name): - """Returns the test directory located at BINARYEN_ROOT/test/[name].""" + """Return the test directory located at BINARYEN_ROOT/test/[name].""" return os.path.join(options.binaryen_test, name) def get_tests(test_dir, extensions=[], recursive=False): - """Returns the list of test files in a given directory. 'extensions' is a - list of file extensions. If 'extensions' is empty, returns all files. + """Return the list of test files in a given directory. + + 'extensions' is a list of file extensions. If 'extensions' is empty, returns all files. """ tests = [] star = '**/*' if recursive else '*' @@ -403,7 +418,6 @@ def get_tests(test_dir, extensions=[], recursive=False): 'threads/atomic.wast', ] SPEC_TESTSUITE_PROPOSALS_TO_SKIP = [ - 'wide-arithmetic', ] # Paths are relative to the test/spec/testsuite directory @@ -443,15 +457,6 @@ def get_tests(test_dir, extensions=[], recursive=False): 'token.wast', # Lexer should require spaces between strings and non-paren tokens ] -if get_platform() == 'linux': - SPEC_TESTSUITE_TESTS_TO_SKIP += [ - # Errors on Linux x86_64 with musl, https://github.com/WebAssembly/binaryen/pull/8557 - 'f32.wast', - 'f64.wast', - 'simd_f32x4_rounding.wast', - 'simd_f64x2_rounding.wast', - ] - def _can_run_spec_test(test): test = Path(test) @@ -470,23 +475,22 @@ def _can_run_spec_test(test): # check utilities -def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], - binary_suffix='.fromBinary', base_name=None, stdout=None): +def binary_format_check(wast, verify_final_result=True, base_name=None, stdout=None): # checks we can convert the wast to binary and back as_file = f"{base_name}-a.wasm" if base_name is not None else "a.wasm" disassembled_file = f"{base_name}-ab.wast" if base_name is not None else "ab.wast" - print(' (binary format check)', file=stdout) - cmd = WASM_AS + [wast, '-o', as_file, '-all'] + wasm_as_args - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' (binary format check)', file=stdout) + cmd = WASM_AS + [wast, '-o', as_file, '-all', '-g'] + verbose_log(' ', ' '.join(cmd), file=stdout) if os.path.exists(as_file): os.unlink(as_file) subprocess.check_call(cmd, stdout=subprocess.PIPE) assert os.path.exists(as_file) cmd = WASM_DIS + [as_file, '-o', disassembled_file, '-all'] - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' ', ' '.join(cmd), file=stdout) if os.path.exists(disassembled_file): os.unlink(disassembled_file) subprocess.check_call(cmd, stdout=subprocess.PIPE) @@ -494,12 +498,12 @@ def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], # make sure it is a valid wast cmd = WASM_OPT + [disassembled_file, '-all', '-q'] - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' ', ' '.join(cmd), file=stdout) subprocess.check_call(cmd, stdout=subprocess.PIPE) if verify_final_result: actual = open(disassembled_file).read() - fail_if_not_identical_to_file(actual, wast + binary_suffix) + fail_if_not_identical_to_file(actual, wast + '.fromBinary') return disassembled_file diff --git a/scripts/test/support.py b/scripts/test/support.py index 6af99cabfb5..c79bf26ae6c 100644 --- a/scripts/test/support.py +++ b/scripts/test/support.py @@ -22,10 +22,10 @@ def split_wast(wastFile): - ''' - Returns a list of pairs of module definitions and assertions. + """Return a list of pairs of module definitions and assertions. + Module invalidity tests, as well as (module definition ...) and (module instance ...) are skipped. - ''' + """ # if it's a binary, leave it as is, we can't split it wast = None if not wastFile.endswith('.wasm'): @@ -137,11 +137,12 @@ def _subprocess_run(*args, **kwargs): def run_command(cmd, expected_status=0, stdout=None, stderr=None, expected_err=None, err_contains=False, err_ignore=None): - ''' + """Run a subprocess, returning its output. + stderr - None, subprocess.PIPE, subprocess.STDOUT or a file handle / io.StringIO to write stdout to stdout - File handle to print debug messages to returns the process's stdout - ''' + """ if expected_err is not None: assert stderr == subprocess.PIPE or stderr is None, \ "Can't redirect stderr if using expected_err" diff --git a/scripts/test/wasm2js.py b/scripts/test/wasm2js.py index 4af2c55d837..76b788841e9 100644 --- a/scripts/test/wasm2js.py +++ b/scripts/test/wasm2js.py @@ -16,6 +16,7 @@ import subprocess from . import shared, support +from .shared import print_heading basic_tests = shared.get_tests(os.path.join(shared.options.binaryen_test, 'lit', 'basic')) # memory64 is not supported in wasm2js yet (but may be with BigInt eventually). @@ -159,7 +160,7 @@ def test_asserts_output(): def test_wasm2js(): - print('\n[ checking wasm2js testcases... ]\n') + print_heading('checking wasm2js testcases...') check_for_stale_files() if shared.skip_if_on_windows('wasm2js'): return @@ -168,7 +169,7 @@ def test_wasm2js(): def update_wasm2js_tests(): - print('\n[ checking wasm2js ]\n') + print_heading('checking wasm2js') for opt in (0, 1): for wasm in basic_tests + spec_tests + wasm2js_tests: diff --git a/scripts/test/wasm_opt.py b/scripts/test/wasm_opt.py index 6e45d3bd6ac..de51497139c 100644 --- a/scripts/test/wasm_opt.py +++ b/scripts/test/wasm_opt.py @@ -15,12 +15,14 @@ import os import shutil import subprocess +import sys from . import shared, support +from .shared import print_heading def test_wasm_opt(): - print('\n[ checking wasm-opt -o notation... ]\n') + print_heading('checking wasm-opt -o notation...') for extra_args in [[], ['--no-validation']]: wast = os.path.join(shared.options.binaryen_test, 'hello_world.wat') @@ -30,7 +32,7 @@ def test_wasm_opt(): support.run_command(cmd) shared.fail_if_not_identical_to_file(open(out).read(), wast) - print('\n[ checking wasm-opt binary reading/writing... ]\n') + print_heading('checking wasm-opt binary reading/writing...') shutil.copyfile(os.path.join(shared.options.binaryen_test, 'hello_world.wat'), 'a.wat') shared.delete_from_orbit('a.wasm') @@ -40,7 +42,7 @@ def test_wasm_opt(): support.run_command(shared.WASM_OPT + ['a.wasm', '-o', 'b.wast', '-S', '-q']) assert open('b.wast', 'rb').read()[0] != 0, 'we emit text with -S' - print('\n[ checking wasm-opt passes... ]\n') + print_heading('checking wasm-opt passes...') for t in shared.get_tests(shared.get_test_dir('passes'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -60,6 +62,12 @@ def test_wasm_opt(): opts = [('--' + p if not p.startswith('O') and p != 'g' else '-' + p) for p in passes] actual = '' for module, asserts in support.split_wast(t): + # Flush stdout/stderr between each test. This prevent confusing + # interleaving in output of github CI + # TODO: Find a better, more systematic way to achieve this that + # works for all test suites. + sys.stdout.flush() + sys.stderr.flush() assert len(asserts) == 0 support.write_wast('split.wast', module) cmd = shared.WASM_OPT + opts + ['split.wast', '-q'] @@ -87,7 +95,7 @@ def test_wasm_opt(): with open('a.wat') as actual: shared.fail_if_not_identical_to_file(actual.read(), t + '.wat') - print('\n[ checking wasm-opt parsing & printing... ]\n') + print_heading('checking wasm-opt parsing & printing...') for t in shared.get_tests(shared.get_test_dir('print'), ['.wast']): print('..', os.path.basename(t)) @@ -104,13 +112,13 @@ def test_wasm_opt(): def update_wasm_opt_tests(): - print('\n[ updating wasm-opt -o notation... ]\n') + print_heading('updating wasm-opt -o notation...') wast = os.path.join(shared.options.binaryen_test, 'hello_world.wat') cmd = shared.WASM_OPT + [wast, '-o', 'a.wast', '-S'] support.run_command(cmd) open(wast, 'w').write(open('a.wast').read()) - print('\n[ updating wasm-opt parsing & printing... ]\n') + print_heading('updating wasm-opt parsing & printing...') for t in shared.get_tests(shared.get_test_dir('print'), ['.wast']): print('..', os.path.basename(t)) wasm = t.replace('.wast', '') @@ -126,7 +134,7 @@ def update_wasm_opt_tests(): with open(wasm + '.minified.txt', 'wb') as o: o.write(actual) - print('\n[ updating wasm-opt passes... ]\n') + print_heading('updating wasm-opt passes...') for t in shared.get_tests(shared.get_test_dir('passes'), ['.wast', '.wasm']): print('..', os.path.basename(t)) # windows has some failures that need to be investigated: diff --git a/scripts/update_help_checks.py b/scripts/update_help_checks.py index 6acaf9ee126..5471f3533a9 100755 --- a/scripts/update_help_checks.py +++ b/scripts/update_help_checks.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A test case update script for lit help checks. -""" +"""A test case update script for lit help checks.""" import os import subprocess diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index 0444b51eca7..d399fae8493 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -27,6 +27,10 @@ import sys import tempfile +from test import support + +INTERNAL_SEPARATOR = '[update-lit-checks-separator]' + script_dir = os.path.dirname(__file__) script_name = os.path.basename(__file__) @@ -70,9 +74,7 @@ def warn(msg): def itertests(args): - """ - Yield (filename, lines) for each test specified in the command line args - """ + """Yield (filename, lines) for each test specified in the command line args.""" for pattern in args.tests: tests = glob.glob(pattern, recursive=True) if not tests: @@ -110,8 +112,27 @@ def run_command(args, test, tmp, command): command = command.replace('%s', test) command = command.replace('%S', os.path.dirname(test)) command = command.replace('%t', tmp) - command = command.replace('foreach', os.path.join(script_dir, 'foreach.py')) - return subprocess.check_output(command, shell=True, env=env).decode('utf-8') + + match = re.match(r'^(.*)\s*foreach\s+(\S+)\s+(\S+)\s+(.*)$', command) + if match: + prefix = match.group(1) + infile = match.group(2) + tempfile = match.group(3) + cmd_rest = match.group(4) + + outputs = [] + for i, (module, _asserts) in enumerate(support.split_wast(infile)): + tempname = tempfile + '.' + str(i) + with open(tempname, 'w') as temp: + print(module, file=temp) + new_command = prefix + ' ' + cmd_rest + ' ' + tempname + out = subprocess.check_output(new_command, shell=True, env=env).decode('utf-8') + outputs.append(out) + + return f"\n{INTERNAL_SEPARATOR}\n".join(outputs) + else: + assert 'foreach' not in command, 'bad foreach matching: ' + command + return subprocess.check_output(command, shell=True, env=env).decode('utf-8') def find_end(module, start): @@ -185,7 +206,7 @@ def parse_output_modules(text): return modules -def parse_output_fuzz_exec(text): +def parse_output_fuzz_exec(text, first_named_item): # Returns the same data as `parse_output_modules`, but can't tell where # module boundaries are, so always just returns items for a single module. items = [] @@ -196,22 +217,42 @@ def parse_output_fuzz_exec(text): # in the input. name = '$' + func.group("name") items.append((('func', name), [line])) - elif line.startswith('[host limit'): - # Skip mentions of host limits that we hit. This can happen even - # before we reach the execution of a function (if it happens during - # instantiation of the module), in which case |items| may be empty, - # and we'd error on the code below. - pass elif line: - assert items, 'unexpected non-invocation line' - items[-1][1].append(line) + if not items: + # Early output before any export was executed. Associate it with + # the first named item, so it appears before everything else + # (which is when it executes). + items.append((first_named_item, [line])) + else: + items[-1][1].append(line) return [items] +def split_outputs(output): + return re.split(r'\n?' + re.escape(INTERNAL_SEPARATOR) + r'\n?', output) + + +def get_modules_named_items(lines): + # Return a list, one entry per module, each entry being the named items for + # that module. + modules_text = split_modules('\n'.join(lines)) + modules_named_items = [] + for module_text in modules_text: + named_items = [] + for line in module_text.split('\n'): + match = ITEM_RE.match(line) + if match: + _, kind, name = indentKindName(match) + named_items.append((kind, name)) + modules_named_items.append(named_items) + return modules_named_items + + def get_command_output(args, kind, test, lines, tmp): # Return list of maps from prefixes to lists of module items of the form # ((kind, name), [line]). The outer list has an entry for each module. command_output = [] + modules_named_items = get_modules_named_items(lines) for line in find_run_lines(test, lines): commands = [cmd.strip() for cmd in line.rsplit('|', 1)] if (len(commands) > 2 or @@ -232,12 +273,30 @@ def get_command_output(args, kind, test, lines, tmp): output = run_command(args, test, tmp, commands[0]) if prefix: - if kind == 'wat': - module_outputs = parse_output_modules(output) - elif kind == 'fuzz-exec': - module_outputs = parse_output_fuzz_exec(output) - else: - assert False, "unknown output kind" + outputs = split_outputs(output) + module_outputs = [] + if len(outputs) != len(modules_named_items): + warn(f'Mismatch between output parts ({len(outputs)}) and ' + f'input modules ({len(modules_named_items)}).') + for i, out in enumerate(outputs): + if i >= len(modules_named_items): + break + mod_named_items = modules_named_items[i] + first_named_item = mod_named_items[0] if mod_named_items else None + if kind == 'wat': + mod_out = parse_output_modules(out) + if mod_out: + module_outputs.append(mod_out[0]) + else: + module_outputs.append([]) + elif kind == 'fuzz-exec': + mod_out = parse_output_fuzz_exec(out, first_named_item) + if mod_out: + module_outputs.append(mod_out[0]) + else: + module_outputs.append([]) + else: + assert False, "unknown output kind" for i in range(len(module_outputs)): if len(command_output) == i: command_output.append({}) @@ -261,6 +320,13 @@ def update_test(args, test, lines, tmp): # Skip the notice if it is already in the output lines = lines[1:] + named_items = [] + for line in lines: + match = ITEM_RE.match(line) + if match: + _, kind, name = indentKindName(match) + named_items.append((kind, name)) + command_output = get_command_output(args, output_kind, test, lines, tmp) prefixes = {prefix for module_output in command_output for prefix in module_output.keys()} @@ -277,13 +343,6 @@ def update_test(args, test, lines, tmp): filtered.append(lines[-1]) lines = filtered - named_items = [] - for line in lines: - match = ITEM_RE.match(line) - if match: - _, kind, name = indentKindName(match) - named_items.append((kind, name)) - notice_args = '' if all_items: notice_args += ' --all-items' diff --git a/src/analysis/lattice.h b/src/analysis/lattice.h index 977bea0d5a9..719bae69128 100644 --- a/src/analysis/lattice.h +++ b/src/analysis/lattice.h @@ -17,9 +17,9 @@ #ifndef wasm_analysis_lattice_h #define wasm_analysis_lattice_h -#if __cplusplus >= 202002L +#if __has_include() #include -#endif // __cplusplus >= 202002L +#endif namespace wasm::analysis { @@ -37,7 +37,7 @@ inline LatticeComparison reverseComparison(LatticeComparison comparison) { } } -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template concept Lattice = requires(const L& lattice, @@ -61,7 +61,7 @@ concept Lattice = requires(const L& lattice, // The analysis framework only uses bottom elements and least upper bounds (i.e. // joins) directly, so lattices do not necessarily need to implement top -// elements and greatest lower bounds (i.e. meets) to be useable, even though +// elements and greatest lower bounds (i.e. meets) to be usable, even though // they are required for mathematical lattices. Implementing top elements and // meets does have the benefit of making a lattice generically invertable, // though. See lattices/inverted.h. @@ -78,12 +78,12 @@ concept FullLattice = { lattice.meet(elem, constElem) } noexcept -> std::same_as; }; -#else // __cplusplus >= 202002L +#else // defined(__cpp_lib_concepts) #define Lattice typename #define FullLattice typename -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/abstraction.h b/src/analysis/lattices/abstraction.h index bc503518c9f..13a7043767e 100644 --- a/src/analysis/lattices/abstraction.h +++ b/src/analysis/lattices/abstraction.h @@ -22,7 +22,7 @@ #include "../lattice.h" #include "support/utilities.h" -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) #include "analysis/lattices/bool.h" #endif @@ -218,7 +218,7 @@ template struct Abstraction { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/array.h b/src/analysis/lattices/array.h index 7ac0273022b..8cc637e93b9 100644 --- a/src/analysis/lattices/array.h +++ b/src/analysis/lattices/array.h @@ -54,7 +54,7 @@ template struct Array { } Element getTop() const noexcept -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) requires FullLattice #endif { @@ -101,7 +101,7 @@ template struct Array { // Pairwise meet on the elements. bool meet(Element& meetee, const Element& meeter) const noexcept -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) requires FullLattice #endif { @@ -113,7 +113,7 @@ template struct Array { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(Lattice, 1>>); #endif diff --git a/src/analysis/lattices/bool.h b/src/analysis/lattices/bool.h index ee1b2149f79..8dba0bdcbc9 100644 --- a/src/analysis/lattices/bool.h +++ b/src/analysis/lattices/bool.h @@ -75,9 +75,9 @@ struct Bool { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/conetype.h b/src/analysis/lattices/conetype.h index 5e5484fc630..2f2bda74b2c 100644 --- a/src/analysis/lattices/conetype.h +++ b/src/analysis/lattices/conetype.h @@ -174,7 +174,7 @@ struct ConeType { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice); static_assert(FullLattice); #endif diff --git a/src/analysis/lattices/flat.h b/src/analysis/lattices/flat.h index aa26b101d99..a26b145c65c 100644 --- a/src/analysis/lattices/flat.h +++ b/src/analysis/lattices/flat.h @@ -21,7 +21,7 @@ #include #include -#if __cplusplus >= 202002L +#if __has_include() #include #endif @@ -30,7 +30,7 @@ namespace wasm::analysis { -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template concept Flattenable = std::copyable && std::equality_comparable; @@ -118,7 +118,7 @@ struct Flat { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/int.h b/src/analysis/lattices/int.h index 28c39fab6e8..280918099be 100644 --- a/src/analysis/lattices/int.h +++ b/src/analysis/lattices/int.h @@ -26,7 +26,7 @@ namespace wasm::analysis { // The lattice of integers of the given type `T`, ordered by <. The min integer // is the bottom element and the max integer is the top element. -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template #else template @@ -59,12 +59,12 @@ using UInt32 = Integer; using Int64 = Integer; using UInt64 = Integer; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); static_assert(FullLattice); static_assert(FullLattice); static_assert(FullLattice); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/inverted.h b/src/analysis/lattices/inverted.h index b70e58968e3..917c5710899 100644 --- a/src/analysis/lattices/inverted.h +++ b/src/analysis/lattices/inverted.h @@ -52,7 +52,7 @@ template struct Inverted { // Deduction guide. template Inverted(L&&) -> Inverted; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/lift.h b/src/analysis/lattices/lift.h index ec0f57967a3..ddd1ee96dc6 100644 --- a/src/analysis/lattices/lift.h +++ b/src/analysis/lattices/lift.h @@ -77,7 +77,7 @@ template struct Lift { // Deduction guide. template Lift(L&&) -> Lift; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/shared.h b/src/analysis/lattices/shared.h index f345014b93d..60e627613ec 100644 --- a/src/analysis/lattices/shared.h +++ b/src/analysis/lattices/shared.h @@ -121,9 +121,9 @@ template struct SharedPath { // Deduction guide. template SharedPath(L&&) -> SharedPath; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/stack.h b/src/analysis/lattices/stack.h index 494d9f037d8..1f3908a4e02 100644 --- a/src/analysis/lattices/stack.h +++ b/src/analysis/lattices/stack.h @@ -185,7 +185,7 @@ template struct Stack { // Deduction guide. template Stack(L&&) -> Stack; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/tuple.h b/src/analysis/lattices/tuple.h index d63d81f47f0..fe122df0620 100644 --- a/src/analysis/lattices/tuple.h +++ b/src/analysis/lattices/tuple.h @@ -137,7 +137,7 @@ template struct Tuple { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(FullLattice>); #endif diff --git a/src/analysis/lattices/valtype.h b/src/analysis/lattices/valtype.h index d63432ac668..eda53aa567f 100644 --- a/src/analysis/lattices/valtype.h +++ b/src/analysis/lattices/valtype.h @@ -73,7 +73,7 @@ struct ValType { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); #endif diff --git a/src/analysis/lattices/vector.h b/src/analysis/lattices/vector.h index 050b0f40812..930d0f934b6 100644 --- a/src/analysis/lattices/vector.h +++ b/src/analysis/lattices/vector.h @@ -154,7 +154,7 @@ template struct Vector { // Deduction guide. template Vector(L&&, size_t) -> Vector; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(Lattice>>); #endif diff --git a/src/analysis/monotone-analyzer.h b/src/analysis/monotone-analyzer.h index 91251103248..a24c38baef1 100644 --- a/src/analysis/monotone-analyzer.h +++ b/src/analysis/monotone-analyzer.h @@ -22,7 +22,7 @@ template class MonotoneCFGAnalyzer { std::vector states; public: - // Will constuct BlockState objects corresponding to BasicBlocks from the + // Will construct BlockState objects corresponding to BasicBlocks from the // given CFG. MonotoneCFGAnalyzer(L& lattice, TxFn& txfn, CFG& cfg); diff --git a/src/analysis/reaching-definitions-transfer-function.h b/src/analysis/reaching-definitions-transfer-function.h index 7a4fe1afcbe..3cca04839d4 100644 --- a/src/analysis/reaching-definitions-transfer-function.h +++ b/src/analysis/reaching-definitions-transfer-function.h @@ -26,7 +26,7 @@ namespace wasm::analysis { // When collecting results, the transfer function takes the states and converts // it into a map of LocalGets to LocalSets which affect it. The fictitious -// inital value LocalSetes will be converted to nullptrs. +// initial value LocalSetes will be converted to nullptrs. class ReachingDefinitionsTransferFunction : public VisitorTransferFunc fakeInitialValueSets; @@ -54,7 +54,7 @@ class ReachingDefinitionsTransferFunction // Helper function which creates fictitious LocalSets for a function, // inserts them into fakeInitialValueSets and fakeSetPtrs. It returns a // vector of actual LocalSets in the function and fictitious LocalSets for - // use when instatitating the lattice. + // use when instantiating the lattice. static std::vector listLocalSets(Function* func, std::vector& fakeInitialValueSets, diff --git a/src/analysis/transfer-function.h b/src/analysis/transfer-function.h index 58d2033efdf..02010a85916 100644 --- a/src/analysis/transfer-function.h +++ b/src/analysis/transfer-function.h @@ -17,11 +17,16 @@ #ifndef wasm_analysis_transfer_function_h #define wasm_analysis_transfer_function_h -#if __cplusplus >= 202002L - +#if __has_include() #include -#include +#endif +#if __has_include() #include +#endif + +#if defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) + +#include #include "cfg.h" #include "lattice.h" @@ -47,10 +52,10 @@ concept TransferFunctionImpl = requires( } // namespace wasm::analysis -#else // __cplusplus >= 202002L +#else // defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) #define TransferFunction typename -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) #endif // wasm_analysis_transfer_function_h diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index d7dcb8224f0..a0f613aa5a2 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -508,6 +508,9 @@ BinaryenFeatures BinaryenFeatureMultibyte(void) { BinaryenFeatures BinaryenFeatureCustomPageSizes(void) { return static_cast(FeatureSet::CustomPageSizes); } +BinaryenFeatures BinaryenFeatureWideArithmetic(void) { + return static_cast(FeatureSet::WideArithmetic); +} BinaryenFeatures BinaryenFeatureAll(void) { return static_cast(FeatureSet::All); } @@ -687,6 +690,10 @@ BinaryenOp BinaryenLtFloat64(void) { return LtFloat64; } BinaryenOp BinaryenLeFloat64(void) { return LeFloat64; } BinaryenOp BinaryenGtFloat64(void) { return GtFloat64; } BinaryenOp BinaryenGeFloat64(void) { return GeFloat64; } +BinaryenOp BinaryenAddInt128(void) { return AddInt128; } +BinaryenOp BinaryenSubInt128(void) { return SubInt128; } +BinaryenOp BinaryenMulWideSInt64(void) { return MulWideSInt64; } +BinaryenOp BinaryenMulWideUInt64(void) { return MulWideUInt64; } BinaryenOp BinaryenAtomicRMWAdd(void) { return RMWAdd; } BinaryenOp BinaryenAtomicRMWSub(void) { return RMWSub; } BinaryenOp BinaryenAtomicRMWAnd(void) { return RMWAnd; } @@ -795,12 +802,20 @@ BinaryenOp BinaryenRelaxedMaddVecF32x4(void) { return RelaxedMaddVecF32x4; } BinaryenOp BinaryenRelaxedNmaddVecF32x4(void) { return RelaxedNmaddVecF32x4; } BinaryenOp BinaryenRelaxedMaddVecF64x2(void) { return RelaxedMaddVecF64x2; } BinaryenOp BinaryenRelaxedNmaddVecF64x2(void) { return RelaxedNmaddVecF64x2; } -BinaryenOp BinaryenLaneselectI8x16(void) { return LaneselectI8x16; } -BinaryenOp BinaryenLaneselectI16x8(void) { return LaneselectI16x8; } -BinaryenOp BinaryenLaneselectI32x4(void) { return LaneselectI32x4; } -BinaryenOp BinaryenLaneselectI64x2(void) { return LaneselectI64x2; } -BinaryenOp BinaryenDotI8x16I7x16AddSToVecI32x4(void) { - return DotI8x16I7x16AddSToVecI32x4; +BinaryenOp BinaryenRelaxedLaneselectI8x16(void) { + return RelaxedLaneselectI8x16; +} +BinaryenOp BinaryenRelaxedLaneselectI16x8(void) { + return RelaxedLaneselectI16x8; +} +BinaryenOp BinaryenRelaxedLaneselectI32x4(void) { + return RelaxedLaneselectI32x4; +} +BinaryenOp BinaryenRelaxedLaneselectI64x2(void) { + return RelaxedLaneselectI64x2; +} +BinaryenOp BinaryenRelaxedDotI8x16I7x16AddSToVecI32x4(void) { + return RelaxedDotI8x16I7x16AddSToVecI32x4; } BinaryenOp BinaryenAnyTrueVec128(void) { return AnyTrueVec128; } BinaryenOp BinaryenAbsVecI8x16(void) { return AbsVecI8x16; } @@ -1021,6 +1036,9 @@ BinaryenOp BinaryenDemoteZeroVecF64x2ToVecF32x4(void) { BinaryenOp BinaryenPromoteLowVecF32x4ToVecF64x2(void) { return PromoteLowVecF32x4ToVecF64x2; } +BinaryenOp BinaryenPromoteLowVecF16x8ToVecF32x4(void) { + return PromoteLowVecF16x8ToVecF32x4; +} BinaryenOp BinaryenRelaxedTruncSVecF32x4ToVecI32x4(void) { return RelaxedTruncSVecF32x4ToVecI32x4; } @@ -1044,8 +1062,8 @@ BinaryenOp BinaryenRelaxedMaxVecF64x2(void) { return RelaxedMaxVecF64x2; } BinaryenOp BinaryenRelaxedQ15MulrSVecI16x8(void) { return RelaxedQ15MulrSVecI16x8; } -BinaryenOp BinaryenDotI8x16I7x16SToVecI16x8(void) { - return DotI8x16I7x16SToVecI16x8; +BinaryenOp BinaryenRelaxedDotI8x16I7x16SToVecI16x8(void) { + return RelaxedDotI8x16I7x16SToVecI16x8; } BinaryenOp BinaryenRefAsNonNull(void) { return RefAsNonNull; } BinaryenOp BinaryenRefAsExternInternalize(void) { return AnyConvertExtern; } @@ -1309,6 +1327,26 @@ BinaryenExpressionRef BinaryenBinary(BinaryenModuleRef module, Builder(*(Module*)module) .makeBinary(BinaryOp(op), (Expression*)left, (Expression*)right)); } +BinaryenExpressionRef BinaryenWideIntAddSub(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef leftLow, + BinaryenExpressionRef leftHigh, + BinaryenExpressionRef rightLow, + BinaryenExpressionRef rightHigh) { + return Builder(*(Module*)module) + .makeWideIntAddSub(WideIntAddSubOp(op), + (Expression*)leftLow, + (Expression*)leftHigh, + (Expression*)rightLow, + (Expression*)rightHigh); +} +BinaryenExpressionRef BinaryenWideIntMul(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef left, + BinaryenExpressionRef right) { + return Builder(*(Module*)module) + .makeWideIntMul(WideIntMulOp(op), (Expression*)left, (Expression*)right); +} BinaryenExpressionRef BinaryenSelect(BinaryenModuleRef module, BinaryenExpressionRef condition, BinaryenExpressionRef ifTrue, @@ -2940,6 +2978,105 @@ void BinaryenBinarySetRight(BinaryenExpressionRef expr, assert(rightExpr); static_cast(expression)->right = (Expression*)rightExpr; } +// WideIntAddSub +BinaryenOp BinaryenWideIntAddSubGetOp(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->op; +} +void BinaryenWideIntAddSubSetOp(BinaryenExpressionRef expr, BinaryenOp op) { + auto* expression = (Expression*)expr; + assert(expression->is()); + static_cast(expression)->op = WideIntAddSubOp(op); +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftLow(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->leftLow; +} +void BinaryenWideIntAddSubSetLeftLow(BinaryenExpressionRef expr, + BinaryenExpressionRef leftLowExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftLowExpr); + static_cast(expression)->leftLow = (Expression*)leftLowExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftHigh(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->leftHigh; +} +void BinaryenWideIntAddSubSetLeftHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef leftHighExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftHighExpr); + static_cast(expression)->leftHigh = (Expression*)leftHighExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetRightLow(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->rightLow; +} +void BinaryenWideIntAddSubSetRightLow(BinaryenExpressionRef expr, + BinaryenExpressionRef rightLowExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightLowExpr); + static_cast(expression)->rightLow = (Expression*)rightLowExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetRightHigh(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->rightHigh; +} +void BinaryenWideIntAddSubSetRightHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef rightHighExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightHighExpr); + static_cast(expression)->rightHigh = + (Expression*)rightHighExpr; +} +// WideIntMul +BinaryenOp BinaryenWideIntMulGetOp(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->op; +} +void BinaryenWideIntMulSetOp(BinaryenExpressionRef expr, BinaryenOp op) { + auto* expression = (Expression*)expr; + assert(expression->is()); + static_cast(expression)->op = WideIntMulOp(op); +} +BinaryenExpressionRef BinaryenWideIntMulGetLeft(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->left; +} +void BinaryenWideIntMulSetLeft(BinaryenExpressionRef expr, + BinaryenExpressionRef leftExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftExpr); + static_cast(expression)->left = (Expression*)leftExpr; +} +BinaryenExpressionRef BinaryenWideIntMulGetRight(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->right; +} +void BinaryenWideIntMulSetRight(BinaryenExpressionRef expr, + BinaryenExpressionRef rightExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightExpr); + static_cast(expression)->right = (Expression*)rightExpr; +} // Select BinaryenExpressionRef BinaryenSelectGetIfTrue(BinaryenExpressionRef expr) { auto* expression = (Expression*)expr; @@ -5420,7 +5557,7 @@ BinaryenIndex BinaryenGetNumElementSegments(BinaryenModuleRef module) { } BinaryenExpressionRef BinaryenElementSegmentGetOffset(BinaryenElementSegmentRef elem) { - if (((ElementSegment*)elem)->table.isNull()) { + if (((ElementSegment*)elem)->isPassive()) { Fatal() << "elem segment is passive."; } return ((ElementSegment*)elem)->offset; @@ -5474,12 +5611,12 @@ void BinaryenSetMemory(BinaryenModuleRef module, for (BinaryenIndex i = 0; i < numSegments; i++) { auto explicitName = segmentNames && segmentNames[i]; auto name = explicitName ? Name(segmentNames[i]) : Name::fromInt(i); - auto curr = Builder::makeDataSegment(name, - memory->name, - segmentPassives[i], - (Expression*)segmentOffsets[i], - segmentDatas[i], - segmentSizes[i]); + auto curr = + Builder::makeDataSegment(name, + segmentPassives[i] ? Name() : memory->name, + (Expression*)segmentOffsets[i], + segmentDatas[i], + segmentSizes[i]); curr->hasExplicitName = explicitName; ((Module*)module)->addDataSegment(std::move(curr)); } @@ -5489,7 +5626,7 @@ void BinaryenSetMemory(BinaryenModuleRef module, // Memory segments -uint32_t BinaryenGetNumMemorySegments(BinaryenModuleRef module) { +uint32_t BinaryenGetNumDataSegments(BinaryenModuleRef module) { return ((Module*)module)->dataSegments.size(); } BinaryenDataSegmentRef BinaryenGetDataSegment(BinaryenModuleRef module, @@ -5507,8 +5644,8 @@ BinaryenDataSegmentRef BinaryenGetDataSegmentByIndex(BinaryenModuleRef module, const char* BinaryenDataSegmentGetName(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->name.str.data(); } -uint32_t BinaryenGetMemorySegmentByteOffset(BinaryenModuleRef module, - BinaryenDataSegmentRef segment) { +uint32_t BinaryenGetDataSegmentByteOffset(BinaryenModuleRef module, + BinaryenDataSegmentRef segment) { auto* wasm = (Module*)module; auto globalOffset = [&](const Expression* const& expr, @@ -5625,14 +5762,13 @@ bool BinaryenMemoryIs64(BinaryenModuleRef module, const char* name) { } return memory->is64(); } -size_t BinaryenGetMemorySegmentByteLength(BinaryenDataSegmentRef segment) { +size_t BinaryenGetDataSegmentByteLength(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->data.size(); } -bool BinaryenGetMemorySegmentPassive(BinaryenDataSegmentRef segment) { - return ((DataSegment*)segment)->isPassive; +bool BinaryenGetDataSegmentPassive(BinaryenDataSegmentRef segment) { + return ((DataSegment*)segment)->isPassive(); } -void BinaryenCopyMemorySegmentData(BinaryenDataSegmentRef segment, - char* buffer) { +void BinaryenCopyDataSegmentData(BinaryenDataSegmentRef segment, char* buffer) { std::copy(((DataSegment*)segment)->data.cbegin(), ((DataSegment*)segment)->data.cend(), buffer); @@ -5647,12 +5783,12 @@ void BinaryenAddDataSegment(BinaryenModuleRef module, auto* wasm = (Module*)module; auto name = segmentName ? Name(segmentName) : Name::fromInt(wasm->dataSegments.size()); - auto curr = Builder::makeDataSegment(name, - memoryName ? memoryName : "0", - segmentPassive, - (Expression*)segmentOffset, - segmentData, - segmentSize); + auto curr = Builder::makeDataSegment( + name, + segmentPassive ? Name() : (memoryName ? memoryName : "0"), + (Expression*)segmentOffset, + segmentData, + segmentSize); curr->hasExplicitName = segmentName ? true : false; wasm->addDataSegment(std::move(curr)); } @@ -5752,9 +5888,13 @@ void BinaryenSetTrapsNeverHappen(bool on) { globalPassOptions.trapsNeverHappen = on; } -bool BinaryenGetClosedWorld(void) { return globalPassOptions.closedWorld; } +bool BinaryenGetClosedWorld(void) { + return globalPassOptions.worldMode == WorldMode::Closed; +} -void BinaryenSetClosedWorld(bool on) { globalPassOptions.closedWorld = on; } +void BinaryenSetClosedWorld(bool on) { + globalPassOptions.worldMode = on ? WorldMode::Closed : WorldMode::Open; +} bool BinaryenGetLowMemoryUnused(void) { return globalPassOptions.lowMemoryUnused; @@ -6193,7 +6333,7 @@ void BinaryenElementSegmentSetTable(BinaryenElementSegmentRef elem, ((ElementSegment*)elem)->table = table; } bool BinaryenElementSegmentIsPassive(BinaryenElementSegmentRef elem) { - return ((ElementSegment*)elem)->table.isNull(); + return ((ElementSegment*)elem)->isPassive(); } // diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 10225ce027a..f798496ad1c 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -58,7 +58,9 @@ #if defined(__EMSCRIPTEN__) #include #define BINARYEN_API EMSCRIPTEN_KEEPALIVE -#elif defined(_MSC_VER) && !defined(BUILD_STATIC_LIBRARY) +#elif defined(_MSC_VER) && defined(BUILD_SHARED_LIBS) +// TODO: This is not yet used since we disabled BUILD_SHARED_LIBS under +// _MSC_VER in CMakeLists.txt #define BINARYEN_API __declspec(dllexport) #else #define BINARYEN_API @@ -246,6 +248,7 @@ BINARYEN_API BinaryenFeatures BinaryenFeatureCallIndirectOverlong(void); BINARYEN_API BinaryenFeatures BinaryenFeatureRelaxedAtomics(void); BINARYEN_API BinaryenFeatures BinaryenFeatureMultibyte(void); BINARYEN_API BinaryenFeatures BinaryenFeatureCustomPageSizes(void); +BINARYEN_API BinaryenFeatures BinaryenFeatureWideArithmetic(void); BINARYEN_API BinaryenFeatures BinaryenFeatureAll(void); // Modules @@ -430,6 +433,10 @@ BINARYEN_API BinaryenOp BinaryenLtFloat64(void); BINARYEN_API BinaryenOp BinaryenLeFloat64(void); BINARYEN_API BinaryenOp BinaryenGtFloat64(void); BINARYEN_API BinaryenOp BinaryenGeFloat64(void); +BINARYEN_API BinaryenOp BinaryenAddInt128(void); +BINARYEN_API BinaryenOp BinaryenSubInt128(void); +BINARYEN_API BinaryenOp BinaryenMulWideSInt64(void); +BINARYEN_API BinaryenOp BinaryenMulWideUInt64(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWAdd(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWSub(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWAnd(void); @@ -522,11 +529,11 @@ BINARYEN_API BinaryenOp BinaryenRelaxedMaddVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedNmaddVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedMaddVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedNmaddVecF64x2(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI8x16(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI16x8(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI32x4(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI64x2(void); -BINARYEN_API BinaryenOp BinaryenDotI8x16I7x16AddSToVecI32x4(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI8x16(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI16x8(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI32x4(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI64x2(void); +BINARYEN_API BinaryenOp BinaryenRelaxedDotI8x16I7x16AddSToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenAnyTrueVec128(void); BINARYEN_API BinaryenOp BinaryenPopcntVecI8x16(void); BINARYEN_API BinaryenOp BinaryenAbsVecI8x16(void); @@ -684,6 +691,7 @@ BINARYEN_API BinaryenOp BinaryenTruncSatZeroSVecF64x2ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenTruncSatZeroUVecF64x2ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenDemoteZeroVecF64x2ToVecF32x4(void); BINARYEN_API BinaryenOp BinaryenPromoteLowVecF32x4ToVecF64x2(void); +BINARYEN_API BinaryenOp BinaryenPromoteLowVecF16x8ToVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncSVecF32x4ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncUVecF32x4ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncZeroSVecF64x2ToVecI32x4(void); @@ -695,7 +703,7 @@ BINARYEN_API BinaryenOp BinaryenRelaxedMaxVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedMinVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedMaxVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedQ15MulrSVecI16x8(void); -BINARYEN_API BinaryenOp BinaryenDotI8x16I7x16SToVecI16x8(void); +BINARYEN_API BinaryenOp BinaryenRelaxedDotI8x16I7x16SToVecI16x8(void); BINARYEN_API BinaryenOp BinaryenRefAsNonNull(void); BINARYEN_API BinaryenOp BinaryenRefAsExternInternalize(void); BINARYEN_API BinaryenOp BinaryenRefAsExternExternalize(void); @@ -839,6 +847,18 @@ BINARYEN_API BinaryenExpressionRef BinaryenBinary(BinaryenModuleRef module, BinaryenExpressionRef left, BinaryenExpressionRef right); BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSub(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef leftLow, + BinaryenExpressionRef leftHigh, + BinaryenExpressionRef rightLow, + BinaryenExpressionRef rightHigh); +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMul(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef left, + BinaryenExpressionRef right); +BINARYEN_API BinaryenExpressionRef BinaryenSelect(BinaryenModuleRef module, BinaryenExpressionRef condition, BinaryenExpressionRef ifTrue, @@ -1702,6 +1722,62 @@ BinaryenBinaryGetRight(BinaryenExpressionRef expr); BINARYEN_API void BinaryenBinarySetRight(BinaryenExpressionRef expr, BinaryenExpressionRef rightExpr); +// WideIntAddSub + +// Gets the operation being performed by a wide int add/sub expression. +BINARYEN_API BinaryenOp BinaryenWideIntAddSubGetOp(BinaryenExpressionRef expr); +// Sets the operation being performed by a wide int add/sub expression. +BINARYEN_API void BinaryenWideIntAddSubSetOp(BinaryenExpressionRef expr, + BinaryenOp op); +// Gets the left low expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftLow(BinaryenExpressionRef expr); +// Sets the left low expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetLeftLow(BinaryenExpressionRef expr, + BinaryenExpressionRef leftLowExpr); +// Gets the left high expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftHigh(BinaryenExpressionRef expr); +// Sets the left high expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetLeftHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef leftHighExpr); +// Gets the right low expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetRightLow(BinaryenExpressionRef expr); +// Sets the right low expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetRightLow(BinaryenExpressionRef expr, + BinaryenExpressionRef rightLowExpr); +// Gets the right high expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetRightHigh(BinaryenExpressionRef expr); +// Sets the right high expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetRightHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef rightHighExpr); + +// WideIntMul + +// Gets the operation being performed by a wide int mul expression. +BINARYEN_API BinaryenOp BinaryenWideIntMulGetOp(BinaryenExpressionRef expr); +// Sets the operation being performed by a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetOp(BinaryenExpressionRef expr, + BinaryenOp op); +// Gets the left expression of a wide int mul expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMulGetLeft(BinaryenExpressionRef expr); +// Sets the left expression of a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetLeft(BinaryenExpressionRef expr, + BinaryenExpressionRef leftExpr); +// Gets the right expression of a wide int mul expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMulGetRight(BinaryenExpressionRef expr); +// Sets the right expression of a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetRight(BinaryenExpressionRef expr, + BinaryenExpressionRef rightExpr); + // Select // Gets the expression becoming selected by a `select` expression if the @@ -3003,25 +3079,24 @@ BINARYEN_API bool BinaryenMemoryIsShared(BinaryenModuleRef module, BINARYEN_API bool BinaryenMemoryIs64(BinaryenModuleRef module, const char* name); -// Memory segments. Query utilities. +// Data segments. Query utilities. BINARYEN_REF(DataSegment); -BINARYEN_API uint32_t BinaryenGetNumMemorySegments(BinaryenModuleRef module); +BINARYEN_API uint32_t BinaryenGetNumDataSegments(BinaryenModuleRef module); BINARYEN_API BinaryenDataSegmentRef BinaryenGetDataSegment(BinaryenModuleRef module, const char* segmentName); BINARYEN_API BinaryenDataSegmentRef BinaryenGetDataSegmentByIndex(BinaryenModuleRef module, BinaryenIndex index); BINARYEN_API const char* BinaryenDataSegmentGetName(BinaryenDataSegmentRef segment); -BINARYEN_API uint32_t BinaryenGetMemorySegmentByteOffset( +BINARYEN_API uint32_t BinaryenGetDataSegmentByteOffset( BinaryenModuleRef module, BinaryenDataSegmentRef segment); BINARYEN_API size_t -BinaryenGetMemorySegmentByteLength(BinaryenDataSegmentRef segment); -BINARYEN_API bool -BinaryenGetMemorySegmentPassive(BinaryenDataSegmentRef segment); -BINARYEN_API void BinaryenCopyMemorySegmentData(BinaryenDataSegmentRef segment, - char* buffer); +BinaryenGetDataSegmentByteLength(BinaryenDataSegmentRef segment); +BINARYEN_API bool BinaryenGetDataSegmentPassive(BinaryenDataSegmentRef segment); +BINARYEN_API void BinaryenCopyDataSegmentData(BinaryenDataSegmentRef segment, + char* buffer); BINARYEN_API void BinaryenAddDataSegment(BinaryenModuleRef module, const char* segmentName, const char* memoryName, @@ -3062,7 +3137,7 @@ BINARYEN_API void BinaryenModulePrintStackIR(BinaryenModuleRef module); BINARYEN_API void BinaryenModulePrintAsmjs(BinaryenModuleRef module); // Validate a module, showing errors on problems. -// @return 0 if an error occurred, 1 if validated succesfully +// @return 0 if an error occurred, 1 if validated successfully BINARYEN_API bool BinaryenModuleValidate(BinaryenModuleRef module); // Runs the standard optimization passes on the module. Uses the currently set @@ -3283,7 +3358,7 @@ BINARYEN_API BinaryenModuleAllocateAndWriteResult BinaryenModuleAllocateAndWrite(BinaryenModuleRef module, const char* sourceMapUrl); -// Serialize a module in s-expression form. Implicity allocates the returned +// Serialize a module in s-expression form. Implicitly allocates the returned // char* with malloc(), and expects the user to free() them manually // once not needed anymore. BINARYEN_API char* BinaryenModuleAllocateAndWriteText(BinaryenModuleRef module); diff --git a/src/cfg/Relooper.h b/src/cfg/Relooper.h index 28a19033196..b89c8a39ffd 100644 --- a/src/cfg/Relooper.h +++ b/src/cfg/Relooper.h @@ -15,7 +15,7 @@ */ /* -This is an optimized C++ implemention of the Relooper algorithm originally +This is an optimized C++ implementation of the Relooper algorithm originally developed as part of Emscripten. This implementation includes optimizations added since the original academic paper [1] was published about it. diff --git a/src/cfg/cfg-traversal.h b/src/cfg/cfg-traversal.h index 2afc76a6835..ca5021e9696 100644 --- a/src/cfg/cfg-traversal.h +++ b/src/cfg/cfg-traversal.h @@ -304,7 +304,7 @@ struct CFGWalker : public PostWalker { } } - // Exception thrown. Note outselves so that we will create a link to each + // Exception thrown. Note ourselves so that we will create a link to each // catch within the try / each destination block within the try_table when // we get there. self->throwingInstsStack[i].push_back(self->currBasicBlock); diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index 132806efcf1..2560826ea80 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -5,7 +5,7 @@ // NOLINTBEGIN auto op = *keyword; -char buf[33] = {}; +char buf[36] = {}; // Ensure we do not copy more than the buffer can hold if (op.size() >= sizeof(buf)) { goto parse_error; @@ -505,12 +505,34 @@ switch (buf[0]) { default: goto parse_error; } } - case 'd': - if (op == "f16x8.div"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DivVecF16x8)); - return Ok{}; + case 'd': { + switch (buf[7]) { + case 'e': { + switch (buf[14]) { + case '3': + if (op == "f16x8.demote_f32x4_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::DemoteZeroVecF32x4ToVecF16x8)); + return Ok{}; + } + goto parse_error; + case '6': + if (op == "f16x8.demote_f64x2_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::DemoteZeroVecF64x2ToVecF16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; + } + } + case 'i': + if (op == "f16x8.div"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DivVecF16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'e': { switch (buf[7]) { case 'q': @@ -1127,16 +1149,27 @@ switch (buf[0]) { } } case 'p': { - switch (buf[8]) { - case 'a': - if (op == "f32x4.pmax"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMaxVecF32x4)); - return Ok{}; + switch (buf[7]) { + case 'm': { + switch (buf[8]) { + case 'a': + if (op == "f32x4.pmax"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMaxVecF32x4)); + return Ok{}; + } + goto parse_error; + case 'i': + if (op == "f32x4.pmin"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMinVecF32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; - case 'i': - if (op == "f32x4.pmin"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMinVecF32x4)); + } + case 'r': + if (op == "f32x4.promote_low_f16x8"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::PromoteLowVecF16x8ToVecF32x4)); return Ok{}; } goto parse_error; @@ -1819,12 +1852,6 @@ switch (buf[0]) { return Ok{}; } goto parse_error; - case 'd': - if (op == "i16x8.dot_i8x16_i7x16_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotI8x16I7x16SToVecI16x8)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[7]) { case 'q': @@ -1994,12 +2021,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i16x8.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI16x8)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -2129,12 +2150,29 @@ switch (buf[0]) { goto parse_error; case 'r': { switch (buf[8]) { - case 'l': - if (op == "i16x8.relaxed_q15mulr_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedQ15MulrSVecI16x8)); - return Ok{}; + case 'l': { + switch (buf[14]) { + case 'd': + if (op == "i16x8.relaxed_dot_i8x16_i7x16_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedDotI8x16I7x16SToVecI16x8)); + return Ok{}; + } + goto parse_error; + case 'l': + if (op == "i16x8.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI16x8)); + return Ok{}; + } + goto parse_error; + case 'q': + if (op == "i16x8.relaxed_q15mulr_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedQ15MulrSVecI16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'p': if (op == "i16x8.replace_lane"sv) { CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI16x8, 8)); @@ -2965,23 +3003,12 @@ switch (buf[0]) { return Ok{}; } goto parse_error; - case 'd': { - switch (buf[11]) { - case '1': - if (op == "i32x4.dot_i16x8_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotSVecI16x8ToVecI32x4)); - return Ok{}; - } - goto parse_error; - case '8': - if (op == "i32x4.dot_i8x16_i7x16_add_s"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::DotI8x16I7x16AddSToVecI32x4)); - return Ok{}; - } - goto parse_error; - default: goto parse_error; + case 'd': + if (op == "i32x4.dot_i16x8_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotSVecI16x8ToVecI32x4)); + return Ok{}; } - } + goto parse_error; case 'e': { switch (buf[7]) { case 'q': @@ -3140,12 +3167,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i32x4.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI32x4)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -3248,38 +3269,55 @@ switch (buf[0]) { case 'r': { switch (buf[8]) { case 'l': { - switch (buf[21]) { - case '3': { - switch (buf[26]) { - case 's': - if (op == "i32x4.relaxed_trunc_f32x4_s"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncSVecF32x4ToVecI32x4)); - return Ok{}; - } - goto parse_error; - case 'u': - if (op == "i32x4.relaxed_trunc_f32x4_u"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncUVecF32x4ToVecI32x4)); - return Ok{}; - } - goto parse_error; - default: goto parse_error; + switch (buf[14]) { + case 'd': + if (op == "i32x4.relaxed_dot_i8x16_i7x16_add_s"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedDotI8x16I7x16AddSToVecI32x4)); + return Ok{}; } - } - case '6': { - switch (buf[26]) { - case 's': - if (op == "i32x4.relaxed_trunc_f64x2_s_zero"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroSVecF64x2ToVecI32x4)); - return Ok{}; + goto parse_error; + case 'l': + if (op == "i32x4.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI32x4)); + return Ok{}; + } + goto parse_error; + case 't': { + switch (buf[21]) { + case '3': { + switch (buf[26]) { + case 's': + if (op == "i32x4.relaxed_trunc_f32x4_s"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncSVecF32x4ToVecI32x4)); + return Ok{}; + } + goto parse_error; + case 'u': + if (op == "i32x4.relaxed_trunc_f32x4_u"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncUVecF32x4ToVecI32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; - case 'u': - if (op == "i32x4.relaxed_trunc_f64x2_u_zero"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroUVecF64x2ToVecI32x4)); - return Ok{}; + } + case '6': { + switch (buf[26]) { + case 's': + if (op == "i32x4.relaxed_trunc_f64x2_s_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroSVecF64x2ToVecI32x4)); + return Ok{}; + } + goto parse_error; + case 'u': + if (op == "i32x4.relaxed_trunc_f64x2_u_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroUVecF64x2ToVecI32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } default: goto parse_error; } } @@ -3394,12 +3432,23 @@ switch (buf[0]) { switch (buf[4]) { case 'a': { switch (buf[5]) { - case 'd': - if (op == "i64.add"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AddInt64)); - return Ok{}; + case 'd': { + switch (buf[7]) { + case '\0': + if (op == "i64.add"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AddInt64)); + return Ok{}; + } + goto parse_error; + case '1': + if (op == "i64.add128"sv) { + CHECK_ERR(makeWideIntAddSub(ctx, pos, annotations, WideIntAddSubOp::AddInt128)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'n': if (op == "i64.and"sv) { CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AndInt64)); @@ -3947,12 +3996,34 @@ switch (buf[0]) { default: goto parse_error; } } - case 'm': - if (op == "i64.mul"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::MulInt64)); - return Ok{}; + case 'm': { + switch (buf[7]) { + case '\0': + if (op == "i64.mul"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::MulInt64)); + return Ok{}; + } + goto parse_error; + case '_': { + switch (buf[13]) { + case 's': + if (op == "i64.mul_wide_s"sv) { + CHECK_ERR(makeWideIntMul(ctx, pos, annotations, WideIntMulOp::MulWideSInt64)); + return Ok{}; + } + goto parse_error; + case 'u': + if (op == "i64.mul_wide_u"sv) { + CHECK_ERR(makeWideIntMul(ctx, pos, annotations, WideIntMulOp::MulWideUInt64)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; + } + } + default: goto parse_error; } - goto parse_error; + } case 'n': if (op == "i64.ne"sv) { CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::NeInt64)); @@ -4080,12 +4151,23 @@ switch (buf[0]) { default: goto parse_error; } } - case 'u': - if (op == "i64.sub"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::SubInt64)); - return Ok{}; + case 'u': { + switch (buf[7]) { + case '\0': + if (op == "i64.sub"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::SubInt64)); + return Ok{}; + } + goto parse_error; + case '1': + if (op == "i64.sub128"sv) { + CHECK_ERR(makeWideIntAddSub(ctx, pos, annotations, WideIntAddSubOp::SubInt128)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } default: goto parse_error; } } @@ -4331,12 +4413,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i64x2.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI64x2)); - return Ok{}; - } - goto parse_error; case 'e': if (op == "i64x2.le_s"sv) { CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::LeSVecI64x2)); @@ -4375,12 +4451,23 @@ switch (buf[0]) { default: goto parse_error; } } - case 'r': - if (op == "i64x2.replace_lane"sv) { - CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI64x2, 2)); - return Ok{}; + case 'r': { + switch (buf[8]) { + case 'l': + if (op == "i64x2.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI64x2)); + return Ok{}; + } + goto parse_error; + case 'p': + if (op == "i64x2.replace_lane"sv) { + CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI64x2, 2)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 's': { switch (buf[7]) { case 'h': { @@ -4560,12 +4647,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i8x16.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI8x16)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -4689,12 +4770,23 @@ switch (buf[0]) { goto parse_error; case 'r': { switch (buf[8]) { - case 'l': - if (op == "i8x16.relaxed_swizzle"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedSwizzleVecI8x16)); - return Ok{}; + case 'l': { + switch (buf[14]) { + case 'l': + if (op == "i8x16.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI8x16)); + return Ok{}; + } + goto parse_error; + case 's': + if (op == "i8x16.relaxed_swizzle"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedSwizzleVecI8x16)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'p': if (op == "i8x16.replace_lane"sv) { CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI8x16, 16)); diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index d15abcfd4a0..96a1f41a424 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -216,6 +216,8 @@ struct ExpressionInterpreter : OverriddenVisitor { WASM_UNREACHABLE("TODO"); } } + Flow visitWideIntAddSub(WideIntAddSub* curr) { WASM_UNREACHABLE("TODO"); } + Flow visitWideIntMul(WideIntMul* curr) { WASM_UNREACHABLE("TODO"); } Flow visitSelect(Select* curr) { WASM_UNREACHABLE("TODO"); } Flow visitDrop(Drop* curr) { WASM_UNREACHABLE("TODO"); } Flow visitReturn(Return* curr) { WASM_UNREACHABLE("TODO"); } diff --git a/src/ir/ExpressionManipulator.cpp b/src/ir/ExpressionManipulator.cpp index 51ed7552d48..23a36ebdcc7 100644 --- a/src/ir/ExpressionManipulator.cpp +++ b/src/ir/ExpressionManipulator.cpp @@ -21,7 +21,7 @@ namespace wasm::ExpressionManipulator { Expression* flexibleCopy(Expression* original, Module& wasm, CustomCopier custom) { - // Perform the copy using a stack of tasks (avoiding recusion). + // Perform the copy using a stack of tasks (avoiding recursion). struct CopyTask { // The thing to copy. Expression* original; diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index 24afae568c5..07820ed6eee 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -109,6 +109,8 @@ void ReFinalize::visitMemoryFill(MemoryFill* curr) { curr->finalize(); } void ReFinalize::visitConst(Const* curr) { curr->finalize(); } void ReFinalize::visitUnary(Unary* curr) { curr->finalize(); } void ReFinalize::visitBinary(Binary* curr) { curr->finalize(); } +void ReFinalize::visitWideIntAddSub(WideIntAddSub* curr) { curr->finalize(); } +void ReFinalize::visitWideIntMul(WideIntMul* curr) { curr->finalize(); } void ReFinalize::visitSelect(Select* curr) { curr->finalize(); } void ReFinalize::visitDrop(Drop* curr) { curr->finalize(); } void ReFinalize::visitReturn(Return* curr) { curr->finalize(); } diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 07b7004b8c8..089b21f86e0 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -22,7 +22,7 @@ namespace wasm { -// CRTP visitor for determining constaints on the types of expression children. +// CRTP visitor for determining constraints on the types of expression children. // For each child of the visited expression, calls a callback with the VarTypes // giving the constraint on the child: // @@ -447,6 +447,9 @@ template struct ChildTyper : OverriddenVisitor { case TruncSatUVecF16x8ToVecI16x8: case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: + case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: case AnyTrueVec128: case AllTrueVecI8x16: case AllTrueVecI16x8: @@ -693,7 +696,7 @@ template struct ChildTyper : OverriddenVisitor { case SwizzleVecI8x16: case RelaxedSwizzleVecI8x16: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: note(&curr->left, Type::v128); note(&curr->right, Type::v128); break; @@ -702,6 +705,18 @@ template struct ChildTyper : OverriddenVisitor { } } + void visitWideIntAddSub(WideIntAddSub* curr) { + note(&curr->leftLow, Type::i64); + note(&curr->leftHigh, Type::i64); + note(&curr->rightLow, Type::i64); + note(&curr->rightHigh, Type::i64); + } + + void visitWideIntMul(WideIntMul* curr) { + note(&curr->left, Type::i64); + note(&curr->right, Type::i64); + } + void visitSelect(Select* curr, std::optional type = std::nullopt) { if (type) { note(&curr->ifTrue, *type); diff --git a/src/ir/cost.h b/src/ir/cost.h index 1cf67abe9e6..1cb70e87aac 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -284,6 +284,9 @@ struct CostAnalyzer : public OverriddenVisitor { case TruncSatUVecF16x8ToVecI16x8: case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: + case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: ret = 1; break; case InvalidUnary: @@ -564,7 +567,7 @@ struct CostAnalyzer : public OverriddenVisitor { case SwizzleVecI8x16: case RelaxedSwizzleVecI8x16: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: ret = 1; break; case InvalidBinary: @@ -572,6 +575,13 @@ struct CostAnalyzer : public OverriddenVisitor { } return ret + visit(curr->left) + visit(curr->right); } + CostType visitWideIntAddSub(WideIntAddSub* curr) { + return 1 + visit(curr->leftLow) + visit(curr->leftHigh) + + visit(curr->rightLow) + visit(curr->rightHigh); + } + CostType visitWideIntMul(WideIntMul* curr) { + return 4 + visit(curr->left) + visit(curr->right); + } CostType visitSelect(Select* curr) { return 1 + visit(curr->condition) + visit(curr->ifTrue) + visit(curr->ifFalse); @@ -605,17 +615,17 @@ struct CostAnalyzer : public OverriddenVisitor { CostType ret = 0; switch (curr->op) { case Bitselect: - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: case MaddVecF16x8: case NmaddVecF16x8: case RelaxedMaddVecF32x4: case RelaxedNmaddVecF32x4: case RelaxedMaddVecF64x2: case RelaxedNmaddVecF64x2: - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedDotI8x16I7x16AddSToVecI32x4: ret = 1; break; } diff --git a/src/ir/effects.cpp b/src/ir/effects.cpp index 2f9dbddad7e..017057a1349 100644 --- a/src/ir/effects.cpp +++ b/src/ir/effects.cpp @@ -17,9 +17,9 @@ #include "ir/effects.h" #include "wasm.h" -namespace std { +namespace wasm { -std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { +std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects) { o << "EffectAnalyzer {\n"; if (effects.branchesOut) { o << "branchesOut\n"; @@ -87,10 +87,10 @@ std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { if (effects.implicitTrap) { o << "implicitTrap\n"; } - if (effects.readOrder != wasm::MemoryOrder::Unordered) { + if (effects.readOrder != MemoryOrder::Unordered) { o << "readOrder " << effects.readOrder << "\n"; } - if (effects.writeOrder != wasm::MemoryOrder::Unordered) { + if (effects.writeOrder != MemoryOrder::Unordered) { o << "writeOrder " << effects.writeOrder << "\n"; } if (effects.throws_) { @@ -160,4 +160,4 @@ std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { return o; } -} // namespace std +} // namespace wasm diff --git a/src/ir/effects.h b/src/ir/effects.h index 1e05ab0fb7c..56cea0ad4fc 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -18,10 +18,12 @@ #define wasm_ir_effects_h #include +#include #include "ir/intrinsics.h" #include "pass.h" #include "support/name.h" +#include "support/utilities.h" #include "wasm-traversal.h" #include "wasm-type.h" #include "wasm.h" @@ -32,7 +34,7 @@ namespace wasm { class EffectAnalyzer { public: - EffectAnalyzer(const PassOptions& passOptions, Module& module) + EffectAnalyzer(const PassOptions& passOptions, const Module& module) : ignoreImplicitTraps(passOptions.ignoreImplicitTraps), trapsNeverHappen(passOptions.trapsNeverHappen), branchesOut(false), calls(false), readsMemory(false), writesMemory(false), @@ -46,7 +48,7 @@ class EffectAnalyzer { features(module.features) {} EffectAnalyzer(const PassOptions& passOptions, - Module& module, + const Module& module, Expression* ast) : EffectAnalyzer(passOptions, module) { walk(ast); @@ -136,13 +138,13 @@ class EffectAnalyzer { // more here.) bool hasReturnCallThrow : 1; - Module& module; + const Module& module; FeatureSet features; std::set localsRead; std::set localsWritten; - std::set mutableGlobalsRead; - std::set globalsWritten; + std::unordered_set mutableGlobalsRead; + std::unordered_set globalsWritten; // The nested depth of try-catch_all. If an instruction that may throw is // inside an inner try-catch_all, we don't mark it as 'throws_', because it @@ -302,10 +304,8 @@ class EffectAnalyzer { // (e.g., if we write, we must remain ordered before someone that reads). // // This assumes the things whose effects we are comparing will both execute, - // at least if neither of them transfers control flow away. That is, we assume - // that there is no transfer of control flow *between* them: we are comparing - // things appear after each other, perhaps with some other code in the middle, - // but that code does not transfer control flow. It is not valid to call this + // at least if neither of them transfers control flow away. We assume there is + // no transfer of control flow *between* them. It is not valid to call this // method in other situations, like this: // // A @@ -325,12 +325,15 @@ class EffectAnalyzer { // ;; control flow transfer // B // - // That the things being compared both execute only matters in the case of - // traps-never-happen: in that mode we can move traps but only if doing so - // would not make them start to appear when they did not. In the second - // example we can't reorder A and B if B traps, but in the first example we - // can reorder them even if B traps (even if A has a global effect like a - // global.set, since we assume B does not trap in traps-never-happen). + // (Note that if they appear in inside a loop, A and B may overlap or even be + // the same expression; this is fine because A still executes before B, even + // if also executes during and after B across different loop iterations.) That + // A and B both execute only matters in the case of traps-never-happen: in + // that mode we can move traps but only if doing so would not make them start + // to appear when they did not previously. In the example with the br_if we + // can't reorder A and B if B traps, but in the valid examples we can reorder + // them even if B traps (even if A has a global effect like a global.set, + // since we assume B does not trap in traps-never-happen). bool orderedBefore(const EffectAnalyzer& other) const { // Cannot reorder control flow and side effects. if ((transfersControlFlow() && other.hasSideEffects()) || @@ -513,8 +516,8 @@ class EffectAnalyzer { return hasAnything(); } - std::set breakTargets; - std::set delegateTargets; + std::unordered_set breakTargets; + std::unordered_set delegateTargets; private: struct InternalAnalyzer @@ -669,27 +672,34 @@ class EffectAnalyzer { } } + // Handle effects due to a null type arriving in a place where a null input + // causes trapping. That is, handle the case of the type proving that the + // input is null. + // Returns true iff there is no need to consider further effects. + bool trapOnNull(Type type) { + if (type == Type::unreachable) { + return true; + } + assert(type.isRef()); + if (type.isNull()) { + parent.trap = true; + return true; + } + if (type.isNullable()) { + parent.implicitTrap = true; + } + + return false; + } + // Handle effects due to an explicit null check of the operands in `exprs`. // Returns true iff there is no need to consider further effects. bool trapOnNull(std::initializer_list exprs) { for (auto* expr : exprs) { - if (expr && expr->type == Type::unreachable) { + if (expr && trapOnNull(expr->type)) { return true; } } - for (auto* expr : exprs) { - assert(!expr || expr->type.isRef()); - if (expr && expr->type.isNull()) { - parent.trap = true; - return true; - } - } - for (auto* expr : exprs) { - if (expr && expr->type.isNullable()) { - parent.implicitTrap = true; - break; - } - } return false; } @@ -715,71 +725,57 @@ class EffectAnalyzer { } void visitCall(Call* curr) { - // call.without.effects has no effects. if (Intrinsics(parent.module).isCallWithoutEffects(curr)) { + // The only effect this can have is to branch (which is an effect of the + // return, not the call). + if (curr->isReturn) { + parent.branchesOut = true; + } return; } - // Get the target's effects, if they exist. Note that we must handle the - // case of the function not yet existing (we may be executed in the middle - // of a pass, which may have built up calls but not the targets of those - // calls; in such a case, we do not find the targets and therefore assume - // we know nothing about the effects, which is safe). - const EffectAnalyzer* targetEffects = nullptr; - if (auto* target = parent.module.getFunctionOrNull(curr->target)) { - targetEffects = target->effects.get(); + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto* target = parent.module.getFunctionOrNull(curr->target); + target && target->effects) { + callTargetEffects = target->effects.get(); } - - if (curr->isReturn) { - parent.branchesOut = true; - // When EH is enabled, any call can throw. - if (parent.features.hasExceptionHandling() && - (!targetEffects || targetEffects->throws())) { - parent.hasReturnCallThrow = true; - } + addCallEffects(curr, callTargetEffects); + } + void visitCallIndirect(CallIndirect* curr) { + auto* table = parent.module.getTable(curr->table); + if (trapOnNull(table->type)) { + return; } - if (targetEffects) { - // We have effect information for this call target, and can just use - // that. The one change we may want to make is to remove throws_, if the - // target function throws and we know that will be caught anyhow, the - // same as the code below for the general path. We can always filter out - // throws for return calls because they are already more precisely - // captured by `branchesOut`, which models the return, and - // `hasReturnCallThrow`, which models the throw that will happen after - // the return. - if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) { - auto filteredEffects = *targetEffects; - filteredEffects.throws_ = false; - parent.mergeIn(filteredEffects); - } else { - // Just merge in all the effects. - parent.mergeIn(*targetEffects); - } + if (!Type::isSubType(Type(curr->heapType, Nullability::NonNullable), + table->type)) { + parent.trap = true; return; } - parent.calls = true; - // When EH is enabled, any call can throw. Skip this for return calls - // because the throw is already more precisely captured by the combination - // of `hasReturnCallThrow` and `branchesOut`. - if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 && - !curr->isReturn) { - parent.throws_ = true; + // Due to index out of bounds. Type-related traps are handled above and + // may set either implicitTrap or trap (or neither). + parent.implicitTrap = true; + + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto it = parent.module.indirectCallEffects.find(curr->heapType); + it != parent.module.indirectCallEffects.end()) { + callTargetEffects = it->second.get(); } + addCallEffects(curr, callTargetEffects); } - void visitCallIndirect(CallIndirect* curr) { - parent.calls = true; - if (curr->isReturn) { - parent.branchesOut = true; - if (parent.features.hasExceptionHandling()) { - parent.hasReturnCallThrow = true; - } + void visitCallRef(CallRef* curr) { + if (trapOnNull(curr->target)) { + return; } - if (parent.features.hasExceptionHandling() && - (parent.tryDepth == 0 && !curr->isReturn)) { - parent.throws_ = true; + + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto it = parent.module.indirectCallEffects.find( + curr->target->type.getHeapType()); + it != parent.module.indirectCallEffects.end()) { + callTargetEffects = it->second.get(); } + addCallEffects(curr, callTargetEffects); } void visitLocalGet(LocalGet* curr) { parent.localsRead.insert(curr->index); @@ -944,6 +940,8 @@ class EffectAnalyzer { } } } + void visitWideIntAddSub(WideIntAddSub* curr) {} + void visitWideIntMul(WideIntMul* curr) {} void visitSelect(Select* curr) {} void visitDrop(Drop* curr) {} void visitReturn(Return* curr) { parent.branchesOut = true; } @@ -1035,22 +1033,6 @@ class EffectAnalyzer { void visitTupleExtract(TupleExtract* curr) {} void visitRefI31(RefI31* curr) {} void visitI31Get(I31Get* curr) { trapOnNull(curr->i31); } - void visitCallRef(CallRef* curr) { - if (trapOnNull(curr->target)) { - return; - } - if (curr->isReturn) { - parent.branchesOut = true; - if (parent.features.hasExceptionHandling()) { - parent.hasReturnCallThrow = true; - } - } - parent.calls = true; - if (parent.features.hasExceptionHandling() && - (parent.tryDepth == 0 && !curr->isReturn)) { - parent.throws_ = true; - } - } void visitRefTest(RefTest* curr) {} void visitRefCast(RefCast* curr) { @@ -1332,6 +1314,61 @@ class EffectAnalyzer { parent.throws_ = true; } } + + private: + // Populate a call's effects using effects computed from GlobalEffects. Note + // that calls may have other effects that aren't captured by the function + // body of the target (e.g. a call_ref may trap on null refs). + template + void addCallEffectsFromGlobalEffects(const CallType* curr, + const EffectAnalyzer& funcEffects) { + if (curr->isReturn) { + if (funcEffects.throws()) { + parent.hasReturnCallThrow = true; + } + } + + if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) { + // We can ignore a throw here, as the parent catches it. + // + // Also, we can filter out throws for return calls because they are + // already more precisely captured by `branchesOut`, which models the + // return, and `hasReturnCallThrow`, which models the throw that will + // happen after the return. + auto filteredEffects = funcEffects; + filteredEffects.throws_ = false; + parent.mergeIn(filteredEffects); + } else { + parent.mergeIn(funcEffects); + } + } + + // Common effects logic for the 3 types of call: `call`, `call_indirect`, + // and `call_ref`. + template + void addCallEffects(const CallType* curr, + const EffectAnalyzer* callTargetEffects) { + if (curr->isReturn) { + parent.branchesOut = true; + } + + if (callTargetEffects) { + addCallEffectsFromGlobalEffects(curr, *callTargetEffects); + return; + } + + parent.calls = true; + // If EH is enabled and we don't have global effects information, + // assume that the call target may throw. + if (parent.features.hasExceptionHandling()) { + if (curr->isReturn) { + parent.hasReturnCallThrow = true; + } + if (parent.tryDepth == 0 && !curr->isReturn) { + parent.throws_ = true; + } + } + } }; public: @@ -1464,10 +1501,8 @@ class ShallowEffectAnalyzer : public EffectAnalyzer { } }; -} // namespace wasm +std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects); -namespace std { -std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects); -} // namespace std +} // namespace wasm #endif // wasm_ir_effects_h diff --git a/src/ir/eh-utils.cpp b/src/ir/eh-utils.cpp index 43fd1627d31..19f48477a70 100644 --- a/src/ir/eh-utils.cpp +++ b/src/ir/eh-utils.cpp @@ -42,7 +42,7 @@ getFirstPop(Expression* catchBody, bool& isPopNested, Expression**& popPtr) { auto* implicitBlock = catchBody->dynCast(); // Go down the line for the first child until we reach a leaf. A pop should be - // in that first-decendant line. + // in that first-descendant line. Expression** firstChildPtr = nullptr; while (true) { if (firstChild->is()) { diff --git a/src/ir/intrinsics.cpp b/src/ir/intrinsics.cpp index 6dd4c072324..2069b1288e6 100644 --- a/src/ir/intrinsics.cpp +++ b/src/ir/intrinsics.cpp @@ -110,6 +110,15 @@ std::vector Intrinsics::getJSCalledFunctions() { } // ConfigureAlls in a start function make their functions callable. + // + // TODO: Rather than scan the start, which does not handle all cases + // (configureAll can be called from an export), we could remove this and + // expect users to mark all functions as jsCalled. The MarkJSCalled pass scans + // for configureAlls and emits that annotation, so users could basically run + // it, if they don't want to manually annotate. Then the code here could + // get unified into that pass. The errors above (like the elem segment not + // having the right size etc.) could then be improved and/or turned into + // warnings. if (module.start) { auto* start = module.getFunction(module.start); if (!start->imported()) { diff --git a/src/ir/intrinsics.h b/src/ir/intrinsics.h index a9b32f31f1b..2f6809c826b 100644 --- a/src/ir/intrinsics.h +++ b/src/ir/intrinsics.h @@ -33,10 +33,10 @@ namespace wasm { class Intrinsics { - Module& module; + const Module& module; public: - Intrinsics(Module& module) : module(module) {} + Intrinsics(const Module& module) : module(module) {} // Check if an instruction is the Binaryen call.without.effects intrinsic. // diff --git a/src/ir/js-utils.h b/src/ir/js-utils.h index 7591b4fe7e0..105dea499cf 100644 --- a/src/ir/js-utils.h +++ b/src/ir/js-utils.h @@ -45,7 +45,7 @@ inline bool hasPossibleJSPrototypeField(HeapType type) { // Calls flowIn and flowOut on all types that may flow in from or out to JS. template -void iterJSInterface(Module& wasm, In flowIn, Out flowOut) { +void iterJSInterface(const Module& wasm, In flowIn, Out flowOut) { // @binaryen.js.called functions are called from JS. Their parameters flow // in from JS and their results flow back out. for (auto f : Intrinsics(wasm).getJSCalledFunctions()) { diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index e8b1923aacf..9e69405ff7c 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,11 +80,16 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool isReturn) { + auto handleCall = [&](bool isReturn, bool refutesThrowEffect) { + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + mayThrow = mayThrow && !refutesThrowEffect; + if (!self->connectAdjacentBlocks) { - // Control is nonlinear if we return, or if EH is enabled or may be. - if (isReturn || !self->getModule() || - self->getModule()->features.hasExceptionHandling()) { + // Control is nonlinear if we return or throw. Traps don't need to be + // taken into account since they don't break control flow in a way + // that's observable. + if (mayThrow || isReturn) { self->pushTask(SubType::doNoteNonLinear, currp); } } @@ -153,12 +158,57 @@ struct LinearExecutionWalker : public PostWalker { break; } case Expression::Id::CallId: { - handleCall(curr->cast()->isReturn); - return; + auto* call = curr->cast(); + + bool refutesThrowEffect = false; + if (self->getModule()) { + auto* func = self->getModule()->getFunctionOrNull(call->target); + // TODO: `func` might not exist here because of #8753. Fix this + // and remove the null check. + if (func && func->effects) { + refutesThrowEffect = !func->effects->throws_; + } + } + + handleCall(call->isReturn, refutesThrowEffect); + break; } case Expression::Id::CallRefId: { - handleCall(curr->cast()->isReturn); - return; + auto* callRef = curr->cast(); + + bool refutesThrowEffect = [&]() { + if (!self->getModule()) { + return false; + } + if (!callRef->target->type.isRef()) { + // This is an unreachable, so no throws effect. + return true; + } + + auto* effects = find_or_null(self->getModule()->indirectCallEffects, + callRef->target->type.getHeapType()); + if (!effects) { + return false; + } + return !(*effects)->throws_; + }(); + + handleCall(callRef->isReturn, refutesThrowEffect); + break; + } + case Expression::Id::CallIndirectId: { + auto* callIndirect = curr->cast(); + + bool refutesThrowEffect = false; + if (self->getModule()) { + if (auto* effects = find_or_null( + self->getModule()->indirectCallEffects, callIndirect->heapType); + effects) { + refutesThrowEffect = !(*effects)->throws_; + } + } + handleCall(callIndirect->isReturn, refutesThrowEffect); + break; } case Expression::Id::TryId: { self->pushTask(SubType::doVisitTry, currp); diff --git a/src/ir/manipulation.h b/src/ir/manipulation.h index 1ad2b1161bb..dbb34edbe66 100644 --- a/src/ir/manipulation.h +++ b/src/ir/manipulation.h @@ -26,7 +26,7 @@ template inline OutputType* convert(InputType* input) { static_assert(sizeof(OutputType) <= sizeof(InputType), "Can only convert to a smaller size Expression node"); - input->~InputType(); // arena-allocaed, so no destructor, but avoid UB. + input->~InputType(); // arena-allocated, so no destructor, but avoid UB. OutputType* output = (OutputType*)(input); new (output) OutputType; return output; @@ -57,7 +57,7 @@ inline Unreachable* unreachable(InputType* target) { template inline OutputType* convert(InputType* input, MixedArena& allocator) { assert(sizeof(OutputType) <= sizeof(InputType)); - input->~InputType(); // arena-allocaed, so no destructor, but avoid UB. + input->~InputType(); // arena-allocated, so no destructor, but avoid UB. OutputType* output = (OutputType*)(input); new (output) OutputType(allocator); return output; diff --git a/src/ir/match.h b/src/ir/match.h index 3d42eee359a..383ff8d057a 100644 --- a/src/ir/match.h +++ b/src/ir/match.h @@ -67,7 +67,7 @@ namespace wasm::Match { // // Matches Binary expressions. Takes an optional pointer to Binary* at which // to store the matched Binary*, followed by either a BinaryOp or an -// Abstract::Op describing which binary expresions to match, followed by +// Abstract::Op describing which binary expressions to match, followed by // matchers to apply to the binary expression's left and right operands. // // select diff --git a/src/ir/memory-utils.cpp b/src/ir/memory-utils.cpp index 4bd629e2cbc..6c6a75ebbcb 100644 --- a/src/ir/memory-utils.cpp +++ b/src/ir/memory-utils.cpp @@ -94,7 +94,7 @@ bool flatten(Module& wasm) { std::vector data; for (auto& segment : dataSegments) { - if (segment->isPassive) { + if (segment->isPassive()) { return false; } auto* offset = segment->offset->dynCast(); diff --git a/src/ir/memory-utils.h b/src/ir/memory-utils.h index 2929d17ca9f..db9ff2bcba8 100644 --- a/src/ir/memory-utils.h +++ b/src/ir/memory-utils.h @@ -81,7 +81,7 @@ ensureLimitedSegments(Module& module, numDynamic++; } } - hasPassiveSegments |= segment->isPassive; + hasPassiveSegments |= segment->isPassive(); } if (hasPassiveSegments) { diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index d88f21ee24a..619b00d92b6 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -26,28 +26,28 @@ // placeholder function (and eventually to the original secondary // function), allocating a new table slot for the placeholder if necessary. // -// 4. Replace all references to each secondary module's functions in the +// 4. Export globals, tags, tables, and memories from the primary module and +// import them in the secondary modules. If possible, move those module +// items instead to the secondary modules. +// +// 5. Replace all references to each secondary module's functions in the // primary module's and each other secondary module's table segments with // references to imported placeholder functions. // -// 5. Rewrite direct calls from primary functions to secondary functions to be +// 6. Rewrite direct calls from primary functions to secondary functions to be // indirect calls to their placeholder functions (and eventually to their // original secondary functions), allocating new table slots for the // placeholders if necessary. // -// 6. For each primary function directly called from a secondary function, +// 7. For each primary function directly called from a secondary function, // export the primary function if it is not already exported and import it // into each secondary module using it. // -// 7. For each secondary module, create new active table segments in the +// 8. For each secondary module, create new active table segments in the // module that will replace all the placeholder function references in the // table with references to their corresponding secondary functions upon // instantiation. // -// 8. Export globals, tags, tables, and memories from the primary module and -// import them in the secondary modules. If possible, move those module -// items instead to the secondary modules. -// // Functions can be used or referenced three ways in a WebAssembly module: they // can be exported, called, or referenced with ref.func. The above procedure // introduces a layer of indirection to each of those mechanisms that removes @@ -73,7 +73,7 @@ // from the IR before splitting. // #include "ir/module-splitting.h" -#include "ir/export-utils.h" +#include "ir/effects.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -88,7 +88,7 @@ namespace { template void forEachElement(Module& module, F f) { ModuleUtils::iterActiveElementSegments(module, [&](ElementSegment* segment) { - Name base = ""; + Name base; Index offset = 0; if (auto* c = segment->offset->dynCast()) { offset = c->value.getInteger(); @@ -113,13 +113,15 @@ struct TableSlotManager { Expression* makeExpr(Module& module); }; Module& module; + const std::vector>& secondaries; Table* activeTable = nullptr; ElementSegment* activeSegment = nullptr; Slot activeBase; std::map funcIndices; std::vector activeTableSegments; - TableSlotManager(Module& module); + TableSlotManager(Module& module, + const std::vector>& secondaries); Table* makeTable(); ElementSegment* makeElementSegment(); @@ -135,7 +137,7 @@ Expression* TableSlotManager::Slot::makeExpr(Module& module) { auto makeIndex = [&]() { return builder.makeConst(Literal::makeFromInt32(index, table->addressType)); }; - if (global.size()) { + if (global) { Expression* getBase = builder.makeGlobalGet(global, table->addressType); auto addOp = table->is64() ? AddInt64 : AddInt32; return index == 0 ? getBase @@ -150,7 +152,9 @@ void TableSlotManager::addSlot(Name func, Slot slot) { funcIndices.insert({func, slot}); } -TableSlotManager::TableSlotManager(Module& module) : module(module) { +TableSlotManager::TableSlotManager( + Module& module, const std::vector>& secondaries) + : module(module), secondaries(secondaries) { // If possible, just create a new table to manage all primary-to-secondary // calls lazily. Do not re-use slots for functions that will already be in // existing tables, since that is not correct in the face of table mutations. @@ -192,7 +196,7 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { if (activeTableSegments.empty()) { // There are no active segments, so we will lazily create one and start // filling it at index 0. - activeBase = {activeTable->name, "", 0}; + activeBase = {activeTable->name, Name(), 0}; } else if (activeTableSegments.size() == 1 && activeTableSegments[0]->type == funcref && !activeTableSegments[0]->offset->is()) { @@ -219,7 +223,7 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { if (segmentBase + segment->data.size() >= maxIndex) { maxIndex = segmentBase + segment->data.size(); activeSegment = segment; - activeBase = {activeTable->name, "", segmentBase}; + activeBase = {activeTable->name, Name(), segmentBase}; } } } @@ -234,8 +238,25 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { } Table* TableSlotManager::makeTable() { - return module.addTable( - Builder::makeTable(Names::getValidTableName(module, Name::fromInt(0)))); + // Because the active table will be imported in secondary modules, its name + // should not collide with any existing tables in primary and secondary + // modules. + std::unordered_set secondaryTableNames; + for (auto& secondary : secondaries) { + for (auto& table : secondary->tables) { + secondaryTableNames.insert(table->name); + } + } + Name name = Names::getValidName("0", [&](Name test) { + if (module.getTableOrNull(test)) { + return false; + } + if (secondaryTableNames.contains(test)) { + return false; + } + return true; + }); + return module.addTable(Builder::makeTable(name)); } ElementSegment* TableSlotManager::makeElementSegment() { @@ -258,7 +279,7 @@ TableSlotManager::Slot TableSlotManager::getSlot(Name func, HeapType type) { if (activeSegment == nullptr) { if (activeTable == nullptr) { activeTable = makeTable(); - activeBase = {activeTable->name, "", 0}; + activeBase = {activeTable->name, Name(), 0}; } // None of the existing segments should refer to the active table @@ -311,6 +332,10 @@ struct ModuleSplitter { // names. std::unordered_map exportedPrimaryFuncs; + // Map from to their corresponding export names for + // non-function items. + std::unordered_map, Name> exportedPrimaryItems; + // For each table, map placeholder indices to the names of the functions they // replace. std::unordered_map> placeholderMap; @@ -318,36 +343,45 @@ struct ModuleSplitter { // Map from original secondary function name to its trampoline std::unordered_map trampolineMap; + void shareActiveTable(Module* secondary); + // Initialization helpers static std::unique_ptr initSecondary(const Module& primary); static std::unordered_map initExportedPrimaryFuncs(const Module& primary); + static std::unordered_map, Name> + initExportedPrimaryItems(const Module& primary); // Other helpers void exportImportFunction(Name func, const std::set& modules); + void makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind); Name getTrampoline(Name funcName); // Main splitting steps void classifyFunctions(); void moveSecondaryFunctions(); void thunkExportedSecondaryFunctions(); + void shareImportableItems(); void indirectReferencesToSecondaryFunctions(); void indirectCallsToSecondaryFunctions(); void exportImportCalledPrimaryFunctions(); void setupTablePatching(); - void shareImportableItems(); ModuleSplitter(Module& primary, const Config& config) - : config(config), primary(primary), tableManager(primary), - exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)) { + : config(config), primary(primary), tableManager(primary, secondaries), + exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)), + exportedPrimaryItems(initExportedPrimaryItems(primary)) { classifyFunctions(); moveSecondaryFunctions(); thunkExportedSecondaryFunctions(); + shareImportableItems(); indirectReferencesToSecondaryFunctions(); indirectCallsToSecondaryFunctions(); exportImportCalledPrimaryFunctions(); setupTablePatching(); - shareImportableItems(); } }; @@ -443,6 +477,41 @@ ModuleSplitter::initExportedPrimaryFuncs(const Module& primary) { return functionExportNames; } +std::unordered_map, Name> +ModuleSplitter::initExportedPrimaryItems(const Module& primary) { + std::unordered_map, Name> exports; + for (auto& ex : primary.exports) { + if (ex->kind != ExternalKind::Function) { + if (auto* name = ex->getInternalName()) { + exports[std::make_pair(ex->kind, *name)] = ex->name; + } + } + } + return exports; +} + +void ModuleSplitter::makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind) { + secondaryItem.name = primaryItem.name; + secondaryItem.hasExplicitName = primaryItem.hasExplicitName; + secondaryItem.module = config.importNamespace; + auto exportIt = exportedPrimaryItems.find({kind, primaryItem.name}); + if (exportIt != exportedPrimaryItems.end()) { + secondaryItem.base = exportIt->second; + } else { + std::string baseName = + config.newExportPrefix + + (config.minimizeNewExportNames ? minified.getName() : genericExportName); + Name exportName = Names::getValidExportName(primary, baseName); + primary.addExport( + std::make_unique(exportName, kind, primaryItem.name)); + secondaryItem.base = exportName; + exportedPrimaryItems[{kind, primaryItem.name}] = exportName; + } +} + void ModuleSplitter::exportImportFunction(Name funcName, const std::set& modules) { Name exportName; @@ -508,7 +577,9 @@ Name ModuleSplitter::getTrampoline(Name funcName) { primary, std::string("trampoline_") + funcName.toString()); it->second = trampoline; - // Generate the call and the function. + // Generate the call and the function. We generate a direct call here, but + // this will be converted to a call_indirect in + // indirectCallsToSecondaryFunctions. std::vector args; for (Index i = 0; i < oldFunc->getNumParams(); i++) { args.push_back(builder.makeLocalGet(i, oldFunc->getLocalType(i))); @@ -522,6 +593,32 @@ Name ModuleSplitter::getTrampoline(Name funcName) { return trampoline; } +void ModuleSplitter::shareActiveTable(Module* secondary) { + assert(tableManager.activeTable); + auto secondaryTable = + secondary->getTableOrNull(tableManager.activeTable->name); + if (secondaryTable) { + // In case it's already in the secondary module, sync the initial/max + secondaryTable->initial = tableManager.activeTable->initial; + secondaryTable->max = tableManager.activeTable->max; + } else { + secondaryTable = + ModuleUtils::copyTable(tableManager.activeTable, *secondary); + makeImportExport( + *tableManager.activeTable, *secondaryTable, "table", ExternalKind::Table); + } + if (tableManager.activeBase.global) { + auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global); + auto* secondaryGlobal = + secondary->getGlobalOrNull(tableManager.activeBase.global); + if (!secondaryGlobal) { + secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, *secondary); + makeImportExport( + *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); + } + } +} + void ModuleSplitter::thunkExportedSecondaryFunctions() { // Update exports of secondary functions in the primary module to export // wrapper functions that indirectly call the secondary functions. We are @@ -558,163 +655,447 @@ static void walkSegments(Walker& walker, Module* module) { } } -void ModuleSplitter::indirectReferencesToSecondaryFunctions() { - // Turn references to secondary functions into references to thunks that - // perform a direct call to the original referent. The direct calls in the - // thunks will be handled like all other cross-module calls later, in - // |indirectCallsToSecondaryFunctions|. - struct Gatherer : public PostWalker { - ModuleSplitter& parent; +void ModuleSplitter::shareImportableItems() { - Gatherer(ModuleSplitter& parent) : parent(parent) {} + struct UsedNames { + std::unordered_set globals; + std::unordered_set memories; + std::unordered_set tables; + std::unordered_set tags; + }; - // Collect RefFuncs in a map from the function name to all RefFuncs that - // refer to it. We only collect this for secondary funcs. - InsertOrderedMap> map; + struct NameCollector + : public PostWalker> { + UsedNames& used; + NameCollector(UsedNames& used) : used(used) {} - void visitRefFunc(RefFunc* curr) { - Module* currModule = getModule(); - // Add ref.func to the map when - // 1. ref.func's target func is in one of the secondary modules and - // 2. the current module is a different module (either the primary module - // or a different secondary module) - if (parent.allSecondaryFuncs.contains(curr->func) && - (currModule == &parent.primary || - parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->func)) - .get() != currModule)) { - map[curr->func].push_back(curr); - } - } - } gatherer(*this); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, it will create - // unnecessary trampolines. - // - // For example, we have (global $a funcref (ref.func $foo)), and $foo was - // split into a secondary module. Because $a is at this point still in the - // primary module, $foo will be considered to exist in a different module, so - // this will create a trampoline for $foo. But it is possible that later we - // find out $a is exclusively used by that secondary module and move $a there. - // In that case, $a can just reference $foo locally, but if we scan global - // initializers here, we would have created an unnecessary trampoline for - // $foo. - walkSegments(gatherer, &primary); - for (auto& curr : primary.functions) { - if (!curr->imported()) { - gatherer.walkFunction(curr.get()); - } - } - for (auto& secondaryPtr : secondaries) { - gatherer.walkModule(secondaryPtr.get()); - } + void visitExpression(Expression* curr) { +#define DELEGATE_ID curr->_id +#define DELEGATE_START(id) [[maybe_unused]] auto* cast = curr->cast(); +#define DELEGATE_GET_FIELD(id, field) cast->field +#define DELEGATE_FIELD_TYPE(id, field) +#define DELEGATE_FIELD_HEAPTYPE(id, field) +#define DELEGATE_FIELD_CHILD(id, field) +#define DELEGATE_FIELD_INT(id, field) +#define DELEGATE_FIELD_LITERAL(id, field) +#define DELEGATE_FIELD_NAME(id, field) +#define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field) +#define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) +#define DELEGATE_FIELD_ADDRESS(id, field) - // Ignore references to secondary functions that occur in the active segment - // that will contain the imported placeholders. Indirect calls to table slots - // initialized by that segment will already go to the right place once the - // secondary module has been loaded and the table has been patched. - std::unordered_set ignore; - if (tableManager.activeSegment) { - for (auto* expr : tableManager.activeSegment->data) { - if (auto* ref = expr->dynCast()) { - ignore.insert(ref); - } - } +#define DELEGATE_FIELD_NAME_KIND(id, field, kind) \ + if (cast->field.is()) { \ + switch (kind) { \ + case ModuleItemKind::Table: \ + used.tables.insert(cast->field); \ + break; \ + case ModuleItemKind::Memory: \ + used.memories.insert(cast->field); \ + break; \ + case ModuleItemKind::Global: \ + used.globals.insert(cast->field); \ + break; \ + case ModuleItemKind::Tag: \ + used.tags.insert(cast->field); \ + break; \ + case ModuleItemKind::Function: \ + case ModuleItemKind::DataSegment: \ + case ModuleItemKind::ElementSegment: \ + case ModuleItemKind::Invalid: \ + break; \ + } \ } - // Fix up what we found: Generate trampolines as described earlier, and apply - // them. - Builder builder(primary); - // Generate the new trampoline function and add it to the module. - for (auto& [name, refFuncs] : gatherer.map) { - // Find the relevant (non-ignored) RefFuncs. If there are none, we can skip - // creating a thunk entirely. - std::vector relevantRefFuncs; - for (auto* refFunc : refFuncs) { - assert(refFunc->func == name); - if (!ignore.contains(refFunc)) { - relevantRefFuncs.push_back(refFunc); - } - } - if (relevantRefFuncs.empty()) { - continue; +#include "wasm-delegations-fields.def" } + }; - Name trampoline = getTrampoline(name); - // Update RefFuncs to refer to it. - for (auto* refFunc : relevantRefFuncs) { - refFunc->func = trampoline; + // Given a module, collect names used in the module + auto getUsedNames = [&](Module& module) { + UsedNames used; + ModuleUtils::ParallelFunctionAnalysis nameCollector( + module, [&](Function* func, UsedNames& used) { + if (!func->imported()) { + NameCollector(used).walk(func->body); + } + }); + + for (auto& [_, funcUsed] : nameCollector.map) { + used.globals.insert(funcUsed.globals.begin(), funcUsed.globals.end()); + used.memories.insert(funcUsed.memories.begin(), funcUsed.memories.end()); + used.tables.insert(funcUsed.tables.begin(), funcUsed.tables.end()); + used.tags.insert(funcUsed.tags.begin(), funcUsed.tags.end()); } - } -} -void ModuleSplitter::indirectCallsToSecondaryFunctions() { - // Update direct calls of secondary functions to be indirect calls of their - // corresponding table indices instead. - struct CallIndirector : public PostWalker { - ModuleSplitter& parent; - CallIndirector(ModuleSplitter& parent) : parent(parent) {} - void visitCall(Call* curr) { - // Return if the call's target is not in one of the secondary module. - if (!parent.allSecondaryFuncs.contains(curr->target)) { - return; + NameCollector collector(used); + // We shouldn't use collector.walkModuleCode here, because we don't want to + // walk global initializers. At this point, all globals are still in the + // primary module, so if we walk global initializers here, other globals + // appearing in their initializers will all be marked as used in the primary + // module, which is not what we want. + // + // For example, we have (global $a i32 (global.get $b)). Because $a is at + // this point still in the primary module, $b will be marked as "used" in + // the primary module. But $a can be moved to a secondary module later if it + // is used exclusively by that module. Then $b can be also moved, in case it + // doesn't have other uses. But if it is marked as "used" in the primary + // module, it can't. + walkSegments(collector, &module); + for (auto& segment : module.dataSegments) { + if (segment->isActive()) { + used.memories.insert(segment->memory); } - // Return if the current module is the same module as the call's target, - // because we don't need a call_indirect within the same module. - Module* currModule = getModule(); - if (currModule != &parent.primary && - parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) - .get() == currModule) { - return; + } + for (auto& segment : module.elementSegments) { + if (segment->isActive()) { + used.tables.insert(segment->table); } + } - Builder builder(*getModule()); - Index secIndex = parent.funcToSecondaryIndex.at(curr->target); - auto* func = parent.secondaries.at(secIndex)->getFunction(curr->target); - auto tableSlot = - parent.tableManager.getSlot(curr->target, func->type.getHeapType()); + // If primary module has exports, they are "used" in it. Secondary modules + // don't have exports, so this only applies to the primary module. + for (auto& ex : module.exports) { + switch (ex->kind) { + case ExternalKind::Global: + used.globals.insert(*ex->getInternalName()); + break; + case ExternalKind::Memory: + used.memories.insert(*ex->getInternalName()); + break; + case ExternalKind::Table: + used.tables.insert(*ex->getInternalName()); + break; + case ExternalKind::Tag: + used.tags.insert(*ex->getInternalName()); + break; + default: + break; + } + } - replaceCurrent( - builder.makeCallIndirect(tableSlot.tableName, - tableSlot.makeExpr(parent.primary), - curr->operands, - func->type.getHeapType(), - curr->isReturn)); + // Compute the transitive closure of globals referenced in other globals' + // initializers. Since globals can reference other globals, we must ensure + // that if a global is used in a module, all its dependencies are also + // marked as used. + UniqueNonrepeatingDeferredQueue worklist; + for (auto global : used.globals) { + worklist.push(global); + } + while (!worklist.empty()) { + Name name = worklist.pop(); + // At this point all globals are still in the primary module, so this + // exists + auto* global = primary.getGlobal(name); + if (!global->imported() && global->init) { + for (auto* get : FindAll(global->init).list) { + worklist.push(get->name); + used.globals.insert(get->name); + } + } } + return used; }; - CallIndirector callIndirector(*this); - callIndirector.walkModule(&primary); + + UsedNames primaryUsed = getUsedNames(primary); + std::vector secondaryUsed; for (auto& secondaryPtr : secondaries) { - callIndirector.walkModule(secondaryPtr.get()); + secondaryUsed.push_back(getUsedNames(*secondaryPtr)); } -} -void ModuleSplitter::exportImportCalledPrimaryFunctions() { - // Find primary functions called/referred to from the secondary modules. - using CalledPrimaryToModules = std::map>; - for (auto& secondaryPtr : secondaries) { - Module* secondary = secondaryPtr.get(); + // We need to assume the active table and its base global are used in the + // primary module, because we will create segments there later. + if (tableManager.activeTable) { + primaryUsed.tables.insert(tableManager.activeTable->name); + } + if (tableManager.activeBase.global) { + primaryUsed.globals.insert(tableManager.activeBase.global); + } + + // If custom-descirptors is enabled, global initializers can trap. Trapping + // globals should stay in the primary module to preserve the trapping behavior + // upon instantiation. + if (primary.features.hasCustomDescriptors()) { + for (auto& global : primary.globals) { + if (global->init && + EffectAnalyzer(config.passOptions, primary, global->init) + .hasUnremovableSideEffects()) { + primaryUsed.globals.insert(global->name); + } + } + } + + // Given a name and module item kind, returns the list of secondary modules + // using that name + auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { + std::vector usingModules; + for (size_t i = 0; i < secondaries.size(); ++i) { + if ((secondaryUsed[i].*field).contains(name)) { + usingModules.push_back(secondaries[i].get()); + } + } + return usingModules; + }; + + // Share module items with secondary modules. + // 1. Only share an item with the modules that use it + // 2. If an item is used by only a single secondary module, move the item to + // that secondary module. If an item is used by multiple modules (including + // the primary and secondary modules), export the item from the primary and + // import it from the using secondary modules. + + std::vector memoriesToRemove; + for (auto& memory : primary.memories) { + auto usingSecondaries = + getUsingSecondaries(memory->name, &UsedNames::memories); + bool usedInPrimary = primaryUsed.memories.contains(memory->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + ModuleUtils::copyMemory(memory.get(), *secondary); + memoriesToRemove.push_back(memory->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryMemory = + ModuleUtils::copyMemory(memory.get(), *secondary); + makeImportExport( + *memory, *secondaryMemory, "memory", ExternalKind::Memory); + } + } + } + for (auto& name : memoriesToRemove) { + primary.removeMemory(name); + } + + std::vector tablesToRemove; + for (auto& table : primary.tables) { + auto usingSecondaries = + getUsingSecondaries(table->name, &UsedNames::tables); + bool usedInPrimary = primaryUsed.tables.contains(table->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + assert(!secondary->getTableOrNull(table->name)); + ModuleUtils::copyTable(table.get(), *secondary); + tablesToRemove.push_back(table->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); + makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); + } + } + } + for (auto& name : tablesToRemove) { + primary.removeTable(name); + } + + std::vector globalsToRemove; + for (auto& global : primary.globals) { + if (global->mutable_) { + assert(primary.features.hasMutableGlobals() && + "TODO: add wrapper functions for disallowed mutable globals"); + } + + auto usingSecondaries = + getUsingSecondaries(global->name, &UsedNames::globals); + bool inPrimary = primaryUsed.globals.contains(global->name); + + if (!inPrimary && usingSecondaries.empty()) { + // It's not used anywhere, so delete it. Unlike other unused module items + // (memories, tables, and tags) that can just sit in the primary module + // and later be DCE'ed by another pass, we should remove it here, because + // an unused global can contain an initializer that refers to another + // global that will be moved to a secondary module, like + // (global $unused i32 (global.get $a)) // $a is moved to a secondary + globalsToRemove.push_back(global->name); + } else if (!inPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + ModuleUtils::copyGlobal(global.get(), *secondary); + globalsToRemove.push_back(global->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryGlobal = + ModuleUtils::copyGlobal(global.get(), *secondary); + makeImportExport( + *global, *secondaryGlobal, "global", ExternalKind::Global); + } + } + } + for (auto& name : globalsToRemove) { + primary.removeGlobal(name); + } + + std::vector tagsToRemove; + for (auto& tag : primary.tags) { + auto usingSecondaries = getUsingSecondaries(tag->name, &UsedNames::tags); + bool usedInPrimary = primaryUsed.tags.contains(tag->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + ModuleUtils::copyTag(tag.get(), *secondary); + tagsToRemove.push_back(tag->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); + makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); + } + } + } + for (auto& name : tagsToRemove) { + primary.removeTag(name); + } +} + +void ModuleSplitter::indirectReferencesToSecondaryFunctions() { + // Turn references to secondary functions into references to thunks that + // perform a direct call to the original referent. The direct calls in the + // thunks will be handled like all other cross-module calls later, in + // |indirectCallsToSecondaryFunctions|. + struct Gatherer : public PostWalker { + ModuleSplitter& parent; + + Gatherer(ModuleSplitter& parent) : parent(parent) {} + + // Collect RefFuncs in a map from the function name to all RefFuncs that + // refer to it. We only collect this for secondary funcs. + InsertOrderedMap> map; + + void visitRefFunc(RefFunc* curr) { + Module* currModule = getModule(); + // Add ref.func to the map when + // 1. ref.func's target func is in one of the secondary modules and + // 2. the current module is a different module (either the primary module + // or a different secondary module) + if (parent.allSecondaryFuncs.contains(curr->func) && + (currModule == &parent.primary || + parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->func)) + .get() != currModule)) { + map[curr->func].push_back(curr); + } + } + } gatherer(*this); + gatherer.walkModule(&primary); + for (auto& secondaryPtr : secondaries) { + gatherer.walkModule(secondaryPtr.get()); + } + + // Ignore references to secondary functions that occur in the active segment + // that will contain the imported placeholders. Indirect calls to table slots + // initialized by that segment will already go to the right place once the + // secondary module has been loaded and the table has been patched. + std::unordered_set ignore; + if (tableManager.activeSegment) { + for (auto* expr : tableManager.activeSegment->data) { + if (auto* ref = expr->dynCast()) { + ignore.insert(ref); + } + } + } + + // Fix up what we found: Generate trampolines as described earlier, and apply + // them. + Builder builder(primary); + // Generate the new trampoline function and add it to the module. + for (auto& [name, refFuncs] : gatherer.map) { + // Find the relevant (non-ignored) RefFuncs. If there are none, we can skip + // creating a thunk entirely. + std::vector relevantRefFuncs; + for (auto* refFunc : refFuncs) { + assert(refFunc->func == name); + if (!ignore.contains(refFunc)) { + relevantRefFuncs.push_back(refFunc); + } + } + if (relevantRefFuncs.empty()) { + continue; + } + + Name trampoline = getTrampoline(name); + // Update RefFuncs to refer to it. + for (auto* refFunc : relevantRefFuncs) { + refFunc->func = trampoline; + } + } +} + +void ModuleSplitter::indirectCallsToSecondaryFunctions() { + // Update direct calls of secondary functions to be indirect calls of their + // corresponding table indices instead. + struct CallIndirector : public PostWalker { + ModuleSplitter& parent; + std::unordered_set activeTableUsingSecondaries; + CallIndirector(ModuleSplitter& parent) : parent(parent) {} + void visitCall(Call* curr) { + // Return if the call's target is not in one of the secondary module. + if (!parent.allSecondaryFuncs.contains(curr->target)) { + return; + } + // Return if the current module is the same module as the call's target, + // because we don't need a call_indirect within the same module. + Module* currModule = getModule(); + Module* calleeModule = + parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) + .get(); + if (currModule == calleeModule) { + return; + } + + Builder builder(*currModule); + auto* func = calleeModule->getFunction(curr->target); + auto tableSlot = + parent.tableManager.getSlot(curr->target, func->type.getHeapType()); + replaceCurrent( + builder.makeCallIndirect(tableSlot.tableName, + tableSlot.makeExpr(parent.primary), + curr->operands, + func->type.getHeapType(), + curr->isReturn)); + + // Share the active table with the current module (caller). We share the + // active table with with calleeModule later in setupTablePathing. + if (currModule != &parent.primary) { + activeTableUsingSecondaries.insert(currModule); + } + } + }; + CallIndirector callIndirector(*this); + callIndirector.walkModule(&primary); + for (auto& secondaryPtr : secondaries) { + callIndirector.walkModule(secondaryPtr.get()); + } + + for (auto* secondary : callIndirector.activeTableUsingSecondaries) { + shareActiveTable(secondary); + } +} + +void ModuleSplitter::exportImportCalledPrimaryFunctions() { + // Find primary functions called/referred to from the secondary modules. + using CalledPrimaryToModules = std::map>; + struct CallCollector : PostWalker { + const std::unordered_set& primaryFuncs; + CalledPrimaryToModules& calledPrimaryToModules; + CallCollector(const std::unordered_set& primaryFuncs, + CalledPrimaryToModules& calledPrimaryToModules) + : primaryFuncs(primaryFuncs), + calledPrimaryToModules(calledPrimaryToModules) {} + void visitCall(Call* curr) { + if (primaryFuncs.contains(curr->target)) { + calledPrimaryToModules[curr->target].insert(getModule()); + } + } + void visitRefFunc(RefFunc* curr) { + if (primaryFuncs.contains(curr->func)) { + calledPrimaryToModules[curr->func].insert(getModule()); + } + } + }; + + for (auto& secondaryPtr : secondaries) { + Module* secondary = secondaryPtr.get(); ModuleUtils::ParallelFunctionAnalysis callCollector( *secondary, [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { - struct CallCollector : PostWalker { - const std::unordered_set& primaryFuncs; - CalledPrimaryToModules& calledPrimaryToModules; - CallCollector(const std::unordered_set& primaryFuncs, - CalledPrimaryToModules& calledPrimaryToModules) - : primaryFuncs(primaryFuncs), - calledPrimaryToModules(calledPrimaryToModules) {} - void visitCall(Call* curr) { - if (primaryFuncs.contains(curr->target)) { - calledPrimaryToModules[curr->target].insert(getModule()); - } - } - void visitRefFunc(RefFunc* curr) { - if (primaryFuncs.contains(curr->func)) { - calledPrimaryToModules[curr->func].insert(getModule()); - } - } - }; CallCollector(primaryFuncs, calledPrimaryToModules) .walkFunctionInModule(func, secondary); }); @@ -724,6 +1105,9 @@ void ModuleSplitter::exportImportCalledPrimaryFunctions() { calledPrimaryToModules.merge(map); } + CallCollector collector(primaryFuncs, calledPrimaryToModules); + collector.walkModuleCode(secondary); + // Ensure each called primary function is exported and imported for (auto& [func, modules] : calledPrimaryToModules) { exportImportFunction(func, modules); @@ -737,6 +1121,8 @@ void ModuleSplitter::setupTablePatching() { } std::map> moduleToReplacedElems; + Name fillerName; + Type fillerType = Type(Signature(Type::none, Type::none), NonNullable, Exact); // Replace table references to secondary functions with an imported // placeholder that encodes the table index in its name: // `importNamespace`.`index`. @@ -757,22 +1143,38 @@ void ModuleSplitter::setupTablePatching() { Name secondaryName = config.secondaryNames.at(secondaryIndex); auto* secondaryFunc = secondary.getFunction(ref->func); moduleToReplacedElems[&secondary][index] = secondaryFunc; - if (!config.usePlaceholders) { - // TODO: This can create active element segments with lots of nulls. We - // should optimize them like we do data segments with zeros. - elem = Builder(primary).makeRefNull(HeapType::nofunc); - return; + + if (config.usePlaceholders) { + auto placeholder = std::make_unique(); + placeholder->module = config.placeholderNamespacePrefix.toString() + + "." + secondaryName.toString(); + placeholder->base = std::to_string(index); + placeholder->name = Names::getValidFunctionName( + primary, std::string("placeholder_") + placeholder->base.toString()); + placeholder->hasExplicitName = true; + placeholder->type = secondaryFunc->type.with(Inexact); + elem = + Builder(primary).makeRefFunc(placeholder->name, placeholder->type); + primary.addFunction(std::move(placeholder)); + + } else { // !config.usePlaceholders + if (primary.features.hasReferenceTypes()) { + // TODO: This can create active element segments with lots of nulls. + // We should optimize them like we do data segments with zeros. + elem = Builder(primary).makeRefNull(HeapType::nofunc); + return; + } + // When reference-types is not enabled, we can't use a ref.null. Put a + // filler function that contains an unreachable. + if (!fillerName) { + fillerName = Names::getValidFunctionName(primary, "filler"); + auto filler = Builder::makeFunction( + fillerName, fillerType, {}, Builder(primary).makeUnreachable()); + filler->hasExplicitName = true; + primary.addFunction(std::move(filler)); + } + elem = Builder(primary).makeRefFunc(fillerName, fillerType); } - auto placeholder = std::make_unique(); - placeholder->module = config.placeholderNamespacePrefix.toString() + "." + - secondaryName.toString(); - placeholder->base = std::to_string(index); - placeholder->name = Names::getValidFunctionName( - primary, std::string("placeholder_") + placeholder->base.toString()); - placeholder->hasExplicitName = true; - placeholder->type = secondaryFunc->type.with(Inexact); - elem = Builder(primary).makeRefFunc(placeholder->name, placeholder->type); - primary.addFunction(std::move(placeholder)); }); if (moduleToReplacedElems.size() == 0) { @@ -782,10 +1184,10 @@ void ModuleSplitter::setupTablePatching() { for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { Module& secondary = *secondaryPtr; - auto secondaryTable = - ModuleUtils::copyTable(tableManager.activeTable, secondary); + shareActiveTable(&secondary); + auto* secondaryTable = secondary.getTable(tableManager.activeTable->name); - if (tableManager.activeBase.global.size()) { + if (tableManager.activeBase.global) { assert(tableManager.activeTableSegments.size() == 1 && "Unexpected number of segments with non-const base"); assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); @@ -862,366 +1264,6 @@ void ModuleSplitter::setupTablePatching() { } } -void ModuleSplitter::shareImportableItems() { - // Map internal names to (one of) their corresponding export names. Don't - // consider functions because they have already been imported and exported as - // necessary. - std::unordered_map, Name> exports; - for (auto& ex : primary.exports) { - if (ex->kind != ExternalKind::Function) { - if (auto* name = ex->getInternalName()) { - exports[std::make_pair(ex->kind, *name)] = ex->name; - } - } - } - - auto makeImportExport = [&](Importable& primaryItem, - Importable& secondaryItem, - const std::string& genericExportName, - ExternalKind kind) { - secondaryItem.name = primaryItem.name; - secondaryItem.hasExplicitName = primaryItem.hasExplicitName; - secondaryItem.module = config.importNamespace; - auto exportIt = exports.find(std::make_pair(kind, primaryItem.name)); - if (exportIt != exports.end()) { - secondaryItem.base = exportIt->second; - } else { - std::string baseName = - config.newExportPrefix + (config.minimizeNewExportNames - ? minified.getName() - : genericExportName); - Name exportName = Names::getValidExportName(primary, baseName); - primary.addExport(new Export(exportName, kind, primaryItem.name)); - secondaryItem.base = exportName; - exports[std::make_pair(kind, primaryItem.name)] = exportName; - } - }; - - struct UsedNames { - std::unordered_set globals; - std::unordered_set memories; - std::unordered_set tables; - std::unordered_set tags; - }; - - struct NameCollector - : public PostWalker> { - UsedNames& used; - NameCollector(UsedNames& used) : used(used) {} - - void visitExpression(Expression* curr) { -#define DELEGATE_ID curr->_id -#define DELEGATE_START(id) [[maybe_unused]] auto* cast = curr->cast(); -#define DELEGATE_GET_FIELD(id, field) cast->field -#define DELEGATE_FIELD_TYPE(id, field) -#define DELEGATE_FIELD_HEAPTYPE(id, field) -#define DELEGATE_FIELD_CHILD(id, field) -#define DELEGATE_FIELD_INT(id, field) -#define DELEGATE_FIELD_LITERAL(id, field) -#define DELEGATE_FIELD_NAME(id, field) -#define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field) -#define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) -#define DELEGATE_FIELD_ADDRESS(id, field) - -#define DELEGATE_FIELD_NAME_KIND(id, field, kind) \ - if (cast->field.is()) { \ - switch (kind) { \ - case ModuleItemKind::Table: \ - used.tables.insert(cast->field); \ - break; \ - case ModuleItemKind::Memory: \ - used.memories.insert(cast->field); \ - break; \ - case ModuleItemKind::Global: \ - used.globals.insert(cast->field); \ - break; \ - case ModuleItemKind::Tag: \ - used.tags.insert(cast->field); \ - break; \ - case ModuleItemKind::Function: \ - case ModuleItemKind::DataSegment: \ - case ModuleItemKind::ElementSegment: \ - case ModuleItemKind::Invalid: \ - break; \ - } \ - } - -#include "wasm-delegations-fields.def" - } - }; - - // Given a module, collect names used in the module - auto getUsedNames = [&](Module& module) { - UsedNames used; - ModuleUtils::ParallelFunctionAnalysis nameCollector( - module, [&](Function* func, UsedNames& used) { - if (!func->imported()) { - NameCollector(used).walk(func->body); - } - }); - - for (auto& [_, funcUsed] : nameCollector.map) { - used.globals.insert(funcUsed.globals.begin(), funcUsed.globals.end()); - used.memories.insert(funcUsed.memories.begin(), funcUsed.memories.end()); - used.tables.insert(funcUsed.tables.begin(), funcUsed.tables.end()); - used.tags.insert(funcUsed.tags.begin(), funcUsed.tags.end()); - } - - NameCollector collector(used); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, other globals - // appearing in their initializers will all be marked as used in the primary - // module, which is not what we want. - // - // For example, we have (global $a i32 (global.get $b)). Because $a is at - // this point still in the primary module, $b will be marked as "used" in - // the primary module. But $a can be moved to a secondary module later if it - // is used exclusively by that module. Then $b can be also moved, in case it - // doesn't have other uses. But if it is marked as "used" in the primary - // module, it can't. - walkSegments(collector, &module); - for (auto& segment : module.dataSegments) { - if (segment->memory.is()) { - used.memories.insert(segment->memory); - } - } - for (auto& segment : module.elementSegments) { - if (segment->table.is()) { - used.tables.insert(segment->table); - } - } - - // If primary module has exports, they are "used" in it. Secondary modules - // don't have exports, so this only applies to the primary module. - for (auto& ex : module.exports) { - switch (ex->kind) { - case ExternalKind::Global: - used.globals.insert(*ex->getInternalName()); - break; - case ExternalKind::Memory: - used.memories.insert(*ex->getInternalName()); - break; - case ExternalKind::Table: - used.tables.insert(*ex->getInternalName()); - break; - case ExternalKind::Tag: - used.tags.insert(*ex->getInternalName()); - break; - default: - break; - } - } - return used; - }; - - UsedNames primaryUsed = getUsedNames(primary); - std::vector secondaryUsed; - for (auto& secondaryPtr : secondaries) { - secondaryUsed.push_back(getUsedNames(*secondaryPtr)); - } - - // Compute the transitive closure of globals referenced in other globals' - // initializers. Since globals can reference other globals, we must ensure - // that if a global is used in a module, all its dependencies are also marked - // as used. - auto computeTransitiveGlobals = [&](UsedNames& used) { - UniqueNonrepeatingDeferredQueue worklist; - for (auto global : used.globals) { - worklist.push(global); - } - while (!worklist.empty()) { - Name name = worklist.pop(); - // At this point all globals are still in the primary module, so this - // exists - auto* global = primary.getGlobal(name); - if (!global->imported() && global->init) { - for (auto* get : FindAll(global->init).list) { - worklist.push(get->name); - used.globals.insert(get->name); - } - } - } - }; - - computeTransitiveGlobals(primaryUsed); - for (auto& used : secondaryUsed) { - computeTransitiveGlobals(used); - } - - // Given a name and module item kind, returns the list of secondary modules - // using that name - auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { - std::vector usingModules; - for (size_t i = 0; i < secondaries.size(); ++i) { - if ((secondaryUsed[i].*field).contains(name)) { - usingModules.push_back(secondaries[i].get()); - } - } - return usingModules; - }; - - // Share module items with secondary modules. - // 1. Only share an item with the modules that use it - // 2. If an item is used by only a single secondary module, move the item to - // that secondary module. If an item is used by multiple modules (including - // the primary and secondary modules), export the item from the primary and - // import it from the using secondary modules. - - std::vector memoriesToRemove; - for (auto& memory : primary.memories) { - auto usingSecondaries = - getUsingSecondaries(memory->name, &UsedNames::memories); - bool usedInPrimary = primaryUsed.memories.contains(memory->name); - - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - ModuleUtils::copyMemory(memory.get(), *secondary); - memoriesToRemove.push_back(memory->name); - } else { - for (auto* secondary : usingSecondaries) { - auto* secondaryMemory = - ModuleUtils::copyMemory(memory.get(), *secondary); - makeImportExport( - *memory, *secondaryMemory, "memory", ExternalKind::Memory); - } - } - } - for (auto& name : memoriesToRemove) { - primary.removeMemory(name); - } - - std::vector tablesToRemove; - for (auto& table : primary.tables) { - auto usingSecondaries = - getUsingSecondaries(table->name, &UsedNames::tables); - bool usedInPrimary = primaryUsed.tables.contains(table->name); - - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - // In case we copied this table to this secondary module in - // setupTablePatching(), !usedInPrimary can't be satisfied, because the - // primary module should have an element segment that refers to this - // table. - assert(!secondary->getTableOrNull(table->name)); - ModuleUtils::copyTable(table.get(), *secondary); - tablesToRemove.push_back(table->name); - } else { - for (auto* secondary : usingSecondaries) { - // 1. In case we copied this table to this secondary module in - // setupTablePatching(), secondary.getTableOrNull(table->name) is not - // null, and we need to import it. - // 2. As in the case with other module elements, if the table is used in - // the secondary module's instructions, we need to export it. - auto secondaryTable = secondary->getTableOrNull(table->name); - if (!secondaryTable) { - secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); - } - makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); - } - } - } - for (auto& name : tablesToRemove) { - primary.removeTable(name); - } - - std::vector globalsToRemove; - for (auto& global : primary.globals) { - if (global->mutable_) { - assert(primary.features.hasMutableGlobals() && - "TODO: add wrapper functions for disallowed mutable globals"); - } - - auto usingSecondaries = - getUsingSecondaries(global->name, &UsedNames::globals); - bool inPrimary = primaryUsed.globals.contains(global->name); - - if (!inPrimary && usingSecondaries.empty()) { - // It's not used anywhere, so delete it. Unlike other unused module items - // (memories, tables, and tags) that can just sit in the primary module - // and later be DCE'ed by another pass, we should remove it here, because - // an unused global can contain an initialier that refers to another - // global that will be moved to a secondary module, like - // (global $unused i32 (global.get $a)) // $a is moved to a secondary - globalsToRemove.push_back(global->name); - - } else if (!inPrimary && usingSecondaries.size() == 1) { - // We are moving this global to this secondary module - auto* secondary = usingSecondaries[0]; - auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); - globalsToRemove.push_back(global->name); - - if (secondaryGlobal->init) { - // When a global's initializer contains ref.func - for (auto* ref : FindAll(secondaryGlobal->init).list) { - // If ref.func's function is in a different secondary module, we - // create a trampoline here. - if (auto targetIndexIt = funcToSecondaryIndex.find(ref->func); - targetIndexIt != funcToSecondaryIndex.end()) { - if (secondaries[targetIndexIt->second].get() != secondary) { - ref->func = getTrampoline(ref->func); - } - } - // 1. If ref.func's function is in the primary module, we export it - // here. - // 2. If ref.func's function is in a different secondary module and we - // just created a trampoline for it in the primary module above, we - // export the trampoline here. - if (primary.getFunctionOrNull(ref->func)) { - exportImportFunction(ref->func, {secondary}); - } - // If ref.func's function is in the same secondary module, we don't - // need to do anything. The ref.func can directly reference the - // function. - } - } - - } else { // We are NOT moving this global to the secondary module - if (global->init) { - for (auto* ref : FindAll(global->init).list) { - // If we are exporting this global from the primary module, we should - // create a trampoline here, because we skipped doing it for global - // initializers in indirectReferencesToSecondaryFunctions. - if (allSecondaryFuncs.contains(ref->func)) { - ref->func = getTrampoline(ref->func); - } - } - } - - for (auto* secondary : usingSecondaries) { - auto* secondaryGlobal = - ModuleUtils::copyGlobal(global.get(), *secondary); - makeImportExport( - *global, *secondaryGlobal, "global", ExternalKind::Global); - } - } - } - for (auto& name : globalsToRemove) { - primary.removeGlobal(name); - } - - std::vector tagsToRemove; - for (auto& tag : primary.tags) { - auto usingSecondaries = getUsingSecondaries(tag->name, &UsedNames::tags); - bool usedInPrimary = primaryUsed.tags.contains(tag->name); - - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - ModuleUtils::copyTag(tag.get(), *secondary); - tagsToRemove.push_back(tag->name); - } else { - for (auto* secondary : usingSecondaries) { - auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); - makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); - } - } - } - for (auto& name : tagsToRemove) { - primary.removeTag(name); - } -} - } // anonymous namespace Results splitFunctions(Module& primary, const Config& config) { diff --git a/src/ir/module-splitting.h b/src/ir/module-splitting.h index 8260feb6b8d..fd21d052061 100644 --- a/src/ir/module-splitting.h +++ b/src/ir/module-splitting.h @@ -44,11 +44,14 @@ #ifndef wasm_ir_module_splitting_h #define wasm_ir_module_splitting_h +#include "pass.h" #include "wasm.h" namespace wasm::ModuleSplitting { struct Config { + // Pass options to use for effects analysis + PassOptions passOptions; // A vector of set of functions to split into that secondary. Each function // set belongs to a single secondary module. All others are kept in the // primary module. Must not include the start function if it exists. May or diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 01f4e1cbc65..b17d6c772b9 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -146,7 +146,7 @@ ElementSegment* copyElementSegment(const ElementSegment* segment, Module& out) { return out.addElementSegment(std::move(ret)); }; - if (segment->table.isNull()) { + if (segment->isPassive()) { return copy(std::make_unique()); } else { auto offset = ExpressionManipulator::copy(segment->offset, out); @@ -188,8 +188,7 @@ DataSegment* copyDataSegment(const DataSegment* segment, Module& out) { ret->name = segment->name; ret->hasExplicitName = segment->hasExplicitName; ret->memory = segment->memory; - ret->isPassive = segment->isPassive; - if (!segment->isPassive) { + if (segment->isActive()) { auto offset = ExpressionManipulator::copy(segment->offset, out); ret->offset = offset; } @@ -363,8 +362,15 @@ struct TypeInfos { } } void note(Type type) { - for (HeapType ht : type.getHeapTypeChildren()) { - note(ht); + // Handle the common case of a ref directly, to avoid a scan of children. + if (type.isRef()) { + note(type.getHeapType()); + return; + } + if (type.isTuple()) { + for (HeapType ht : type.getHeapTypeChildren()) { + note(ht); + } } } // Ensure a type is included without increasing its count. @@ -374,8 +380,14 @@ struct TypeInfos { } } void include(Type type) { - for (HeapType ht : type.getHeapTypeChildren()) { - include(ht); + if (type.isRef()) { + include(type.getHeapType()); + return; + } + if (type.isTuple()) { + for (HeapType ht : type.getHeapTypeChildren()) { + include(ht); + } } } void noteControlFlow(Signature sig) { @@ -468,12 +480,16 @@ struct CodeScanner : PostWalker { }; void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types); + InsertOrderedMap& types, + WorldMode worldMode); } // anonymous namespace -InsertOrderedMap collectHeapTypeInfo( - Module& wasm, TypeInclusion inclusion, VisibilityHandling visibility) { +InsertOrderedMap +collectHeapTypeInfo(Module& wasm, + WorldMode worldMode, + TypeInclusion inclusion, + VisibilityHandling visibility) { // Collect module-level info. TypeInfos info; CodeScanner(wasm, info).walkModuleCode(&wasm); @@ -580,7 +596,7 @@ InsertOrderedMap collectHeapTypeInfo( } if (visibility == VisibilityHandling::FindVisibility) { - classifyTypeVisibility(wasm, info.info); + classifyTypeVisibility(wasm, info.info, worldMode); } return std::move(info.info); @@ -589,8 +605,9 @@ InsertOrderedMap collectHeapTypeInfo( namespace { void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types) { - for (auto type : getPublicHeapTypes(wasm)) { + InsertOrderedMap& types, + WorldMode worldMode) { + for (auto type : getPublicHeapTypes(wasm, worldMode)) { if (auto it = types.find(type); it != types.end()) { it->second.visibility = Visibility::Public; } @@ -602,6 +619,64 @@ void classifyTypeVisibility(Module& wasm, } } +// Collects all heap types transitively reachable from a root set of types. +// Options are provided to customize the traversal: +// - `includeSupertypes`: if true, declared supertypes are also traversed. +// - `includeRecGroups`: if true, all types in the same recursion group +// are also traversed. +std::vector +getTransitivelyReachable(const std::vector& roots, + bool includeSupertypes, + bool includeRecGroups) { + std::vector result; + std::vector worklist; + std::unordered_set seen; + std::unordered_set seenRecGroups; + + auto note = [&](HeapType type) { + if (type.isBasic()) { + if (seen.insert(type).second) { + result.push_back(type); + } + return; + } + + if (includeRecGroups) { + auto group = type.getRecGroup(); + if (seenRecGroups.insert(group).second) { + for (auto member : group) { + result.push_back(member); + worklist.push_back(member); + } + } + } else { + if (seen.insert(type).second) { + result.push_back(type); + worklist.push_back(type); + } + } + }; + + for (auto type : roots) { + note(type); + } + + while (!worklist.empty()) { + auto curr = worklist.back(); + worklist.pop_back(); + std::optional super = + includeSupertypes ? std::nullopt : curr.getDeclaredSuperType(); + for (auto t : curr.getReferencedHeapTypes()) { + if (super && t == *super) { + continue; + } + note(t); + } + } + + return result; +} + void setIndices(IndexedHeapTypes& indexedTypes) { for (Index i = 0; i < indexedTypes.types.size(); i++) { indexedTypes.indices[indexedTypes.types[i]] = i; @@ -611,7 +686,7 @@ void setIndices(IndexedHeapTypes& indexedTypes) { } // anonymous namespace std::vector collectHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo(wasm); + auto info = collectHeapTypeInfo(wasm, WorldMode::Open); std::vector types; types.reserve(info.size()); for (auto& [type, _] : info) { @@ -620,27 +695,16 @@ std::vector collectHeapTypes(Module& wasm) { return types; } -std::vector getPublicHeapTypes(Module& wasm) { - // Look at the types of imports as exports to get an initial set of public - // types, then traverse the types used by public types and collect the - // transitively reachable public types as well. - std::vector workList; - std::unordered_set publicGroups; - - // The collected types. +std::vector getExposedPublicHeapTypes(Module& wasm) { + // Look at the types of imports and exports to get an initial set of public + // types. std::vector publicTypes; + std::unordered_set seenTypes; auto notePublic = [&](HeapType type) { - if (type.isBasic()) { - return; + if (seenTypes.insert(type).second) { + publicTypes.push_back(type); } - auto group = type.getRecGroup(); - if (!publicGroups.insert(group).second) { - // The groups in this type have already been marked public. - return; - } - publicTypes.insert(publicTypes.end(), group.begin(), group.end()); - workList.insert(workList.end(), group.begin(), group.end()); }; ModuleUtils::iterImportedTags(wasm, [&](Tag* tag) { notePublic(tag->type); }); @@ -697,24 +761,28 @@ std::vector getPublicHeapTypes(Module& wasm) { notePublic(type); } - // Find all the other public types reachable from directly publicized types. - while (!workList.empty()) { - auto curr = workList.back(); - workList.pop_back(); - for (auto t : curr.getReferencedHeapTypes()) { - notePublic(t); + return publicTypes; +} + +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode) { + auto directlyExposed = getExposedPublicHeapTypes(wasm); + auto transitivelyExposed = getTransitivelyReachable( + directlyExposed, /*includeSupertypes=*/true, /*includeRecGroups=*/true); + std::vector publicTypes; + publicTypes.reserve(transitivelyExposed.size()); + for (auto type : transitivelyExposed) { + if (!type.isBasic()) { + publicTypes.push_back(type); } } - - // TODO: In an open world, we need to consider subtypes of public types public - // as well, or potentially even consider all types to be public unless - // otherwise annotated. return publicTypes; } -std::vector getPrivateHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo( - wasm, TypeInclusion::UsedIRTypes, VisibilityHandling::FindVisibility); +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode) { + auto info = collectHeapTypeInfo(wasm, + worldMode, + TypeInclusion::UsedIRTypes, + VisibilityHandling::FindVisibility); std::vector types; types.reserve(info.size()); for (auto& [type, typeInfo] : info) { @@ -726,7 +794,8 @@ std::vector getPrivateHeapTypes(Module& wasm) { } IndexedHeapTypes getOptimizedIndexedHeapTypes(Module& wasm) { - auto counts = collectHeapTypeInfo(wasm, TypeInclusion::BinaryTypes); + auto counts = + collectHeapTypeInfo(wasm, WorldMode::Open, TypeInclusion::BinaryTypes); // Collect the rec groups. std::unordered_map groupIndices; diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 50b67df7cea..860672beef8 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -95,7 +95,7 @@ template inline void iterDefinedMemories(Module& wasm, T visitor) { template inline void iterMemorySegments(Module& wasm, Name memory, T visitor) { for (auto& segment : wasm.dataSegments) { - if (!segment->isPassive && segment->memory == memory) { + if (segment->isActive() && segment->memory == memory) { visitor(segment.get()); } } @@ -104,7 +104,7 @@ inline void iterMemorySegments(Module& wasm, Name memory, T visitor) { template inline void iterActiveDataSegments(Module& wasm, T visitor) { for (auto& segment : wasm.dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { visitor(segment.get()); } } @@ -142,7 +142,7 @@ inline void iterTableSegments(Module& wasm, Name table, T visitor) { template inline void iterActiveElementSegments(Module& wasm, T visitor) { for (auto& segment : wasm.elementSegments) { - if (segment->table.is()) { + if (segment->isActive()) { visitor(segment.get()); } } @@ -472,6 +472,7 @@ struct HeapTypeInfo { InsertOrderedMap collectHeapTypeInfo( Module& wasm, + WorldMode worldMode, TypeInclusion inclusion = TypeInclusion::AllTypes, VisibilityHandling visibility = VisibilityHandling::NoVisibility); @@ -479,13 +480,18 @@ InsertOrderedMap collectHeapTypeInfo( // module, i.e. the types that would appear in the type section. std::vector collectHeapTypes(Module& wasm); -// Collect all the heap types visible on the module boundary that cannot be -// changed. TODO: For open world use cases, this needs to include all subtypes -// of public types as well. -std::vector getPublicHeapTypes(Module& wasm); +// Get the types directly made public by imported or exported module items. For +// example, the types of imported or exported globals or functions, but not +// other types reachable from those types. Includes abstract heap types. +std::vector getExposedPublicHeapTypes(Module& wasm); -// getHeapTypes - getPublicHeapTypes -std::vector getPrivateHeapTypes(Module& wasm); +// Collect all the defined heap types visible on the module boundary that cannot +// be changed, e.g. the defined types from getExposedPublicHeapTypes and those +// they reach. +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode); + +// All the defined heap types that are not public. +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode); struct IndexedHeapTypes { std::vector types; diff --git a/src/ir/names.h b/src/ir/names.h index 61fc137f5a8..083a54f0ef5 100644 --- a/src/ir/names.h +++ b/src/ir/names.h @@ -56,7 +56,7 @@ inline Name getValidName(Name root, if (check(root)) { return root; } - auto prefixed = std::string(root.str) + separator; + auto prefixed = std::string(root.view()) + separator; Index num = hint; while (1) { auto name = prefixed + std::to_string(num); diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 905349f1c68..a8a01841fd6 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -634,6 +634,8 @@ struct InfoCollector addRoot(curr); } void visitBinary(Binary* curr) { addRoot(curr); } + void visitWideIntAddSub(WideIntAddSub* curr) { addRoot(curr); } + void visitWideIntMul(WideIntMul* curr) { addRoot(curr); } void visitSelect(Select* curr) { receiveChildValue(curr->ifTrue, curr); receiveChildValue(curr->ifFalse, curr); @@ -682,7 +684,7 @@ struct InfoCollector SignatureResultLocation{func->type.getHeapType(), i}}); } - if (!options.closedWorld) { + if (options.worldMode == WorldMode::Open) { info.calledFromOutside.insert(curr->func); } } @@ -881,6 +883,7 @@ struct InfoCollector info.links.push_back({SignatureResultLocation{subType, i}, ResultLocation{getFunction(), i}}); } + return true; }); } } @@ -1208,8 +1211,7 @@ struct InfoCollector addRoot(curr, PossibleContents::exactType(curr->type)); } void visitStringConst(StringConst* curr) { - addRoot(curr, - PossibleContents::literal(Literal(std::string(curr->string.str)))); + addRoot(curr, PossibleContents::literal(Literal(curr->string.view()))); } void visitStringMeasure(StringMeasure* curr) { // TODO: optimize when possible @@ -1709,7 +1711,7 @@ void TNHOracle::scan(Function* func, void visitCallRef(CallRef* curr) { // We can only optimize call_ref in closed world, as otherwise the // call can go somewhere we can't see. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { info.callRefs.push_back(curr); } } @@ -1832,7 +1834,7 @@ void TNHOracle::infer() { // that type or a subtype, i.e., might be called when that type is seen in a // call_ref target. std::unordered_map> typeFunctions; - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { auto type = func->type; auto& info = map[wasm.getFunction(func->name)]; @@ -1893,7 +1895,7 @@ void TNHOracle::infer() { // We should only get here in a closed world, in which we know which // functions might be called (the scan phase only notes callRefs if we are // in fact in a closed world). - assert(options.closedWorld); + assert(options.worldMode == WorldMode::Closed); auto iter = typeFunctions.find(targetType.getHeapType()); if (iter == typeFunctions.end()) { @@ -2533,8 +2535,8 @@ Flower::Flower(Module& wasm, const PassOptions& options) } // In open world, public heap types may be written to from the outside. - if (!options.closedWorld) { - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + if (options.worldMode == WorldMode::Open) { + for (auto type : ModuleUtils::getPublicHeapTypes(wasm, options.worldMode)) { if (type.isStruct()) { auto& fields = type.getStruct().fields; for (Index i = 0; i < fields.size(); i++) { @@ -3136,24 +3138,20 @@ void Flower::filterPackedDataReads(PossibleContents& contents, Expression* ref; Index index; unsigned bytes = 0; - Type resultType = Type::none; if (auto* get = expr->dynCast()) { signed_ = get->signed_; ref = get->ref; index = get->index; - resultType = get->type; } else if (auto* get = expr->dynCast()) { signed_ = get->signed_; ref = get->ref; // Arrays are treated as having a single field. index = 0; - resultType = get->type; } else if (auto* load = expr->dynCast()) { signed_ = load->signed_; ref = load->ref; index = 0; bytes = load->bytes; - resultType = load->type; } else { WASM_UNREACHABLE("bad packed read"); } @@ -3161,6 +3159,12 @@ void Flower::filterPackedDataReads(PossibleContents& contents, return; } + Type resultType = expr->type; + if (resultType == Type::unreachable) { + // This read never executes. + return; + } + // If there is no struct or array to read, no value will ever be returned. if (ref->type.isNull()) { contents = PossibleContents::none(); @@ -3280,6 +3284,7 @@ void Flower::readFromData(Type declaredType, [&](HeapType type, Index depth) { connectDuringFlow(DataLocation{type, fieldIndex}, coneReadLocation); + return true; }); // TODO: we can end up with redundant links here if we see one cone first @@ -3349,6 +3354,7 @@ void Flower::writeToData(Expression* ref, cone.type.getHeapType(), normalizedDepth, [&](HeapType type, Index depth) { auto heapLoc = DataLocation{type, fieldIndex}; updateContents(heapLoc, valueContents); + return true; }); } diff --git a/src/ir/principal-type.cpp b/src/ir/principal-type.cpp index c0c502b6e3a..66e87e4f02c 100644 --- a/src/ir/principal-type.cpp +++ b/src/ir/principal-type.cpp @@ -1031,7 +1031,7 @@ bool PrincipalType::compose(const PrincipalType& next) { apply(assignments, *this); // If a type variable was instantiated with bottom type (i.e. unreachable) due - // to popping from an unreachabile stack, the result type may end with some + // to popping from an unreachable stack, the result type may end with some // number of unreachables. This is nonsensical, since `unreachable` is not a // concrete type. We could have alternatively left the variables // uninstantiated, but it would have no corresponding introduction on the left diff --git a/src/ir/properties.h b/src/ir/properties.h index e52902597a0..4b4126d750d 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -521,8 +521,8 @@ inline MemoryOrder getMemoryOrder(Expression* curr) { } // Whether this instruction will be unwritable in the text and binary formats -// because it requires a type index immediate giving the type of a child that -// has unreachable or null type, and therefore does not have a type index. +// because it requires a type index immediate computed from an expression with +// unreachable or null type, and therefore no type index. inline bool hasUnwritableTypeImmediate(Expression* curr) { #define DELEGATE_ID curr->_id @@ -534,6 +534,24 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { } \ } +#define DELEGATE_IMMEDIATE_TYPED_RESULT(id) \ + if (curr->type == Type::unreachable) { \ + if constexpr (id::SpecificId == Expression::Id::RefCastId) { \ + auto* cast = curr->cast(); \ + if (!cast->desc) { \ + return true; \ + } \ + if (!cast->desc->type.isRef()) { \ + return true; \ + } \ + if (!cast->desc->type.getHeapType().getDescribedType()) { \ + return true; \ + } \ + return false; \ + } \ + return true; \ + } + #define DELEGATE_FIELD_CHILD(id, field) #define DELEGATE_FIELD_CHILD_VECTOR(id, field) #define DELEGATE_FIELD_INT(id, field) diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index 8a4677ec9da..940144cba04 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -213,6 +213,8 @@ struct SubtypingDiscoverer : public OverriddenVisitor { void visitConst(Const* curr) {} void visitUnary(Unary* curr) {} void visitBinary(Binary* curr) {} + void visitWideIntAddSub(WideIntAddSub* curr) {} + void visitWideIntMul(WideIntMul* curr) {} void visitSelect(Select* curr) { self()->noteSubtype(curr->ifTrue, curr); self()->noteSubtype(curr->ifFalse, curr); diff --git a/src/ir/subtypes.h b/src/ir/subtypes.h index ae3a27878f5..912d61f878d 100644 --- a/src/ir/subtypes.h +++ b/src/ir/subtypes.h @@ -168,15 +168,19 @@ struct SubTypes { // Efficiently iterate on subtypes of a type, up to a particular depth (depth // 0 means not to traverse subtypes, etc.). The callback function receives - // (type, depth). + // (type, depth) and returns whether to continue the scan, i.e. if it returns + // false, we stop. Returns the last value returned to it, that is, returns + // true if we did not stop early, and false if we did. template - void iterSubTypes(HeapType type, Index depth, F func) const { + bool iterSubTypes(HeapType type, Index depth, F func) const { // Start by traversing the type itself. - func(type, 0); + if (!func(type, 0)) { + return false; + } if (depth == 0) { // Nothing else to scan. - return; + return true; } // getImmediateSubTypes() returns vectors of subtypes, so for efficiency @@ -201,17 +205,21 @@ struct SubTypes { auto& currVec = *item.vec; assert(currDepth <= depth); for (auto type : currVec) { - func(type, currDepth); + if (!func(type, currDepth)) { + return false; + } auto* subVec = &getImmediateSubTypes(type); if (currDepth + 1 <= depth && !subVec->empty()) { work.push_back({subVec, currDepth + 1}); } } } + + return true; } // As above, but iterate to the maximum depth. - template void iterSubTypes(HeapType type, F func) const { + template bool iterSubTypes(HeapType type, F func) const { return iterSubTypes(type, std::numeric_limits::max(), func); } diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index cb10aff82b9..516b12a215b 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -95,13 +95,13 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { for (auto& table : wasm.tables) { if (table->imported()) { - tables[table->name].mayBeModified = true; + tables[table->name].hasSet = true; } } for (auto& ex : wasm.exports) { if (ex->kind == ExternalKind::Table) { - tables[*ex->getInternalName()].mayBeModified = true; + tables[*ex->getInternalName()].hasSet = true; } } @@ -109,7 +109,7 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { // might learn anything new. auto hasUnmodifiableTable = false; for (auto& [_, info] : tables) { - if (!info.mayBeModified) { + if (!info.hasSet) { hasUnmodifiableTable = true; break; } @@ -118,39 +118,54 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { return tables; } - using TablesWithSet = std::unordered_set; + // Miniature form of TableInfo, without things we don't need (some of which + // cause compilation errors on the copies below). + struct MiniTableInfo { + bool hasSet = false; + bool hasGrow = false; + }; - ModuleUtils::ParallelFunctionAnalysis analysis( - wasm, [&](Function* func, TablesWithSet& tablesWithSet) { + using MiniTableInfoMap = std::unordered_map; + + ModuleUtils::ParallelFunctionAnalysis analysis( + wasm, [&](Function* func, MiniTableInfoMap& tableInfoMap) { if (func->imported()) { return; } struct Finder : public PostWalker { - TablesWithSet& tablesWithSet; + MiniTableInfoMap& tableInfoMap; - Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} + Finder(MiniTableInfoMap& tableInfoMap) : tableInfoMap(tableInfoMap) {} void visitTableSet(TableSet* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; } void visitTableFill(TableFill* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; } void visitTableCopy(TableCopy* curr) { - tablesWithSet.insert(curr->destTable); + tableInfoMap[curr->destTable].hasSet = true; } void visitTableInit(TableInit* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; + } + void visitTableGrow(TableGrow* curr) { + tableInfoMap[curr->table].hasGrow = true; } }; - Finder(tablesWithSet).walkFunction(func); + Finder(tableInfoMap).walkFunction(func); }); - for (auto& [_, names] : analysis.map) { - for (auto name : names) { - tables[name].mayBeModified = true; + for (auto& [_, tableInfoMap] : analysis.map) { + for (auto& [tableName, info] : tableInfoMap) { + if (info.hasSet) { + tables[tableName].hasSet = true; + } + if (info.hasGrow) { + tables[tableName].hasGrow = true; + } } } diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index cee88fcdbc7..884f2309797 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -122,9 +122,14 @@ bool usesExpressions(ElementSegment* curr, Module* module); // Information about a table's optimizability. struct TableInfo { - // Whether the table may be modifed at runtime, either because it is imported - // or exported, or table.set operations exist for it in the code. - bool mayBeModified = false; + // Whether the table has writes to it (anything but a grow, see below). The + // writes may be internal, or through imports and exports. + bool hasSet = false; + + // Whether the table may grow. Growing does modify the table, but it only + // appends, so we track this separately from mayBeModified. This allows more + // optimizations in tables that grow but have no other sets. + bool hasGrow = false; // Whether we can assume that the initial contents are immutable. That is, if // a table looks like [a, b, c] in the wasm, and we see a call to index 1, we @@ -144,6 +149,9 @@ struct TableInfo { std::unique_ptr flatTable; + // Whether the contents may change. + bool mayBeModified() const { return hasSet || hasGrow; } + // Whether we can optimize using this table's data on the entry level, that // is, individual entries in the table are known to us, so calls through the // table with known indexes can be inferred, etc. @@ -154,7 +162,10 @@ struct TableInfo { // contents, even if other things might be appended later, which we // cannot infer). // * The table is flat (so we can see what is in it, by index). - return (!mayBeModified || initialContentsImmutable) && flatTable->valid; + // + // Note that we do not check hasGrow, as we can optimize at least *some* + // entries in that case (growth only appends). + return (!hasSet || initialContentsImmutable) && flatTable->valid; } }; diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index 69f29101c86..be9f125e451 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -26,7 +26,7 @@ namespace wasm { -GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) +GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm, WorldMode worldMode) : wasm(wasm), publicGroups(wasm.features) { // Find the heap types that are not publicly observable. Even in a closed // world scenario, don't modify public types because we assume that they may @@ -34,6 +34,7 @@ GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) // will be located in the builder. typeInfo = ModuleUtils::collectHeapTypeInfo( wasm, + worldMode, ModuleUtils::TypeInclusion::UsedIRTypes, ModuleUtils::VisibilityHandling::FindVisibility); @@ -324,6 +325,29 @@ void GlobalTypeRewriter::mapTypes(const TypeMap& oldToNewTypes) { for (auto& tag : wasm.tags) { tag->type = updater.getNew(tag->type); } + + // Update indirect call effects per type. + // When A is rewritten to B, B inherits the effects of A and A loses its + // effects. + std::unordered_map> + newTypeEffects; + for (auto& [oldType, oldEffects] : wasm.indirectCallEffects) { + if (!oldEffects) { + continue; + } + + auto newType = updater.getNew(oldType); + std::shared_ptr& targetEffects = + newTypeEffects[newType]; + if (!targetEffects) { + targetEffects = oldEffects; + } else { + auto merged = std::make_shared(*targetEffects); + merged->mergeIn(*oldEffects); + targetEffects = merged; + } + } + wasm.indirectCallEffects = std::move(newTypeEffects); } void GlobalTypeRewriter::mapTypeNamesAndIndices(const TypeMap& oldToNewTypes) { diff --git a/src/ir/type-updating.h b/src/ir/type-updating.h index 0e050becc59..7aaf02a8234 100644 --- a/src/ir/type-updating.h +++ b/src/ir/type-updating.h @@ -358,7 +358,7 @@ class GlobalTypeRewriter { // private types do not conflict with public types. UniqueRecGroups publicGroups; - GlobalTypeRewriter(Module& wasm); + GlobalTypeRewriter(Module& wasm, WorldMode worldMode); virtual ~GlobalTypeRewriter() {} // Main entry point. This performs the entire process of creating new heap @@ -427,7 +427,9 @@ class GlobalTypeRewriter { // Helper for the repeating pattern of just updating Signature types using a // map of old heap type => new Signature. - static void updateSignatures(const SignatureUpdates& updates, Module& wasm) { + static void updateSignatures(const SignatureUpdates& updates, + Module& wasm, + WorldMode worldMode) { if (updates.empty()) { return; } @@ -436,8 +438,10 @@ class GlobalTypeRewriter { const SignatureUpdates& updates; public: - SignatureRewriter(Module& wasm, const SignatureUpdates& updates) - : GlobalTypeRewriter(wasm), updates(updates) { + SignatureRewriter(Module& wasm, + const SignatureUpdates& updates, + WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), updates(updates) { update(); } @@ -448,7 +452,7 @@ class GlobalTypeRewriter { sig.results = getTempType(iter->second.results); } } - } rewriter(wasm, updates); + } rewriter(wasm, updates, worldMode); } protected: @@ -473,8 +477,8 @@ class TypeMapper : public GlobalTypeRewriter { const TypeUpdates& mapping; - TypeMapper(Module& wasm, const TypeUpdates& mapping) - : GlobalTypeRewriter(wasm), mapping(mapping) {} + TypeMapper(Module& wasm, const TypeUpdates& mapping, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), mapping(mapping) {} void map() { // Update the internals of types (struct fields, signatures, etc.) to diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index d3ac4a2eafa..a3494d31ca4 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -194,6 +194,7 @@ function initializeConstants() { 'CallIndirectOverlong', 'RelaxedAtomics', 'CustomPageSizes', + 'WideArithmetic', 'All' ].forEach(name => { Module['Features'][name] = Module['_BinaryenFeature' + name](); @@ -421,11 +422,11 @@ function initializeConstants() { 'RelaxedNmaddVecF32x4', 'RelaxedMaddVecF64x2', 'RelaxedNmaddVecF64x2', - 'LaneselectI8x16', - 'LaneselectI16x8', - 'LaneselectI32x4', - 'LaneselectI64x2', - 'DotI8x16I7x16AddSToVecI32x4', + 'RelaxedLaneselectI8x16', + 'RelaxedLaneselectI16x8', + 'RelaxedLaneselectI32x4', + 'RelaxedLaneselectI64x2', + 'RelaxedDotI8x16I7x16AddSToVecI32x4', 'AnyTrueVec128', 'PopcntVecI8x16', 'AbsVecI8x16', @@ -594,7 +595,7 @@ function initializeConstants() { 'RelaxedMinVecF64x2', 'RelaxedMaxVecF64x2', 'RelaxedQ15MulrSVecI16x8', - 'DotI8x16I7x16SToVecI16x8', + 'RelaxedDotI8x16I7x16SToVecI16x8', 'RefAsNonNull', 'RefAsExternInternalize', 'RefAsExternExternalize', @@ -612,7 +613,11 @@ function initializeConstants() { 'StringEncodeLossyUTF8Array', 'StringEncodeWTF16Array', 'StringEqEqual', - 'StringEqCompare' + 'StringEqCompare', + 'AddInt128', + 'SubInt128', + 'MulWideSInt64', + 'MulWideUInt64' ].forEach(name => { Module['Operations'][name] = Module[name] = Module['_Binaryen' + name](); }); @@ -1183,6 +1188,18 @@ function wrapModule(module, self = {}) { 'add'(left, right) { return Module['_BinaryenBinary'](module, Module['AddInt64'], left, right); }, + 'add128'(leftLow, leftHigh, rightLow, rightHigh) { + return Module['_BinaryenWideIntAddSub'](module, Module['AddInt128'], leftLow, leftHigh, rightLow, rightHigh); + }, + 'sub128'(leftLow, leftHigh, rightLow, rightHigh) { + return Module['_BinaryenWideIntAddSub'](module, Module['SubInt128'], leftLow, leftHigh, rightLow, rightHigh); + }, + 'mul_wide_s'(left, right) { + return Module['_BinaryenWideIntMul'](module, Module['MulWideSInt64'], left, right); + }, + 'mul_wide_u'(left, right) { + return Module['_BinaryenWideIntMul'](module, Module['MulWideUInt64'], left, right); + }, 'sub'(left, right) { return Module['_BinaryenBinary'](module, Module['SubInt64'], left, right); }, @@ -2769,8 +2786,8 @@ function wrapModule(module, self = {}) { return memoryInfo; }); }; - self['getNumMemorySegments'] = function() { - return Module['_BinaryenGetNumMemorySegments'](module); + self['getNumDataSegments'] = function() { + return Module['_BinaryenGetNumDataSegments'](module); }; /** * Gets the data segment with the given name. @@ -2795,9 +2812,9 @@ function wrapModule(module, self = {}) { return Module['_BinaryenGetDataSegmentByIndex'](module, index); }; /** - * Queries information about a memory segment. + * Queries information about a data segment. * - * @param {number} segment - A MemorySegmentRef referring to the memory segment to get information about. + * @param {number} segment - A DataSegmentRef referring to the data segment to get information about. * @returns {Object} An object containing the following fields: * - `name`: The name of the segment. * - `offset`: If the segment is active, the offset expression of the segment. Otherwise, `null`. @@ -2806,19 +2823,19 @@ function wrapModule(module, self = {}) { * * @throws If the given segment reference is invalid. */ - self['getMemorySegmentInfo'] = function(segment) { - const passive = Boolean(Module['_BinaryenGetMemorySegmentPassive'](segment)); + self['getDataSegmentInfo'] = function(segment) { + const passive = Boolean(Module['_BinaryenGetDataSegmentPassive'](segment)); let offset = null; if (!passive) { - offset = Module['_BinaryenGetMemorySegmentByteOffset'](module, segment); + offset = Module['_BinaryenGetDataSegmentByteOffset'](module, segment); } return { 'name': UTF8ToString(Module['_BinaryenDataSegmentGetName'](segment)), 'offset': offset, 'data': (function(){ - const size = Module['_BinaryenGetMemorySegmentByteLength'](segment); + const size = Module['_BinaryenGetDataSegmentByteLength'](segment); const ptr = _malloc(size); - Module['_BinaryenCopyMemorySegmentData'](segment, ptr); + Module['_BinaryenCopyDataSegmentData'](segment, ptr); const res = new Uint8Array(size); res.set(HEAP8.subarray(ptr, ptr + size)); _free(ptr); @@ -4196,6 +4213,60 @@ Module['Binary'] = makeExpressionWrapper(Module['_BinaryenBinaryId'](), { } }); +Module['WideIntAddSub'] = makeExpressionWrapper(Module['_BinaryenWideIntAddSubId'](), { + 'getOp'(expr) { + return Module['_BinaryenWideIntAddSubGetOp'](expr); + }, + 'setOp'(expr, op) { + Module['_BinaryenWideIntAddSubSetOp'](expr, op); + }, + 'getLeftLow'(expr) { + return Module['_BinaryenWideIntAddSubGetLeftLow'](expr); + }, + 'setLeftLow'(expr, leftLowExpr) { + Module['_BinaryenWideIntAddSubSetLeftLow'](expr, leftLowExpr); + }, + 'getLeftHigh'(expr) { + return Module['_BinaryenWideIntAddSubGetLeftHigh'](expr); + }, + 'setLeftHigh'(expr, leftHighExpr) { + Module['_BinaryenWideIntAddSubSetLeftHigh'](expr, leftHighExpr); + }, + 'getRightLow'(expr) { + return Module['_BinaryenWideIntAddSubGetRightLow'](expr); + }, + 'setRightLow'(expr, rightLowExpr) { + Module['_BinaryenWideIntAddSubSetRightLow'](expr, rightLowExpr); + }, + 'getRightHigh'(expr) { + return Module['_BinaryenWideIntAddSubGetRightHigh'](expr); + }, + 'setRightHigh'(expr, rightHighExpr) { + Module['_BinaryenWideIntAddSubSetRightHigh'](expr, rightHighExpr); + } +}); + +Module['WideIntMul'] = makeExpressionWrapper(Module['_BinaryenWideIntMulId'](), { + 'getOp'(expr) { + return Module['_BinaryenWideIntMulGetOp'](expr); + }, + 'setOp'(expr, op) { + Module['_BinaryenWideIntMulSetOp'](expr, op); + }, + 'getLeft'(expr) { + return Module['_BinaryenWideIntMulGetLeft'](expr); + }, + 'setLeft'(expr, leftExpr) { + Module['_BinaryenWideIntMulSetLeft'](expr, leftExpr); + }, + 'getRight'(expr) { + return Module['_BinaryenWideIntMulGetRight'](expr); + }, + 'setRight'(expr, rightExpr) { + Module['_BinaryenWideIntMulSetRight'](expr, rightExpr); + } +}); + Module['Select'] = makeExpressionWrapper(Module['_BinaryenSelectId'](), { 'getIfTrue'(expr) { return Module['_BinaryenSelectGetIfTrue'](expr); diff --git a/src/literal.h b/src/literal.h index 80dda773410..e982dae68aa 100644 --- a/src/literal.h +++ b/src/literal.h @@ -43,28 +43,18 @@ class Literal { // Note: i31 is stored in the |i32| field, with the lower 31 bits containing // the value if there is one, and the highest bit containing whether there // is a value. Thus, a null is |i32 === 0|. - // - // Externref payloads, which serve to differentiate different external - // references but are otherwise meaningless, are also stored in the i32 - // field, with their low bit set to differentiate an externref with a - // payload from an externalized internal reference, which uses the gcData - // field instead. This scheme supports 31 bits of payload for externrefs, - // which should be sufficient for spec test and fuzzing purposes, but if we - // need more bits we can use the i64 field instead. This scheme also depends - // on the low bit of a shared_ptr not being used. int32_t i32; int64_t i64; uint8_t v128[16]; // A reference to Function data. std::shared_ptr funcData; - // A reference to GC data, either a Struct or an Array. For both of those we - // store the referred data as a Literals object (which is natural for an - // Array, and for a Struct, is just the fields in order). The type is used - // to indicate whether this is a Struct or an Array, and of what type. We - // also use this to store String data, as it is similarly stored on the - // heap. For externalized or internalized references (including strings), - // gcData holds a single value, which is the wrapped internal or external - // reference. + // A reference to GC data, used for structs, arrays, strings, externrefs, + // and internalized externrefs. The GCData contains the struct or array + // fields, or the characters in the string. Externrefs are either + // externalized internal references, in which case the GCData will contain + // the internal reference, or a host reference, in which case the GCData + // will contain an i32 payload. Internalized references contain the wrapped + // externref in the GCData. std::shared_ptr gcData; // A reference to Exn data. std::shared_ptr exnData; @@ -266,11 +256,7 @@ class Literal { lit.i32 = value | 0x80000000; return lit; } - static Literal makeExtern(int32_t payload, Shareability share) { - auto lit = Literal(Type(HeapTypes::ext.getBasic(share), NonNullable)); - lit.i32 = (payload << 1) | 1; - return lit; - } + static Literal makeExtern(int32_t payload, Shareability share); // Wasm has nondeterministic rules for NaN propagation in some operations. For // example. f32.neg is deterministic and just flips the sign, even of a NaN, // but f32.add is nondeterministic, and if one or more of the inputs is a NaN, @@ -308,14 +294,8 @@ class Literal { // Cast to unsigned for the left shift to avoid undefined behavior. return signed_ ? int32_t((uint32_t(i32) << 1)) >> 1 : (i32 & 0x7fffffff); } - bool hasExternPayload() const { - assert(type.getHeapType().isMaybeShared(HeapType::ext)); - return (i32 & 1) == 1; - } - int32_t getExternPayload() const { - assert(hasExternPayload()); - return int32_t(uint32_t(i32) >> 1); - } + bool hasExternPayload() const; + int32_t getExternPayload() const; int64_t geti64() const { assert(type == Type::i64); return i64; @@ -723,6 +703,9 @@ class Literal { Literal truncSatZeroUToI32x4() const; Literal demoteZeroToF32x4() const; Literal promoteLowToF64x2() const; + Literal promoteLowF16x8ToF32x4() const; + Literal demoteZeroF32x4ToF16x8() const; + Literal demoteZeroF64x2ToF16x8() const; Literal truncSatToSI16x8() const; Literal truncSatToUI16x8() const; Literal convertSToF16x8() const; @@ -810,6 +793,19 @@ struct GCData { : values(std::move(values)), desc(desc) {} }; +inline bool Literal::hasExternPayload() const { + if (isNull()) { + return false; + } + assert(type.getHeapType().isMaybeShared(HeapType::ext)); + return gcData->values[0].type == Type::i32; +} + +inline int32_t Literal::getExternPayload() const { + assert(hasExternPayload()); + return gcData->values[0].geti32(); +} + } // namespace wasm namespace std { @@ -854,6 +850,14 @@ template<> struct hash { wasm::rehash(digest, a.geti31(true)); return digest; } + if (type.isMaybeShared(wasm::HeapType::ext)) { + if (a.hasExternPayload()) { + wasm::rehash(digest, a.getExternPayload()); + return digest; + } + wasm::rehash(digest, (*this)(a.internalize())); + return digest; + } if (type.isMaybeShared(wasm::HeapType::any)) { // This may be an extern string that was internalized to |any|. Undo // that to get the actual value. (Rehash here with the existing digest, diff --git a/src/parser/CMakeLists.txt b/src/parser/CMakeLists.txt index 8b7846ca9e9..7d4704dba24 100644 --- a/src/parser/CMakeLists.txt +++ b/src/parser/CMakeLists.txt @@ -2,7 +2,6 @@ FILE(GLOB parser_HEADERS *.h) set(parser_SOURCES context-decls.cpp context-defs.cpp - lexer.cpp parse-1-decls.cpp parse-2-typedefs.cpp parse-3-implicit-types.cpp diff --git a/src/parser/context-decls.cpp b/src/parser/context-decls.cpp index 252185d6634..0298f1cee9a 100644 --- a/src/parser/context-decls.cpp +++ b/src/parser/context-decls.cpp @@ -15,6 +15,7 @@ */ #include "contexts.h" +#include "parsers.h" namespace wasm::WATParser { @@ -180,7 +181,6 @@ Result<> ParseDeclsCtx::addImplicitData(DataStringT&& data) { auto& mem = *wasm.memories.back(); auto d = std::make_unique(); d->memory = mem.name; - d->isPassive = false; d->offset = Builder(wasm).makeConstPtr(0, mem.addressType); d->data = std::move(data); d->name = Names::getValidDataSegmentName(wasm, "implicit-data"); @@ -302,4 +302,41 @@ Result<> ParseDeclsCtx::addTag(Name name, return Ok{}; } +bool ParseDeclsCtx::skipFunctionBody() { + using namespace std::string_view_literals; + size_t depth = 1; + while (depth > 0 && !in.empty()) { + if (in.takeLParen()) { + ++depth; + continue; + } + if (in.takeRParen()) { + --depth; + continue; + } + if (auto kw = in.takeKeyword()) { + if (*kw == "block"sv || *kw == "loop"sv || *kw == "if"sv || + *kw == "try"sv || *kw == "try_table"sv) { + in.takeID(); + (void)typeuse(*this); + continue; + } + if (*kw == "call_indirect"sv || *kw == "return_call_indirect"sv) { + (void)maybeTableidx(*this); + (void)typeuse(*this, false); + continue; + } + continue; + } + // Avoid confusion due to parens inside strings by skipping strings as a + // unit. + if (in.takeString()) { + continue; + } + in.take(1); + in.advance(); + } + return true; +} + } // namespace wasm::WATParser diff --git a/src/parser/context-defs.cpp b/src/parser/context-defs.cpp index c69c428d5bc..d0d170ed2c5 100644 --- a/src/parser/context-defs.cpp +++ b/src/parser/context-defs.cpp @@ -108,7 +108,6 @@ Result<> ParseDefsCtx::addData( Name, Name* mem, std::optional offset, DataStringT, Index pos) { auto& d = wasm.dataSegments[index]; if (offset) { - d->isPassive = false; d->offset = *offset; if (mem) { d->memory = *mem; @@ -118,7 +117,7 @@ Result<> ParseDefsCtx::addData( return in.err(pos, "active data segment with no memory"); } } else { - d->isPassive = true; + d->memory = Name(); } return Ok{}; } diff --git a/src/parser/contexts.h b/src/parser/contexts.h index fbdd8d0505a..06a515e32d1 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -476,6 +476,15 @@ struct NullInstrParserCtx { Result<> makeBinary(Index, const std::vector&, BinaryOp) { return Ok{}; } + Result<> + makeWideIntAddSub(Index, const std::vector&, WideIntAddSubOp) { + return Ok{}; + } + + Result<> makeWideIntMul(Index, const std::vector&, WideIntMulOp) { + return Ok{}; + } + Result<> makeUnary(Index, const std::vector&, UnaryOp) { return Ok{}; } @@ -1074,6 +1083,8 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx { recTypeDefs.push_back({{}, pos, Index(recTypeDefs.size()), {}}); } + bool skipFunctionBody(); + Limits makeLimits(uint64_t n, std::optional m) { return Limits{n, m}; } @@ -1985,7 +1996,7 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { void setSrcLoc(const std::vector& annotations) { const Annotation* annotation = nullptr; for (auto& a : annotations) { - if (a.kind == srcAnnotationKind) { + if (a.kind.view() == std::string_view("src")) { annotation = &a; } } @@ -2157,6 +2168,18 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { return withLoc(pos, irBuilder.makeBinary(op)); } + Result<> makeWideIntAddSub(Index pos, + const std::vector& annotations, + WideIntAddSubOp op) { + return withLoc(pos, irBuilder.makeWideIntAddSub(op)); + } + + Result<> makeWideIntMul(Index pos, + const std::vector& annotations, + WideIntMulOp op) { + return withLoc(pos, irBuilder.makeWideIntMul(op)); + } + Result<> makeUnary(Index pos, const std::vector& annotations, UnaryOp op) { return withLoc(pos, irBuilder.makeUnary(op)); diff --git a/src/parser/lexer.cpp b/src/parser/lexer.cpp deleted file mode 100644 index 5d2aedabe66..00000000000 --- a/src/parser/lexer.cpp +++ /dev/null @@ -1,1188 +0,0 @@ -/* - * Copyright 2023 WebAssembly Community Group participants - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "lexer.h" -#include "support/bits.h" -#include "support/string.h" - -using namespace std::string_view_literals; - -namespace wasm::WATParser { - -Name srcAnnotationKind("src"); - -namespace { - -// ================ -// Lexical Analysis -// ================ - -// The result of lexing a token fragment. -struct LexResult { - std::string_view span; -}; - -// Lexing context that accumulates lexed input to produce a token fragment. -struct LexCtx { -private: - // The input we are lexing. - std::string_view input; - - // How much of the input we have already lexed. - size_t lexedSize = 0; - -public: - explicit LexCtx(std::string_view in) : input(in) {} - - // Return the fragment that has been lexed so far. - std::optional lexed() const { - if (lexedSize > 0) { - return {LexResult{input.substr(0, lexedSize)}}; - } - return {}; - } - - // The next input that has not already been lexed. - std::string_view next() const { return input.substr(lexedSize); } - - // Get the next character without consuming it. - uint8_t peek() const { return next()[0]; } - - // The size of the unlexed input. - size_t size() const { return input.size() - lexedSize; } - - // Whether there is no more input. - bool empty() const { return size() == 0; } - - // Tokens must be separated by spaces or parentheses. - bool canFinish() const; - - // Whether the unlexed input starts with prefix `sv`. - size_t startsWith(std::string_view sv) const { - return next().substr(0, sv.size()) == sv; - } - - // Consume the next `n` characters. - void take(size_t n) { lexedSize += n; } - - // Consume an additional lexed fragment. - void take(const LexResult& res) { lexedSize += res.span.size(); } - - // Consume the prefix and return true if possible. - bool takePrefix(std::string_view sv) { - if (startsWith(sv)) { - take(sv.size()); - return true; - } - return false; - } - - // Consume the rest of the input. - void takeAll() { lexedSize = input.size(); } -}; - -enum OverflowBehavior { DisallowOverflow, IgnoreOverflow }; - -std::optional getDigit(char c) { - if ('0' <= c && c <= '9') { - return c - '0'; - } - return {}; -} - -std::optional getHexDigit(char c) { - if ('0' <= c && c <= '9') { - return c - '0'; - } - if ('A' <= c && c <= 'F') { - return 10 + c - 'A'; - } - if ('a' <= c && c <= 'f') { - return 10 + c - 'a'; - } - return {}; -} - -enum Sign { NoSign, Pos, Neg }; - -// The result of lexing an integer token fragment. -struct LexIntResult : LexResult { - uint64_t n; - Sign sign; - - template bool isUnsigned() { - static_assert(std::is_integral_v && std::is_unsigned_v); - return sign == NoSign && n <= std::numeric_limits::max(); - } - - template bool isSigned() { - static_assert(std::is_integral_v && std::is_signed_v); - if (sign == Neg) { - return uint64_t(std::numeric_limits::min()) <= n || n == 0; - } - return n <= uint64_t(std::numeric_limits::max()); - } -}; - -// Lexing context that accumulates lexed input to produce an integer token -// fragment. -struct LexIntCtx : LexCtx { - using LexCtx::take; - -private: - uint64_t n = 0; - Sign sign = NoSign; - bool overflow = false; - -public: - explicit LexIntCtx(std::string_view in) : LexCtx(in) {} - - // Lex only the underlying span, ignoring the overflow and value. - std::optional lexedRaw() { - if (auto basic = LexCtx::lexed()) { - return LexIntResult{*basic, 0, NoSign}; - } - return {}; - } - - std::optional lexed() { - if (overflow) { - return {}; - } - if (auto basic = LexCtx::lexed()) { - return LexIntResult{*basic, sign == Neg ? -n : n, sign}; - } - return {}; - } - - void takeSign() { - if (takePrefix("+"sv)) { - sign = Pos; - } else if (takePrefix("-"sv)) { - sign = Neg; - } else { - sign = NoSign; - } - } - - bool takeDigit() { - if (!empty()) { - if (auto d = getDigit(peek())) { - take(1); - uint64_t newN = n * 10 + *d; - if (newN < n) { - overflow = true; - } - n = newN; - return true; - } - } - return false; - } - - bool takeHexdigit() { - if (!empty()) { - if (auto h = getHexDigit(peek())) { - take(1); - uint64_t newN = n * 16 + *h; - if (newN < n) { - overflow = true; - } - n = newN; - return true; - } - } - return false; - } - - void take(const LexIntResult& res) { - LexCtx::take(res); - n = res.n; - } -}; - -struct LexFloatResult : LexResult { - // The payload if we lexed a nan with payload. We cannot store the payload - // directly in `d` because we do not know at this point whether we are parsing - // an f32 or f64 and therefore we do not know what the allowable payloads are. - // No payload with NaN means to use the default payload for the expected float - // width. - std::optional nanPayload; - double d; -}; - -struct LexFloatCtx : LexCtx { - std::optional nanPayload; - - LexFloatCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - const double posNan = std::copysign(NAN, 1.0); - const double negNan = std::copysign(NAN, -1.0); - assert(!std::signbit(posNan) && "expected positive NaN to be positive"); - assert(std::signbit(negNan) && "expected negative NaN to be negative"); - auto basic = LexCtx::lexed(); - if (!basic) { - return {}; - } - // strtod does not return NaNs with the expected signs on all platforms. - // TODO: use starts_with once we have C++20. - if (basic->span.substr(0, 3) == "nan"sv || - basic->span.substr(0, 4) == "+nan"sv) { - return LexFloatResult{*basic, nanPayload, posNan}; - } - if (basic->span.substr(0, 4) == "-nan"sv) { - return LexFloatResult{*basic, nanPayload, negNan}; - } - // Do not try to implement fully general and precise float parsing - // ourselves. Instead, call out to std::strtod to do our parsing. This means - // we need to strip any underscores since `std::strtod` does not understand - // them. - std::stringstream ss; - for (const char *curr = basic->span.data(), - *end = curr + basic->span.size(); - curr != end; - ++curr) { - if (*curr != '_') { - ss << *curr; - } - } - std::string str = ss.str(); - char* last; - double d = std::strtod(str.data(), &last); - assert(last == str.data() + str.size() && "could not parse float"); - return LexFloatResult{*basic, {}, d}; - } -}; - -struct LexStrResult : LexResult { - // Allocate a string only if there are escape sequences, otherwise just use - // the original string_view. - std::optional str; - - std::string_view getStr() { - if (str) { - return *str; - } - return span; - } -}; - -struct LexStrCtx : LexCtx { -private: - // Used to build a string with resolved escape sequences. Only used when the - // parsed string contains escape sequences, otherwise we can just use the - // parsed string directly. - std::optional escapeBuilder; - -public: - LexStrCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - if (escapeBuilder) { - return LexStrResult{*basic, {escapeBuilder->str()}}; - } else { - return LexStrResult{*basic, {}}; - } - } - return {}; - } - - void takeChar() { - if (escapeBuilder) { - *escapeBuilder << peek(); - } - LexCtx::take(1); - } - - void ensureBuildingEscaped() { - if (escapeBuilder) { - return; - } - // Drop the opening '"'. - escapeBuilder = std::stringstream{}; - *escapeBuilder << LexCtx::lexed()->span.substr(1); - } - - void appendEscaped(char c) { *escapeBuilder << c; } - - bool appendUnicode(uint64_t u) { - if ((0xd800 <= u && u < 0xe000) || 0x110000 <= u) { - return false; - } - String::writeWTF8CodePoint(*escapeBuilder, u); - return true; - } -}; - -struct LexIdResult : LexResult { - bool isStr = false; - std::optional str; -}; - -struct LexIdCtx : LexCtx { - bool isStr = false; - std::optional str; - - LexIdCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - return LexIdResult{*basic, isStr, str}; - } - return {}; - } -}; - -struct LexAnnotationResult : LexResult { - Annotation annotation; -}; - -struct LexAnnotationCtx : LexCtx { - std::string_view kind; - size_t kindSize = 0; - std::string_view contents; - size_t contentsSize = 0; - - explicit LexAnnotationCtx(std::string_view in) : LexCtx(in) {} - - void startKind() { kind = next(); } - - void takeKind(size_t size) { - kindSize += size; - take(size); - } - - void setKind(std::string_view kind) { - this->kind = kind; - kindSize = kind.size(); - } - - void startContents() { contents = next(); } - - void takeContents(size_t size) { - contentsSize += size; - take(size); - } - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - return LexAnnotationResult{ - *basic, - {Name(kind.substr(0, kindSize)), contents.substr(0, contentsSize)}}; - } - return std::nullopt; - } -}; - -std::optional idchar(std::string_view); -std::optional space(std::string_view); -std::optional keyword(std::string_view); -std::optional integer(std::string_view); -std::optional float_(std::string_view); -std::optional str(std::string_view); -std::optional ident(std::string_view); - -// annotation ::= ';;@' [^\n]* | '(@'idchar+ annotelem* ')' -// annotelem ::= keyword | reserved | uN | sN | fN | string | id -// | '(' annotelem* ')' | '(@'idchar+ annotelem* ')' -std::optional annotation(std::string_view in) { - LexAnnotationCtx ctx(in); - if (ctx.takePrefix(";;@"sv)) { - ctx.setKind(srcAnnotationKind.str); - ctx.startContents(); - if (auto size = ctx.next().find('\n'); size != ""sv.npos) { - ctx.takeContents(size); - } else { - ctx.takeContents(ctx.next().size()); - } - } else if (ctx.takePrefix("(@"sv)) { - ctx.startKind(); - bool hasIdchar = false; - while (auto lexed = idchar(ctx.next())) { - ctx.takeKind(1); - hasIdchar = true; - } - if (!hasIdchar) { - return std::nullopt; - } - ctx.startContents(); - size_t depth = 1; - while (true) { - if (ctx.empty()) { - return std::nullopt; - } - if (auto lexed = space(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = keyword(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = integer(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = float_(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = str(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = ident(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (ctx.startsWith("(@"sv)) { - ctx.takeContents(2); - bool hasIdchar = false; - while (auto lexed = idchar(ctx.next())) { - ctx.takeContents(1); - hasIdchar = true; - } - if (!hasIdchar) { - return std::nullopt; - } - ++depth; - continue; - } - if (ctx.startsWith("("sv)) { - ctx.takeContents(1); - ++depth; - continue; - } - if (ctx.startsWith(")"sv)) { - --depth; - if (depth == 0) { - ctx.take(1); - break; - } - ctx.takeContents(1); - continue; - } - // Unrecognized token. - return std::nullopt; - } - } - return ctx.lexed(); -} - -// comment ::= linecomment | blockcomment -// linecomment ::= ';;' linechar* ('\n' | eof) -// linechar ::= c:char (if c != '\n') -// blockcomment ::= '(;' blockchar* ';)' -// blockchar ::= c:char (if c != ';' and c != '(') -// | ';' (if the next char is not ')') -// | '(' (if the next char is not ';') -// | blockcomment -std::optional comment(std::string_view in) { - LexCtx ctx(in); - if (ctx.size() < 2) { - return {}; - } - - // Line comment - if (!ctx.startsWith(";;@"sv) && ctx.takePrefix(";;"sv)) { - if (auto size = ctx.next().find('\n'); size != ""sv.npos) { - ctx.take(size); - } else { - ctx.takeAll(); - } - return ctx.lexed(); - } - - // Block comment (possibly nested!) - if (ctx.takePrefix("(;"sv)) { - size_t depth = 1; - while (depth > 0 && ctx.size() >= 2) { - if (ctx.takePrefix("(;"sv)) { - ++depth; - } else if (ctx.takePrefix(";)"sv)) { - --depth; - } else { - ctx.take(1); - } - } - if (depth > 0) { - // TODO: Add error production for non-terminated block comment. - return {}; - } - return ctx.lexed(); - } - - return {}; -} - -std::optional spacechar(std::string_view in) { - LexCtx ctx(in); - ctx.takePrefix(" "sv) || ctx.takePrefix("\n"sv) || ctx.takePrefix("\r"sv) || - ctx.takePrefix("\t"sv); - return ctx.lexed(); -} - -// space ::= (' ' | format | comment)* -// format ::= '\t' | '\n' | '\r' -std::optional space(std::string_view in) { - LexCtx ctx(in); - while (ctx.size()) { - if (auto lexed = spacechar(ctx.next())) { - ctx.take(*lexed); - } else if (auto lexed = comment(ctx.next())) { - ctx.take(*lexed); - } else { - break; - } - } - return ctx.lexed(); -} - -bool LexCtx::canFinish() const { - // Logically we want to check for eof, parens, and space. But we don't - // actually want to parse more than a couple characters of space, so check for - // individual space chars or comment starts instead. - return empty() || startsWith("("sv) || startsWith(")"sv) || - spacechar(next()) || startsWith(";;"sv); -} - -// num ::= d:digit => d -// | n:num '_'? d:digit => 10*n + d -// digit ::= '0' => 0 | ... | '9' => 9 -std::optional num(std::string_view in, - OverflowBehavior overflow = DisallowOverflow) { - LexIntCtx ctx(in); - if (ctx.empty()) { - return {}; - } - if (!ctx.takeDigit()) { - return {}; - } - while (true) { - bool under = ctx.takePrefix("_"sv); - if (!ctx.takeDigit()) { - if (!under) { - return overflow == DisallowOverflow ? ctx.lexed() : ctx.lexedRaw(); - } - // TODO: Add error production for trailing underscore. - return {}; - } - } -} - -// hexnum ::= h:hexdigit => h -// | n:hexnum '_'? h:hexdigit => 16*n + h -// hexdigit ::= d:digit => d -// | 'A' => 10 | ... | 'F' => 15 -// | 'a' => 10 | ... | 'f' => 15 -std::optional -hexnum(std::string_view in, OverflowBehavior overflow = DisallowOverflow) { - LexIntCtx ctx(in); - if (!ctx.takeHexdigit()) { - return {}; - } - while (true) { - bool under = ctx.takePrefix("_"sv); - if (!ctx.takeHexdigit()) { - if (!under) { - return overflow == DisallowOverflow ? ctx.lexed() : ctx.lexedRaw(); - } - // TODO: Add error production for trailing underscore. - return {}; - } - } -} - -// uN ::= n:num => n (if n < 2^N) -// | '0x' n:hexnum => n (if n < 2^N) -// sN ::= s:sign n:num => [s]n (if -2^(N-1) <= [s]n < 2^(N-1)) -// | s:sign '0x' n:hexnum => [s]n (if -2^(N-1) <= [s]n < 2^(N-1)) -// sign ::= {} => + | '+' => + | '-' => - -// -// Note: Defer bounds and sign checking until we know what kind of integer we -// expect. -std::optional integer(std::string_view in) { - LexIntCtx ctx(in); - ctx.takeSign(); - if (ctx.takePrefix("0x"sv)) { - if (auto lexed = hexnum(ctx.next())) { - ctx.take(*lexed); - if (ctx.canFinish()) { - return ctx.lexed(); - } - } - // TODO: Add error production for unrecognized hexnum. - return {}; - } - if (auto lexed = num(ctx.next())) { - ctx.take(*lexed); - if (ctx.canFinish()) { - return ctx.lexed(); - } - } - return {}; -} - -// float ::= p:num '.'? => p -// | p:num '.' q:frac => p + q -// | p:num '.'? ('E'|'e') s:sign e:num => p * 10^([s]e) -// | p:num '.' q:frac ('E'|'e') s:sign e:num => (p + q) * 10^([s]e) -// frac ::= d:digit => d/10 -// | d:digit '_'? p:frac => (d + p/10) / 10 -std::optional decfloat(std::string_view in) { - LexCtx ctx(in); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - return {}; - } - // Optional '.' followed by optional frac - if (ctx.takePrefix("."sv)) { - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } - } - if (ctx.takePrefix("E"sv) || ctx.takePrefix("e"sv)) { - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - // TODO: Add error production for missing exponent. - return {}; - } - } - return ctx.lexed(); -} - -// hexfloat ::= '0x' p:hexnum '.'? => p -// | '0x' p:hexnum '.' q:hexfrac => p + q -// | '0x' p:hexnum '.'? ('P'|'p') s:sign e:num => p * 2^([s]e) -// | '0x' p:hexnum '.' q:hexfrac ('P'|'p') s:sign e:num -// => (p + q) * 2^([s]e) -// hexfrac ::= h:hexdigit => h/16 -// | h:hexdigit '_'? p:hexfrac => (h + p/16) / 16 -std::optional hexfloat(std::string_view in) { - LexCtx ctx(in); - if (!ctx.takePrefix("0x"sv)) { - return {}; - } - if (auto lexed = hexnum(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - return {}; - } - // Optional '.' followed by optional hexfrac - if (ctx.takePrefix("."sv)) { - if (auto lexed = hexnum(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } - } - if (ctx.takePrefix("P"sv) || ctx.takePrefix("p"sv)) { - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - // TODO: Add error production for missing exponent. - return {}; - } - } - return ctx.lexed(); -} - -// fN ::= s:sign z:fNmag => [s]z -// fNmag ::= z:float => float_N(z) (if float_N(z) != +/-infinity) -// | z:hexfloat => float_N(z) (if float_N(z) != +/-infinity) -// | 'inf' => infinity -// | 'nan' => nan(2^(signif(N)-1)) -// | 'nan:0x' n:hexnum => nan(n) (if 1 <= n < 2^signif(N)) -std::optional float_(std::string_view in) { - LexFloatCtx ctx(in); - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = hexfloat(ctx.next())) { - ctx.take(*lexed); - } else if (auto lexed = decfloat(ctx.next())) { - ctx.take(*lexed); - } else if (ctx.takePrefix("inf"sv)) { - // nop - } else if (ctx.takePrefix("nan"sv)) { - if (ctx.takePrefix(":0x"sv)) { - if (auto lexed = hexnum(ctx.next())) { - ctx.take(*lexed); - ctx.nanPayload = lexed->n; - } else { - // TODO: Add error production for malformed NaN payload. - return {}; - } - } else { - // No explicit payload necessary; we will inject the default payload - // later. - } - } else { - return {}; - } - if (ctx.canFinish()) { - return ctx.lexed(); - } - return {}; -} - -// idchar ::= '0' | ... | '9' -// | 'A' | ... | 'Z' -// | 'a' | ... | 'z' -// | '!' | '#' | '$' | '%' | '&' | ''' | '*' | '+' -// | '-' | '.' | '/' | ':' | '<' | '=' | '>' | '?' -// | '@' | '\' | '^' | '_' | '`' | '|' | '~' -std::optional idchar(std::string_view in) { - LexCtx ctx(in); - if (ctx.empty()) { - return {}; - } - uint8_t c = ctx.peek(); - // All the allowed characters lie in the range '!' to '~', and within that - // range the vast majority of characters are allowed, so it is significantly - // faster to check for the disallowed characters instead. - if (c < '!' || c > '~') { - return ctx.lexed(); - } - switch (c) { - case '"': - case '(': - case ')': - case ',': - case ';': - case '[': - case ']': - case '{': - case '}': - return ctx.lexed(); - } - ctx.take(1); - return ctx.lexed(); -} - -// string ::= '"' (b*:stringelem)* '"' => concat((b*)*) -// (if |concat((b*)*)| < 2^32) -// stringelem ::= c:stringchar => utf8(c) -// | '\' n:hexdigit m:hexdigit => 16*n + m -// stringchar ::= c:char => c -// (if c >= U+20 && c != U+7f && c != '"' && c != '\') -// | '\t' => \t | '\n' => \n | '\r' => \r -// | '\\' => \ | '\"' => " | '\'' => ' -// | '\u{' n:hexnum '}' => U+(n) -// (if n < 0xD800 and 0xE000 <= n <= 0x110000) -std::optional str(std::string_view in) { - LexStrCtx ctx(in); - if (!ctx.takePrefix("\""sv)) { - return {}; - } - while (!ctx.takePrefix("\""sv)) { - if (ctx.empty()) { - // TODO: Add error production for unterminated string. - return {}; - } - if (ctx.startsWith("\\"sv)) { - // Escape sequences - ctx.ensureBuildingEscaped(); - ctx.take(1); - if (ctx.takePrefix("t"sv)) { - ctx.appendEscaped('\t'); - } else if (ctx.takePrefix("n"sv)) { - ctx.appendEscaped('\n'); - } else if (ctx.takePrefix("r"sv)) { - ctx.appendEscaped('\r'); - } else if (ctx.takePrefix("\\"sv)) { - ctx.appendEscaped('\\'); - } else if (ctx.takePrefix("\""sv)) { - ctx.appendEscaped('"'); - } else if (ctx.takePrefix("'"sv)) { - ctx.appendEscaped('\''); - } else if (ctx.takePrefix("u{"sv)) { - auto lexed = hexnum(ctx.next()); - if (!lexed) { - // TODO: Add error production for malformed unicode escapes. - return {}; - } - ctx.take(*lexed); - if (!ctx.takePrefix("}"sv)) { - // TODO: Add error production for malformed unicode escapes. - return {}; - } - if (!ctx.appendUnicode(lexed->n)) { - // TODO: Add error production for invalid unicode values. - return {}; - } - } else { - LexIntCtx ictx(ctx.next()); - if (!ictx.takeHexdigit() || !ictx.takeHexdigit()) { - // TODO: Add error production for unrecognized escape sequence. - return {}; - } - auto lexed = *ictx.lexed(); - ctx.take(lexed); - ctx.appendEscaped(char(lexed.n)); - } - } else { - // Normal characters - if (uint8_t c = ctx.peek(); c >= 0x20 && c != 0x7F) { - ctx.takeChar(); - } else { - // TODO: Add error production for unescaped control characters. - return {}; - } - } - } - return ctx.lexed(); -} - -// id ::= '$' idchar+ | '$' str -std::optional ident(std::string_view in) { - LexIdCtx ctx(in); - if (!ctx.takePrefix("$"sv)) { - return {}; - } - // Quoted identifier e.g. $"foo" - if (auto s = str(ctx.next())) { - if (!String::isUTF8(s->getStr())) { - return {}; - } - - // empty names, including $"" are not allowed. - if (s->span == "\"\"") { - return {}; - } - - ctx.isStr = true; - ctx.str = s->str; - ctx.take(*s); - } else if (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - while (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - } - } else { - return {}; - } - if (ctx.canFinish()) { - return ctx.lexed(); - } - return {}; -} - -// keyword ::= ( 'a' | ... | 'z' ) idchar* (if literal terminal in grammar) -// reserved ::= idchar+ -// -// The "keyword" token we lex here covers both keywords as well as any reserved -// tokens that match the keyword format. This saves us from having to enumerate -// all the valid keywords here. These invalid keywords will still produce -// errors, just at a higher level of the parser. -std::optional keyword(std::string_view in) { - LexCtx ctx(in); - if (ctx.empty()) { - return {}; - } - uint8_t start = ctx.peek(); - if ('a' <= start && start <= 'z') { - ctx.take(1); - } else { - return {}; - } - while (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - } - return ctx.lexed(); -} - -} // anonymous namespace - -void Lexer::skipSpace() { - while (true) { - if (auto ctx = annotation(next())) { - pos += ctx->span.size(); - annotations.push_back(ctx->annotation); - continue; - } - if (auto ctx = space(next())) { - pos += ctx->span.size(); - continue; - } - break; - } -} - -std::optional Lexer::peekChar() const { - auto n = next(); - if (n.empty()) { - return std::nullopt; - } - - return n[0]; -} - -bool Lexer::takeLParen() { - if (LexCtx(next()).startsWith("("sv)) { - ++pos; - advance(); - return true; - } - return false; -} - -bool Lexer::takeRParen() { - if (LexCtx(next()).startsWith(")"sv)) { - ++pos; - advance(); - return true; - } - return false; -} - -std::optional Lexer::takeString() { - if (auto result = str(next())) { - pos += result->span.size(); - advance(); - if (result->str) { - return result->str; - } - // Remove quotes. - return std::string(result->span.substr(1, result->span.size() - 2)); - } - return std::nullopt; -} - -std::optional Lexer::takeID() { - if (auto result = ident(next())) { - pos += result->span.size(); - advance(); - if (result->str) { - return Name(*result->str); - } - if (result->isStr) { - // Remove '$' and quotes. - return Name(result->span.substr(2, result->span.size() - 3)); - } - // Remove '$'. - return Name(result->span.substr(1)); - } - return std::nullopt; -} - -std::optional Lexer::takeKeyword() { - if (auto result = keyword(next())) { - pos += result->span.size(); - advance(); - return result->span; - } - return std::nullopt; -} - -bool Lexer::takeKeyword(std::string_view expected) { - if (auto result = keyword(next()); result && result->span == expected) { - pos += expected.size(); - advance(); - return true; - } - return false; -} - -std::optional Lexer::takeOffset() { - if (auto result = keyword(next())) { - if (result->span.substr(0, 7) != "offset="sv) { - return std::nullopt; - } - Lexer subLexer(result->span.substr(7)); - if (auto o = subLexer.takeU64()) { - pos += result->span.size(); - advance(); - return o; - } - } - return std::nullopt; -} - -std::optional Lexer::takeAlign() { - if (auto result = keyword(next())) { - if (result->span.substr(0, 6) != "align="sv) { - return std::nullopt; - } - Lexer subLexer(result->span.substr(6)); - if (auto o = subLexer.takeU32()) { - if (Bits::popCount(*o) != 1) { - return std::nullopt; - } - pos += result->span.size(); - advance(); - return o; - } - } - return std::nullopt; -} - -template std::optional Lexer::takeU() { - static_assert(std::is_integral_v && std::is_unsigned_v); - if (auto result = integer(next()); result && result->isUnsigned()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - // TODO: Add error production for unsigned overflow. - return std::nullopt; -} - -template std::optional Lexer::takeS() { - static_assert(std::is_integral_v && std::is_signed_v); - if (auto result = integer(next()); result && result->isSigned()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - return std::nullopt; -} - -template std::optional Lexer::takeI() { - static_assert(std::is_integral_v && std::is_unsigned_v); - if (auto result = integer(next())) { - if (result->isUnsigned() || result->isSigned>()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - } - return std::nullopt; -} - -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); - -std::optional Lexer::takeF64() { - constexpr int signif = 52; - constexpr uint64_t payloadMask = (1ull << signif) - 1; - constexpr uint64_t nanDefault = 1ull << (signif - 1); - if (auto result = float_(next())) { - double d = result->d; - if (std::isnan(d)) { - // Inject payload. - uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; - if (payload == 0 || payload > payloadMask) { - // TODO: Add error production for out-of-bounds payload. - return std::nullopt; - } - uint64_t bits; - static_assert(sizeof(bits) == sizeof(d)); - memcpy(&bits, &d, sizeof(bits)); - bits = (bits & ~payloadMask) | payload; - memcpy(&d, &bits, sizeof(bits)); - } - pos += result->span.size(); - advance(); - return d; - } - if (auto result = integer(next())) { - pos += result->span.size(); - advance(); - if (result->sign == Neg) { - if (result->n == 0) { - return -0.0; - } - return double(int64_t(result->n)); - } - return double(result->n); - } - return std::nullopt; -} - -std::optional Lexer::takeF32() { - constexpr int signif = 23; - constexpr uint32_t payloadMask = (1u << signif) - 1; - constexpr uint64_t nanDefault = 1ull << (signif - 1); - if (auto result = float_(next())) { - float f = result->d; - if (std::isnan(f)) { - // Validate and inject payload. - uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; - if (payload == 0 || payload > payloadMask) { - // TODO: Add error production for out-of-bounds payload. - return std::nullopt; - } - uint32_t bits; - static_assert(sizeof(bits) == sizeof(f)); - memcpy(&bits, &f, sizeof(bits)); - bits = (bits & ~payloadMask) | payload; - memcpy(&f, &bits, sizeof(bits)); - } - pos += result->span.size(); - advance(); - return f; - } - if (auto result = integer(next())) { - pos += result->span.size(); - advance(); - if (result->sign == Neg) { - if (result->n == 0) { - return -0.0f; - } - return float(int64_t(result->n)); - } - return float(result->n); - } - return std::nullopt; -} - -TextPos Lexer::position(const char* c) const { - assert(size_t(c - buffer.data()) <= buffer.size()); - TextPos pos{1, 0}; - for (const char* p = buffer.data(); p != c; ++p) { - if (*p == '\n') { - pos.line++; - pos.col = 0; - } else { - pos.col++; - } - } - return pos; -} - -bool TextPos::operator==(const TextPos& other) const { - return line == other.line && col == other.col; -} - -std::ostream& operator<<(std::ostream& os, const TextPos& pos) { - return os << pos.line << ":" << pos.col; -} - -} // namespace wasm::WATParser diff --git a/src/parser/lexer.h b/src/parser/lexer.h index ac6549f0de8..a65bac4eb85 100644 --- a/src/parser/lexer.h +++ b/src/parser/lexer.h @@ -14,34 +14,42 @@ * limitations under the License. */ +#ifndef parser_lexer_h +#define parser_lexer_h + +#include +#include #include #include #include -#include #include #include +#include #include #include +#include +#include "support/bits.h" #include "support/name.h" #include "support/result.h" #include "support/string.h" -#ifndef parser_lexer_h -#define parser_lexer_h - namespace wasm::WATParser { struct TextPos { size_t line; size_t col; - bool operator==(const TextPos& other) const; + bool operator==(const TextPos& other) const { + return line == other.line && col == other.col; + } bool operator!=(const TextPos& other) const { return !(*this == other); } - - friend std::ostream& operator<<(std::ostream& os, const TextPos& pos); }; +inline std::ostream& operator<<(std::ostream& os, const TextPos& pos) { + return os << pos.line << ":" << pos.col; +} + // =========== // Annotations // =========== @@ -51,8 +59,6 @@ struct Annotation { std::string_view contents; }; -extern Name srcAnnotationKind; - // ===== // Lexer // ===== @@ -63,13 +69,13 @@ struct Lexer { std::vector annotations; std::optional file; + static bool isSpacechar(uint8_t c); + public: std::string_view buffer; - Lexer(std::string_view buffer, std::optional file = std::nullopt) - : file(file), buffer(buffer) { - setPos(0); - } + Lexer(std::string_view buffer, + std::optional file = std::nullopt); size_t getPos() const { return pos; } @@ -78,42 +84,34 @@ struct Lexer { advance(); } + // Consume the next `n` characters. + void take(size_t n) { pos += n; } + void takeAll() { pos = buffer.size(); } + + // Whether the unlexed input starts with prefix `sv`. + size_t startsWith(std::string_view sv) const { + return next().starts_with(sv); + } + std::optional peekChar() const; + bool peekLParen() { return peek() == uint8_t('('); } + bool takeLParen(); - bool peekLParen() { return Lexer(*this).takeLParen(); } + bool peekRParen() { return peek() == uint8_t(')'); } bool takeRParen(); - bool peekRParen() { return Lexer(*this).takeRParen(); } - - bool takeUntilParen() { - while (true) { - if (empty()) { - return false; - } - if (peekLParen() || peekRParen()) { - return true; - } - // Do not count the parentheses in strings. - if (takeString()) { - continue; - } - ++pos; - advance(); - } - } + bool takeUntilParen(); std::optional takeID(); + std::optional peekKeyword(); + std::optional takeKeyword(); bool takeKeyword(std::string_view expected); - std::optional peekKeyword() { - return Lexer(*this).takeKeyword(); - } - std::optional takeOffset(); std::optional takeAlign(); @@ -125,62 +123,43 @@ struct Lexer { std::optional takeU8() { return takeU(); } std::optional takeI8() { return takeI(); } - std::optional takeF64(); std::optional takeF32(); + std::optional takeF64(); std::optional takeString(); - std::optional takeName() { - auto str = takeString(); - if (!str || !String::isUTF8(*str)) { - return std::nullopt; - } - return Name(*str); - } + std::optional takeName(); - bool takeSExprStart(std::string_view expected) { - auto original = *this; - if (takeLParen() && takeKeyword(expected)) { - return true; - } - *this = original; - return false; - } + bool takeSExprStart(std::string_view expected); - bool peekSExprStart(std::string_view expected) { - auto original = *this; - if (!takeLParen()) { - return false; - } - bool ret = takeKeyword(expected); - *this = original; - return ret; - } + bool peekSExprStart(std::string_view expected); std::string_view next() const { return buffer.substr(pos); } + std::optional peek() const { + if (empty()) { + return std::nullopt; + } + return uint8_t(buffer[pos]); + } + void advance() { annotations.clear(); skipSpace(); } bool empty() const { return pos == buffer.size(); } + size_t remaining() const { return buffer.size() - pos; } TextPos position(const char* c) const; + TextPos position(size_t i) const { return position(buffer.data() + i); } TextPos position(std::string_view span) const { return position(span.data()); } TextPos position() const { return position(getPos()); } - [[nodiscard]] Err err(size_t pos, std::string reason) { - std::stringstream msg; - if (file) { - msg << *file << ":"; - } - msg << position(pos) << ": error: " << reason; - return Err{msg.str()}; - } + [[nodiscard]] Err err(size_t pos, std::string reason); [[nodiscard]] Err err(std::string reason) { return err(getPos(), reason); } @@ -192,13 +171,1010 @@ struct Lexer { } private: + std::optional getDigit(char c); + + std::optional getHexDigit(char c); + + // Consume the prefix and return true if possible. + bool takePrefix(std::string_view sv); + + std::optional takeDigit(); + + std::optional takeHexdigit(); + + enum OverflowBehavior { DisallowOverflow, IgnoreOverflow }; + + std::optional takeNum(OverflowBehavior behavior = DisallowOverflow); + + std::optional + takeHexnum(OverflowBehavior behavior = DisallowOverflow); + + enum Sign { NoSign, Pos, Neg }; + + Sign takeSign(); + + struct LexedInteger { + uint64_t n; + Sign sign; + + template bool isUnsigned(); + template bool isSigned(); + }; + + std::optional takeInteger(); + template std::optional takeU(); + template std::optional takeS(); + template std::optional takeI(); + std::optional takeDecfloat(); + + std::optional takeHexfloat(); + + struct LexedFloat { + std::optional nanPayload; + double d; + }; + + std::optional takeFloat(); + + struct StringOrView : std::variant { + using std::variant::variant; + std::string_view str() const { + return std::visit([](auto& s) -> std::string_view { return s; }, *this); + } + }; + + std::optional takeStr(); + + bool idchar(); + + std::optional takeIdent(); + + bool spacechar(); + + bool takeSpacechar(); + + bool takeComment(); + + bool takeSpace(); + + std::optional takeAnnotation(); + void skipSpace(); + + bool canFinish(); }; +inline Lexer::Lexer(std::string_view buffer, std::optional file) + : file(file), buffer(buffer) { + setPos(0); +} + +inline std::optional Lexer::peekChar() const { + if (auto c = peek()) { + return char(*c); + } + return std::nullopt; +} + +inline bool Lexer::takeLParen() { + if (peekLParen()) { + take(1); + advance(); + return true; + } + return false; +} + +inline bool Lexer::takeRParen() { + if (peekRParen()) { + take(1); + advance(); + return true; + } + return false; +} + +inline bool Lexer::takeUntilParen() { + while (true) { + if (empty()) { + return false; + } + if (peekLParen() || peekRParen()) { + return true; + } + // Do not count the parentheses in strings. + if (takeString()) { + continue; + } + ++pos; + advance(); + } +} + +inline std::optional Lexer::takeID() { + if (auto result = takeIdent()) { + auto name = Name(result->str()); + advance(); + return name; + } + return std::nullopt; +} + +inline std::optional Lexer::peekKeyword() { + auto start = peek(); + if (!start || *start < 'a' || *start > 'z') { + return std::nullopt; + } + auto startPos = pos; + take(1); + while (idchar()) { + take(1); + } + auto ret = buffer.substr(startPos, pos - startPos); + pos = startPos; + return ret; +} + +inline std::optional Lexer::takeKeyword() { + auto keyword = peekKeyword(); + if (keyword) { + take(keyword->size()); + advance(); + } + return keyword; +} + +inline bool Lexer::takeKeyword(std::string_view expected) { + if (!startsWith(expected)) { + return false; + } + auto startPos = pos; + take(expected.size()); + if (canFinish()) { + advance(); + return true; + } + pos = startPos; + return false; +} + +inline std::optional Lexer::takeOffset() { + using namespace std::string_view_literals; + auto startPos = pos; + if (auto offset = takeKeyword()) { + if (!offset->starts_with("offset="sv)) { + pos = startPos; + return std::nullopt; + } + Lexer subLexer(offset->substr(7)); + if (auto o = subLexer.takeU64()) { + advance(); + return o; + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeAlign() { + using namespace std::string_view_literals; + auto startPos = pos; + if (auto result = takeKeyword()) { + if (!result->starts_with("align="sv)) { + pos = startPos; + return std::nullopt; + } + Lexer subLexer(result->substr(6)); + if (auto o = subLexer.takeU32()) { + if (Bits::popCount(*o) != 1) { + pos = startPos; + return std::nullopt; + } + advance(); + return o; + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeF32() { + constexpr int signif = 23; + constexpr uint32_t payloadMask = (1u << signif) - 1; + constexpr uint64_t nanDefault = 1ull << (signif - 1); + auto startPos = pos; + if (auto result = takeFloat()) { + float f = result->d; + if (std::isnan(f)) { + // Validate and inject payload. + uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; + if (payload == 0 || payload > payloadMask) { + // TODO: Add error production for out-of-bounds payload. + pos = startPos; + return std::nullopt; + } + uint32_t bits; + static_assert(sizeof(bits) == sizeof(f)); + memcpy(&bits, &f, sizeof(bits)); + bits = (bits & ~payloadMask) | payload; + memcpy(&f, &bits, sizeof(bits)); + } + advance(); + return f; + } + if (auto result = takeInteger()) { + advance(); + if (result->sign == Neg) { + if (result->n == 0) { + return -0.0f; + } + return -static_cast(result->n); + } + return static_cast(result->n); + } + return std::nullopt; +} + +inline std::optional Lexer::takeF64() { + constexpr int signif = 52; + constexpr uint64_t payloadMask = (1ull << signif) - 1; + constexpr uint64_t nanDefault = 1ull << (signif - 1); + auto startPos = pos; + if (auto result = takeFloat()) { + double d = result->d; + if (std::isnan(d)) { + // Inject payload. + uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; + if (payload == 0 || payload > payloadMask) { + // TODO: Add error production for out-of-bounds payload. + pos = startPos; + return std::nullopt; + } + uint64_t bits; + static_assert(sizeof(bits) == sizeof(d)); + memcpy(&bits, &d, sizeof(bits)); + bits = (bits & ~payloadMask) | payload; + memcpy(&d, &bits, sizeof(bits)); + } + advance(); + return d; + } + if (auto result = takeInteger()) { + advance(); + if (result->sign == Neg) { + if (result->n == 0) { + return -0.0; + } + return -static_cast(result->n); + } + return static_cast(result->n); + } + return std::nullopt; +} + +inline std::optional Lexer::takeString() { + if (auto str = takeStr()) { + advance(); + if (auto* s = std::get_if(&*str)) { + return std::move(*s); + } + auto view = std::get(*str); + return std::string(view); + } + return std::nullopt; +} + +inline std::optional Lexer::takeName() { + auto str = takeString(); + if (!str || !String::isUTF8(*str)) { + return std::nullopt; + } + return Name(*str); +} + +inline bool Lexer::takeSExprStart(std::string_view expected) { + auto original = *this; + if (takeLParen() && takeKeyword(expected)) { + return true; + } + *this = original; + return false; +} + +inline bool Lexer::peekSExprStart(std::string_view expected) { + auto original = *this; + if (!takeLParen()) { + return false; + } + bool ret = takeKeyword(expected); + *this = original; + return ret; +} + +inline TextPos Lexer::position(const char* c) const { + assert(size_t(c - buffer.data()) <= buffer.size()); + TextPos pos{1, 0}; + for (const char* p = buffer.data(); p != c; ++p) { + if (*p == '\n') { + pos.line++; + pos.col = 0; + } else { + pos.col++; + } + } + return pos; +} + +inline Err Lexer::err(size_t pos, std::string reason) { + std::stringstream msg; + if (file) { + msg << *file << ":"; + } + msg << position(pos) << ": error: " << reason; + return Err{msg.str()}; +} + +inline std::optional Lexer::getDigit(char c) { + if ('0' <= c && c <= '9') { + return c - '0'; + } + return std::nullopt; +} + +inline std::optional Lexer::getHexDigit(char c) { + if (auto d = getDigit(c)) { + return d; + } + if ('A' <= c && c <= 'F') { + return 10 + c - 'A'; + } + if ('a' <= c && c <= 'f') { + return 10 + c - 'a'; + } + return std::nullopt; +} + +inline bool Lexer::takePrefix(std::string_view sv) { + if (startsWith(sv)) { + take(sv.size()); + return true; + } + return false; +} + +inline std::optional Lexer::takeDigit() { + if (auto c = peek()) { + if (auto d = getDigit(*c)) { + take(1); + return d; + } + } + return std::nullopt; +} + +inline std::optional Lexer::takeHexdigit() { + if (auto c = peek()) { + if (auto h = getHexDigit(*c)) { + take(1); + return h; + } + } + return std::nullopt; +} + +inline std::optional Lexer::takeNum(OverflowBehavior behavior) { + using namespace std::string_view_literals; + auto startPos = pos; + bool overflow = false; + uint64_t n = 0; + if (auto d = takeDigit()) { + n = *d; + } else { + return std::nullopt; + } + while (true) { + bool under = takePrefix("_"sv); + if (auto d = takeDigit()) { + uint64_t newN = n * 10 + *d; + if (newN < n) { + overflow = true; + } + n = newN; + continue; + } + if (!under && (!overflow || behavior == IgnoreOverflow)) { + return n; + } + // TODO: Add error productions for trailing underscore and overflow. + pos = startPos; + return std::nullopt; + } +} + +inline std::optional Lexer::takeHexnum(OverflowBehavior behavior) { + using namespace std::string_view_literals; + auto startPos = pos; + bool overflow = false; + uint64_t n = 0; + if (auto d = takeHexdigit()) { + n = *d; + } else { + return std::nullopt; + } + while (true) { + bool under = takePrefix("_"sv); + if (auto d = takeHexdigit()) { + uint64_t newN = n * 16 + *d; + if (newN < n) { + overflow = true; + } + n = newN; + continue; + } + if (!under && (!overflow || behavior == IgnoreOverflow)) { + return n; + } + // TODO: Add error productions for trailing underscore and overflow. + pos = startPos; + return std::nullopt; + } +} + +inline Lexer::Sign Lexer::takeSign() { + if (auto c = peek()) { + if (*c == '+') { + take(1); + return Pos; + } + if (*c == '-') { + take(1); + return Neg; + } + } + return NoSign; +} + +template bool Lexer::LexedInteger::isUnsigned() { + static_assert(std::is_integral_v && std::is_unsigned_v); + return sign == NoSign && n <= std::numeric_limits::max(); +} + +template bool Lexer::LexedInteger::isSigned() { + static_assert(std::is_integral_v && std::is_signed_v); + if (sign == Neg) { + // Absolute value of min() for two's complement integers is max() + 1. + uint64_t absMin = uint64_t(std::numeric_limits::max()) + 1; + return n <= absMin; + } + return n <= uint64_t(std::numeric_limits::max()); +} + +inline std::optional Lexer::takeInteger() { + using namespace std::string_view_literals; + auto startPos = pos; + auto sign = takeSign(); + if (takePrefix("0x"sv)) { + if (auto n = takeHexnum()) { + if (canFinish()) { + return LexedInteger{*n, sign}; + } + } + // TODO: Add error production for unrecognized hexnum. + pos = startPos; + return std::nullopt; + } + if (auto n = takeNum()) { + if (canFinish()) { + return LexedInteger{*n, sign}; + } + } + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeU() { + static_assert(std::is_integral_v && std::is_unsigned_v); + auto startPos = pos; + if (auto result = takeInteger(); result && result->isUnsigned()) { + advance(); + return static_cast(result->n); + } + // TODO: Add error production for unsigned overflow. + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeS() { + static_assert(std::is_integral_v && std::is_signed_v); + auto startPos = pos; + if (auto result = takeInteger(); result && result->isSigned()) { + advance(); + if (result->sign == Neg) { + return static_cast(-result->n); + } + return static_cast(result->n); + } + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeI() { + static_assert(std::is_integral_v && std::is_unsigned_v); + auto startPos = pos; + if (auto result = takeInteger()) { + if (result->isUnsigned() || result->isSigned>()) { + advance(); + if (result->sign == Neg) { + return static_cast(-result->n); + } + return static_cast(result->n); + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeDecfloat() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takeNum(IgnoreOverflow)) { + return std::nullopt; + } + // Optional '.' followed by optional frac + if (takePrefix("."sv)) { + takeNum(IgnoreOverflow); + } + if (takePrefix("E"sv) || takePrefix("e"sv)) { + // Optional sign + takeSign(); + if (!takeNum(IgnoreOverflow)) { + // TODO: Add error production for missing exponent. + pos = startPos; + return std::nullopt; + } + } + return buffer.substr(startPos, pos - startPos); +} + +inline std::optional Lexer::takeHexfloat() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("0x"sv)) { + return std::nullopt; + } + if (!takeHexnum(IgnoreOverflow)) { + pos = startPos; + return std::nullopt; + } + // Optional '.' followed by optional hexfrac + if (takePrefix("."sv)) { + takeHexnum(IgnoreOverflow); + } + if (takePrefix("P"sv) || takePrefix("p"sv)) { + // Optional sign + takeSign(); + if (!takeNum(IgnoreOverflow)) { + // TODO: Add error production for missing exponent. + pos = startPos; + return std::nullopt; + } + } + return buffer.substr(startPos, pos - startPos); +} + +inline std::optional Lexer::takeFloat() { + using namespace std::string_view_literals; + auto startPos = pos; + std::optional nanPayload; + bool isNan = false; + // Optional sign + auto sign = takeSign(); + if (takeHexfloat() || takeDecfloat() || takePrefix("inf"sv)) { + // nop. + } else if (takePrefix("nan"sv)) { + isNan = true; + if (takePrefix(":0x"sv)) { + if (auto n = takeHexnum()) { + nanPayload = n; + } else { + // TODO: Add error production for malformed NaN payload. + pos = startPos; + return std::nullopt; + } + } else { + // No explicit payload necessary; we will inject the default payload + // later. + } + } else { + pos = startPos; + return std::nullopt; + } + if (!canFinish()) { + pos = startPos; + return std::nullopt; + } + // strtod does not return NaNs with the expected signs on all platforms. + if (isNan) { + if (sign == Neg) { + const double negNan = std::copysign(NAN, -1.0); + assert(std::signbit(negNan) && "expected negative NaN to be negative"); + return LexedFloat{nanPayload, negNan}; + } else { + const double posNan = std::copysign(NAN, 1.0); + assert(!std::signbit(posNan) && "expected positive NaN to be positive"); + return LexedFloat{nanPayload, posNan}; + } + } + // Do not try to implement fully general and precise float parsing + // ourselves. Instead, call out to std::strtod to do our parsing. This means + // we need to strip any underscores since `std::strtod` does not understand + // them. + std::stringstream ss; + for (const char *curr = buffer.data() + startPos, *end = buffer.data() + pos; + curr != end; + ++curr) { + if (*curr != '_') { + ss << *curr; + } + } + std::string str = ss.str(); + char* last; + double d = std::strtod(str.data(), &last); + assert(last == str.data() + str.size() && "could not parse float"); + return LexedFloat{std::nullopt, d}; +} + +inline std::optional Lexer::takeStr() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("\""sv)) { + return std::nullopt; + } + // Used to build a string with resolved escape sequences. Only used when the + // parsed string contains escape sequences, otherwise we can just use the + // parsed string directly. + std::optional escapeBuilder; + auto ensureBuildingEscaped = [&]() { + if (escapeBuilder) { + return; + } + // Drop the opening '"'. + escapeBuilder = std::stringstream{}; + *escapeBuilder << buffer.substr(startPos + 1, pos - startPos - 1); + }; + while (!takePrefix("\""sv)) { + if (empty()) { + // TODO: Add error production for unterminated string. + pos = startPos; + return std::nullopt; + } + if (startsWith("\\"sv)) { + // Escape sequences + ensureBuildingEscaped(); + take(1); + if (empty()) { + pos = startPos; + return std::nullopt; + } + auto c = peek(); + if (!c) { + pos = startPos; + return std::nullopt; + } + take(1); + switch (*c) { + case 't': + *escapeBuilder << '\t'; + break; + case 'n': + *escapeBuilder << '\n'; + break; + case 'r': + *escapeBuilder << '\r'; + break; + case '\\': + *escapeBuilder << '\\'; + break; + case '"': + *escapeBuilder << '"'; + break; + case '\'': + *escapeBuilder << '\''; + break; + case 'u': { + if (!takePrefix("{"sv)) { + pos = startPos; + return std::nullopt; + } + auto code = takeHexnum(); + if (!code) { + // TODO: Add error production for malformed unicode escapes. + pos = startPos; + return std::nullopt; + } + if (!takePrefix("}"sv)) { + // TODO: Add error production for malformed unicode escapes. + pos = startPos; + return std::nullopt; + } + if ((0xd800 <= *code && *code < 0xe000) || 0x110000 <= *code) { + // TODO: Add error production for invalid unicode values. + pos = startPos; + return std::nullopt; + } + String::writeWTF8CodePoint(*escapeBuilder, *code); + break; + } + default: { + // Byte escape: \hh + // We already took the first h as c. + auto first = getHexDigit(*c); + auto second = takeHexdigit(); + if (!first || !second) { + // TODO: Add error production for unrecognized escape sequence. + pos = startPos; + return std::nullopt; + } + *escapeBuilder << char(*first * 16 + *second); + } + } + } else { + // Normal characters + uint8_t c = *peek(); + if (c >= 0x20 && c != 0x7F) { + if (escapeBuilder) { + *escapeBuilder << c; + } + take(1); + } else { + // TODO: Add error production for unescaped control characters. + pos = startPos; + return std::nullopt; + } + } + } + if (escapeBuilder) { + return escapeBuilder->str(); + } + // Drop the quotes. + return buffer.substr(startPos + 1, pos - startPos - 2); +} + +inline bool Lexer::idchar() { + auto c = peek(); + if (!c) { + return false; + } + // All the allowed characters lie in the range '!' to '~', and within that + // range the vast majority of characters are allowed, so it is significantly + // faster to check for the disallowed characters instead. + if (*c < '!' || *c > '~') { + return false; + } + switch (*c) { + case '"': + case '(': + case ')': + case ',': + case ';': + case '[': + case ']': + case '{': + case '}': + return false; + } + return true; +} + +inline std::optional Lexer::takeIdent() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("$"sv)) { + return {}; + } + // Quoted identifier e.g. $"foo" + std::optional str; + if ((str = takeStr())) { + if (str->str().empty() || !String::isUTF8(str->str())) { + pos = startPos; + return std::nullopt; + } + } else if (idchar()) { + take(1); + while (idchar()) { + take(1); + } + } else { + pos = startPos; + return std::nullopt; + } + if (canFinish()) { + if (str) { + return str; + } + // Drop the "$". + return buffer.substr(startPos + 1, pos - startPos - 1); + } + pos = startPos; + return std::nullopt; +} + +inline bool Lexer::isSpacechar(uint8_t c) { + switch (c) { + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } +} + +inline bool Lexer::spacechar() { + if (auto c = peek()) { + return isSpacechar(*c); + } + return false; +} + +inline bool Lexer::takeSpacechar() { + if (spacechar()) { + take(1); + return true; + } + return false; +} + +inline bool Lexer::takeComment() { + using namespace std::string_view_literals; + + if (remaining() < 2) { + return false; + } + + // Line comment + if (!startsWith(";;@"sv) && takePrefix(";;"sv)) { + if (auto size = next().find('\n'); size != ""sv.npos) { + take(size); + } else { + takeAll(); + } + return true; + } + + // Block comment (possibly nested!) + if (takePrefix("(;"sv)) { + size_t depth = 1; + while (depth > 0 && remaining() >= 2) { + if (takePrefix("(;"sv)) { + ++depth; + } else if (takePrefix(";)"sv)) { + --depth; + } else { + take(1); + } + } + if (depth > 0) { + // TODO: Add error production for non-terminated block comment. + return false; + } + return true; + } + + return false; +} + +inline bool Lexer::takeSpace() { + bool taken = false; + while (remaining() && (takeSpacechar() || takeComment())) { + taken = true; + continue; + } + return taken; +} + +inline std::optional Lexer::takeAnnotation() { + using namespace std::string_view_literals; + auto startPos = pos; + std::string_view kind; + std::string_view contents; + if (takePrefix(";;@"sv)) { + kind = "src"sv; + auto contentPos = pos; + if (auto size = next().find('\n'); size != ""sv.npos) { + take(size); + } else { + takeAll(); + } + contents = buffer.substr(contentPos, pos - contentPos); + } else if (takePrefix("(@"sv)) { + auto kindPos = pos; + bool hasIdchar = false; + while (idchar()) { + take(1); + hasIdchar = true; + } + if (!hasIdchar) { + pos = startPos; + return std::nullopt; + } + kind = buffer.substr(kindPos, pos - kindPos); + auto contentPos = pos; + size_t depth = 1; + while (true) { + if (empty()) { + pos = startPos; + return std::nullopt; + } + if (takeSpace() || takeKeyword() || takeInteger() || takeFloat() || + takeStr() || takeIdent()) { + continue; + } + if (takePrefix("(@"sv)) { + bool hasIdchar = false; + while (idchar()) { + take(1); + hasIdchar = true; + } + if (!hasIdchar) { + pos = startPos; + return std::nullopt; + } + ++depth; + continue; + } + if (takeLParen()) { + ++depth; + continue; + } + if (takePrefix(")"sv)) { + --depth; + if (depth == 0) { + break; + } + continue; + } + // Unrecognized token. + pos = startPos; + return std::nullopt; + } + contents = buffer.substr(contentPos, pos - contentPos - 1); + } else { + return std::nullopt; + } + return Annotation{Name(kind), contents}; +} + +inline void Lexer::skipSpace() { + while (true) { + if (auto annotation = takeAnnotation()) { + annotations.emplace_back(*std::move(annotation)); + continue; + } + if (takeSpace()) { + continue; + } + break; + } +} + +inline bool Lexer::canFinish() { + // Logically we want to check for eof, parens, and space. But we don't + // actually want to parse more than a couple characters of space, so check + // for individual space chars or comment starts instead. + using namespace std::string_view_literals; + auto c = peek(); + if (!c) { + return true; + } + return isSpacechar(*c) || *c == '(' || *c == ')' || startsWith(";;"sv); +} + } // namespace wasm::WATParser #endif // parser_lexer_h diff --git a/src/parser/parsers.h b/src/parser/parsers.h index c6919d0b3ed..ffbcdfae233 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -91,6 +91,14 @@ Result<> makeNop(Ctx&, Index, const std::vector&); template Result<> makeBinary(Ctx&, Index, const std::vector&, BinaryOp op); template +Result<> makeWideIntAddSub(Ctx&, + Index, + const std::vector&, + WideIntAddSubOp op); +template +Result<> +makeWideIntMul(Ctx&, Index, const std::vector&, WideIntMulOp op); +template Result<> makeUnary(Ctx&, Index, const std::vector&, UnaryOp op); template Result<> makeSelect(Ctx&, Index, const std::vector&); @@ -1340,7 +1348,7 @@ loop(Ctx& ctx, const std::vector& annotations, bool folded) { // | '(' 'try' label blocktype '(' 'do' instr* ')' // ('(' 'catch' tagidx instr* ')')* // ('(' 'catch_all' instr* ')')? ')' -// | 'try' label blocktype instr* 'deledate' label +// | 'try' label blocktype instr* 'delegate' label // | '(' 'try' label blocktype '(' 'do' instr* ')' // '(' 'delegate' label ')' ')' template @@ -1592,6 +1600,22 @@ Result<> makeBinary(Ctx& ctx, return ctx.makeBinary(pos, annotations, op); } +template +Result<> makeWideIntAddSub(Ctx& ctx, + Index pos, + const std::vector& annotations, + WideIntAddSubOp op) { + return ctx.makeWideIntAddSub(pos, annotations, op); +} + +template +Result<> makeWideIntMul(Ctx& ctx, + Index pos, + const std::vector& annotations, + WideIntMulOp op) { + return ctx.makeWideIntMul(pos, annotations, op); +} + template Result<> makeUnary(Ctx& ctx, Index pos, @@ -3491,6 +3515,7 @@ template MaybeResult<> func(Ctx& ctx) { typename Ctx::TypeUseT type; Exactness exact = Exact; std::optional localVars; + bool skipped = false; if (import) { auto use = exacttypeuse(ctx); @@ -3505,13 +3530,14 @@ template MaybeResult<> func(Ctx& ctx) { CHECK_ERR(l); localVars = *l; } - if (!ctx.skipFunctionBody()) { + skipped = ctx.skipFunctionBody(); + if (!skipped) { CHECK_ERR(instrs(ctx)); ctx.setSrcLoc(ctx.in.takeAnnotations()); } } - if (!ctx.skipFunctionBody() && !ctx.in.takeRParen()) { + if ((import || !skipped) && !ctx.in.takeRParen()) { return ctx.in.err("expected end of function"); } diff --git a/src/pass.h b/src/pass.h index f5088e5e7ee..8bbf9612a90 100644 --- a/src/pass.h +++ b/src/pass.h @@ -110,6 +110,30 @@ struct InliningOptions { Index partialInliningIfs = 0; }; +// Assume code outside of the module does not inspect or interact with GC and +// function references, with the goal of being able to aggressively optimize all +// user-defined types. The outside may hold on to references and pass them back +// in, but may not inspect their contents, call them, construct them, or reflect +// on their types in any way. +// +// By default we do not make this assumption, and assume anything that escapes +// to the outside may be inspected in detail, which prevents us from e.g. +// changing the type of any value that may escape except by refining it (so we +// can't remove or refine fields on an escaping struct type, for example, +// unless the new type declares the original type as a supertype). +// +// Note that the module can still have imports and exports - otherwise it +// could do nothing at all! - so the meaning of "closed world" is a little +// subtle here. We do still want to keep imports and exports unchanged, as +// they form a contract with the outside world. For example, if an import has +// two parameters, we can't remove one of them. A nuance regarding that is how +// type equality works between wasm modules using the isorecursive type +// system: not only do we need to not remove a parameter as just mentioned, +// but we also want to keep types of things on the boundary unchanged. For +// example, we should not change an exported function's signature, as the +// outside may need that type to properly call the export. +enum class WorldMode { Open, Closed }; + struct PassOptions { friend Pass; @@ -196,29 +220,7 @@ struct PassOptions { // creates it and we know it is all zeros right before the active segments are // applied.) bool zeroFilledMemory = false; - // Assume code outside of the module does not inspect or interact with GC and - // function references, with the goal of being able to aggressively optimize - // all user-defined types. The outside may hold on to references and pass them - // back in, but may not inspect their contents, call them, or reflect on their - // types in any way. - // - // By default we do not make this assumption, and assume anything that escapes - // to the outside may be inspected in detail, which prevents us from e.g. - // changing the type of any value that may escape except by refining it (so we - // can't remove or refine fields on an escaping struct type, for example, - // unless the new type declares the original type as a supertype). - // - // Note that the module can still have imports and exports - otherwise it - // could do nothing at all! - so the meaning of "closed world" is a little - // subtle here. We do still want to keep imports and exports unchanged, as - // they form a contract with the outside world. For example, if an import has - // two parameters, we can't remove one of them. A nuance regarding that is how - // type equality works between wasm modules using the isorecursive type - // system: not only do we need to not remove a parameter as just mentioned, - // but we also want to keep types of things on the boundary unchanged. For - // example, we should not change an exported function's signature, as the - // outside may need that type to properly call the export. - bool closedWorld = false; + WorldMode worldMode = WorldMode::Open; // Whether to try to preserve debug info through, which are special calls. bool debugInfo = false; // Whether to generate StackIR during binary writing. This is on by default diff --git a/src/passes/AbstractTypeRefining.cpp b/src/passes/AbstractTypeRefining.cpp index b75153e3d2d..a22488dd1c4 100644 --- a/src/passes/AbstractTypeRefining.cpp +++ b/src/passes/AbstractTypeRefining.cpp @@ -87,7 +87,7 @@ struct AbstractTypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "AbstractTypeRefining requires --closed-world"; } @@ -116,7 +116,8 @@ struct AbstractTypeRefining : public Pass { // module, given closed world, but we'd also need to make sure that // we don't need to make any changes to public types that refer to // them. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { createdTypes.insert(type); } @@ -289,8 +290,10 @@ struct AbstractTypeRefining : public Pass { // that for Unsubtyping. class AbstractTypeRefiningTypeMapper : public TypeMapper { public: - AbstractTypeRefiningTypeMapper(Module& wasm, const TypeUpdates& mapping) - : TypeMapper(wasm, mapping) {} + AbstractTypeRefiningTypeMapper(Module& wasm, + const TypeUpdates& mapping, + WorldMode worldMode) + : TypeMapper(wasm, mapping, worldMode) {} std::optional getDeclaredSuperType(HeapType oldType) override { // We do not want to update subtype relationships. @@ -298,7 +301,8 @@ struct AbstractTypeRefining : public Pass { } }; - AbstractTypeRefiningTypeMapper(*module, mapping).map(); + AbstractTypeRefiningTypeMapper(*module, mapping, getPassOptions().worldMode) + .map(); // Refinalize to propagate the type changes we made. For example, a refined // cast may lead to a struct.get reading a more refined type using that diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index a8410a24c05..feea9adf7c2 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -1730,7 +1730,7 @@ struct AsyncifyLocals : public WalkerPass> { } // anonymous namespace static std::string getFullImportName(Name module, Name base) { - return std::string(module.str) + '.' + base.toString(); + return module.toString() + '.' + base.toString(); } struct Asyncify : public Pass { diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index c2952e174b8..9a3e1738e93 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -65,6 +65,7 @@ set(passes_SOURCES LocalSubtyping.cpp LogExecution.cpp LoopInvariantCodeMotion.cpp + MarkJSCalled.cpp Memory64Lowering.cpp MemoryPacking.cpp MergeBlocks.cpp @@ -89,6 +90,7 @@ set(passes_SOURCES PostEmscripten.cpp Precompute.cpp Print.cpp + PrintBoundary.cpp PrintCallGraph.cpp PrintFeatures.cpp PrintFunctionMap.cpp @@ -105,6 +107,7 @@ set(passes_SOURCES TraceCalls.cpp RandomizeBranchHints.cpp RedundantSetElimination.cpp + RemoveExports.cpp RemoveImports.cpp RemoveMemoryInit.cpp RemoveNonJSOps.cpp diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index 65e3e3471ed..7964818121a 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -56,11 +56,14 @@ // #include +#include +#include #include "ir/branch-utils.h" #include "ir/effects.h" #include "ir/eh-utils.h" #include "ir/find_all.h" +#include "ir/iteration.h" #include "ir/label-utils.h" #include "ir/utils.h" #include "pass.h" @@ -74,9 +77,9 @@ static const Index WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH = 3; struct ExpressionMarker : public PostWalker> { - std::set& marked; + std::unordered_set& marked; - ExpressionMarker(std::set& marked, Expression* expr) + ExpressionMarker(std::unordered_set& marked, Expression* expr) : marked(marked) { walk(expr); } @@ -122,13 +125,16 @@ struct CodeFolding // pass state - std::map> breakTails; // break target name => tails - // that reach it + std::unordered_map> + breakTails; // break target name => tails + // that reach it std::vector unreachableTails; // tails leading to (unreachable) std::vector returnTails; // tails leading to (return) - std::set unoptimizables; // break target names that we can't handle - std::set modifieds; // modified code should not be processed - // again, wait for next pass + std::unordered_set + unoptimizables; // break target names that we can't handle + std::unordered_set + modifieds; // modified code should not be processed + // again, wait for next pass // walking @@ -246,7 +252,7 @@ struct CodeFolding auto* right = curr->ifFalse->dynCast(); // If one is a block and the other isn't, and the non-block is a tail of the // other, we can fold that - for our convenience, we just add a block and - // run the rest of the optimization mormally. + // run the rest of the optimization normally. auto maybeAddBlock = [this](Block* block, Expression*& other) -> Block* { // If other is a suffix of the block, wrap it in a block. // @@ -294,6 +300,7 @@ struct CodeFolding returnTails.clear(); unoptimizables.clear(); modifieds.clear(); + exitingBranchCache.clear(); if (needEHFixups) { EHUtils::handleBlockNestedPops(func, *getModule()); } @@ -301,11 +308,104 @@ struct CodeFolding } private: + // Cache of exiting branch names, populated on demand. Only queried roots + // are stored. An empty set means no exiting branches. + std::unordered_map> exitingBranchCache; + + bool hasExitingBranches(Expression* expr) { + auto it = exitingBranchCache.find(expr); + if (it != exitingBranchCache.end()) { + return !it->second.empty(); + } + return !populateExitingBranchCache(expr).empty(); + } + + // Walk |root| bottom-up computing exiting branches. Name sets are kept + // transiently (moved from children, erased after merge). Only the root's + // name set is persisted. Already-cached subtrees are skipped via scan(), + // and their cached names are merged in precisely. + // Returns a reference to the root's cached set (which may be empty). + const std::unordered_set& populateExitingBranchCache(Expression* root) { + struct CachePopulator + : public PostWalker> { + std::unordered_map>& resultCache; + std::unordered_map> nameSets; + + CachePopulator( + std::unordered_map>& resultCache) + : resultCache(resultCache) {} + + static void scan(CachePopulator* self, Expression** currp) { + auto* curr = *currp; + if (self->resultCache.count(curr)) { + return; + } + PostWalker>::scan(self, currp); + } + + void visitExpression(Expression* curr) { + std::unordered_set targets; + + ChildIterator children(curr); + for (auto* child : children) { + auto it = nameSets.find(child); + if (it != nameSets.end()) { + if (targets.empty()) { + targets = std::move(it->second); + } else { + targets.merge(it->second); + } + nameSets.erase(it); + } else { + // Child was skipped by scan() — merge its cached names. + auto cacheIt = resultCache.find(child); + if (cacheIt != resultCache.end() && !cacheIt->second.empty()) { + if (targets.empty()) { + targets = cacheIt->second; + } else { + targets.insert(cacheIt->second.begin(), cacheIt->second.end()); + } + } + } + } + + BranchUtils::operateOnScopeNameUses( + curr, [&](Name& name) { targets.insert(name); }); + + BranchUtils::operateOnScopeNameDefs(curr, [&](Name& name) { + if (name.is()) { + targets.erase(name); + } + }); + + if (!targets.empty()) { + nameSets[curr] = std::move(targets); + } + } + }; + CachePopulator populator(exitingBranchCache); + populator.walk(root); + auto it = populator.nameSets.find(root); + if (it != populator.nameSets.end()) { + return exitingBranchCache[root] = std::move(it->second); + } + return exitingBranchCache[root] = {}; + } + // check if we can move a list of items out of another item. we can't do so // if one of the items has a branch to something inside outOf that is not // inside that item bool canMove(const std::vector& items, Expression* outOf) { - auto allTargets = BranchUtils::getBranchTargets(outOf); + return canMove(items, outOf, BranchUtils::getBranchTargets(outOf)); + } + + // Overload that accepts pre-computed branch targets to avoid redundant + // O(N) getBranchTargets calls. + bool canMove(const std::vector& items, + Expression* outOf, + const BranchUtils::NameSet& allTargets) { for (auto* item : items) { auto exiting = BranchUtils::getExitingBranches(item); std::vector intersection; @@ -539,11 +639,18 @@ struct CodeFolding // we are just starting; num > 0 means that tails is guaranteed to be // equal in the last num items, so we can merge there, but we look for // deeper merges first. + // bodyTargets is lazily computed on first need and then passed to recursive + // calls to avoid repeated O(N) getBranchTargets walks over the function body. // returns whether we optimized something. - bool optimizeTerminatingTails(std::vector& tails, Index num = 0) { + bool optimizeTerminatingTails(std::vector& tails, + Index num = 0, + BranchUtils::NameSet* bodyTargets = nullptr) { if (tails.size() < 2) { return false; } + // Storage for body branch targets, declared here so it outlives the + // pointer stored in bodyTargets. + BranchUtils::NameSet localBodyTargets; // remove things that are untoward and cannot be optimized tails.erase( std::remove_if(tails.begin(), @@ -594,19 +701,21 @@ struct CodeFolding for (auto* item : items) { saved += Measurer::measure(item) * (tails.size() - 1); } - // compure the cost: in non-fallthroughs, we are replacing the final + // compute the cost: in non-fallthroughs, we are replacing the final // element with a br; for a fallthrough, if there is one, we must // add a return element (for the function body, so it doesn't reach us) - // TODO: handle fallthroughts for return + // TODO: handle fallthroughs for return Index cost = tails.size(); // we also need to add two blocks: for us to break to, and to contain // that block and the merged code. very possibly one of the blocks // can be removed, though cost += WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH; // if we cannot merge to the end, then we definitely need 2 blocks, - // and a branch - // TODO: efficiency, entire body - if (!canMove(items, getFunction()->body)) { + // and a branch. Use the pre-computed bodyTargets to avoid repeated + // O(N) getBranchTargets calls. + assert(bodyTargets); + bool canMoveItems = canMove(items, getFunction()->body, *bodyTargets); + if (!canMoveItems) { cost += 1 + WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH; // TODO: to do this, we need to maintain a map of element=>parent, // so that we can insert the new blocks in the right place @@ -632,9 +741,7 @@ struct CodeFolding // TODO: this should not be a problem in // *non*-terminating tails, but // double-verify that - if (EffectAnalyzer( - getPassOptions(), *getModule(), newItem) - .hasExternalBreakTargets()) { + if (hasExitingBranches(newItem)) { return true; } return false; @@ -644,9 +751,10 @@ struct CodeFolding if (next.size() >= 2) { // now we want to find a mergeable item - any item that is equal among a // subset - std::map hashes; // expression => hash value + std::unordered_map + hashes; // expression => hash value // hash value => expressions with that hash - std::map> hashed; + std::unordered_map> hashed; for (auto& tail : next) { auto* item = getItem(tail, num); auto hash = hashes[item] = ExpressionAnalyzer::hash(item); @@ -654,7 +762,7 @@ struct CodeFolding } // look at each hash value exactly once. we do this in a deterministic // order by iterating over a vector retaining insertion order. - std::set seen; + std::unordered_set seen; for (auto& tail : next) { auto* item = getItem(tail, num); auto digest = hashes[item]; @@ -703,7 +811,14 @@ struct CodeFolding // as the changes may influence us. we leave further opts to further // passes (as this is rare in practice, it's generally not a perf // issue, but TODO optimize) - if (optimizeTerminatingTails(explore, num + 1)) { + // Compute body branch targets once and share across recursive + // calls to avoid repeated O(N) tree walks. + if (!bodyTargets) { + localBodyTargets = + BranchUtils::getBranchTargets(getFunction()->body); + bodyTargets = &localBodyTargets; + } + if (optimizeTerminatingTails(explore, num + 1, bodyTargets)) { return true; } } diff --git a/src/passes/CodePushing.cpp b/src/passes/CodePushing.cpp index 67bef95c468..31aaf151e28 100644 --- a/src/passes/CodePushing.cpp +++ b/src/passes/CodePushing.cpp @@ -204,7 +204,7 @@ class Pusher { auto* pushable = isPushable(list[i]); if (pushable) { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // we can't push this, so further pushables must pass it cumulativeEffects.mergeIn(effects); } else { @@ -354,7 +354,7 @@ class Pusher { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // This can't be moved forward. Add it to the things that are not // moving. cumulativeEffects.walk(list[i]); @@ -424,7 +424,7 @@ class Pusher { // TODO: After pushing we could recurse and run both this function and // optimizeSegment in that location. For now, leave that to later - // cycles of the optimizer, as this case seems rairly rare. + // cycles of the optimizer, as this case seems fairly rare. return true; }; diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index e2b75e88de8..66f45bb6047 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -328,31 +328,23 @@ struct FunctionOptimizer : public WalkerPass> { } values[2]; // Handle one of the subtypes of the relevant type. We check what value it - // has for the field, and update |values|. If we hit a problem, we mark us - // as having failed. - auto fail = false; + // has for the field, and update |values|. If we hit a problem, we stop + // early. auto handleType = [&](HeapType type, Index depth) { - if (fail) { - // TODO: Add a mechanism to halt |iterSubTypes| in the middle, as once - // we fail there is no point to further iterating. - return; - } - auto iter = refTestInfos.find({type, Exact}); if (iter == refTestInfos.end()) { // This type has no allocations, so we can ignore it: it is abstract. - return; + return true; } auto value = iter->second[index]; if (!value.hasNoted()) { // Also abstract and ignorable. - return; + return true; } if (!value.isConstant()) { // The value here is not constant, so give up entirely. - fail = true; - return; + return false; } // Consider the constant value compared to previous ones. @@ -375,14 +367,15 @@ struct FunctionOptimizer : public WalkerPass> { // least, we can do that if there is another iteration: If it's already // the last, we've failed to find only two values. if (i == 1) { - fail = true; - return; + return false; } } + + return true; }; - subTypes.iterSubTypes(refHeapType, handleType); - if (fail) { + // If we stopped early, we hit a problem and failed. + if (!subTypes.iterSubTypes(refHeapType, handleType)) { return; } @@ -549,7 +542,7 @@ struct ConstantFieldPropagation : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "CFP requires --closed-world"; } @@ -677,6 +670,7 @@ struct ConstantFieldPropagation : public Pass { if (readable[{sub, Exact}][dst.index].combine(val)) { applyCopiesFrom(sub, Exact, dst.index, val); } + return true; }); } else { // The copy destination is exact, so there are no subtypes to diff --git a/src/passes/DataFlowOpts.cpp b/src/passes/DataFlowOpts.cpp index 79878f4c962..a3d169ec8ce 100644 --- a/src/passes/DataFlowOpts.cpp +++ b/src/passes/DataFlowOpts.cpp @@ -135,7 +135,7 @@ struct DataFlowOpts : public WalkerPass> { } // Now we know that all our DataFlow inputs are constant, and all // our Binaryen IR representations of them are constant too. RUn - // precompute, which will transform the expression into a constanat. + // precompute, which will transform the expression into a constant. Module temp; // XXX we should copy expr here, in principle, and definitely will need to // when we do arbitrarily regenerated expressions diff --git a/src/passes/DeNaN.cpp b/src/passes/DeNaN.cpp index 0251a0c589f..01845f4a6b0 100644 --- a/src/passes/DeNaN.cpp +++ b/src/passes/DeNaN.cpp @@ -201,7 +201,7 @@ struct DeNaN : public WalkerPass< module->addFunction(std::move(func)); }; - // Check if a contant v128 may contain f32 or f64 NaNs. + // Check if a constant v128 may contain f32 or f64 NaNs. bool hasNaNLane(Const* c) { assert(c->type == Type::v128); auto value = c->value; diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 11dc2cec1fb..1ebc576fa9a 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -235,7 +235,7 @@ struct DAE2 : public Pass { } optimizeReferencedFuncs = - getPassOptions().closedWorld && wasm->features.hasGC(); + getPassOptions().worldMode == WorldMode::Closed && wasm->features.hasGC(); TIME(Timer timer); @@ -579,7 +579,8 @@ void DAE2::analyzeModule() { // // TODO: Analyze tags and remove their unused parameters. std::unordered_set unrewritableRoots; - publicHeapTypes = ModuleUtils::getPublicHeapTypes(*wasm); + publicHeapTypes = + ModuleUtils::getPublicHeapTypes(*wasm, getPassOptions().worldMode); for (auto type : publicHeapTypes) { if (type.isSignature()) { unrewritableRoots.insert(getRootType(type)); @@ -728,7 +729,8 @@ void DAE2::computeFixedPoint() { struct DAETypeUpdater : GlobalTypeRewriter { DAE2& parent; DAETypeUpdater(DAE2& parent) - : GlobalTypeRewriter(*parent.wasm), parent(parent) {} + : GlobalTypeRewriter(*parent.wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifySignature(HeapType oldType, Signature& sig) override { // All signature types in a type tree will have the same parameters removed diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index 38f5df3e34f..2c0809b8dba 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -31,6 +31,7 @@ #include "call-utils.h" #include "ir/drop.h" +#include "ir/eh-utils.h" #include "ir/find_all.h" #include "ir/table-utils.h" #include "ir/utils.h" @@ -52,6 +53,8 @@ struct FunctionDirectizer : public WalkerPass> { FunctionDirectizer(const TableUtils::TableInfoMap& tables) : tables(tables) {} + bool optimized = false; + void visitCallIndirect(CallIndirect* curr) { auto& table = tables.at(curr->table); if (!table.canOptimizeByEntry()) { @@ -62,6 +65,7 @@ struct FunctionDirectizer : public WalkerPass> { std::vector operands(curr->operands.begin(), curr->operands.end()); makeDirectCall(operands, curr->target, table, curr); + optimized = true; return; } @@ -74,6 +78,7 @@ struct FunctionDirectizer : public WalkerPass> { *getFunction(), *getModule())) { replaceCurrent(calls); + optimized = true; // Note that types may have changed, as the utility here can add locals // which require fixups if they are non-nullable, for example. changedTypes = true; @@ -81,8 +86,17 @@ struct FunctionDirectizer : public WalkerPass> { } } + bool hasTry = false; + + void visitTry(Try* curr) { hasTry = true; } + void doWalkFunction(Function* func) { WalkerPass>::doWalkFunction(func); + + if (optimized && hasTry) { + EHUtils::handleBlockNestedPops(func, *getModule()); + } + if (changedTypes) { ReFinalize().walkFunctionInModule(func, getModule()); } @@ -113,18 +127,23 @@ struct FunctionDirectizer : public WalkerPass> { // The index is out of bounds for the initial table's content. This may // trap, but it may also not trap if the table is modified later (if a // function is appended to it). - if (!table.mayBeModified) { + if (!table.mayBeModified()) { return CallUtils::Trap{}; } else { // The table may be modified, so it might be appended to. We should only - // get here in the case that the initial contents are immutable, as - // otherwise we have nothing to optimize at all. - assert(table.initialContentsImmutable); + // get here in the case that the initial contents are immutable, or the + // table can grow, as otherwise we have nothing to optimize at all. + assert(table.initialContentsImmutable || table.hasGrow); return CallUtils::Unknown{}; } } auto name = flatTable.names[index]; if (!name.is()) { + // No segment wrote to this part of the initial contents of the table. + // This must trap, as we only get here if we can optimize such cases, + // relying on the fact that the table cannot be modified, or at least the + // initial contents cannot be. + assert(!table.hasSet || table.initialContentsImmutable); return CallUtils::Trap{}; } auto* func = getModule()->getFunction(name); diff --git a/src/passes/DuplicateFunctionElimination.cpp b/src/passes/DuplicateFunctionElimination.cpp index b86d657dc51..88253e00583 100644 --- a/src/passes/DuplicateFunctionElimination.cpp +++ b/src/passes/DuplicateFunctionElimination.cpp @@ -64,7 +64,7 @@ struct DuplicateFunctionElimination : public Pass { }); // Find actually equal functions and prepare to replace them std::map replacements; - std::set duplicates; + std::unordered_set duplicates; for (auto& [_, group] : hashGroups) { Index size = group.size(); if (size == 1) { diff --git a/src/passes/EncloseWorld.cpp b/src/passes/EncloseWorld.cpp index 34ceb51bfc2..2061937d8b6 100644 --- a/src/passes/EncloseWorld.cpp +++ b/src/passes/EncloseWorld.cpp @@ -17,7 +17,7 @@ // // "Closes" the world, in the sense of making it more compatible with the // --closed-world flag, in a potentially destructive manner. This is mainly -// useful for fuzzing (in that a random module is usually very incomptable with +// useful for fuzzing (in that a random module is usually very incompatible with // closed world, with most types being public and hence unoptimizable, but // running this pass makes as many as we can fully private). // diff --git a/src/passes/Flatten.cpp b/src/passes/Flatten.cpp index 1c2cfbcd536..10c791f609e 100644 --- a/src/passes/Flatten.cpp +++ b/src/passes/Flatten.cpp @@ -265,7 +265,7 @@ struct Flatten // br_if leaves a value on the stack if not taken, which later can // be the last element of the enclosing innermost block and flow // out. The local we created using 'getTempForBreakTarget' returns - // the return type of the block this branch is targetting, which may + // the return type of the block this branch is targeting, which may // not be the same with the innermost block's return type. For // example, // (block $any (result anyref) diff --git a/src/passes/GUFA.cpp b/src/passes/GUFA.cpp index a4567aaea6d..533c033524e 100644 --- a/src/passes/GUFA.cpp +++ b/src/passes/GUFA.cpp @@ -97,8 +97,8 @@ struct GUFAOptimizer std::unordered_map newContents; Expression* replaceCurrent(Expression* rep) { + optimized = true; newContents[rep] = oracle.getContents(getCurrent()); - return WalkerPass< PostWalker>>::replaceCurrent(rep); @@ -140,7 +140,6 @@ struct GUFAOptimizer // code. replaceCurrent(getDroppedChildrenAndAppend( curr, wasm, options, builder.makeUnreachable())); - optimized = true; return; } @@ -169,7 +168,6 @@ struct GUFAOptimizer // valid here. if (Type::isSubType(c->type, curr->type)) { replaceCurrent(getDroppedChildrenAndAppend(curr, wasm, options, c)); - optimized = true; } else { // The type is not compatible: we cannot place |c| in this location, even // though we have proven it is the only value possible here. @@ -214,7 +212,6 @@ struct GUFAOptimizer assert(Properties::isConstantExpression(c)); replaceCurrent(getDroppedChildrenAndAppend( curr, wasm, options, builder.makeUnreachable())); - optimized = true; } } } diff --git a/src/passes/GenerateDynCalls.cpp b/src/passes/GenerateDynCalls.cpp index a49ff1408d9..2645994461a 100644 --- a/src/passes/GenerateDynCalls.cpp +++ b/src/passes/GenerateDynCalls.cpp @@ -18,7 +18,7 @@ // Create `dynCall` helper functions used by emscripten. These allow JavaScript // to call back into WebAssembly given a function pointer (table index). These // are used primarily to implement the `invoke` functions which in turn are used -// to implment exceptions handling and setjmp/longjmp. Creates one for each +// to implement exceptions handling and setjmp/longjmp. Creates one for each // signature in the indirect function table. // diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 06e25edf090..720752372aa 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -22,176 +22,343 @@ #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" -#include "support/unique_deferring_queue.h" +#include "support/graph_traversal.h" +#include "support/strongly_connected_components.h" +#include "support/utilities.h" #include "wasm.h" namespace wasm { -struct GenerateGlobalEffects : public Pass { - void run(Module* module) override { - // First, we do a scan of each function to see what effects they have, - // including which functions they call directly (so that we can compute - // transitive effects later). - - struct FuncInfo { - // Effects in this function. - std::optional effects; - - // Directly-called functions from this function. - std::unordered_set calledFunctions; - }; - - ModuleUtils::ParallelFunctionAnalysis analysis( - *module, [&](Function* func, FuncInfo& funcInfo) { - if (func->imported()) { - // Imports can do anything, so we need to assume the worst anyhow, - // which is the same as not specifying any effects for them in the - // map (which we do by not setting funcInfo.effects). - return; - } +namespace { + +struct FuncInfo { + // Effects in this function. nullopt means that we don't know what effects + // this function has, so we conservatively assume all effects. + // Nullopt cases won't be copied to Function::effects. + std::optional effects; + + // Directly-called functions from this function. + std::unordered_set calledFunctions; + + // Types that are targets of indirect calls. + std::unordered_set indirectCalledTypes; +}; + +std::map analyzeFuncs(Module& module, + const PassOptions& passOptions) { + ModuleUtils::ParallelFunctionAnalysis analysis( + module, [&](Function* func, FuncInfo& funcInfo) { + if (func->imported()) { + // Imports can do anything, so we need to assume the worst anyhow, + // which is the same as not specifying any effects for them in the + // map (which we do by not setting funcInfo.effects). + return; + } - // Gather the effects. - funcInfo.effects.emplace(getPassOptions(), *module, func); - - if (funcInfo.effects->calls) { - // There are calls in this function, which we will analyze in detail. - // Clear the |calls| field first, and we'll handle calls of all sorts - // below. - funcInfo.effects->calls = false; - - // Clear throws as well, as we are "forgetting" calls right now, and - // want to forget their throwing effect as well. If we see something - // else that throws, below, then we'll note that there. - funcInfo.effects->throws_ = false; - - struct CallScanner - : public PostWalker> { - Module& wasm; - PassOptions& options; - FuncInfo& funcInfo; - - CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo) - : wasm(wasm), options(options), funcInfo(funcInfo) {} - - void visitExpression(Expression* curr) { - ShallowEffectAnalyzer effects(options, wasm, curr); - if (auto* call = curr->dynCast()) { - // Note the direct call. - funcInfo.calledFunctions.insert(call->target); - } else if (effects.calls) { - // This is an indirect call of some sort, so we must assume the - // worst. To do so, clear the effects, which indicates nothing - // is known (so anything is possible). - // TODO: We could group effects by function type etc. - funcInfo.effects.reset(); + // Gather the effects. + funcInfo.effects.emplace(passOptions, module, func); + + if (funcInfo.effects->calls) { + // There are calls in this function, which we will analyze in detail. + // Clear the |calls| field first, and we'll handle calls of all sorts + // below. + funcInfo.effects->calls = false; + + // Clear throws as well, as we are "forgetting" calls right now, and + // want to forget their throwing effect as well. If we see something + // else that throws, below, then we'll note that there. + funcInfo.effects->throws_ = false; + + struct CallScanner + : public PostWalker> { + Module& wasm; + const PassOptions& options; + FuncInfo& funcInfo; + + CallScanner(Module& wasm, + const PassOptions& options, + FuncInfo& funcInfo) + : wasm(wasm), options(options), funcInfo(funcInfo) {} + + void visitExpression(Expression* curr) { + ShallowEffectAnalyzer effects(options, wasm, curr); + if (auto* call = curr->dynCast()) { + // Note the direct call. + funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls && + options.worldMode == WorldMode::Closed) { + HeapType type; + if (auto* callRef = curr->dynCast()) { + // call_ref on unreachable does not have a call effect, + // so this must be a HeapType. + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; } else { - // No call here, but update throwing if we see it. (Only do so, - // however, if we have effects; if we cleared it - see before - - // then we assume the worst anyhow, and have nothing to update.) - if (effects.throws_ && funcInfo.effects) { - funcInfo.effects->throws_ = true; - } + funcInfo.effects = std::nullopt; + return; + } + + funcInfo.indirectCalledTypes.insert(type); + } else if (effects.calls) { + assert(options.worldMode == WorldMode::Open); + funcInfo.effects = std::nullopt; + } else { + // No call here, but update throwing if we see it. (Only do so, + // however, if we have effects; if we cleared it - see before - + // then we assume the worst anyhow, and have nothing to update.) + if (effects.throws_ && funcInfo.effects) { + funcInfo.effects->throws_ = true; } } - }; - CallScanner scanner(*module, getPassOptions(), funcInfo); - scanner.walkFunction(func); - } - }); - - // Compute the transitive closure of effects. To do so, first construct for - // each function a list of the functions that it is called by (so we need to - // propogate its effects to them), and then we'll construct the closure of - // that. - // - // callers[foo] = [func that calls foo, another func that calls foo, ..] - // - std::unordered_map> callers; - - // Our work queue contains info about a new call pair: a call from a caller - // to a called function, that is information we then apply and propagate. - using CallPair = std::pair; // { caller, called } - UniqueDeferredQueue work; - for (auto& [func, info] : analysis.map) { - for (auto& called : info.calledFunctions) { - work.push({func->name, called}); + } + }; + CallScanner scanner(module, passOptions, funcInfo); + scanner.walkFunction(func); + } + }); + + return std::move(analysis.map); +} + +using CallGraphNode = std::variant; + +// Call graph for indirect and direct calls. +// +// key (caller) -> value (callee) +// Function -> Function : direct call +// Function -> HeapType : indirect call to the given HeapType +// HeapType -> Function : The function `callee` has the type `caller`. The +// HeapType may essentially 'call' any of its +// potential implementations. +// HeapType -> HeapType : `callee` is a subtype of `caller`. A call_ref +// could target any subtype of the ref, so we need to +// aggregate effects of subtypes of the target type. +// +// If we're running in an open world, we only include Function -> Function +// edges, and don't compute effects for indirect calls, conservatively assuming +// the worst. +using CallGraph = + std::unordered_map>; + +CallGraph buildCallGraph(const Module& module, + const std::map& funcInfos, + WorldMode worldMode) { + CallGraph callGraph; + if (worldMode == WorldMode::Open) { + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; + + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); } } - // Compute the transitive closure of the call graph, that is, fill out - // |callers| so that it contains the list of all callers - even through a - // chain - of each function. - while (!work.empty()) { - auto [caller, called] = work.pop(); + return callGraph; + } - // We must not already have an entry for this call (that would imply we - // are doing wasted work). - assert(!callers[called].contains(caller)); + std::unordered_set allFunctionTypes; + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; - // Apply the new call information. - callers[called].insert(caller); + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); + } - // We just learned that |caller| calls |called|. It also calls - // transitively, which we need to propagate to all places unaware of that - // information yet. - // - // caller => called => called by called - // - auto& calledInfo = analysis.map[module->getFunction(called)]; - for (auto calledByCalled : calledInfo.calledFunctions) { - if (!callers[calledByCalled].contains(caller)) { - work.push({caller, calledByCalled}); - } + // Function -> Type + allFunctionTypes.insert(caller->type.getHeapType()); + for (HeapType calleeType : callerInfo.indirectCalledTypes) { + callees.insert(calleeType); + + // Add the key to ensure the lookup doesn't fail for indirect calls to + // uninhabited types. + callGraph[calleeType]; + } + + // Type -> Function + callGraph[caller->type.getHeapType()].insert(caller); + } + + // Type -> Type + // Do a DFS up the type heirarchy for all function implementations. + // We are essentially walking up each supertype chain and adding edges from + // super -> subtype, but doing it via DFS to avoid repeated work. + Graph superTypeGraph(allFunctionTypes.begin(), + allFunctionTypes.end(), + [&callGraph](auto&& push, HeapType t) { + // Not needed except that during lookup we expect the + // key to exist. + callGraph[t]; + + if (auto super = t.getDeclaredSuperType()) { + callGraph[*super].insert(t); + push(*super); + } + }); + (void)superTypeGraph.traverseDepthFirst(); + + return callGraph; +} + +constexpr auto UnknownEffects = nullptr; + +// Merges effects from another connected component (const EffectAnalyzer*) or a +// function (std::optional&). +template +void mergeMaybeEffects(std::shared_ptr& dest, + const EffectAnalyzerPtr& src) { + if (dest == UnknownEffects) { + return; + } + if (!src) { + dest = UnknownEffects; + return; + } + + dest->mergeIn(*src); +} + +// Propagate effects from callees to callers transitively +// e.g. if A -> B -> C (A calls B which calls C) +// Then B inherits effects from C and A inherits effects from both B and C. +// +// Generate SCC for the call graph, then traverse it in reverse topological +// order processing each callee before its callers. When traversing: +// - Merge all of the effects of functions within the CC +// - Also merge the (already computed) effects of each callee CC +// - Add trap effects for potentially recursive call chains +void propagateEffects( + const Module& module, + const PassOptions& passOptions, + std::map& funcInfos, + std::unordered_map>& + typeEffects, + const CallGraph& callGraph) { + // We only care about Functions that are roots, not types. + // A type would be a root if a function exists with that type, but no-one + // indirect calls the type. + std::vector funcNodes; + for (const auto& [node, _] : callGraph) { + if (std::holds_alternative(node)) { + funcNodes.push_back(node); + } + } + + struct CallGraphSCCs + : SCCs::iterator, CallGraphSCCs> { + + const std::map& funcInfos; + const CallGraph& callGraph; + const Module& module; + + CallGraphSCCs(std::vector& nodes, + const std::map& funcInfos, + const CallGraph& callGraph, + const Module& module) + : SCCs::iterator, CallGraphSCCs>(nodes.begin(), + nodes.end()), + funcInfos(funcInfos), callGraph(callGraph), module(module) {} + + void pushChildren(CallGraphNode node) { + for (CallGraphNode callee : callGraph.at(node)) { + push(callee); } } + }; + CallGraphSCCs sccs(funcNodes, funcInfos, callGraph, module); - // Now that we have transitively propagated all static calls, apply that - // information. First, apply infinite recursion: if a function can call - // itself then it might recurse infinitely, which we consider an effect (a - // trap). - for (auto& [func, info] : analysis.map) { - if (callers[func->name].contains(func->name)) { - if (info.effects) { - info.effects->trap = true; - } + std::vector> componentEffects; + // Points to an index in componentEffects + std::unordered_map nodeComponents; + + for (auto ccIterator : sccs) { + auto& ccEffects = componentEffects.emplace_back( + std::make_shared(passOptions, module)); + std::vector cc(ccIterator.begin(), ccIterator.end()); + + std::vector ccFuncs; + for (CallGraphNode node : cc) { + nodeComponents.emplace(node, componentEffects.size() - 1); + if (auto** func = std::get_if(&node)) { + ccFuncs.push_back(*func); } } - // Next, apply function effects to their callers. - for (auto& [func, info] : analysis.map) { - auto& funcEffects = info.effects; + std::unordered_set calleeSccs; + for (CallGraphNode caller : cc) { + for (CallGraphNode callee : callGraph.at(caller)) { + calleeSccs.insert(nodeComponents.at(callee)); + } + } - for (auto& caller : callers[func->name]) { - auto& callerEffects = analysis.map[module->getFunction(caller)].effects; - if (!callerEffects) { - // Nothing is known for the caller, which is already the worst case. - continue; - } + // Merge in effects from callees + for (int calleeScc : calleeSccs) { + const auto& calleeComponentEffects = componentEffects.at(calleeScc); + mergeMaybeEffects(ccEffects, calleeComponentEffects.get()); + } - if (!funcEffects) { - // Nothing is known for the called function, which means nothing is - // known for the caller either. - callerEffects.reset(); - continue; + // Add trap effects for potential cycles. + if (cc.size() > 1) { + if (ccEffects != UnknownEffects) { + ccEffects->trap = true; + } + } else if (ccFuncs.size() == 1) { + // It's possible for a CC to only contain 1 type, but that is not a + // cycle in the call graph. + auto* func = ccFuncs[0]; + if (funcInfos.at(func).calledFunctions.contains(func->name)) { + if (ccEffects != UnknownEffects) { + ccEffects->trap = true; } - - // Add func's effects to the caller. - callerEffects->mergeIn(*funcEffects); } + } else if (ccFuncs.empty() && calleeSccs.empty()) { + // This node came from an indirect call to an uninhabited type. + // This CC must consist of exactly one type, because an uninhabited type + // can't make any indirect calls to other types. + // + // Since the type is uninhabited, this call must trap. + assert(cc.size() == 1); + ccEffects->trap = true; } - // Generate the final data, starting from a blank slate where nothing is - // known. - for (auto& [func, info] : analysis.map) { - func->effects.reset(); - if (!info.effects) { - continue; + // Aggregate effects within this CC + if (ccEffects) { + for (Function* f : ccFuncs) { + const auto& effects = funcInfos.at(f).effects; + mergeMaybeEffects(ccEffects, effects); } + } - func->effects = std::make_shared(*info.effects); + // Assign each function's effects to its CC effects. + for (auto node : cc) { + std::visit(overloaded{[&](HeapType type) { + if (ccEffects != UnknownEffects) { + typeEffects[type] = ccEffects; + } + }, + [&](Function* f) { f->effects = ccEffects; }}, + node); } } +} + +struct GenerateGlobalEffects : public Pass { + void run(Module* module) override { + std::map funcInfos = + analyzeFuncs(*module, getPassOptions()); + + auto callGraph = + buildCallGraph(*module, funcInfos, getPassOptions().worldMode); + + propagateEffects(*module, + getPassOptions(), + funcInfos, + module->indirectCallEffects, + callGraph); + } }; struct DiscardGlobalEffects : public Pass { @@ -202,6 +369,8 @@ struct DiscardGlobalEffects : public Pass { } }; +} // namespace + Pass* createGenerateGlobalEffectsPass() { return new GenerateGlobalEffects(); } Pass* createDiscardGlobalEffectsPass() { return new DiscardGlobalEffects(); } diff --git a/src/passes/GlobalRefining.cpp b/src/passes/GlobalRefining.cpp index 87dc5b259cf..95389f8ccfc 100644 --- a/src/passes/GlobalRefining.cpp +++ b/src/passes/GlobalRefining.cpp @@ -80,7 +80,7 @@ struct GlobalRefining : public Pass { std::unordered_set exportedGlobals(exportedGlobalsVec.begin(), exportedGlobalsVec.end()); for (auto* global : exportedGlobalsVec) { - if (getPassOptions().closedWorld || global->mutable_) { + if (getPassOptions().worldMode == WorldMode::Closed || global->mutable_) { unoptimizable.insert(global->name); } } diff --git a/src/passes/GlobalStructInference.cpp b/src/passes/GlobalStructInference.cpp index ae9d14488be..8de7f20fa50 100644 --- a/src/passes/GlobalStructInference.cpp +++ b/src/passes/GlobalStructInference.cpp @@ -109,7 +109,7 @@ struct GlobalStructInference : public Pass { subTypes = std::make_unique(*module); } - if (getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Closed) { analyzeClosedWorld(module); } @@ -473,8 +473,8 @@ struct GlobalStructInference : public Pass { if (values.size() == 1) { // The case of 1 value is simple: trap if the ref is null, and // otherwise return the value. Since the field is immutable, there - // cannot have been any writes to it we must synchonize with, so we do - // not need a fence. + // cannot have been any writes to it we must synchronize with, so we + // do not need a fence. replaceCurrent(builder.makeSequence( builder.makeDrop(builder.makeRefAs(RefAsNonNull, ref)), getReadValue(values[0], fieldIndex, field, curr))); @@ -655,7 +655,7 @@ struct GlobalStructInference : public Pass { } }; - // Find the optimization opportunitites in parallel. + // Find the optimization opportunities in parallel. ModuleUtils::ParallelFunctionAnalysis optimization( *module, [&](Function* func, GlobalsToUnnest& globalsToUnnest) { if (func->imported()) { diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index dcb28817ea0..46eb698eaa4 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -153,8 +153,7 @@ struct GlobalTypeOptimization : public Pass { if (!module->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "GTO requires --closed-world"; } @@ -207,7 +206,8 @@ struct GlobalTypeOptimization : public Pass { propagator.propagateToSubTypes(dataFromSupersMap); // Find the public types, which we must not modify. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -424,7 +424,7 @@ struct GlobalTypeOptimization : public Pass { std::unordered_set subtypesExposed; // Mark the relevant prototype field as read and return true iff we newly - // know we have to propate the exposure to subtypes. + // know we have to propagate the exposure to subtypes. auto noteExposed = [&](HeapType type, Exactness exact = Inexact) -> bool { if (auto desc = type.getDescriptorType(); desc && JSUtils::hasPossibleJSPrototypeField(*desc)) { @@ -479,7 +479,8 @@ struct GlobalTypeOptimization : public Pass { public: TypeRewriter(Module& wasm, GlobalTypeOptimization& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto& newFields = struct_.fields; diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index adba4f0ae36..59c1e7e3fc5 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -461,40 +461,31 @@ struct EscapeAnalyzer { } } void visitArraySet(ArraySet* curr) { - if (!curr->index->is()) { - // Array operations on nonconstant indexes do not escape in the normal - // sense, but they do escape from our being able to analyze them, so - // stop as soon as we see one. - return; - } - - // As StructGet. - if (curr->ref == child) { + // Arrays flowing into array operations on nonconstant indexes do not + // escape in the normal sense, but they do escape from our being able to + // analyze them, so stop as soon as we see one. + if (child == curr->ref && curr->index->is()) { escapes = false; fullyConsumes = true; } } void visitArrayGet(ArrayGet* curr) { - if (!curr->index->is()) { - return; + if (child == curr->ref && curr->index->is()) { + escapes = false; + fullyConsumes = true; } - escapes = false; - fullyConsumes = true; } void visitArrayRMW(ArrayRMW* curr) { - if (!curr->index->is()) { - return; - } - if (curr->ref == child) { + if (child == curr->ref && curr->index->is()) { escapes = false; fullyConsumes = true; } } void visitArrayCmpxchg(ArrayCmpxchg* curr) { - if (!curr->index->is()) { - return; - } - if (curr->ref == child || curr->expected == child) { + // Allocations flowing into `expected` are fully consumed and + // optimizable even if the index is not constant. + if (child == curr->expected || + (child == curr->ref && curr->index->is())) { escapes = false; fullyConsumes = true; } @@ -871,6 +862,13 @@ struct Struct2Local : PostWalker { return; } + if (curr->type == Type::unreachable) { + // We must not modify unreachable code here, as we will replace it with a + // const, which has a concrete type (similar to the situation with + // local.get in other cases in this pass). + return; + } + // This test operates on the allocation, which means we can compute whether // it will succeed statically. We do not even need // GCTypeUtils::evaluateCastCheck because we know the allocation's type @@ -1137,83 +1135,84 @@ struct Struct2Local : PostWalker { // The allocation might flow into `ref` or `expected`, but not // `replacement`, because then it would be considered to have escaped. - if (analyzer.getInteraction(curr->expected) == - ParentChildInteraction::Flows) { - // Since the allocation does not escape, it cannot possibly match the - // value already in the struct. The cmpxchg will just do a read. Drop the - // other arguments and do the atomic read at the end, when the cmpxchg - // would have happened. Use a nullable scratch local in case we also - // optimize `ref` later and need to replace it with a null. - auto refType = curr->ref->type.with(Nullable); - auto refScratch = builder.addVar(func, refType); - auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); - auto* getRefScratch = builder.makeLocalGet(refScratch, refType); - auto* structGet = builder.makeStructGet( - curr->index, getRefScratch, curr->order, curr->type); - auto* block = builder.makeBlock({setRefScratch, - builder.makeDrop(curr->expected), - builder.makeDrop(curr->replacement), - structGet}); + if (analyzer.getInteraction(curr->ref) == ParentChildInteraction::Flows) { + [[maybe_unused]] auto& field = fields[curr->index]; + auto type = curr->type; + assert(type == field.type); + assert(!field.isPacked()); + + // Hold everything in scratch locals, just like for other RMW ops and + // struct.new. Use a nullable (shared) eqref local for `expected` to + // accommodate any allowed optimized or unoptimized value there. + auto expectedType = type; + if (type.isRef()) { + expectedType = Type( + HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable); + } + auto oldScratch = builder.addVar(func, type); + auto expectedScratch = builder.addVar(func, expectedType); + auto replacementScratch = builder.addVar(func, type); + auto local = localIndexes[curr->index]; + + auto* block = builder.makeBlock( + {builder.makeDrop(curr->ref), + builder.makeLocalSet(expectedScratch, curr->expected), + builder.makeLocalSet(replacementScratch, curr->replacement), + builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type))}); + + // Create the check for whether we should do the exchange. + auto* lhs = builder.makeLocalGet(local, type); + auto* rhs = builder.makeLocalGet(expectedScratch, expectedType); + Expression* pred; + if (type.isRef()) { + pred = builder.makeRefEq(lhs, rhs); + } else { + pred = + builder.makeBinary(Abstract::getBinary(type, Abstract::Eq), lhs, rhs); + } + + // The conditional exchange. + block->list.push_back(builder.makeIf( + pred, + builder.makeLocalSet(local, + builder.makeLocalGet(replacementScratch, type)))); + + // Unstash the old value. + block->list.push_back(builder.makeLocalGet(oldScratch, type)); + block->type = type; replaceCurrent(block); - // Record the new data flow into and out of the new scratch local. This is - // necessary in case `ref` gets processed later so we can detect that it - // flows to the new struct.atomic.get, which may need to be replaced. - analyzer.parents.setParent(curr->ref, setRefScratch); - analyzer.scratchInfo.insert({setRefScratch, getRefScratch}); - analyzer.parents.setParent(getRefScratch, structGet); return; } - if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { + if (analyzer.getInteraction(curr->expected) != + ParentChildInteraction::Flows) { // Since the allocation does not flow from `ref`, it must not flow through // this cmpxchg at all. return; } - [[maybe_unused]] auto& field = fields[curr->index]; - auto type = curr->type; - assert(type == field.type); - assert(!field.isPacked()); - - // Hold everything in scratch locals, just like for other RMW ops and - // struct.new. Use a nullable (shared) eqref local for `expected` to - // accommodate any allowed optimized or unoptimized value there. - auto expectedType = type; - if (type.isRef()) { - expectedType = - Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable); - } - auto oldScratch = builder.addVar(func, type); - auto expectedScratch = builder.addVar(func, expectedType); - auto replacementScratch = builder.addVar(func, type); - auto local = localIndexes[curr->index]; - - auto* block = builder.makeBlock( - {builder.makeDrop(curr->ref), - builder.makeLocalSet(expectedScratch, curr->expected), - builder.makeLocalSet(replacementScratch, curr->replacement), - builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type))}); - - // Create the check for whether we should do the exchange. - auto* lhs = builder.makeLocalGet(local, type); - auto* rhs = builder.makeLocalGet(expectedScratch, expectedType); - Expression* pred; - if (type.isRef()) { - pred = builder.makeRefEq(lhs, rhs); - } else { - pred = - builder.makeBinary(Abstract::getBinary(type, Abstract::Eq), lhs, rhs); - } - - // The conditional exchange. - block->list.push_back( - builder.makeIf(pred, - builder.makeLocalSet( - local, builder.makeLocalGet(replacementScratch, type)))); - - // Unstash the old value. - block->list.push_back(builder.makeLocalGet(oldScratch, type)); - block->type = type; + // Since the allocation does not escape, it cannot possibly match the value + // already in the struct. The cmpxchg will just do a read. Drop the other + // arguments and do the atomic read at the end, when the cmpxchg would have + // happened. Use a nullable scratch local in case we also optimize `ref` + // later and need to replace it with a null. + auto refType = curr->ref->type.with(Nullable); + auto refScratch = builder.addVar(func, refType); + auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); + auto* getRefScratch = builder.makeLocalGet(refScratch, refType); + auto* structGet = builder.makeStructGet( + curr->index, getRefScratch, curr->order, curr->type); + auto* block = builder.makeBlock({setRefScratch, + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + structGet}); replaceCurrent(block); + // Record the new data flow into and out of the new scratch local. This is + // necessary in case `ref` gets processed later so we can detect that it + // flows to the new struct.atomic.get, which may need to be replaced. + analyzer.parents.setParent(curr->ref, setRefScratch); + analyzer.scratchInfo.insert({setRefScratch, getRefScratch}); + analyzer.parents.setParent(getRefScratch, structGet); + return; } void visitArrayCmpxchg(ArrayCmpxchg* curr) { @@ -1232,9 +1231,15 @@ struct Struct2Local : PostWalker { auto refScratch = builder.addVar(func, refType); auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); auto* getRefScratch = builder.makeLocalGet(refScratch, refType); + + auto indexScratch = builder.addVar(func, Type::i32); + auto* setIndexScratch = builder.makeLocalSet(indexScratch, curr->index); + auto* getIndexScratch = builder.makeLocalGet(indexScratch, Type::i32); + auto* arrayGet = builder.makeArrayGet( - getRefScratch, curr->index, curr->order, curr->type); + getRefScratch, getIndexScratch, curr->order, curr->type); auto* block = builder.makeBlock({setRefScratch, + setIndexScratch, builder.makeDrop(curr->expected), builder.makeDrop(curr->replacement), arrayGet}); @@ -1466,20 +1471,20 @@ struct Array2Struct : PostWalker { return; } - auto index = getIndex(curr->index); - if (index >= numFields) { - replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), - builder.makeDrop(curr->expected), - builder.makeDrop(curr->replacement), - builder.makeUnreachable()})); - refinalize = true; - return; - } - // The allocation might flow into `ref` or `expected`, but not // `replacement`, because then it would be considered to have escaped. if (analyzer.getInteraction(curr->ref) == ParentChildInteraction::Flows) { - // The accessed array is being optimzied. Convert the ArrayCmpxchg into a + auto index = getIndex(curr->index); + if (index >= numFields) { + replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + builder.makeUnreachable()})); + refinalize = true; + return; + } + + // The accessed array is being optimized. Convert the ArrayCmpxchg into a // StructCmpxchg. replaceCurrent(builder.makeStructCmpxchg( index, curr->ref, curr->expected, curr->replacement, curr->order)); diff --git a/src/passes/HeapStoreOptimization.cpp b/src/passes/HeapStoreOptimization.cpp index c720c5f0a41..6c6e729e744 100644 --- a/src/passes/HeapStoreOptimization.cpp +++ b/src/passes/HeapStoreOptimization.cpp @@ -181,7 +181,7 @@ struct HeapStoreOptimization // effects. auto firstEffects = effects(list[i]); auto secondEffects = effects(list[j]); - if (secondEffects.invalidates(firstEffects)) { + if (firstEffects.orderedBefore(secondEffects)) { return false; } @@ -241,7 +241,7 @@ struct HeapStoreOptimization if (!new_->isWithDefault()) { for (Index i = index + 1; i < operands.size(); i++) { auto operandEffects = effects(operands[i]); - if (operandEffects.invalidates(setValueEffects)) { + if (operandEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -252,7 +252,7 @@ struct HeapStoreOptimization // if it exists. if (new_->desc) { auto descEffects = effects(new_->desc); - if (descEffects.invalidates(setValueEffects)) { + if (descEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -264,7 +264,7 @@ struct HeapStoreOptimization // the optimization X' would happen first. ShallowEffectAnalyzer structNewEffects( getPassOptions(), *getModule(), new_); - if (structNewEffects.invalidates(setValueEffects)) { + if (structNewEffects.orderedBefore(setValueEffects)) { return false; } diff --git a/src/passes/I64ToI32Lowering.cpp b/src/passes/I64ToI32Lowering.cpp index 86377067f6f..ddf072caa2b 100644 --- a/src/passes/I64ToI32Lowering.cpp +++ b/src/passes/I64ToI32Lowering.cpp @@ -489,7 +489,7 @@ struct I64ToI32Lowering : public WalkerPass> { return; } // We cannot break this up into smaller operations as it must be atomic. - // Lower to an instrinsic function that wasm2js will implement. + // Lower to an intrinsic function that wasm2js will implement. TempVar lowBits = getTemp(); TempVar highBits = getTemp(); auto* getLow = builder->makeCall( @@ -1553,6 +1553,14 @@ struct I64ToI32Lowering : public WalkerPass> { } } + void visitWideIntAddSub(WideIntAddSub* curr) { + WASM_UNREACHABLE("TODO: wide arithmetic lowering"); + } + + void visitWideIntMul(WideIntMul* curr) { + WASM_UNREACHABLE("TODO: wide arithmetic lowering"); + } + void visitSelect(Select* curr) { if (handleUnreachable(curr)) { return; diff --git a/src/passes/Inlining.cpp b/src/passes/Inlining.cpp index 13d0b7630f1..cd328a7e1be 100644 --- a/src/passes/Inlining.cpp +++ b/src/passes/Inlining.cpp @@ -63,7 +63,7 @@ enum class InliningMode { // We do not know yet if this function can be inlined, as that has // not been computed yet. Unknown, - // This function cannot be inlinined in any way. + // This function cannot be inlined in any way. Uninlineable, // This function can be inlined fully, that is, normally: the entire function // can be inlined. This is in contrast to split/partial inlining, see below. @@ -1260,7 +1260,7 @@ struct Inlining : public Pass { // whether to optimize where we inline bool optimize = false; - // the information for each function. recomputed in each iteraction + // the information for each function. recomputed in each interaction NameInfoMap infos; std::unique_ptr functionSplitter; diff --git a/src/passes/InstrumentBranchHints.cpp b/src/passes/InstrumentBranchHints.cpp index 656397a5ad1..9a80598877f 100644 --- a/src/passes/InstrumentBranchHints.cpp +++ b/src/passes/InstrumentBranchHints.cpp @@ -28,9 +28,9 @@ // into // // @metadata.branch.hint B -// ;; log the ID of the condition (123), the prediction (B), and the actual -// ;; runtime result (temp == condition). -// if (temp = condition; log(123, B, temp); temp) { +// ;; log the actual runtime result (condition), the prediction (B), and the +// ;; ID (123), and return that result. +// if (log(condition, B, 123)) { // X // } else { // Y @@ -39,19 +39,20 @@ // Concretely, we emit calls to this logging function: // // (import "fuzzing-support" "log-branch" -// (func $log-branch (param i32 i32 i32)) ;; ID, prediction, actual +// (func $log-branch (param i32 i32 i32) (result i32)) // ) // // This can be used to verify that branch hints are accurate, by implementing // the import like this for example: // -// imports['fuzzing-support']['log-branch'] = (id, prediction, actual) => { +// imports['fuzzing-support']['log-branch'] = (actual, prediction, id) => { // // We only care about truthiness of the expected and actual values. // expected = +!!expected; // actual = +!!actual; // // Throw if the hint said this branch would be taken, but it was not, or // // vice versa. // if (expected != actual) throw `Bad branch hint! (${id})`; +// return actual; // }; // // A pass to delete branch hints is also provided, which finds instrumentations @@ -63,28 +64,28 @@ // would do this transformation: // // @metadata.branch.hint A -// if (temp = condition; log(10, A, temp); temp) { // 10 matches one of 10,20 +// if (log(condition, A, 10)) { // 10 matches one of 10,20 // X // } // @metadata.branch.hint B -// if (temp = condition; log(99, B, temp); temp) { // 99 does not match +// if (log(condition, B, 99)) { // 99 does not match // Y // } // // => // // // Used to be a branch hint here, but it was deleted. -// if (temp = condition; log(10, A, temp); temp) { +// if (log(condition, A, 10)) { // X // } // @metadata.branch.hint B // this one is unmodified. -// if (temp = condition; log(99, B, temp); temp) { +// if (log(condition, B, 99)) { // Y // } // // A pass to undo the instrumentation is also provided, which does // -// if (temp = condition; log(123, A, temp); temp) { +// if (log(condition, A, 123)) { // X // } // @@ -95,12 +96,8 @@ // } // -#include "ir/drop.h" -#include "ir/eh-utils.h" -#include "ir/find_all.h" -#include "ir/local-graph.h" +#include "ir/effects.h" #include "ir/names.h" -#include "ir/parents.h" #include "ir/properties.h" #include "ir/utils.h" #include "pass.h" @@ -132,8 +129,6 @@ int branchId = 1; struct InstrumentBranchHints : public WalkerPass> { - using Super = WalkerPass>; - // The internal name of our import. Name logBranch; @@ -147,8 +142,6 @@ struct InstrumentBranchHints // TODO: BrOn, but the condition there is not an i32 - bool addedInstrumentation = false; - template void processCondition(T* curr) { if (curr->condition->type == Type::unreachable) { // This branch is not even reached. @@ -166,25 +159,11 @@ struct InstrumentBranchHints int id = branchId++; // Instrument the condition. - auto tempLocal = builder.addVar(getFunction(), Type::i32); - auto* set = builder.makeLocalSet(tempLocal, curr->condition); auto* idConst = builder.makeConst(Literal(int32_t(id))); auto* guess = builder.makeConst(Literal(int32_t(*likely))); - auto* get1 = builder.makeLocalGet(tempLocal, Type::i32); - auto* log = builder.makeCall(logBranch, {idConst, guess, get1}, Type::none); - auto* get2 = builder.makeLocalGet(tempLocal, Type::i32); - curr->condition = builder.makeBlock({set, log, get2}); - addedInstrumentation = true; - } - void doWalkFunction(Function* func) { - Super::doWalkFunction(func); - - // Our added blocks may have caused nested pops. - if (addedInstrumentation) { - EHUtils::handleBlockNestedPops(func, *getModule()); - addedInstrumentation = false; - } + curr->condition = + builder.makeCall(logBranch, {curr->condition, guess, idConst}, Type::i32); } void doWalkModule(Module* module) { @@ -192,7 +171,12 @@ struct InstrumentBranchHints // This file already has our import. We nop it out, as whatever the // current code does may be dangerous (it may log incorrect hints). auto* func = module->getFunction(existing); - func->body = Builder(*module).makeNop(); + Builder builder(*module); + if (func->getSig().results == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } func->module = func->base = Name(); func->type = func->type.with(Exact); } @@ -200,7 +184,7 @@ struct InstrumentBranchHints // Add our import. auto* func = module->addFunction(Builder::makeFunction( Names::getValidFunctionName(*module, BASE), - Type(Signature({Type::i32, Type::i32, Type::i32}, Type::none), + Type(Signature({Type::i32, Type::i32, Type::i32}, Type::i32), NonNullable, Inexact), {})); @@ -209,7 +193,7 @@ struct InstrumentBranchHints logBranch = func->name; // Walk normally, using logBranch as we go. - Super::doWalkModule(module); + PostWalker::doWalkModule(module); // Update ref.func type changes. ReFinalize().run(getPassRunner(), module); @@ -227,12 +211,6 @@ struct InstrumentationProcessor : public WalkerPass> { // The internal name of our import. Name logBranch; - // A LocalGraph, so we can identify the pattern. - std::unique_ptr localGraph; - - // A map of expressions to their parents, so we can identify the pattern. - std::unique_ptr parents; - Sub* self() { return static_cast(this); } void visitIf(If* curr) { self()->processCondition(curr); } @@ -245,15 +223,6 @@ struct InstrumentationProcessor : public WalkerPass> { // TODO: BrOn, but the condition there is not an i32 - void doWalkFunction(Function* func) { - localGraph = std::make_unique(func, this->getModule()); - localGraph->computeSetInfluences(); - - parents = std::make_unique(func->body); - - Super::doWalkFunction(func); - } - void doWalkModule(Module* module) { logBranch = getLogBranchImport(module); if (!logBranch) { @@ -263,75 +232,6 @@ struct InstrumentationProcessor : public WalkerPass> { Super::doWalkModule(module); } - - // Helpers - - // Instrumentation info for a chunk of code that is the result of the - // instrumentation pass. - struct Instrumentation { - // The condition before the instrumentation (a pointer to it, so we can - // replace it). - Expression** originalCondition; - // The call to the logging that the instrumentation added. - Call* call; - }; - - // Check if an expression's condition is an instrumentation, and return the - // info if so. - std::optional getInstrumentation(Expression* condition) { - // We must identify this pattern: - // - // (br_if - // (block - // (local.set $temp (condition)) - // (call $log (id, prediction, (local.get $temp))) - // (local.get $temp) - // ) - // - // The block may vanish during roundtrip though, so we just follow back from - // the last local.get, which appears in the condition: - // - // (local.set $temp (condition)) - // (call $log (id, prediction, (local.get $temp))) - // (br_if - // (local.get $temp) - // - auto* fallthrough = Properties::getFallthrough( - condition, this->getPassOptions(), *this->getModule()); - auto* get = fallthrough->template dynCast(); - if (!get) { - return {}; - } - auto& sets = localGraph->getSets(get); - if (sets.size() != 1) { - return {}; - } - auto* set = *sets.begin(); - if (!set) { - return {}; - } - auto& gets = localGraph->getSetInfluences(set); - if (gets.size() != 2) { - return {}; - } - // The set has two gets: the get in the condition we began at, and - // another. - LocalGet* otherGet = nullptr; - for (auto* get2 : gets) { - if (get2 != get) { - otherGet = get2; - } - } - assert(otherGet); - // See if that other get is used in a logging. The parent should be a - // logging call. - auto* call = parents->getParent(otherGet)->template dynCast(); - if (!call || call->target != logBranch) { - return {}; - } - // Great, this is indeed a prior instrumentation. - return Instrumentation{&set->value, call}; - } }; struct DeleteBranchHints : public InstrumentationProcessor { @@ -340,15 +240,27 @@ struct DeleteBranchHints : public InstrumentationProcessor { // The set of IDs to delete. std::unordered_set idsToDelete; + std::optional getBranchID(Expression* condition, + const PassOptions& passOptions, + Module& wasm) { + auto* call = + Properties::getFallthrough(condition, getPassOptions(), *getModule()) + ->dynCast(); + if (!call || call->target != logBranch || call->operands.size() != 3) { + return std::nullopt; + } + auto* c = call->operands[2]->dynCast(); + if (!c || c->type != Type::i32) { + return std::nullopt; + } + return c->value.geti32(); + } + template void processCondition(T* curr) { - if (auto info = getInstrumentation(curr->condition)) { - if (auto* c = info->call->operands[0]->template dynCast()) { - auto id = c->value.geti32(); - if (idsToDelete.contains(id)) { - // Remove the branch hint. - getFunction()->codeAnnotations[curr].branchLikely = {}; - } - } + if (auto id = getBranchID(curr->condition, getPassOptions(), *getModule()); + id && idsToDelete.contains(*id)) { + // Remove the branch hint. + getFunction()->codeAnnotations[curr].branchLikely = std::nullopt; } } @@ -365,43 +277,31 @@ struct DeleteBranchHints : public InstrumentationProcessor { }; struct DeInstrumentBranchHints - : public InstrumentationProcessor { + : public WalkerPass> { - template void processCondition(T* curr) { - if (auto info = getInstrumentation(curr->condition)) { - // Replace the instrumented condition with the original one (swap so that - // the IR remains valid: we cannot use the same expression twice in our - // IR, and the original condition is still used in another place, until - // we remove the logging calls; since we will remove the calls anyhow, we - // just need some valid IR there). - std::swap(curr->condition, *info->originalCondition); + // The internal name of our import. + Name logBranch; + + void visitCall(Call* curr) { + if (curr->target == logBranch) { + // Replace the call with its first operand (the original condition). + replaceCurrent(curr->operands[0]); } } - void visitFunction(Function* func) { - if (func->imported()) { - return; - } - // At the very end, remove all logging calls (we use them during the main - // walk to identify instrumentation). - for (auto** callp : FindAllPointers(func->body).list) { - auto* call = (*callp)->cast(); - if (call->target == logBranch) { - Builder builder(*getModule()); - Expression* last; - if (call->type == Type::none) { - last = builder.makeNop(); - } else { - last = builder.makeUnreachable(); - } - *callp = getDroppedChildrenAndAppend(call, - *getModule(), - getPassOptions(), - last, - // We know the call is removable. - DropMode::IgnoreParentEffects); - } + void doWalkModule(Module* module) { + logBranch = getLogBranchImport(module); + if (!logBranch) { + Fatal() + << "No branch hint logging import found. Was this code instrumented?"; } + + // Mark the log-branch import as having no side effects - we are removing it + // entirely here, and its effect should not stop us when we compute effects. + module->getFunction(logBranch)->effects = + std::make_shared(getPassOptions(), *module); + + WalkerPass>::doWalkModule(module); } }; diff --git a/src/passes/J2CLItableMerging.cpp b/src/passes/J2CLItableMerging.cpp index 68e610755d2..55bcfc6a917 100644 --- a/src/passes/J2CLItableMerging.cpp +++ b/src/passes/J2CLItableMerging.cpp @@ -72,7 +72,7 @@ struct J2CLItableMerging : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "--merge-j2cl-itables requires --closed-world"; } @@ -384,7 +384,8 @@ struct J2CLItableMerging : public Pass { public: TypeRewriter(Module& wasm, J2CLItableMerging& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto structInfoIt = parent.structInfoByVtableType.find(oldStructType); diff --git a/src/passes/J2CLOpts.cpp b/src/passes/J2CLOpts.cpp index 759ef7cc287..0474a31ef74 100644 --- a/src/passes/J2CLOpts.cpp +++ b/src/passes/J2CLOpts.cpp @@ -197,7 +197,7 @@ class ConstantHoister : public WalkerPass> { } Name getEnclosingClass(Name name) { - return Name(name.str.substr(name.str.find_last_of('@'))); + return Name(name.view().substr(name.view().find_last_of('@'))); } AssignmentCountMap& assignmentCounts; diff --git a/src/passes/LLVMMemoryCopyFillLowering.cpp b/src/passes/LLVMMemoryCopyFillLowering.cpp index 9ccf2934a5a..e61e9891d16 100644 --- a/src/passes/LLVMMemoryCopyFillLowering.cpp +++ b/src/passes/LLVMMemoryCopyFillLowering.cpp @@ -59,13 +59,13 @@ struct LLVMMemoryCopyFillLowering // Check for the presence of any passive data or table segments. for (auto& segment : module->dataSegments) { - if (segment->isPassive) { + if (segment->isPassive()) { Fatal() << "memory.copy lowering should only be run on modules with " "no passive segments"; } } for (auto& segment : module->elementSegments) { - if (!segment->table.is()) { + if (segment->isPassive()) { Fatal() << "memory.copy lowering should only be run on modules with" " no passive segments"; } diff --git a/src/passes/LLVMNontrappingFPToIntLowering.cpp b/src/passes/LLVMNontrappingFPToIntLowering.cpp index d14e58af806..382d4155b36 100644 --- a/src/passes/LLVMNontrappingFPToIntLowering.cpp +++ b/src/passes/LLVMNontrappingFPToIntLowering.cpp @@ -74,7 +74,7 @@ struct LLVMNonTrappingFPToIntLoweringImpl Builder builder(*getModule()); Index v = Builder::addVar(getFunction(), curr->value->type); // if fabs(operand) < INT_MAX then use the trapping operation, else return - // INT_MIN. The altnernate value is correct for the case where the input is + // INT_MIN. The alternate value is correct for the case where the input is // INT_MIN itself; otherwise it's UB so any value will do. replaceCurrent(builder.makeIf( builder.makeBinary( diff --git a/src/passes/LegalizeJSInterface.cpp b/src/passes/LegalizeJSInterface.cpp index 7b6a4c44cdd..ecf1adbc3be 100644 --- a/src/passes/LegalizeJSInterface.cpp +++ b/src/passes/LegalizeJSInterface.cpp @@ -161,9 +161,6 @@ struct LegalizeJSInterface : public Pass { module->removeFunction(pair.first); } } - - module->removeExport(GET_TEMP_RET_EXPORT); - module->removeExport(SET_TEMP_RET_EXPORT); } private: diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 0233d17061d..2ad15b731fe 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -497,7 +497,9 @@ struct Checker continue; } auto& originalInfo = kv.second; - if (effects.invalidates(originalInfo.effects)) { + // Check whether curr must remain before COPY. We use ORIGINAL's effects + // in the check because we know they are the same as COPY's effects. + if (effects.orderedBefore(originalInfo.effects)) { invalidated.push_back(original); } } diff --git a/src/passes/LoopInvariantCodeMotion.cpp b/src/passes/LoopInvariantCodeMotion.cpp index 6add5134a79..c524b82ae9d 100644 --- a/src/passes/LoopInvariantCodeMotion.cpp +++ b/src/passes/LoopInvariantCodeMotion.cpp @@ -65,11 +65,14 @@ struct LoopInvariantCodeMotion // is ok to do so. EffectAnalyzer effectsSoFar(getPassOptions(), *getModule()); // The loop's total effects also matter. For example, a store - // in the loop means we can't move a load outside. + // in the loop means we can't move a load outside. We discard the local + // reads and writes because we analyze them separately. // FIXME: we look at the loop "tail" area too, after the last // possible branch back, which can cause false positives // for bad effect interactions. EffectAnalyzer loopEffects(getPassOptions(), *getModule(), loop); + loopEffects.localsRead.clear(); + loopEffects.localsWritten.clear(); // Note all the sets in each loop, and how many per index. Currently // EffectAnalyzer can't do that, and we need it to know if we // can move a set out of the loop (if there is another set @@ -123,9 +126,8 @@ struct LoopInvariantCodeMotion // take into account global state like interacting loads and // stores. bool unsafeToMove = effects.writesGlobalState() || - effectsSoFar.invalidates(effects) || - (effects.readsMutableGlobalState() && - loopEffects.writesGlobalState()); + effectsSoFar.orderedBefore(effects) || + loopEffects.orderedBefore(effects); // TODO: look into optimizing this with exceptions. for now, disallow if (effects.throws() || loopEffects.throws()) { unsafeToMove = true; diff --git a/src/passes/MarkJSCalled.cpp b/src/passes/MarkJSCalled.cpp new file mode 100644 index 00000000000..c05c75062c5 --- /dev/null +++ b/src/passes/MarkJSCalled.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Users should mark JS-called functions using @binaryen.js.called. This pass +// helps by auto-marking them where possible. The main thing this does is to +// find any configureAll calls and mark the functions referred to there. +// +// We do automatically handle configureAll in the start function (in +// intrinsics.cpp), so this pass is only needed for other uses of configureAll, +// like from an export. +// + +#include "ir/find_all.h" +#include "ir/intrinsics.h" +#include "ir/module-utils.h" +#include "pass.h" +#include "wasm.h" + +namespace wasm { + +struct MarkJSCalled : public Pass { + void run(Module* module) override { + Intrinsics intrinsics(*module); + + // See if there even is a configureAll. + auto hasConfigureAll = false; + for (auto& func : module->functions) { + if (intrinsics.isConfigureAll(func.get())) { + hasConfigureAll = true; + break; + } + } + if (!hasConfigureAll) { + return; + } + + using JSCalledSet = std::unordered_set; + + ModuleUtils::ParallelFunctionAnalysis analysis( + *module, [&](Function* func, JSCalledSet& jsCalled) { + if (func->imported()) { + return; + } + + FindAll calls(func->body); + for (auto* call : calls.list) { + if (intrinsics.isConfigureAll(call)) { + for (auto name : intrinsics.getConfigureAllFunctions(call)) { + jsCalled.insert(name); + } + } + } + }); + + for (auto& [_, jsCalled] : analysis.map) { + for (auto name : jsCalled) { + module->getFunction(name)->funcAnnotations.jsCalled = true; + } + } + } +}; + +Pass* createMarkJSCalledPass() { return new MarkJSCalled(); } + +} // namespace wasm diff --git a/src/passes/Memory64Lowering.cpp b/src/passes/Memory64Lowering.cpp index a3877254df7..67409fc3ab5 100644 --- a/src/passes/Memory64Lowering.cpp +++ b/src/passes/Memory64Lowering.cpp @@ -192,7 +192,7 @@ struct Memory64Lowering : public WalkerPass> { auto& module = *getModule(); // passive segments don't have any offset to adjust - if (segment->isPassive || !module.getMemory(segment->memory)->is64()) { + if (segment->isPassive() || !module.getMemory(segment->memory)->is64()) { return; } @@ -300,7 +300,7 @@ struct Memory64Lowering : public WalkerPass> { auto& module = *getModule(); // Passive segments don't have any offset to update. - if (segment->table.isNull() || !module.getTable(segment->table)->is64()) { + if (segment->isPassive() || !module.getTable(segment->table)->is64()) { return; } diff --git a/src/passes/MemoryPacking.cpp b/src/passes/MemoryPacking.cpp index 9d94492ecb2..b43abac2787 100644 --- a/src/passes/MemoryPacking.cpp +++ b/src/passes/MemoryPacking.cpp @@ -44,7 +44,7 @@ namespace wasm { namespace { -// A subsection of an orginal memory segment. If `isZero` is true, memory.fill +// A subsection of an original memory segment. If `isZero` is true, memory.fill // will be used instead of memory.init for this range. struct Range { bool isZero; @@ -215,7 +215,7 @@ bool MemoryPacking::canOptimize( // Check if it is ok for us to optimize. Address maxAddress = 0; for (auto& segment : dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { auto* c = segment->offset->dynCast(); // If an active segment has a non-constant offset, then what gets written // cannot be known until runtime. That is, the active segments are written @@ -250,7 +250,7 @@ bool MemoryPacking::canOptimize( // TODO: optimize in the trampling case DisjointSpans space; for (auto& segment : dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { auto* c = segment->offset->cast(); Address start = c->value.getUnsigned(); DisjointSpans::Span span{start, start + segment->data.size()}; @@ -283,7 +283,7 @@ bool MemoryPacking::canSplit(const std::unique_ptr& segment, for (auto* referrer : referrers) { if (auto* curr = referrer->dynCast()) { - if (segment->isPassive) { + if (segment->isPassive()) { // Do not try to split if there is a nonconstant offset or size if (!curr->offset->is() || !curr->size->is()) { return false; @@ -296,7 +296,7 @@ bool MemoryPacking::canSplit(const std::unique_ptr& segment, } // Active segments can only be split if they have constant offsets - return segment->isPassive || segment->offset->is(); + return segment->isPassive() || segment->offset->is(); } void MemoryPacking::calculateRanges(Module* module, @@ -351,7 +351,7 @@ void MemoryPacking::calculateRanges(Module* module, // entire segment and that all its arguments are constants. These assumptions // are true of all memory.inits generated by the tools. size_t threshold = 0; - if (segment->isPassive) { + if (segment->isPassive()) { // Passive segment metadata size threshold += 2; // Zeroes on the edge do not increase the number of segments or data.drops, @@ -450,7 +450,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { void visitMemoryInit(MemoryInit* curr) { Builder builder(*getModule()); auto* segment = getModule()->getDataSegment(curr->segment); - size_t maxRuntimeSize = segment->isPassive ? segment->data.size() : 0; + size_t maxRuntimeSize = segment->isPassive() ? segment->data.size() : 0; bool mustNop = false; bool mustTrap = false; auto* offset = curr->offset->dynCast(); @@ -483,7 +483,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { builder.makeDrop(curr->size), builder.makeUnreachable())); needsRefinalizing = true; - } else if (!segment->isPassive) { + } else if (segment->isActive()) { // trap if (dest > memory.size | offset | size) != 0 replaceCurrent(builder.makeIf( builder.makeBinary( @@ -494,7 +494,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { } } void visitDataDrop(DataDrop* curr) { - if (!getModule()->getDataSegment(curr->segment)->isPassive) { + if (getModule()->getDataSegment(curr->segment)->isActive()) { ExpressionManipulator::nop(curr); } } @@ -569,7 +569,7 @@ void MemoryPacking::dropUnusedSegments( bool used = false; auto referrersIt = referrers.find(segments[i]->name); bool hasReferrers = referrersIt != referrers.end(); - if (segments[i]->isPassive) { + if (segments[i]->isPassive()) { if (hasReferrers) { for (auto* referrer : referrersIt->second) { if (!referrer->is()) { @@ -623,7 +623,7 @@ void MemoryPacking::createSplitSegments( continue; } Expression* offset = nullptr; - if (!segment->isPassive) { + if (segment->isActive()) { if (auto* c = segment->offset->dynCast()) { if (c->value.type == Type::i32) { offset = addStartAndOffset( @@ -651,7 +651,7 @@ void MemoryPacking::createSplitSegments( if (segment->name.is()) { // Name the first range after the original segment and all following // ranges get numbered accordingly. This means that for segments that - // canot be split (segments that contains a single range) the input and + // cannot be split (segments that contains a single range) the input and // output segment have the same name. if (!segmentCount) { name = segment->name; @@ -663,7 +663,6 @@ void MemoryPacking::createSplitSegments( } auto curr = Builder::makeDataSegment(name, segment->memory, - segment->isPassive, offset, segment->data.data() + range.start, range.end - range.start); diff --git a/src/passes/MergeBlocks.cpp b/src/passes/MergeBlocks.cpp index 3013f67ac9b..7010d6d15f7 100644 --- a/src/passes/MergeBlocks.cpp +++ b/src/passes/MergeBlocks.cpp @@ -498,29 +498,11 @@ struct MergeBlocks // ) // at which point the block is on the outside and potentially mergeable with // an outer block - Block* optimize(Expression* curr, - Expression*& child, - Block* outer = nullptr, - Expression** dependency1 = nullptr, - Expression** dependency2 = nullptr) { + Block* + optimize(Expression* curr, Expression*& child, Block* outer = nullptr) { if (!child) { return outer; } - if ((dependency1 && *dependency1) || (dependency2 && *dependency2)) { - // there are dependencies, things we must be reordered through. make sure - // no problems there - EffectAnalyzer childEffects(getPassOptions(), *getModule(), child); - if (dependency1 && *dependency1 && - EffectAnalyzer(getPassOptions(), *getModule(), *dependency1) - .invalidates(childEffects)) { - return outer; - } - if (dependency2 && *dependency2 && - EffectAnalyzer(getPassOptions(), *getModule(), *dependency2) - .invalidates(childEffects)) { - return outer; - } - } if (auto* block = child->dynCast()) { if (!block->name.is() && block->list.size() >= 2) { auto* back = block->list.back(); @@ -665,7 +647,7 @@ struct MergeBlocks EffectAnalyzer blockChildEffects( getPassOptions(), *getModule(), blockChild); for (auto& effects : childEffects) { - if (blockChildEffects.invalidates(effects)) { + if (effects.orderedBefore(blockChildEffects)) { fail = true; break; } diff --git a/src/passes/MinifyImportsAndExports.cpp b/src/passes/MinifyImportsAndExports.cpp index 882f9a8b5d9..8c43ee694c3 100644 --- a/src/passes/MinifyImportsAndExports.cpp +++ b/src/passes/MinifyImportsAndExports.cpp @@ -112,9 +112,9 @@ struct MinifyImportsAndExports : public Pass { std::cout << ','; } std::cout << "\n ["; - String::printEscaped(std::cout, key.first.str) << ", "; - String::printEscaped(std::cout, key.second.str) << ", "; - String::printEscaped(std::cout, new_.str) << "]"; + String::printEscaped(std::cout, key.first.view()) << ", "; + String::printEscaped(std::cout, key.second.view()) << ", "; + String::printEscaped(std::cout, new_.view()) << "]"; } } std::cout << "\n ],\n\"exports\": ["; @@ -127,8 +127,8 @@ struct MinifyImportsAndExports : public Pass { std::cout << ','; } std::cout << "\n ["; - String::printEscaped(std::cout, key.second.str) << ", "; - String::printEscaped(std::cout, new_.str) << "]"; + String::printEscaped(std::cout, key.second.view()) << ", "; + String::printEscaped(std::cout, new_.view()) << "]"; } } std::cout << "\n ]\n"; diff --git a/src/passes/MinimizeRecGroups.cpp b/src/passes/MinimizeRecGroups.cpp index 306426af5b9..e6757c8ac96 100644 --- a/src/passes/MinimizeRecGroups.cpp +++ b/src/passes/MinimizeRecGroups.cpp @@ -302,6 +302,7 @@ struct MinimizeRecGroups : Pass { auto typeInfo = ModuleUtils::collectHeapTypeInfo( *module, + getPassOptions().worldMode, ModuleUtils::TypeInclusion::AllTypes, ModuleUtils::VisibilityHandling::FindVisibility); @@ -311,10 +312,10 @@ struct MinimizeRecGroups : Pass { // generate new groups with the same shape. std::unordered_set publicGroups; for (auto& [type, info] : typeInfo) { + typeIndices.insert({type, typeIndices.size()}); if (info.visibility == ModuleUtils::Visibility::Private) { // We can optimize private types. types.push_back(type); - typeIndices.insert({type, typeIndices.size()}); } else { publicGroups.insert(type.getRecGroup()); } @@ -771,7 +772,7 @@ struct MinimizeRecGroups : Pass { ++i; } } - GlobalTypeRewriter rewriter(wasm); + GlobalTypeRewriter rewriter(wasm, getPassOptions().worldMode); rewriter.mapTypes(oldToNew); rewriter.mapTypeNamesAndIndices(oldToNew); } diff --git a/src/passes/Monomorphize.cpp b/src/passes/Monomorphize.cpp index 2798eaec4ba..f4536692402 100644 --- a/src/passes/Monomorphize.cpp +++ b/src/passes/Monomorphize.cpp @@ -636,7 +636,7 @@ struct Monomorphize : public Pass { return; } - // TODO: ignore calls with unreachable operands for simplicty + // TODO: ignore calls with unreachable operands for simplicity // Compute the call context, and the new operands that the call would send // if we use that context. diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp index 55f9a72899e..a6ea8fa2ac2 100644 --- a/src/passes/OptimizeCasts.cpp +++ b/src/passes/OptimizeCasts.cpp @@ -237,16 +237,16 @@ struct EarlyCastFinder void visitExpression(Expression* curr) { // A new one is instantiated for each expression to determine - // if a cast can be moved past it. + // if a cast can be moved backward past it. ShallowEffectAnalyzer currAnalyzer(options, *getModule(), curr); - if (testRefCast.invalidates(currAnalyzer)) { + if (currAnalyzer.orderedBefore(testRefCast)) { for (size_t i = 0; i < numLocals; i++) { flushRefCastResult(i, *getModule()); } } - if (testRefAs.invalidates(currAnalyzer)) { + if (currAnalyzer.orderedBefore(testRefAs)) { for (size_t i = 0; i < numLocals; i++) { flushRefAsResult(i, *getModule()); } @@ -387,7 +387,7 @@ struct EarlyCastApplier : public PostWalker { } }; -// Find the best casted verisons of local.gets: other local.gets with the same +// Find the best casted versions of local.gets: other local.gets with the same // value, but cast to a more refined type. struct BestCastFinder : public LinearExecutionWalker { diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index e388775caab..47964f33713 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -783,7 +783,7 @@ struct OptimizeInstructions curr->op == DivUInt32) { // u32(x) / C ==> u32(x) >= C iff C > 2^31 // We avoid applying this for C == 2^31 due to conflict - // with other rule which transform to more prefereble + // with other rule which transform to more preferable // right shift operation. curr->op = c == -1 ? EqInt32 : GeUInt32; return replaceCurrent(curr); @@ -813,7 +813,7 @@ struct OptimizeInstructions c > std::numeric_limits::min() && curr->op == DivUInt64) { // u64(x) / C ==> u64(u64(x) >= C) iff C > 2^63 // We avoid applying this for C == 2^31 due to conflict - // with other rule which transform to more prefereble + // with other rule which transform to more preferable // right shift operation. // And apply this only for shrinkLevel == 0 due to it // increasing size by one byte. @@ -1478,7 +1478,7 @@ struct OptimizeInstructions // To avoid such risks we should keep in mind the following: // // * Before removing a cast we should use its type information in the best - // way we can. Only after doing so should a cast be removed. In the exmaple + // way we can. Only after doing so should a cast be removed. In the example // above, that means first seeing that the ref.test must return 1, and only // then possibly removing the ref.cast. // * Do not remove a cast if removing it might remove useful information for @@ -1673,7 +1673,7 @@ struct OptimizeInstructions // // TODO We could recurse here. // TODO We could do similar things for casts (rule out an impossible arm). - // TODO Worth thinking about an 'assume' instrinsic of some form that + // TODO Worth thinking about an 'assume' intrinsic of some form that // annotates knowledge about a value, or another mechanism to allow // that information to be passed around. @@ -2783,74 +2783,94 @@ struct OptimizeInstructions } // Check if two consecutive inputs to an instruction are equal. As they are - // consecutive, no code can execeute in between them, which simplies the + // consecutive, no code can execute in between them, which simplifies the // problem here (and which is the case we care about in this pass, which does // simple peephole optimizations - all we care about is a single instruction // at a time, and its inputs). bool areConsecutiveInputsEqual(Expression* left, Expression* right) { - // When we look for a tee/get pair, we can consider the fallthrough values - // for the first, as the fallthrough happens last (however, we must use - // NoTeeBrIf as we do not want to look through the tee). We cannot do this - // on the second, however, as there could be effects in the middle. - // TODO: Use effects here perhaps. - left = - Properties::getFallthrough(left, - getPassOptions(), - *getModule(), - Properties::FallthroughBehavior::NoTeeBrIf); - if (areMatchingTeeAndGet(left, right)) { - return true; + // The fallthrough expression of `left` produces its value. That value may + // depend on effects from other non-fallthrough expressions in `left`, but + // those expressions are generally executed before the fallthrough value and + // will affect the values of `left` and `right` equally, so we can ignore + // them. The exceptions are `local.tee` instructions and br_if conditions, + // which execute after the fallthrough and might affect only the value of + // `right`. + // TODO: We should use a custom getFallthrough that ignores whether br_if + // conditions and values can be reordered, since we can handle that more + // precisely here. + // TODO: When the fallthrough is an If (meaning the other branch must never + // return), we should ignore effects in that non-returning branch. + EffectAnalyzer interferingEffects(getPassOptions(), *getModule()); + bool matchingTeeAndGet = false; + while (true) { + left = + Properties::getFallthrough(left, + getPassOptions(), + *getModule(), + Properties::FallthroughBehavior::NoTeeBrIf); + if (auto* tee = left->dynCast()) { + assert(tee->isTee()); + // If `right` reads directly from this local.tee, then we know their + // values are the same. We know no children of this tee will be executed + // after it, so we need not look for further effects. But there might be + // interfering sets in previous br_if conditions, so we cannot just + // return here. + // TODO: Calculate `right`'s fallthrough first in case the fallthrough + // is the matching get. + if (areMatchingTeeAndGet(left, right)) { + matchingTeeAndGet = true; + left = getFallthrough(left); + break; + } + interferingEffects.visit(tee); + left = tee->value; + continue; + } + if (auto* br = left->dynCast(); br && br->condition) { + assert(br->value); + // NB: We don't need to worry about the branch effect because any branch + // at runtime must skip past the parent expression, so it would not + // matter how that parent expression gets optimized. + interferingEffects.walk(br->condition); + left = br->value; + continue; + } + // We have found the real fallthrough expression. + break; } - // Ignore extraneous things and compare them syntactically. We can also - // look at the full fallthrough for both sides now. - auto* originalLeft = left; - left = getFallthrough(left); - auto* originalRight = right; - right = getFallthrough(right); - if (!ExpressionAnalyzer::equal(left, right)) { - return false; + // We similarly want to find the fallthrough expression of `right`. But this + // time, it is the expressions that execute before, not after, the + // fallthrough that can affect its value. + while (true) { + auto* next = Properties::getImmediateFallthrough( + right, getPassOptions(), *getModule()); + if (next == right) { + // We have found the fallthrough expression. + break; + } + // Gather the effects of all the non-fallthrough children of the + // container. + for (auto* child : ChildIterator(right)) { + if (child == next) { + // Skip children that execute after the fallthrough value, such as + // br_if conditions. + break; + } + interferingEffects.walk(child); + } + right = next; } - // We must also not have non-fallthrough effects that invalidate us, such as - // this situation: - // - // (local.get $x) - // (block - // (local.set $x ..) - // (local.get $x) - // ) - // - // The fallthroughs are identical, but the set may cause us to read a - // different value. - if (originalRight != right) { - // TODO: We could be more precise here and ignore right itself in - // originalRightEffects. - auto originalRightEffects = effects(originalRight); - auto rightEffects = effects(right); - if (originalRightEffects.invalidates(rightEffects)) { - return false; - } + // We have both fallthrough expressions. See if they look the same. + if (!matchingTeeAndGet && !ExpressionAnalyzer::equal(left, right)) { + return false; } - // The same, with left, as we can have this situation: - // - // (local.tee $x ..) - // (something using $x) - // ) - // (something using $x) - // - // The fallthroughs are identical, but the tee may cause us to read a - // different value. - if (originalLeft != left) { - auto originalLeftEffects = effects(originalLeft); - // |left == right| here (we would have exited early, otherwise, above), so - // we could compute either. Compute |left| as it might have better cache - // locality. - auto leftEffects = effects(left); - if (originalLeftEffects.invalidates(leftEffects)) { - return false; - } + // They do look the same! Make sure nothing executed in between them can + // affect the value of `right` and make it different from `left`. + if (interferingEffects.orderedBefore(effects(right))) { + return false; } // To be equal, they must also be known to return the same result @@ -3463,7 +3483,8 @@ struct OptimizeInstructions } // remove added/subbed zeros struct ZeroRemover : public PostWalker { - // TODO: we could save the binarys and costs we drop, and reuse them later + // TODO: we could save the Binary and Const nodes we drop, and reuse them + // later PassOptions& passOptions; @@ -4586,7 +4607,7 @@ struct OptimizeInstructions c1->value = Literal::makeFromInt32(total, c1->type); return inner; } else { - // overflow. Handle different scenarious + // overflow. Handle different scenarios if (hasAnyRotateShift(op)) { // overflow always accepted in rotation shifts c1->value = Literal::makeFromInt32(effectiveTotal, c1->type); @@ -5025,7 +5046,7 @@ struct OptimizeInstructions switch (curr->op) { case TruncSFloat64ToInt32: case TruncSatSFloat64ToInt32: { - // i32 -> f64 -> i32 rountripping optimization: + // i32 -> f64 -> i32 roundtripping optimization: // i32.trunc(_sat)_f64_s(f64.convert_i32_s(x)) ==> x Expression* x; if (matches(curr->value, unary(ConvertSInt32ToFloat64, any(&x)))) { @@ -5035,7 +5056,7 @@ struct OptimizeInstructions } case TruncUFloat64ToInt32: case TruncSatUFloat64ToInt32: { - // u32 -> f64 -> u32 rountripping optimization: + // u32 -> f64 -> u32 roundtripping optimization: // i32.trunc(_sat)_f64_u(f64.convert_i32_u(x)) ==> x Expression* x; if (matches(curr->value, unary(ConvertUInt32ToFloat64, any(&x)))) { @@ -5760,7 +5781,7 @@ struct OptimizeInstructions } } - if (!neverFold) { + if (!neverFold && curr->condition->type != Type::unreachable) { // Identical code on both arms can be folded out, e.g. // // (select @@ -5850,7 +5871,7 @@ struct OptimizeInstructions if (validTypes && validEffects && validChildren) { // Replace ifTrue with its child. curr->ifTrue = ifTrueChild; - // Relace ifFalse with its child, and reuse that node outside. + // Replace ifFalse with its child, and reuse that node outside. auto* reuse = curr->ifFalse; curr->ifFalse = ifFalseChild; // curr's type may have changed, if the instructions we moved out diff --git a/src/passes/Outlining.cpp b/src/passes/Outlining.cpp index c52213fdc61..840a84bad37 100644 --- a/src/passes/Outlining.cpp +++ b/src/passes/Outlining.cpp @@ -662,7 +662,7 @@ struct ReconstructStringifyWalker }; struct Outlining : public Pass { - void run(Module* module) { + void run(Module* module) override { HashStringifyWalker stringify; // Walk the module and create a "string representation" of the program. stringify.walkModule(module); diff --git a/src/passes/Poppify.cpp b/src/passes/Poppify.cpp index a7f3e5cccd7..c12c7203c0d 100644 --- a/src/passes/Poppify.cpp +++ b/src/passes/Poppify.cpp @@ -456,7 +456,7 @@ class PoppifyFunctionsPass : public Pass { } // anonymous namespace class PoppifyPass : public Pass { - void run(Module* module) { + void run(Module* module) override { PassRunner subRunner(getPassRunner()); subRunner.add(std::make_unique()); // TODO: Enable this once it handles Poppy blocks correctly diff --git a/src/passes/PostEmscripten.cpp b/src/passes/PostEmscripten.cpp index 72630663591..533e5258ba6 100644 --- a/src/passes/PostEmscripten.cpp +++ b/src/passes/PostEmscripten.cpp @@ -82,7 +82,7 @@ static void calcSegmentOffsets(Module& wasm, OffsetSearcher(std::unordered_map& offsets) : offsets(offsets) {} void visitMemoryInit(MemoryInit* curr) { - // The desitination of the memory.init is either a constant + // The destination of the memory.init is either a constant // or the result of an addition with __memory_base in the // case of PIC code. auto* dest = curr->dest->dynCast(); @@ -107,7 +107,7 @@ static void calcSegmentOffsets(Module& wasm, } for (unsigned i = 0; i < wasm.dataSegments.size(); ++i) { auto& segment = wasm.dataSegments[i]; - if (segment->isPassive) { + if (segment->isPassive()) { auto it = passiveOffsets.find(segment->name); if (it != passiveOffsets.end()) { segmentOffsets.push_back(it->second); diff --git a/src/passes/Precompute.cpp b/src/passes/Precompute.cpp index a5f8da4b42c..fbaf09232b9 100644 --- a/src/passes/Precompute.cpp +++ b/src/passes/Precompute.cpp @@ -104,7 +104,7 @@ class PrecomputingExpressionRunner // Limit evaluation depth for 2 reasons: first, it is highly unlikely // that we can do anything useful to precompute a hugely nested expression - // (we should succed at smaller parts of it first). Second, a low limit is + // (we should succeed at smaller parts of it first). Second, a low limit is // helpful to avoid platform differences in native stack sizes. static const Index MAX_DEPTH = 50; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 5432a2f471b..86ce8595683 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -320,63 +320,6 @@ struct PrintSExpression : public UnifiedExpressionVisitor { void visitTryTable(TryTable* curr); void printUnreachableReplacement(Expression* curr); - bool maybePrintUnreachableReplacement(Expression* curr, Type type); - void visitRefCast(RefCast* curr) { - if ((curr->desc && curr->desc->type != Type::unreachable) || - !maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitStructNew(StructNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNew(ArrayNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewData(ArrayNewData* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewElem(ArrayNewElem* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewFixed(ArrayNewFixed* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitContNew(ContNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitContBind(ContBind* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitResume(Resume* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitResumeThrow(ResumeThrow* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitStackSwitch(StackSwitch* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } // Module-level visitors void handleSignature(Function* curr, bool printImplicitNames = false); @@ -779,17 +722,17 @@ struct PrintExpressionContents case Bitselect: o << "v128.bitselect"; break; - case LaneselectI8x16: - o << "i8x16.laneselect"; + case RelaxedLaneselectI8x16: + o << "i8x16.relaxed_laneselect"; break; - case LaneselectI16x8: - o << "i16x8.laneselect"; + case RelaxedLaneselectI16x8: + o << "i16x8.relaxed_laneselect"; break; - case LaneselectI32x4: - o << "i32x4.laneselect"; + case RelaxedLaneselectI32x4: + o << "i32x4.relaxed_laneselect"; break; - case LaneselectI64x2: - o << "i64x2.laneselect"; + case RelaxedLaneselectI64x2: + o << "i64x2.relaxed_laneselect"; break; case MaddVecF16x8: o << "f16x8.madd"; @@ -809,8 +752,8 @@ struct PrintExpressionContents case RelaxedNmaddVecF64x2: o << "f64x2.relaxed_nmadd"; break; - case DotI8x16I7x16AddSToVecI32x4: - o << "i32x4.dot_i8x16_i7x16_add_s"; + case RelaxedDotI8x16I7x16AddSToVecI32x4: + o << "i32x4.relaxed_dot_i8x16_i7x16_add_s"; break; } restoreNormalColor(o); @@ -1401,6 +1344,15 @@ struct PrintExpressionContents case ConvertUVecI16x8ToVecF16x8: o << "f16x8.convert_i16x8_u"; break; + case PromoteLowVecF16x8ToVecF32x4: + o << "f32x4.promote_low_f16x8"; + break; + case DemoteZeroVecF32x4ToVecF16x8: + o << "f16x8.demote_f32x4_zero"; + break; + case DemoteZeroVecF64x2ToVecF16x8: + o << "f16x8.demote_f64x2_zero"; + break; case InvalidUnary: WASM_UNREACHABLE("unvalid unary operator"); } @@ -2070,8 +2022,8 @@ struct PrintExpressionContents case RelaxedQ15MulrSVecI16x8: o << "i16x8.relaxed_q15mulr_s"; break; - case DotI8x16I7x16SToVecI16x8: - o << "i16x8.dot_i8x16_i7x16_s"; + case RelaxedDotI8x16I7x16SToVecI16x8: + o << "i16x8.relaxed_dot_i8x16_i7x16_s"; break; case InvalidBinary: @@ -2079,6 +2031,34 @@ struct PrintExpressionContents } restoreNormalColor(o); } + void visitWideIntAddSub(WideIntAddSub* curr) { + prepareColor(o); + switch (curr->op) { + case AddInt128: { + o << "i64.add128"; + break; + } + case SubInt128: { + o << "i64.sub128"; + break; + } + } + restoreNormalColor(o); + } + void visitWideIntMul(WideIntMul* curr) { + prepareColor(o); + switch (curr->op) { + case MulWideSInt64: { + o << "i64.mul_wide_s"; + break; + } + case MulWideUInt64: { + o << "i64.mul_wide_u"; + break; + } + } + restoreNormalColor(o); + } void visitSelect(Select* curr) { prepareColor(o) << "select"; restoreNormalColor(o); @@ -2586,7 +2566,7 @@ struct PrintExpressionContents // Re-encode from WTF-16 to WTF-8. std::stringstream wtf8; [[maybe_unused]] bool valid = - String::convertWTF16ToWTF8(wtf8, curr->string.str); + String::convertWTF16ToWTF8(wtf8, curr->string.view()); assert(valid); // TODO: Use wtf8.view() once we have C++20. String::printEscaped(o, wtf8.str()); @@ -3140,19 +3120,6 @@ void PrintSExpression::printUnreachableReplacement(Expression* curr) { decIndent(); } -bool PrintSExpression::maybePrintUnreachableReplacement(Expression* curr, - Type type) { - // When we cannot print an instruction because the child from which it's - // supposed to get a type immediate is unreachable, then we print a - // semantically-equivalent block that drops each of the children and ends in - // an unreachable. - if (type == Type::unreachable) { - printUnreachableReplacement(curr); - return true; - } - return false; -} - static bool requiresExplicitFuncType(HeapType type) { // When the `(type $f)` in a function's typeuse is omitted, the typeuse // matches or declares an MVP function type. When the intended type is not an @@ -3223,7 +3190,7 @@ void PrintSExpression::visitExport(Export* curr) { o << '('; printMedium(o, "export "); std::stringstream escaped; - String::printEscaped(escaped, curr->name.str); + String::printEscaped(escaped, curr->name.view()); printText(o, escaped.str(), false) << " ("; switch (curr->kind) { case ExternalKind::Function: @@ -3252,8 +3219,8 @@ void PrintSExpression::visitExport(Export* curr) { void PrintSExpression::emitImportHeader(Importable* curr) { printMedium(o, "import "); std::stringstream escapedModule, escapedBase; - String::printEscaped(escapedModule, curr->module.str); - String::printEscaped(escapedBase, curr->base.str); + String::printEscaped(escapedModule, curr->module.view()); + String::printEscaped(escapedBase, curr->base.view()); printText(o, escapedModule.str(), false) << ' '; printText(o, escapedBase.str(), false) << ' '; } @@ -3476,7 +3443,7 @@ void PrintSExpression::visitElementSegment(ElementSegment* curr) { printMedium(o, "elem "); curr->name.print(o); - if (curr->table.is()) { + if (curr->isActive()) { if (usesExpressions || currModule->tables.size() > 1) { // tableuse o << " (table "; @@ -3556,7 +3523,7 @@ void PrintSExpression::visitMemory(Memory* curr) { } void PrintSExpression::visitDataSegment(DataSegment* curr) { - if (!curr->isPassive && !curr->offset) { + if (curr->isActive() && !curr->offset) { // This data segment must have been created from the datacount section but // not parsed yet. Skip it. return; @@ -3566,7 +3533,7 @@ void PrintSExpression::visitDataSegment(DataSegment* curr) { printMajor(o, "data "); curr->name.print(o); o << ' '; - if (!curr->isPassive) { + if (curr->isActive()) { assert(!currModule || currModule->memories.size() > 0); if (!currModule || curr->memory != currModule->memories[0]->name) { o << "(memory "; @@ -3766,7 +3733,7 @@ class MinifiedPrinter : public Printer { Pass* createMinifiedPrinterPass() { return new MinifiedPrinter(); } -// Prints out a module withough elision, i.e., the full ast +// Prints out a module without elision, i.e., the full ast class FullPrinter : public Printer { public: @@ -3865,7 +3832,7 @@ printStackInst(StackInst* inst, std::ostream& o, Function* func) { break; } default: - WASM_UNREACHABLE("unexpeted op"); + WASM_UNREACHABLE("unexpected op"); } return o; } @@ -3958,7 +3925,7 @@ static std::ostream& printStackIR(StackIR* ir, PrintSExpression& printer) { break; } default: - WASM_UNREACHABLE("unexpeted op"); + WASM_UNREACHABLE("unexpected op"); } o << '\n'; } @@ -4006,6 +3973,10 @@ std::ostream& operator<<(std::ostream& o, wasm::ModuleExpression pair) { } std::ostream& operator<<(std::ostream& o, wasm::ShallowExpression expression) { + if (Properties::hasUnwritableTypeImmediate(expression.expr)) { + o << "(; unreachable " << getExpressionName(expression.expr) << " ;)"; + return o; + } wasm::PrintSExpression printer(o); printer.setModule(expression.module); wasm::PrintExpressionContents(printer).visit(expression.expr); diff --git a/src/passes/PrintBoundary.cpp b/src/passes/PrintBoundary.cpp new file mode 100644 index 00000000000..2bd63f256e3 --- /dev/null +++ b/src/passes/PrintBoundary.cpp @@ -0,0 +1,181 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Prints the boundary - the imports and exports - in a convenient JSON format. +// Only enough information for JavaScript is provided (for the full info, parse +// the wat or wasm). +// +// Usage: +// +// wasm-opt --print-boundary=OUTFILE +// +// If OUTFILE is not provided, prints to stdout. +// +// Example: +// +// { +// 'imports': [ +// { +// 'module': 'foo', // foo.bar +// 'base': 'bar', +// 'kind': 'func', +// 'type': { +// 'params': ['i32', '(ref func)'], +// 'results': ['f64'] +// }, +// }, +// [..] +// ], +// 'exports': [ +// { +// 'name': 'foo', +// 'kind': 'global', +// 'type': 'i32', +// }, +// [..] +// ] +// } +// + +#include "ir/module-utils.h" +#include "pass.h" +#include "support/file.h" +#include "support/json.h" +#include "wasm.h" + +namespace wasm { + +struct PrintBoundary : public Pass { + bool modifiesBinaryenIR() override { return false; } + + void run(Module* module) override { + std::string target = getArgumentOrDefault("print-boundary", ""); + + // Imports. + auto imports = json::Value::makeArray(); + + ModuleUtils::iterImportable( + *module, [&](ExternalKind kind, Importable* import) { + auto item = json::Value::makeObject(); + item["module"] = json::Value::make(import->module.view()); + item["base"] = json::Value::make(import->base.view()); + item["kind"] = getKindName(kind); + item["type"] = getExternalType(kind, import->name, *module); + imports->push_back(item); + }); + + // Exports. + auto exports = json::Value::makeArray(); + + for (auto& exp : module->exports) { + auto item = json::Value::makeObject(); + item["name"] = json::Value::make(exp->name.view()); + item["kind"] = getKindName(exp->kind); + item["type"] = + getExternalType(exp->kind, *exp->getInternalName(), *module); + exports->push_back(item); + } + + // Emit the final structure + json::Value root; + root.setObject(); + root["imports"] = imports; + root["exports"] = exports; + + Output output(target, Flags::BinaryOption::Text); + root.stringify(output.getStream(), true /* pretty */); + } + + // Emits an array of multivalue types. For a signature, emits params and + // results. + // + // We emit an array only when needed, unless forceArray is set. + json::Value::Ref getTypes(Type type, bool forceArray = false) { + if (type.isRef()) { + auto heapType = type.getHeapType(); + if (heapType.isSignature()) { + auto sig = heapType.getSignature(); + auto ret = json::Value::makeObject(); + // Always emit arrays for params and results. + ret["params"] = getTypes(sig.params, true); + ret["results"] = getTypes(sig.results, true); + return ret; + } + } + + // Simplify the output, avoiding an array for a single value. + if (!forceArray && type.size() == 1) { + return json::Value::make(type.toString()); + } + + auto ret = json::Value::makeArray(); + for (auto t : type) { + ret->push_back(json::Value::make(t.toString())); + } + return ret; + } + + // For an imported or exported thing (something external), and its name, + // return the type info we report for it. + json::Value::Ref getExternalType(ExternalKind kind, Name name, Module& wasm) { + switch (kind) { + case ExternalKind::Function: + return getTypes(wasm.getFunction(name)->type); + case ExternalKind::Table: + return getTypes(wasm.getTable(name)->type); + case ExternalKind::Memory: + return getTypes(wasm.getMemory(name)->addressType); + case ExternalKind::Global: + return getTypes(wasm.getGlobal(name)->type); + case ExternalKind::Tag: + // Wrap it in a Type so that getTypes can handle it. That will print the + // params and results as we expect. + return getTypes(Type(wasm.getTag(name)->type, NonNullable)); + case ExternalKind::Invalid: + break; + } + WASM_UNREACHABLE("invalid ExternalKind"); + } + + json::Value::Ref getKindName(ExternalKind kind) { + const char* name = nullptr; + switch (kind) { + case ExternalKind::Function: + name = "func"; + break; + case ExternalKind::Table: + name = "table"; + break; + case ExternalKind::Memory: + name = "memory"; + break; + case ExternalKind::Global: + name = "global"; + break; + case ExternalKind::Tag: + name = "tag"; + break; + case ExternalKind::Invalid: + WASM_UNREACHABLE("invalid ExternalKind"); + } + return json::Value::make(name); + } +}; + +Pass* createPrintBoundaryPass() { return new PrintBoundary(); } + +} // namespace wasm diff --git a/src/passes/RedundantSetElimination.cpp b/src/passes/RedundantSetElimination.cpp index 7ede2ba35e9..c5017848b1d 100644 --- a/src/passes/RedundantSetElimination.cpp +++ b/src/passes/RedundantSetElimination.cpp @@ -23,7 +23,7 @@ // A risk here is that we extend live ranges, e.g. we may use the default // value at the very end of a function, keeping that local alive throughout. // For that reason it is probably better to run this near the end of -// optimization, and especially after coalesce-locals. A final vaccum +// optimization, and especially after coalesce-locals. A final vacuum // should be done after it, as this pass can leave around drop()s of // values no longer necessary. // @@ -123,7 +123,7 @@ struct RedundantSetElimination // Use a value numbering for the values of expressions. ValueNumbering valueNumbering; - // In additon to valueNumbering, each block has values for each merge. + // In addition to valueNumbering, each block has values for each merge. std::unordered_map> blockMergeValues; diff --git a/src/passes/RemoveExports.cpp b/src/passes/RemoveExports.cpp new file mode 100644 index 00000000000..5207a6f2e1a --- /dev/null +++ b/src/passes/RemoveExports.cpp @@ -0,0 +1,67 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Remove exports using a wildcard. For example: +// +// --remove-exports=__* +// +// In this case we will remove all exports with names like "__foo" and "__bar". +// +// Exports can also be specified as a comma-separated list, and can be a +// response file. +// + +#include "pass.h" +#include "support/file.h" +#include "support/string.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +struct RemoveExports : public Pass { + void run(Module* module) override { + std::string param = + getArgument(name, "Usage usage: wasm-opt --" + name + "=WILDCARD"); + + param = String::trim(read_possible_response_file(param)); + + String::Split patterns(param, String::Split::NewLineOr(",")); + patterns = handleBracketingOperators(patterns); + + std::vector toRemove; + for (auto& exp : module->exports) { + for (auto& pattern : patterns) { + if (String::wildcardMatch(pattern, exp->name.toString())) { + toRemove.push_back(exp->name); + break; + } + } + } + + for (auto& name : toRemove) { + module->removeExport(name); + } + } +}; + +} // anonymous namespace + +Pass* createRemoveExportsPass() { return new RemoveExports(); } + +} // namespace wasm diff --git a/src/passes/RemoveRelaxedSIMD.cpp b/src/passes/RemoveRelaxedSIMD.cpp index b09319d8fce..61f65830d12 100644 --- a/src/passes/RemoveRelaxedSIMD.cpp +++ b/src/passes/RemoveRelaxedSIMD.cpp @@ -64,7 +64,7 @@ struct RemoveRelaxedSIMD : WalkerPass> { case RelaxedMinVecF64x2: case RelaxedMaxVecF64x2: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: replace(curr); return; default: @@ -78,11 +78,11 @@ struct RemoveRelaxedSIMD : WalkerPass> { case RelaxedNmaddVecF32x4: case RelaxedMaddVecF64x2: case RelaxedNmaddVecF64x2: - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: + case RelaxedDotI8x16I7x16AddSToVecI32x4: replace(curr); return; default: diff --git a/src/passes/RemoveUnusedBrs.cpp b/src/passes/RemoveUnusedBrs.cpp index bb539a5c823..bc7e1e5b7c3 100644 --- a/src/passes/RemoveUnusedBrs.cpp +++ b/src/passes/RemoveUnusedBrs.cpp @@ -1192,7 +1192,7 @@ struct RemoveUnusedBrs : public WalkerPass> { : public PostWalker> { // Map of all labels (branch targets) to the branches going to them. (We - // only care about blocks here, and not loops, but for simplicitly we + // only care about blocks here, and not loops, but for simplicity we // store all branch targets since blocks are 99% of that set anyhow. Any // loops are ignored later.) std::unordered_map> labelToBranches; @@ -1224,11 +1224,7 @@ struct RemoveUnusedBrs : public WalkerPass> { // if this block has just one child, a sub-block, then jumps to the // former are jumps to us, really if (auto* child = list[0]->dynCast()) { - // the two blocks must have the same type for us to update the - // branch, as otherwise one block may be unreachable and the other - // concrete, so one might lack a value - if (child->name.is() && child->name != curr->name && - child->type == curr->type) { + if (child->name.is()) { redirectBranches(child, curr->name); } } diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 34418009cf4..fe1c6c91685 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -314,7 +314,7 @@ struct Analyzer { void prepare() { for (auto& elem : module->elementSegments) { - if (!elem->table) { + if (elem->isPassive()) { continue; } auto& flatTableInfo = flatTableInfoMap[elem->table]; @@ -432,17 +432,19 @@ struct Analyzer { // Note a possible call of a function reference as well, if something else // might be written into the table during runtime. - // TODO: Add an option for immutable initial content like Directize? + // TODO: Add an option for immutable initial content like Directize? Can + // also check for grow without set, which leaves initial entries + // fixed. if (!tableInfoMap) { tableInfoMap = TableUtils::computeTableInfo(*module); } - if ((*tableInfoMap)[table].mayBeModified) { + if ((*tableInfoMap)[table].mayBeModified()) { useCallRefType(type); } } void useRefFunc(Name func) { - if (!options.closedWorld) { + if (options.worldMode == WorldMode::Open) { // The world is open, so assume the worst and something (inside or outside // of the module) can call this. use({ModuleElementKind::Function, func}); @@ -496,6 +498,7 @@ struct Analyzer { } } unreadStructFieldExprMap.erase(subStructField); + return true; }); } } @@ -607,8 +610,8 @@ struct Analyzer { // outside of the code we can see), and when it is reached (if it's // unreachable then we don't know the type, and can defer that to DCE to // remove). - if (!options.closedWorld || curr->type == Type::unreachable || - !curr->is()) { + if (options.worldMode == WorldMode::Open || + curr->type == Type::unreachable || !curr->is()) { for (auto* child : ChildIterator(curr)) { use(child); } @@ -761,6 +764,9 @@ struct Analyzer { } else if (kind == ModuleElementKind::ElementSegment) { // TODO: We could empty out parts of the segment we don't need. auto* segment = module->getElementSegment(value); + if (segment->offset) { + addReferences(segment->offset); + } for (auto* item : segment->data) { addReferences(item); } @@ -856,7 +862,7 @@ struct RemoveUnusedModuleElements : public Pass { } }; ModuleUtils::iterActiveDataSegments(*module, [&](DataSegment* segment) { - if (segment->memory.is()) { + if (segment->isActive()) { auto* memory = module->getMemory(segment->memory); maybeRootSegment(ModuleElementKind::DataSegment, segment->name, @@ -868,7 +874,7 @@ struct RemoveUnusedModuleElements : public Pass { }); ModuleUtils::iterActiveElementSegments( *module, [&](ElementSegment* segment) { - if (segment->table.is()) { + if (segment->isActive()) { auto* table = module->getTable(segment->table); maybeRootSegment(ModuleElementKind::ElementSegment, segment->name, diff --git a/src/passes/RemoveUnusedTypes.cpp b/src/passes/RemoveUnusedTypes.cpp index b3b0f4a6dd9..3e4e2f83b88 100644 --- a/src/passes/RemoveUnusedTypes.cpp +++ b/src/passes/RemoveUnusedTypes.cpp @@ -39,14 +39,14 @@ struct RemoveUnusedTypes : Pass { // would change the identity of $A. Currently we would incorrectly remove // $unused. To fix that, we need to fix our collection of public types to // consider $A (and $unused) public in an open world. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "RemoveUnusedTypes requires --closed-world"; } // We're not changing the contents of any of the types, so we just round // trip them through GlobalTypeRewriter which will put all the private types // in a single new rec group and leave out all the unused types. - GlobalTypeRewriter(*module).update(); + GlobalTypeRewriter(*module, getPassOptions().worldMode).update(); } }; diff --git a/src/passes/ReorderTypes.cpp b/src/passes/ReorderTypes.cpp index e120822da51..0b394890aab 100644 --- a/src/passes/ReorderTypes.cpp +++ b/src/passes/ReorderTypes.cpp @@ -41,8 +41,8 @@ struct ReorderingTypeRewriter : GlobalTypeRewriter { static constexpr float maxFactor = 1.0; static constexpr Index numFactors = 21; - ReorderingTypeRewriter(Module& wasm, bool forTesting) - : GlobalTypeRewriter(wasm), forTesting(forTesting) {} + ReorderingTypeRewriter(Module& wasm, bool forTesting, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), forTesting(forTesting) {} std::vector getSortedTypes(PredecessorGraph preds) override { auto numTypes = preds.size(); @@ -142,11 +142,12 @@ struct ReorderTypes : Pass { } // See note in RemoveUnusedTypes. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "ReorderTypes requires --closed-world"; } - ReorderingTypeRewriter(*module, forTesting).update(); + ReorderingTypeRewriter(*module, forTesting, getPassOptions().worldMode) + .update(); } }; diff --git a/src/passes/SafeHeap.cpp b/src/passes/SafeHeap.cpp index fe5b21507fb..d5a4f8843de 100644 --- a/src/passes/SafeHeap.cpp +++ b/src/passes/SafeHeap.cpp @@ -17,7 +17,7 @@ // // Instruments code to check for incorrect heap access. This checks // for dereferencing 0 (null pointer access), reading past the valid -// top of sbrk()-addressible memory, and incorrect alignment notation. +// top of sbrk()-addressable memory, and incorrect alignment notation. // #include "asmjs/shared-constants.h" diff --git a/src/passes/SeparateDataSegments.cpp b/src/passes/SeparateDataSegments.cpp index bd684dbe82c..34a7855f12b 100644 --- a/src/passes/SeparateDataSegments.cpp +++ b/src/passes/SeparateDataSegments.cpp @@ -44,7 +44,7 @@ struct SeparateDataSegments : public Pass { Address base = std::stoi(baseStr); size_t lastEnd = 0; for (auto& seg : module->dataSegments) { - if (seg->isPassive) { + if (seg->isPassive()) { Fatal() << "separating passive segments not implemented"; } if (!seg->offset->is()) { diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index ae920839fd4..fc95a66bad8 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -65,7 +65,7 @@ struct SignaturePruning : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "SignaturePruning requires --closed-world"; } @@ -187,7 +187,8 @@ struct SignaturePruning : public Pass { } // Find the public types, which cannot be modified. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].optimizable = false; } @@ -315,7 +316,7 @@ struct SignaturePruning : public Pass { // Create a new signature. When the TypeRewriter operates below it will // modify the existing heap type in place to change its signature to this // one. TypeRewriter will also ensure that distinct types remain - // disctinct, even if they have the same signature after optimization. + // distinct, even if they have the same signature after optimization. newSignatures[type] = Signature(Type(newParams), sig.results); // removeParameters() updates the type as it goes, but in this pass we @@ -339,7 +340,8 @@ struct SignaturePruning : public Pass { } // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); if (callTargetsToLocalize.empty()) { return false; diff --git a/src/passes/SignatureRefining.cpp b/src/passes/SignatureRefining.cpp index 792aedd9d5c..5137b1899a5 100644 --- a/src/passes/SignatureRefining.cpp +++ b/src/passes/SignatureRefining.cpp @@ -156,7 +156,8 @@ struct SignatureRefining : public Pass { } // Find the public types, which we must not modify. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].canModify = false; } @@ -168,13 +169,12 @@ struct SignatureRefining : public Pass { false; } - // Continuations must not have params refined, because we do not update - // their users (e.g. cont.bind, resume) with new types. - // TODO: support refining continuations + // Continuations must not have params or results refined, because we do not + // update their users (e.g. cont.bind, resume) with new types. if (module->features.hasStackSwitching()) { for (auto type : ModuleUtils::collectHeapTypes(*module)) { if (type.isContinuation()) { - allInfo[type.getContinuation().type].canModifyParams = false; + allInfo[type.getContinuation().type].canModify = false; } } } @@ -338,7 +338,8 @@ struct SignatureRefining : public Pass { CodeUpdater(*this, *module).run(getPassRunner(), module); // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); // Update intrinsics. updateIntrinsics(module, allInfo); diff --git a/src/passes/SimplifyGlobals.cpp b/src/passes/SimplifyGlobals.cpp index 14cd6f6de9e..83eeb84e84f 100644 --- a/src/passes/SimplifyGlobals.cpp +++ b/src/passes/SimplifyGlobals.cpp @@ -148,7 +148,7 @@ struct GlobalUseScanner : public WalkerPass> { // if (global % 17 < 4) { global = 1 } // // What we want to disallow is using the global to actually do something that - // is noticeeable *aside* from writing the global, like this: + // is noticeable *aside* from writing the global, like this: // // if (global ? foo() : bar()) { .. } // @@ -582,7 +582,7 @@ struct SimplifyGlobals : public Pass { } if (info.imported || info.exported) { - // If the global is observable from the outside, we can't do anythng + // If the global is observable from the outside, we can't do anything // here. // // TODO: optimize the case of an imported but immutable global, etc. diff --git a/src/passes/SimplifyLocals.cpp b/src/passes/SimplifyLocals.cpp index bf7902443bc..08791380a5e 100644 --- a/src/passes/SimplifyLocals.cpp +++ b/src/passes/SimplifyLocals.cpp @@ -102,11 +102,11 @@ struct SimplifyLocals // a list of all sinkable traces that exit a block. the last // is falling off the end, others are branches. this is used for // block returns - std::map> blockBreaks; + std::unordered_map> blockBreaks; // blocks that we can't produce a block return value for them. // (switch target, or some other reason) - std::set unoptimizableBlocks; + std::unordered_set unoptimizableBlocks; // A stack of sinkables from the current traversal state. When // execution reaches an if-else, it splits, and can then diff --git a/src/passes/Souperify.cpp b/src/passes/Souperify.cpp index 9ed11e550a4..041ea286946 100644 --- a/src/passes/Souperify.cpp +++ b/src/passes/Souperify.cpp @@ -662,7 +662,7 @@ struct Printer { std::cout << ", "; printInternal(node->getValue(2)); } else { - WASM_UNREACHABLE("unexecpted node type"); + WASM_UNREACHABLE("unexpected node type"); } } diff --git a/src/passes/StringLifting.cpp b/src/passes/StringLifting.cpp index cd3a8ffabb6..2c8572c025a 100644 --- a/src/passes/StringLifting.cpp +++ b/src/passes/StringLifting.cpp @@ -70,10 +70,11 @@ struct StringLifting : public Pass { // Encode from WTF-8 to WTF-16. auto wtf8 = global->base; std::stringstream wtf16; - bool valid = String::convertWTF8ToWTF16(wtf16, wtf8.str); + bool valid = String::convertWTF8ToWTF16(wtf16, wtf8.view()); if (!valid) { Fatal() << "Bad string to lift: " << wtf8; } + // TODO: Use wtf16.view() once we have C++20. importedStrings[global->name] = wtf16.str(); found = true; } @@ -101,7 +102,7 @@ struct StringLifting : public Pass { continue; } // The index in the array is the basename. - Index index = std::stoi(std::string(global->base.str)); + Index index = std::stoi(std::string(global->base.view())); if (index >= array.size()) { Fatal() << "StringLifting: bad index in string.const section"; } @@ -222,7 +223,7 @@ struct StringLifting : public Pass { auto iter = parent.importedStrings.find(curr->name); if (iter != parent.importedStrings.end()) { auto wtf16 = iter->second; - replaceCurrent(Builder(*getModule()).makeStringConst(wtf16.str)); + replaceCurrent(Builder(*getModule()).makeStringConst(wtf16.view())); modified = true; } } diff --git a/src/passes/StringLowering.cpp b/src/passes/StringLowering.cpp index b4641bda134..c9e836aefe8 100644 --- a/src/passes/StringLowering.cpp +++ b/src/passes/StringLowering.cpp @@ -153,7 +153,7 @@ struct StringGathering : public Pass { // Re-encode from WTF-16 to WTF-8 to make the name easier to read. std::stringstream wtf8; [[maybe_unused]] bool valid = - String::convertWTF16ToWTF8(wtf8, string.str); + String::convertWTF16ToWTF8(wtf8, string.view()); assert(valid); // Then escape it because identifiers must be valid UTF-8. // TODO: Use wtf8.view() and escaped.view() once we have C++20. @@ -246,7 +246,7 @@ struct StringLowering : public StringGathering { if (auto* c = global->init->dynCast()) { std::stringstream utf8; if (useMagicImports && - String::convertUTF16ToUTF8(utf8, c->string.str)) { + String::convertUTF16ToUTF8(utf8, c->string.view())) { global->module = stringConstsModule; global->base = Name(utf8.str()); } else { @@ -263,7 +263,7 @@ struct StringLowering : public StringGathering { } else { json << ','; } - String::printEscapedJSON(json, c->string.str); + String::printEscapedJSON(json, c->string.view()); jsonImportIndex++; } global->init = nullptr; @@ -338,7 +338,7 @@ struct StringLowering : public StringGathering { // Strings turn into externref. updates[HeapType::string] = HeapType::ext; - TypeMapper(*module, updates).map(); + TypeMapper(*module, updates, getPassOptions().worldMode).map(); } // Imported string functions. diff --git a/src/passes/TypeFinalizing.cpp b/src/passes/TypeFinalizing.cpp index 5ba3459da46..85d218da327 100644 --- a/src/passes/TypeFinalizing.cpp +++ b/src/passes/TypeFinalizing.cpp @@ -52,7 +52,8 @@ struct TypeFinalizing : public Pass { // Note we don't need to worry about signature-called functions here // (configureAll) because such calls don't care about finality. - auto privateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto privateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); for (auto type : privateTypes) { // If we are finalizing types then we can only do that to leaf types. If // we are unfinalizing, we can do that unconditionally. @@ -66,7 +67,8 @@ struct TypeFinalizing : public Pass { public: TypeRewriter(Module& wasm, TypeFinalizing& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyTypeBuilderEntry(TypeBuilder& typeBuilder, Index i, diff --git a/src/passes/TypeGeneralizing.cpp b/src/passes/TypeGeneralizing.cpp index 139ba8efbbd..749372ff107 100644 --- a/src/passes/TypeGeneralizing.cpp +++ b/src/passes/TypeGeneralizing.cpp @@ -433,6 +433,8 @@ struct TransferFn : OverriddenVisitor { void visitConst(Const* curr) {} void visitUnary(Unary* curr) {} void visitBinary(Binary* curr) {} + void visitWideIntAddSub(WideIntAddSub* curr) {} + void visitWideIntMul(WideIntMul* curr) {} void visitSelect(Select* curr) { if (curr->type.isRef()) { @@ -560,7 +562,7 @@ struct TransferFn : OverriddenVisitor { for (size_t i = 0; i < numParams; ++i) { if (candidateSig.params[i] != sig.params[i]) { // Generalizing further would restrict how much we could generalize - // this argument, so we choose not to generalize futher. + // this argument, so we choose not to generalize further. // TODO: Experiment with making the opposite choice. goto done; } @@ -815,7 +817,7 @@ struct TransferFn : OverriddenVisitor { auto srcType = curr->srcRef->type.getHeapType(); if (destType.isBottom() || srcType.isBottom()) { // This will be emitted as unreachable. Do not require anything of the - // input, exept that the bottom refs remain bottom. + // input, except that the bottom refs remain bottom. clearStack(); auto nullref = Type(HeapType::none, Nullable); push(destType.isBottom() ? nullref : Type::none); diff --git a/src/passes/TypeMerging.cpp b/src/passes/TypeMerging.cpp index 5853ead9ad6..64a2df6bb7d 100644 --- a/src/passes/TypeMerging.cpp +++ b/src/passes/TypeMerging.cpp @@ -243,13 +243,14 @@ void TypeMerging::run(Module* module_) { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeMerging requires --closed-world"; } // First, find all the cast types and private types. We will need these to // determine whether types are eligible to be merged. - mergeable = ModuleUtils::getPrivateHeapTypes(*module); + mergeable = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); privateTypes = std::unordered_set(mergeable.begin(), mergeable.end()); auto casts = findCastTypes(); @@ -303,7 +304,8 @@ bool TypeMerging::merge(MergeKind kind) { Partitions partitions; #if TYPE_MERGING_DEBUG - auto printedPrivateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto printedPrivateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); using Fallback = IndexedTypeNameGenerator; Fallback printPrivate(printedPrivateTypes, "private."); ModuleTypeNameGenerator print(*module, printPrivate); @@ -467,7 +469,7 @@ bool TypeMerging::merge(MergeKind kind) { // Normally splitting partitions like this would require re-running DFA // minimization afterward, but in this case it is not possible that the // manual splits cause types in any other partition to become - // differentiatable. A type and its subtype cannot differ by referring to + // differentiable. A type and its subtype cannot differ by referring to // different, unrelated types in the same position because then they would // not be in a valid subtype relationship. std::vector> newPartitions; @@ -640,7 +642,7 @@ void TypeMerging::applyMerges() { // We found things to optimize! Rewrite types in the module to apply those // changes. - TypeMapper(*module, replacements).map(); + TypeMapper(*module, replacements, getPassOptions().worldMode).map(); } bool shapeEq(HeapType a, HeapType b) { diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index 201360e5aca..720233cf9ae 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -145,7 +145,7 @@ struct TypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeRefining requires --closed-world"; } @@ -262,7 +262,8 @@ struct TypeRefining : public Pass { bool canOptimize = false; // We cannot modify public types. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -454,7 +455,8 @@ struct TypeRefining : public Pass { public: TypeRewriter(Module& wasm, TypeRefining& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { const auto& oldFields = oldStructType.getStruct().fields; diff --git a/src/passes/TypeSSA.cpp b/src/passes/TypeSSA.cpp index 2c99f91ed76..ee63e5ee4b8 100644 --- a/src/passes/TypeSSA.cpp +++ b/src/passes/TypeSSA.cpp @@ -167,7 +167,7 @@ struct Analyzer void note(Expression**, Constraints type) { // Check closed type constraints for exactness. Other kinds of type - // constaints do not concern us. + // constraints do not concern us. // TODO: Handle tuples? for (auto varType : type) { if (auto* t = std::get_if(&varType)) { @@ -198,8 +198,8 @@ struct Analyzer void visitGlobal(Global* global) { // This could be more precise by checking that the init expression is not - // null before inhibiting optimization, or by just inhibiting optmization of - // the allocations used in the initialization, but this is simpler. + // null before inhibiting optimization, or by just inhibiting optimization + // of the allocations used in the initialization, but this is simpler. for (auto type : global->type) { if (type.isExact()) { disallowedTypes.insert(type.getHeapType()); diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index b3eac815170..866e32ba58a 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -567,8 +567,7 @@ struct Unsubtyping : Pass, Noter { if (!wasm->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "Unsubtyping requires --closed-world"; } @@ -635,7 +634,8 @@ struct Unsubtyping : Pass, Noter { void analyzePublicTypes(Module& wasm) { // We cannot change supertypes for anything public. - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(wasm, getPassOptions().worldMode)) { if (auto super = type.getDeclaredSuperType()) { noteSubtype(type, *super); } @@ -645,7 +645,7 @@ struct Unsubtyping : Pass, Noter { } } - void analyzeJSInterface(Module& wasm) { + void analyzeJSInterface(const Module& wasm) { if (!wasm.features.hasCustomDescriptors()) { return; } @@ -1038,7 +1038,8 @@ struct Unsubtyping : Pass, Noter { struct Rewriter : GlobalTypeRewriter { Unsubtyping& parent; Rewriter(Unsubtyping& parent, Module& wasm) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} std::optional getDeclaredSuperType(HeapType type) override { if (auto super = parent.types.getSupertype(type); super && !super->isBasic()) { @@ -1066,7 +1067,7 @@ struct Unsubtyping : Pass, Noter { } // TODO: Consider running the fixup only if we are actually removing any // descriptors. This would require a better way of detecting this than - // collecing and iterating over all the types, though. + // collecting and iterating over all the types, though. struct Rewriter : WalkerPass> { const TypeTree& types; diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 2b5ec3f191b..ac645e77549 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -38,8 +39,18 @@ struct Vacuum : public WalkerPass> { std::unique_ptr create() override { return std::make_unique(); } + // Track whether we need to fix up pops at the end: adding a block in a Try + // can require that. + bool hasTry = false; + bool addedBlocks = false; + void doWalkFunction(Function* func) { walk(func->body); + + if (hasTry && addedBlocks) { + EHUtils::handleBlockNestedPops(func, *getModule()); + } + ReFinalize().walkFunctionInModule(func, getModule()); } @@ -122,6 +133,7 @@ struct Vacuum : public WalkerPass> { if (curr->type.isDefaultable()) { auto* dummy = Builder(*getModule()) .makeConstantExpression(Literal::makeZeros(curr->type)); + addedBlocks = true; return getDroppedChildrenAndAppend( curr, *getModule(), getPassOptions(), dummy); } @@ -417,7 +429,7 @@ struct Vacuum : public WalkerPass> { } // sink a drop into an arm of an if-else if the other arm ends in an // unreachable, as it if is a branch, this can make that branch optimizable - // and more vaccuming possible + // and more vacuuming possible auto* iff = curr->value->dynCast(); if (iff && iff->ifFalse && iff->type.isConcrete()) { // reuse the drop in both cases @@ -438,6 +450,8 @@ struct Vacuum : public WalkerPass> { } void visitTry(Try* curr) { + hasTry = true; + // If try's body does not throw, the whole try-catch can be replaced with // the try's body. if (!EffectAnalyzer(getPassOptions(), *getModule(), curr->body).throws()) { diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index bcf3242e78c..df1b3cb809e 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -178,7 +178,7 @@ void PassRegistry::registerPasses() { registerPass( "func-metrics", "reports function metrics", createFunctionMetricsPass); registerPass("generate-dyncalls", - "generate dynCall fuctions used by emscripten ABI", + "generate dynCall functions used by emscripten ABI", createGenerateDynCallsPass); registerPass( "generate-i64-dyncalls", @@ -277,6 +277,9 @@ void PassRegistry::registerPasses() { registerPass("limit-segments", "attempt to merge segments to fit within web limits", createLimitSegmentsPass); + registerPass("mark-js-called", + "mark js called functions (using configureAll) as doing so", + createMarkJSCalledPass); registerPass("memory64-lowering", "lower loads and stores to a 64-bit memory to instead use a " "32-bit one", @@ -367,7 +370,7 @@ void PassRegistry::registerPasses() { "pick load signs based on their uses", createPickLoadSignsPass); registerPass( - "poppify", "Tranform Binaryen IR into Poppy IR", createPoppifyPass); + "poppify", "Transform Binaryen IR into Poppy IR", createPoppifyPass); registerPass("post-emscripten", "miscellaneous optimizations for Emscripten-generated code", createPostEmscriptenPass); @@ -390,6 +393,8 @@ void PassRegistry::registerPasses() { createPrintFeaturesPass); registerPass( "print-full", "print in full s-expression format", createFullPrinterPass); + registerPass( + "print-boundary", "print boundary in JSON format", createPrintBoundaryPass); registerPass( "print-call-graph", "print call graph", createPrintCallGraphPass); @@ -412,6 +417,9 @@ void PassRegistry::registerPasses() { registerPass("remove-relaxed-simd", "replaces relaxed SIMD instructions with unreachable", createRemoveRelaxedSIMDPass); + registerPass("remove-exports", + "removes exports using a wildcard", + createRemoveExportsPass); registerPass("remove-imports", "removes imports and replaces them with nops", createRemoveImportsPass); @@ -755,7 +763,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { addIfNoDWARFIssues("once-reduction"); } if (wasm->features.hasGC() && options.optimizeLevel >= 2) { - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("type-refining"); addIfNoDWARFIssues("signature-pruning"); addIfNoDWARFIssues("signature-refining"); @@ -765,11 +773,11 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { // remove ref.funcs that were once assigned to vtables but are no longer // needed, which can allow more code to be removed globally. After those, // constant field propagation can be more effective. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("gto"); } addIfNoDWARFIssues("remove-unused-module-elements"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("remove-unused-types"); // Allow ref.tests in cfp if we are aggressively optimizing for speed. if (options.optimizeLevel >= 3) { @@ -779,7 +787,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { } } addIfNoDWARFIssues("gsi"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("abstract-type-refining"); addIfNoDWARFIssues("unsubtyping"); } diff --git a/src/passes/passes.h b/src/passes/passes.h index be06369a9f8..0e53028144e 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -87,6 +87,7 @@ Pass* createInstrumentLocalsPass(); Pass* createInstrumentMemoryPass(); Pass* createLLVMMemoryCopyFillLoweringPass(); Pass* createLoopInvariantCodeMotionPass(); +Pass* createMarkJSCalledPass(); Pass* createMemory64LoweringPass(); Pass* createMemoryPackingPass(); Pass* createMergeBlocksPass(); @@ -128,6 +129,7 @@ Pass* createPostEmscriptenPass(); Pass* createPrecomputePass(); Pass* createPrecomputePropagatePass(); Pass* createPrinterPass(); +Pass* createPrintBoundaryPass(); Pass* createPrintCallGraphPass(); Pass* createPrintFeaturesPass(); Pass* createPrintFunctionMapPass(); @@ -135,6 +137,7 @@ Pass* createPropagateGlobalsGloballyPass(); Pass* createRandomizeBranchHintsPass(); Pass* createRemoveNonJSOpsPass(); Pass* createRemoveRelaxedSIMDPass(); +Pass* createRemoveExportsPass(); Pass* createRemoveImportsPass(); Pass* createRemoveMemoryInitPass(); Pass* createRemoveUnusedBrsPass(); diff --git a/src/support/CMakeLists.txt b/src/support/CMakeLists.txt index d264979e7a8..3fc559ff36b 100644 --- a/src/support/CMakeLists.txt +++ b/src/support/CMakeLists.txt @@ -7,6 +7,7 @@ set(support_SOURCES debug.cpp dfa_minimization.cpp file.cpp + int128.cpp intervals.cpp istring.cpp json.cpp diff --git a/src/support/delta_debugging.h b/src/support/delta_debugging.h new file mode 100644 index 00000000000..7cdafc32554 --- /dev/null +++ b/src/support/delta_debugging.h @@ -0,0 +1,171 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef wasm_support_delta_debugging_h +#define wasm_support_delta_debugging_h + +#include +#include +#include + +#include "support/index.h" +namespace wasm { + +// Use the delta debugging algorithm (Zeller 2002, +// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of +// items necessary to preserve some property. `working` is the minimal set of +// items found so far and `test` is the smaller set of items that should be +// tested next. After testing, call `accept()`, `reject()`, or `resolve(bool +// accepted)` to update the working and test sets appropriately. +template struct DeltaDebugger { + std::vector working; + std::vector test; + +private: + Index numPartitions = 1; + Index currentPartition = 0; + bool testingComplements = false; + bool triedEmpty = false; + bool isFinished = false; + std::vector> partitions; + +public: + DeltaDebugger(std::vector items) : working(std::move(items)) {} + + bool finished() const { + return isFinished || (triedEmpty && working.size() <= 1); + } + Index partitionCount() { return numPartitions; } + Index partitionIndex() { return currentPartition; } + + void accept() { + if (finished()) { + return; + } + + if (test.empty()) { + triedEmpty = true; + } + + working = std::move(test); + + // We might be finished now even if we weren't before. + if (finished()) { + return; + } + + if (!testingComplements) { + numPartitions = 2; + } else { + numPartitions = std::max(numPartitions - 1, Index(2)); + } + testingComplements = false; + currentPartition = 0; + updateTest(); + } + + void reject() { + if (test.empty()) { + triedEmpty = true; + numPartitions = 2; + updateTest(); + return; + } + + if (finished()) { + return; + } + + ++currentPartition; + if (currentPartition >= partitions.size()) { + // No need to test complements if there are only two partitions, since + // that is no different. + if (!testingComplements && numPartitions > 2) { + testingComplements = true; + currentPartition = 0; + } else { + if (numPartitions >= working.size()) { + isFinished = true; + return; + } + // Refine the partitions. + numPartitions = std::min(Index(working.size()), 2 * numPartitions); + testingComplements = false; + currentPartition = 0; + } + } + updateTest(); + } + + // Convenience wrapper for when there is already a bool determining whether to + // accept or reject the current test sequence. + void resolve(bool success) { + if (success) { + accept(); + } else { + reject(); + } + } + +private: + void updateTest() { + if (finished()) { + test.clear(); + return; + } + + if (currentPartition == 0 && !testingComplements) { + generatePartitions(); + } + + if (!testingComplements) { + test = partitions[currentPartition]; + } else { + test.clear(); + test.reserve(working.size() - partitions[currentPartition].size()); + for (size_t i = 0; i < partitions.size(); ++i) { + if (i != currentPartition) { + test.insert(test.end(), partitions[i].begin(), partitions[i].end()); + } + } + } + } + + void generatePartitions() { + partitions.clear(); + size_t size = working.size(); + assert(numPartitions != 0 && numPartitions <= size); + + size_t basePartitionSize = size / numPartitions; + size_t rem = size % numPartitions; + size_t idx = 0; + for (size_t i = 0; i < numPartitions; ++i) { + size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); + if (partitionSize > 0) { + std::vector partition; + partition.reserve(partitionSize); + for (size_t j = 0; j < partitionSize; ++j) { + partition.push_back(working[idx++]); + } + partitions.emplace_back(std::move(partition)); + } + } + } +}; + +} // namespace wasm + +#endif // wasm_support_delta_debugging_h diff --git a/src/support/graph_traversal.h b/src/support/graph_traversal.h new file mode 100644 index 00000000000..282aee27256 --- /dev/null +++ b/src/support/graph_traversal.h @@ -0,0 +1,89 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if __has_include() +#include +#endif +#include +#include +#include +#include + +namespace wasm { + +// SuccessorFunction should be an invocable that takes a 'push' function (which +// is an invocable that takes a `const T&`), and a `const T&`. i.e. +// SuccessorFunction should call `push` for each neighbor of the T that it's +// called with. +// TODO: We don't have a good way to write this with concepts today. +// Something like this should do it, but we hit an ICE on dwarf symbols in debug +// builds: requires requires(const SuccessorFunction& successors, const T& t) { +// successors([](const T&) { }, t); } +template class Graph { +public: +#if defined(__cpp_lib_concepts) + template Sen> + requires std::convertible_to, T> +#else + template +#endif + Graph(It rootsBegin, Sen rootsEnd, SuccessorFunction successors) + : roots(rootsBegin, rootsEnd), successors(std::move(successors)) { + } + + // Traverse the graph depth-first, calling `successors` exactly once for each + // node (unless the node appears multiple times in `roots`). Return the set of + // nodes visited. + std::unordered_set traverseDepthFirst() const { + std::vector stack(roots.begin(), roots.end()); + std::unordered_set visited(roots.begin(), roots.end()); + + auto maybePush = [&](const T& t) { + auto [_, inserted] = visited.insert(t); + if (inserted) { + stack.push_back(t); + } + }; + + while (!stack.empty()) { + auto curr = std::move(stack.back()); + stack.pop_back(); + + successors(maybePush, curr); + } + + return visited; + } + +private: + std::vector roots; + SuccessorFunction successors; +}; + +#if defined(__cpp_lib_concepts) +template Sen, + typename SuccessorFunction> +Graph(It, Sen, SuccessorFunction) + -> Graph, std::decay_t>; +#else +template +Graph(It, Sen, SuccessorFunction) + -> Graph::value_type, + std::decay_t>; +#endif + +} // namespace wasm diff --git a/src/support/int128.cpp b/src/support/int128.cpp new file mode 100644 index 00000000000..2eae18b6516 --- /dev/null +++ b/src/support/int128.cpp @@ -0,0 +1,120 @@ +// Copyright 2026 WebAssembly Community Group participants +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "support/int128.h" + +namespace wasm { + +#ifdef __SIZEOF_INT128__ + +Int128 mul_wide_s(uint64_t lhs, uint64_t rhs) { + __int128 result = static_cast<__int128>(static_cast(lhs)) * + static_cast<__int128>(static_cast(rhs)); + return {static_cast(result >> 64), static_cast(result)}; +} + +Int128 mul_wide_u(uint64_t lhs, uint64_t rhs) { + unsigned __int128 result = + static_cast(lhs) * static_cast(rhs); + return {static_cast(result >> 64), static_cast(result)}; +} + +#else + +Int128 mul_wide_s(uint64_t lhs, uint64_t rhs) { + return detail::mul_wide_s_fallback(lhs, rhs); +} + +Int128 mul_wide_u(uint64_t lhs, uint64_t rhs) { + return detail::mul_wide_u_fallback(lhs, rhs); +} + +#endif + +namespace detail { + +Int128 mul_wide_s_fallback(uint64_t lhs, uint64_t rhs) { + auto [high, low] = mul_wide_u_fallback(lhs, rhs); + + // If lhs is negative, then it looks like 2**64 is added to its unsigned value + // so we computed + // (lhs + 2**64) * rhs + // = lhs * rhs + 2**64 * rhs + // We need to subtract 2**64 * rhs, so just subtract rhs directly from the + // high bits. + // + // If lhs AND rhs are negative then we computed + // (lhs + 2**64) * (rhs + 2**64) + // = lhs * rhs + 2**64 * rhs + 2**64 * lhs + 2**128 + // The last term overflowed and had no effect, so it's enough to do the two + // subtractions in the two different branches. + if (static_cast(lhs) < 0) { + high -= rhs; + } + if (static_cast(rhs) < 0) { + high -= lhs; + } + + return {high, low}; +} + +Int128 mul_wide_u_fallback(uint64_t lhs, uint64_t rhs) { + // Decompose lhs and rhs into 4 32-bit numbers and distribute to compute: + // (lhsHigh * 2^32 + lhsLow) * (rhsHigh * 2^32 + rhsLow) + // + // (lhsHigh * rhsHigh) * 2^64 [Upper 64 bits] + // (lhsHigh * rhsLow + lhsLow * rhsHigh) * 2^32 [Middle 64 bits] + // (lhsLow * rhsLow) [Lower 64 bits] + + uint64_t lhsLow = lhs & 0xffffffff; + uint64_t lhsHigh = lhs >> 32; + uint64_t rhsLow = rhs & 0xffffffff; + uint64_t rhsHigh = rhs >> 32; + + uint64_t lowLow = lhsLow * rhsLow; + uint64_t lowHigh = lhsLow * rhsHigh; + uint64_t highLow = lhsHigh * rhsLow; + uint64_t highHigh = lhsHigh * rhsHigh; + + // The lowest 32 bits consist only of lowLow (without its carry) + // + // The next 32 bits consist of `lowHigh + highLow + the carry of lowlow` + // (again this may carry to the next 32) Start by adding the carry which is + // guaranteed to not overflow 64 bits. Overflow can't happen because lowHigh + // is max (2**32 - 1)**2 and `lowLow >> 32` is no more than 32 bits, (2**32 - + // 1)**2 + (2**32 -1) < 2**64 - 1 + uint64_t highOfLow = (lowLow >> 32) + lowHigh; + + // We might have a carry into the next 32 (the low of the high), mask it out + // now so we can add highLow. + uint64_t carry = highOfLow >> 32; + + // This is also guaranteed to not overflow by the same logic. + highOfLow = (highOfLow & 0xffffffff) + highLow; + + // highOfLow might have exceeded 32 bits again, carry it again + uint64_t carry2 = highOfLow >> 32; + + uint64_t lower = (lowLow & 0xffffffff) | (highOfLow << 32); + + // No need to worry about overflow here, since 128 bits is always enough to + // store the product of two 64-bit ints. + uint64_t higher = carry + carry2 + highHigh; + + return {higher, lower}; +} + +} // namespace detail + +} // namespace wasm diff --git a/src/support/int128.h b/src/support/int128.h new file mode 100644 index 00000000000..2d86f9fb57e --- /dev/null +++ b/src/support/int128.h @@ -0,0 +1,45 @@ +// Copyright 2026 WebAssembly Community Group participants +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef wasm_support_int128_h +#define wasm_support_int128_h + +#include + +namespace wasm { + +struct Int128 { + uint64_t high; + uint64_t low; + + bool operator==(const Int128& other) const { + return high == other.high && low == other.low; + } +}; + +// Computes the 128-bit product of two signed 64-bit integers. +Int128 mul_wide_s(uint64_t lhs, uint64_t rhs); + +// Computes the 128-bit product of two unsigned 64-bit integers. +Int128 mul_wide_u(uint64_t lhs, uint64_t rhs); + +namespace detail { +// Fallback implementations exposed for testing. +Int128 mul_wide_s_fallback(uint64_t lhs, uint64_t rhs); +Int128 mul_wide_u_fallback(uint64_t lhs, uint64_t rhs); +} // namespace detail + +} // namespace wasm + +#endif // wasm_support_i128_h diff --git a/src/support/istring.cpp b/src/support/istring.cpp index 60c8b59be6d..d254dcc4cff 100644 --- a/src/support/istring.cpp +++ b/src/support/istring.cpp @@ -14,32 +14,39 @@ * limitations under the License. */ +#include +#include + #include "istring.h" #include "mixed_arena.h" namespace wasm { -std::string_view IString::interned(std::string_view s, bool reuse) { - // We need a set of string_views that can be modified in-place to minimize - // the number of lookups we do. Since set elements cannot normally be - // modified, wrap the string_views in a container that provides mutability - // even through a const reference. - struct MutStringView { - mutable std::string_view str; - MutStringView(std::string_view str) : str(str) {} - }; - struct MutStringViewHash { - size_t operator()(const MutStringView& mut) const { - return std::hash{}(mut.str); +const char* IString::interned(std::string_view s) { + // A set of interned Views, i.e., that contains our pascal-style strings. We + // need to query this using a std::string_view, as that is what we receive as + // input (turning it into pascal-style storage would add overhead). To do so, + // use overloading in the hash and equality functions (which works thanks to + // `is_transparent`). + struct InternedHash { + using is_transparent = void; + size_t operator()(View v) const { + return std::hash{}(v.view()); + } + size_t operator()(std::string_view sv) const { + return std::hash{}(sv); } }; - struct MutStringViewEqual { - bool operator()(const MutStringView& a, const MutStringView& b) const { - return a.str == b.str; + struct InternedEqual { + using is_transparent = void; + bool operator()(View a, View b) const { return a.view() == b.view(); } + bool operator()(std::string_view a, View b) const { return a == b.view(); } + bool operator()(View a, std::string_view b) const { return a.view() == b; } + bool operator()(std::string_view a, std::string_view b) const { + return a == b; } }; - using StringSet = - std::unordered_set; + using StringSet = std::unordered_set; // The authoritative global set of interned string views. static StringSet globalStrings; @@ -54,34 +61,37 @@ std::string_view IString::interned(std::string_view s, bool reuse) { // A thread-local cache of strings to reduce contention. thread_local static StringSet localStrings; - auto [localIt, localInserted] = localStrings.insert(s); - if (!localInserted) { + if (auto it = localStrings.find(s); it != localStrings.end()) { // We already had a local copy of this string. - return localIt->str; + return it->internal; } // No copy yet in the local cache. Check the global cache. std::unique_lock lock(mutex); - auto [globalIt, globalInserted] = globalStrings.insert(s); - if (!globalInserted) { + if (auto it = globalStrings.find(s); it != globalStrings.end()) { // We already had a global copy of this string. Cache it locally. - localIt->str = globalIt->str; - return localIt->str; + localStrings.insert(*it); + return it->internal; } - if (!reuse) { - // We have a new string, but it doesn't have a stable address. Create a copy - // of the data at a stable address we can use. Make sure it is null - // terminated so legacy uses that get a C string still work. - char* data = (char*)arena.allocSpace(s.size() + 1, 1); - std::copy(s.begin(), s.end(), data); - data[s.size()] = '\0'; - s = std::string_view(data, s.size()); - } + // We have a new string. Create a copy of the data at a stable address with a + // header we can use. Make sure it is null terminated so legacy uses that get + // a C string still work. + size_t size = s.size(); + // The string's size must fit in 32 bits. + assert(size <= std::numeric_limits::max()); + char* buffer = + (char*)arena.allocSpace(sizeof(uint32_t) + size + 1, alignof(uint32_t)); + *(uint32_t*)(buffer) = size; + char* data = buffer + sizeof(uint32_t); + std::copy(s.begin(), s.end(), data); + data[size] = '\0'; // Intern our new string. - localIt->str = globalIt->str = s; - return s; + View v{data}; + globalStrings.insert(v); + localStrings.insert(v); + return data; } } // namespace wasm diff --git a/src/support/istring.h b/src/support/istring.h index dad20b8fb00..bc7d2380dd2 100644 --- a/src/support/istring.h +++ b/src/support/istring.h @@ -33,21 +33,55 @@ namespace wasm { struct IString { private: - static std::string_view interned(std::string_view s, bool reuse = true); + static const char* interned(std::string_view s); public: - const std::string_view str; + // Strings are stored in Pascal style: a size followed by the characters. We + // keep the internal pointer pointing to the data, so that data() is a no-op; + // computing the size, which is more rare, requires looking back and doing a + // load. + // + // The size is limited to 4 bytes, so the maximum string we support is 4GB. + // + // The alternative approach of using a string_view here, i.e., keeping the + // pointer and size in the IString, uses more more memory. That is, this + // optimization saves a lot of space, because while it adds 4 bytes to each + // interned string itself, we tend to have many views on each. + // + // We provide a View here, which is a simple interface. Users that need more + // convert to a std::string_view with .view() or a cast. + struct View { + const char* internal = nullptr; + const char* data() const { return internal; } + size_t size() const { + return internal ? *(const uint32_t*)(internal - 4) : 0; + } + char operator[](size_t x) const { return internal[x]; } + std::string_view view() const { + if (!internal) { + // No size to read. + return {}; + } + return {internal, size()}; + } + }; + const View str; + + std::string_view view() const { return str.view(); } - IString() = default; + // Use an explicit constructor instead of `= default` because some older + // compilers (e.g. Apple Clang in older Xcode versions) delete the default + // constructor if there is a const member without an in-class initializer, + // even if that member's type has a default constructor. + // FIXME: Use `= default` once we bump the min clang/Xcode version that + // we support. + IString() : str({nullptr}) {} - // TODO: This is a wildly unsafe default inherited from the previous - // implementation. Change it? - IString(std::string_view str, bool reuse = true) - : str(interned(str, reuse)) {} + IString(View v) : str(v) {} - // But other C strings generally do need to be copied. - IString(const char* str) : str(interned(str, false)) {} - IString(const std::string& str) : str(interned(str, false)) {} + IString(std::string_view s) : str{interned(s)} {} + IString(const char* str) : str{interned(str)} {} + IString(const std::string& str) : str{interned(str)} {} IString(const IString& other) = default; @@ -57,17 +91,24 @@ struct IString { bool operator==(const IString& other) const { // Fast! No need to compare contents due to interning - return str.data() == other.str.data(); + return str.internal == other.str.internal; } bool operator!=(const IString& other) const { return !(*this == other); } - bool operator<(const IString& other) const { return str < other.str; } - bool operator<=(const IString& other) const { return str <= other.str; } - bool operator>(const IString& other) const { return str > other.str; } - bool operator>=(const IString& other) const { return str >= other.str; } + bool operator<(const IString& other) const { + if (str.internal == other.str.internal) { + return false; + } + return view() < other.view(); + } + bool operator<=(const IString& other) const { + return *this == other || *this < other; + } + bool operator>(const IString& other) const { return !(*this <= other); } + bool operator>=(const IString& other) const { return !(*this < other); } char operator[](int x) const { return str[x]; } - explicit operator bool() const { return str.data() != nullptr; } + explicit operator bool() const { return str.internal != nullptr; } // TODO: deprecate? bool is() const { return bool(*this); } @@ -75,13 +116,12 @@ struct IString { std::string toString() const { return {str.data(), str.size()}; } - bool equals(std::string_view other) const { return str == other; } + bool equals(std::string_view other) const { return str.view() == other; } bool startsWith(std::string_view prefix) const { - // TODO: Use C++20 `starts_with`. - return str.substr(0, prefix.size()) == prefix; + return view().starts_with(prefix); } - bool startsWith(IString str) const { return startsWith(str.str); } + bool startsWith(IString other) const { return startsWith(other.view()); } // Disambiguate for string literals. template bool startsWith(const char (&str)[N]) const { @@ -89,13 +129,9 @@ struct IString { } bool endsWith(std::string_view suffix) const { - // TODO: Use C++20 `ends_with`. - if (suffix.size() > str.size()) { - return false; - } - return str.substr(str.size() - suffix.size()) == suffix; + return view().ends_with(suffix); } - bool endsWith(IString str) const { return endsWith(str.str); } + bool endsWith(IString other) const { return endsWith(other.view()); } // Disambiguate for string literals. template bool endsWith(const char (&str)[N]) const { @@ -103,7 +139,7 @@ struct IString { } IString substr(size_t pos, size_t len = std::string_view::npos) const { - return IString(str.substr(pos, len)); + return IString(view().substr(pos, len)); } size_t size() const { return str.size(); } @@ -120,7 +156,7 @@ template<> struct hash { }; inline std::ostream& operator<<(std::ostream& os, const wasm::IString& str) { - return os << str.str; + return os << str.view(); } } // namespace std diff --git a/src/support/json.cpp b/src/support/json.cpp index dd94719d47d..7858cfba319 100644 --- a/src/support/json.cpp +++ b/src/support/json.cpp @@ -19,30 +19,80 @@ namespace json { -void Value::stringify(std::ostream& os, bool pretty) { - if (isString()) { - std::stringstream wtf16; - [[maybe_unused]] bool valid = - wasm::String::convertWTF8ToWTF16(wtf16, getIString().str); - assert(valid); - // TODO: Use wtf16.view() once we have C++20. - wasm::String::printEscapedJSON(os, wtf16.str()); - } else if (isArray()) { - os << '['; - auto first = true; - for (auto& item : getArray()) { - if (first) { - first = false; - } else { - // TODO pretty whitespace - os << ','; +void Value::stringify(std::ostream& os, bool pretty, int indent) { + auto doIndent = [&]() { + for (int i = 0; i < indent; i++) { + os << ' '; + } + }; + + auto maybeNewline = [&]() { + if (pretty) { + os << '\n'; + doIndent(); + } + }; + + switch (type) { + case String: { + std::stringstream wtf16; + [[maybe_unused]] bool valid = + wasm::String::convertWTF8ToWTF16(wtf16, getIString().view()); + assert(valid); + // TODO: Use wtf16.view() once we have C++20. + wasm::String::printEscapedJSON(os, wtf16.str()); + return; + } + case Array: { + os << '['; + indent++; + auto first = true; + for (auto& item : getArray()) { + if (first) { + first = false; + } else { + os << ','; + } + maybeNewline(); + item->stringify(os, pretty, indent); + } + indent--; + maybeNewline(); + os << ']'; + return; + } + case Object: { + os << '{'; + indent++; + auto first = true; + for (auto& [key, value] : getObject()) { + if (first) { + first = false; + } else { + os << ','; + } + maybeNewline(); + os << "\"" << key << "\":"; + if (pretty) { + os << ' '; + } + value->stringify(os, pretty, indent); } - item->stringify(os, pretty); + indent--; + maybeNewline(); + os << '}'; + return; } - os << ']'; - } else { - WASM_UNREACHABLE("TODO: stringify all of JSON"); - } + case Number: + os << getNumber(); + return; + case Null: + os << "null"; + return; + case Bool: + os << (getBool() ? "true" : "false"); + return; + }; } } // namespace json diff --git a/src/support/json.h b/src/support/json.h index 23d0749f045..98e46b063fd 100644 --- a/src/support/json.h +++ b/src/support/json.h @@ -37,6 +37,7 @@ #include #include +#include "support/insert_ordered.h" #include "support/istring.h" #include "support/safe_integer.h" #include "support/string.h" @@ -67,7 +68,18 @@ struct Value { Ref& operator[](IString x) { return (*this->get())[x]; } }; + static Ref make() { return Ref(new Value); } template static Ref make(T t) { return Ref(new Value(t)); } + static Ref makeArray() { + Ref ret(new Value); + ret->setArray(); + return ret; + } + static Ref makeObject() { + Ref ret(new Value); + ret->setObject(); + return ret; + } enum Type { String = 0, @@ -81,7 +93,7 @@ struct Value { Type type = Null; using ArrayStorage = std::vector; - using ObjectStorage = std::unordered_map; + using ObjectStorage = wasm::InsertOrderedMap; // MSVC does not allow unrestricted unions: // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2544.pdf @@ -102,6 +114,10 @@ struct Value { // constructors all copy their input Value() {} explicit Value(const char* s) : type(Null) { setString(s); } + explicit Value(const std::string& s) : type(Null) { setString(s.c_str()); } + explicit Value(const std::string_view& s) : type(Null) { + setString(std::string(s)); + } explicit Value(double n) : type(Null) { setNumber(n); } explicit Value(ArrayStorage& a) : type(Null) { setArray(); @@ -202,6 +218,10 @@ struct Value { assert(isArray()); return *arr; } + ObjectStorage& getObject() { + assert(isObject()); + return *obj; + } bool& getBool() { assert(isBool()); return boo; @@ -378,7 +398,7 @@ struct Value { return curr; } - void stringify(std::ostream& os, bool pretty = false); + void stringify(std::ostream& os, bool pretty = false, int indent = 0); // String operations diff --git a/src/support/mixed_arena.h b/src/support/mixed_arena.h index bb06d1aa062..a5f00281e3f 100644 --- a/src/support/mixed_arena.h +++ b/src/support/mixed_arena.h @@ -32,7 +32,7 @@ // // Arena-style bump allocation is important for two reasons: First, so that // allocation is quick, and second, so that allocated items are close together, -// which is cache-friendy. Arena allocation is also useful for a minor third +// which is cache-friendly. Arena allocation is also useful for a minor third // reason which is to make freeing all the items in an arena very quick. // // Each WebAssembly Module has an arena allocator, which should be used @@ -100,7 +100,7 @@ struct MixedArena { // allocator for us there. but carefully, as others may do so as // well. we may waste a few allocations here, but it doesn't matter // as this can only happen as the chain is built up, i.e., - // O(# of cores) per allocator, and our allocatrs are long-lived. + // O(# of cores) per allocator, and our allocators are long-lived. if (!allocated) { allocated = new MixedArena(); // has our thread id } diff --git a/src/support/name.cpp b/src/support/name.cpp index 4c599defca0..34b7546fba9 100644 --- a/src/support/name.cpp +++ b/src/support/name.cpp @@ -46,6 +46,7 @@ std::ostream& Name::print(std::ostream& o) const { // TODO: This is not spec-compliant since the spec does not yet support // quoted identifiers and has a limited set of valid idchars. o << '$'; + auto str = view(); if (size() >= 1 && std::all_of(str.begin(), str.end(), isIDChar)) { return o << str; } else { diff --git a/src/support/name.h b/src/support/name.h index 8e3f7a291d5..d6c71f732c9 100644 --- a/src/support/name.h +++ b/src/support/name.h @@ -33,9 +33,10 @@ namespace wasm { struct Name : public IString { Name() : IString() {} - Name(std::string_view str) : IString(str, false) {} - Name(const char* str) : IString(str, false) {} + Name(std::string_view str) : IString(str) {} + Name(const char* str) : IString(str) {} Name(IString str) : IString(str) {} + Name(IString::View str) : IString(str) {} Name(const std::string& str) : IString(str) {} // String literals do not need to be copied. Note: Not safe to construct from @@ -50,13 +51,11 @@ struct Name : public IString { } } - static Name fromInt(size_t i) { - return IString(std::to_string(i).c_str(), false); - } + static Name fromInt(size_t i) { return IString(std::to_string(i).c_str()); } bool hasSubstring(IString substring) { // TODO: Use C++23 `contains`. - return str.find(substring.str) != std::string_view::npos; + return view().find(substring.view()) != std::string_view::npos; } std::ostream& print(std::ostream& o) const; diff --git a/src/support/path.h b/src/support/path.h index 8383bc53ea8..82c9f751845 100644 --- a/src/support/path.h +++ b/src/support/path.h @@ -44,7 +44,7 @@ char getPathSeparator(); std::string getDirName(const std::string& path); std::string getBaseName(const std::string& path); -// Get the binaryen root dor. +// Get the binaryen root dir. std::string getBinaryenRoot(); // Get the binaryen bin dir. diff --git a/src/support/stdckdint.h b/src/support/stdckdint.h index 42e87f9a26d..c5132058332 100644 --- a/src/support/stdckdint.h +++ b/src/support/stdckdint.h @@ -29,12 +29,20 @@ template bool ckd_add(T* output, T a, T b) { // Atm this polyfill only supports unsigned types. static_assert(std::is_unsigned_v); - T result = a + b; - if (result < a) { - return true; - } - *output = result; - return false; + *output = a + b; + return *output < a; +#endif +} + +template bool ckd_sub(T* output, T a, T b) { +#if __has_builtin(__builtin_sub_overflow) + return __builtin_sub_overflow(a, b, output); +#else + // Atm this polyfill only supports unsigned types. + static_assert(std::is_unsigned_v); + + *output = a - b; + return *output > a; #endif } diff --git a/src/support/topological_sort.h b/src/support/topological_sort.h index c0e1a93712e..c96047e07b1 100644 --- a/src/support/topological_sort.h +++ b/src/support/topological_sort.h @@ -38,7 +38,7 @@ struct CycleException {}; // An adjacency list containing edges from vertices to their successors. Uses // `Index` because we are primarily sorting elements of Wasm modules. If we ever -// need to sort signficantly larger objects, we might need to switch to +// need to sort significantly larger objects, we might need to switch to // `size_t` or make this a template parameter. using Graph = std::vector>; diff --git a/src/support/utilities.h b/src/support/utilities.h index 3f40111c451..0aad86c94e1 100644 --- a/src/support/utilities.h +++ b/src/support/utilities.h @@ -94,6 +94,27 @@ class Fatal { #define WASM_UNREACHABLE(msg) wasm::handle_unreachable() #endif +// Helper to create an invocable with an overloaded operator(), for use with +// std::visit e.g. +// std::visit( +// overloaded{ +// [](const A& a) { ... }, +// [](const B& b) { ... }}, +// variant) +template struct overloaded : Ts... { + using Ts::operator()...; +}; + +template overloaded(Ts...) -> overloaded; + +// Lookup a value from `map` and return a pointer to the underlying value +// or nullptr if not present. Returns a const pointer if `map` is const and +// non-const otherwise +auto* find_or_null(auto& map, const auto& key) { + auto it = map.find(key); + return it != map.end() ? &it->second : nullptr; +} + } // namespace wasm #endif // wasm_support_utilities_h diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 516d9f60603..9918b845cf8 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -143,6 +143,11 @@ struct LoggingExternalInterface : public ShellExternalInterface { } } + void init(Module& wasm, ModuleRunner& instance_) override { + ShellExternalInterface::init(wasm, instance_); + instance = &instance_; + } + Literal getImportedFunction(Function* import) override { if (linkedInstances.contains(import->module)) { return getImportInstance(import)->getExportedFunction(import->base); @@ -180,6 +185,9 @@ struct LoggingExternalInterface : public ShellExternalInterface { } } std::cout << "]\n"; + if (import->base == "log-branch") { + return arguments[0]; + } return {}; } else if (import->base == "throw") { // Throw something, depending on the value of the argument. 0 means @@ -363,8 +371,6 @@ struct LoggingExternalInterface : public ShellExternalInterface { } return false; } - - void setModuleRunner(ModuleRunner* instance_) { instance = instance_; } }; class FuzzerImportResolver @@ -409,7 +415,7 @@ class FuzzerImportResolver // fuzz_shell.js. Index payload = 0; for (auto name : {name.module, name.name}) { - for (auto c : name.str) { + for (auto c : name.view()) { payload = (payload + static_cast(c)) % 251; } } @@ -467,7 +473,10 @@ struct ExecutionResults { secondInterface = std::make_unique( loggings, *second, linkedInstances); secondInstance = std::make_shared( - *second, secondInterface.get(), linkedInstances); + *second, + secondInterface.get(), + linkedInstances, + std::make_shared(linkedInstances)); instantiate(*secondInstance, *secondInterface); } @@ -484,6 +493,8 @@ struct ExecutionResults { // This should be ignored and not compared with, as optimizations can // change whether a host limit is reached. ignore = true; + } catch (const WasmException&) { + std::cout << "[exception thrown: start]\n"; } } @@ -493,7 +504,6 @@ struct ExecutionResults { // SIMD instructions. instance.setRelaxedBehavior(ModuleRunner::RelaxedBehavior::Execute); instance.instantiate(); - interface.setModuleRunner(&instance); } void callExports(Module& wasm, ModuleRunner& instance) { diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 78057877031..da3daced9dd 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -121,10 +121,10 @@ class TranslateToFuzzReader { public: TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); void pickPasses(OptimizationOptions& options); void setAllowMemory(bool allowMemory_) { allowMemory = allowMemory_; } @@ -132,6 +132,7 @@ class TranslateToFuzzReader { void setPreserveImportsAndExports(bool preserveImportsAndExports_) { preserveImportsAndExports = preserveImportsAndExports_; } + void setAgainstJS(bool againstJS_) { againstJS = againstJS_; } void setImportedModule(std::string importedModuleName); void build(); @@ -140,7 +141,7 @@ class TranslateToFuzzReader { private: // Whether the module will be tested in a closed-world environment. - bool closedWorld; + WorldMode worldMode; Builder builder; Random random; Intrinsics intrinsics; @@ -159,6 +160,11 @@ class TranslateToFuzzReader { // existing testcase (using initial-content). bool preserveImportsAndExports = false; + // Whether the wasm will be used from JS and in no other way. This lets us + // modify the wasm in ways that keep it valid from JS's point of view, but + // which might cause issues when linked against wasm or used otherwise. + bool againstJS = false; + // An optional module to import from. std::optional importedModule; @@ -370,6 +376,9 @@ class TranslateToFuzzReader { bool isValidPublicType(Type type) { return publicTypeValidator.isValidPublicType(type); } + bool isValidPublicType(HeapType type) { + return publicTypeValidator.isValidPublicType(type); + } // Function operations. The main processFunctions() loop will call addFunction // as well as modFunction(). @@ -409,6 +418,10 @@ class TranslateToFuzzReader { void fixAfterChanges(Function* func); void modifyInitialFunctions(); + // Mutate the JS boundary, that is, make changes on the wasm side that JS + // would not be broken by (JS does not care about types). + void mutateJSBoundary(); + // Note a global for use during code generation. void useGlobalLater(Global* global); @@ -464,6 +477,9 @@ class TranslateToFuzzReader { Expression* makeGlobalGet(Type type); Expression* makeGlobalSet(Type type); Expression* makeTupleMake(Type type); + Expression* makeWideIntAddSub(Type type); + Expression* makeWideIntMul(Type type); + Expression* makeWideIntExpression(Type type); Expression* makeTupleExtract(Type type); Expression* makePointer(); Expression* makeNonAtomicLoad(Type type); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index ba786b927a3..457a371a008 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -19,6 +19,7 @@ #include "ir/glbs.h" #include "ir/iteration.h" #include "ir/local-structural-dominance.h" +#include "ir/lubs.h" #include "ir/module-utils.h" #include "ir/names.h" #include "ir/subtype-exprs.h" @@ -61,8 +62,8 @@ std::vector getMemoryOrders(const FeatureSet& features) { TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld) - : wasm(wasm), closedWorld(closedWorld), builder(wasm), + WorldMode worldMode) + : wasm(wasm), worldMode(worldMode), builder(wasm), random(std::move(input), wasm.features), intrinsics(wasm), loggableTypes(getLoggableTypes(wasm.features)), atomicMemoryOrders(getMemoryOrders(wasm.features)), @@ -122,10 +123,9 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld) - : TranslateToFuzzReader(wasm, - read_file>(filename, Flags::Binary), - closedWorld) {} + WorldMode worldMode) + : TranslateToFuzzReader( + wasm, read_file>(filename, Flags::Binary), worldMode) {} void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Pick random passes to further shape the wasm. This is similar to how we @@ -273,8 +273,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Most of these depend on closed world, so just set that. Set it both // on the global pass options, and in the internal state of this // TranslateToFuzzReader instance. - options.passOptions.closedWorld = true; - closedWorld = true; + options.passOptions.worldMode = WorldMode::Closed; + worldMode = WorldMode::Closed; switch (upTo(16)) { case 0: @@ -342,8 +342,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { options.passOptions.shrinkLevel = upTo(3); } - if (!options.passOptions.closedWorld && oneIn(2)) { - options.passOptions.closedWorld = true; + if (options.passOptions.worldMode == WorldMode::Open && oneIn(2)) { + options.passOptions.worldMode = WorldMode::Closed; } // Prune things that error in JS if we call them (like SIMD), some of the @@ -413,6 +413,10 @@ void TranslateToFuzzReader::build() { PassRunner runner(&wasm); ReFinalize().run(&runner, &wasm); ReFinalize().walkModuleCode(&wasm); + + if (againstJS) { + mutateJSBoundary(); + } } void TranslateToFuzzReader::setupMemory() { @@ -446,13 +450,13 @@ void TranslateToFuzzReader::setupMemory() { auto segment = builder.makeDataSegment(); segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(i)), false); - segment->isPassive = bool(upTo(2)); + bool isPassive = bool(upTo(2)); size_t segSize = upTo(fuzzParams->USABLE_MEMORY * 2); segment->data.resize(segSize); for (size_t j = 0; j < segSize; j++) { segment->data[j] = upTo(512); } - if (!segment->isPassive) { + if (!isPassive) { segment->offset = builder.makeConst( Literal::makeFromInt32(memCovered, memory->addressType)); memCovered += segSize; @@ -639,7 +643,7 @@ void TranslateToFuzzReader::setupTables() { std::any_of(wasm.elementSegments.begin(), wasm.elementSegments.end(), [&](auto& segment) { - return segment->table.is() && segment->type == funcref; + return segment->isActive() && segment->type == funcref; }); auto addressType = wasm.getTable(funcrefTableName)->addressType; if (!hasFuncrefElemSegment) { @@ -841,6 +845,17 @@ void TranslateToFuzzReader::setupTags() { jsTag->base = "jstag"; wasm.addTag(std::move(jsTag)); } + + // Export some tags, sometimes. + if (!preserveImportsAndExports) { + for (auto& tag : wasm.tags) { + if (isValidPublicType(tag->type) && oneIn(2)) { + auto exportName = Names::getValidExportName(wasm, tag->name); + wasm.addExport( + Builder::makeExport(exportName, tag->name, ExternalKind::Tag)); + } + } + } } void TranslateToFuzzReader::addTag() { @@ -854,7 +869,7 @@ void TranslateToFuzzReader::finalizeMemory() { auto& memory = wasm.memories[0]; for (auto& segment : wasm.dataSegments) { Address maxOffset = segment->data.size(); - if (!segment->isPassive) { + if (segment->isActive()) { if (!wasm.features.hasGC()) { // Using a non-imported global in a segment offset is not valid in wasm // unless GC is enabled. This can occur due to us adding a local @@ -1667,7 +1682,7 @@ void TranslateToFuzzReader::processFunctions() { // Also fix up closed world, if we need to. We must do this at the end, so // nothing can break the closed world assumptions after. - if (closedWorld) { + if (worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { if (!func->imported()) { fixClosedWorld(func.get()); @@ -2178,7 +2193,7 @@ void TranslateToFuzzReader::mutate(Function* func) { } void TranslateToFuzzReader::fixClosedWorld(Function* func) { - assert(closedWorld); + assert(worldMode == WorldMode::Closed); struct Fixer : public ExpressionStackWalker> { @@ -2381,8 +2396,246 @@ void TranslateToFuzzReader::modifyInitialFunctions() { } // Remove a start function - the fuzzing harness expects code to run only - // from exports. - wasm.start = Name(); + // from exports. When preserving imports and exports, however, we need to + // keep any start method, as it may be important to keep the contract between + // the wasm and the outside. + if (!preserveImportsAndExports) { + wasm.start = Name(); + } +} + +void TranslateToFuzzReader::mutateJSBoundary() { + assert(againstJS); + + // Scan to find functions whose address is taken. We cannot modify their + // signatures at all. + + struct FunctionInfo { + // Whether there are references to this function itself. + bool reffed = false; + + // Calls to imports from this function. + std::vector callImports; + }; + + using NameInfoMap = std::unordered_map; + + struct FunctionInfoScanner + : public WalkerPass> { + // Not parallel for simplicity, see the map update below. + + bool modifiesBinaryenIR() override { return false; } + + NameInfoMap& map; + + FunctionInfoScanner(NameInfoMap& map) : map(map) {} + + std::unique_ptr create() override { + return std::make_unique(map); + } + + void visitCall(Call* curr) { + if (getModule()->getFunction(curr->target)->imported()) { + map[curr->target].callImports.push_back(curr); + } + + // Return calls add a dependency similar to references: we cannot refine + // the callee without coordination with the caller. + if (curr->isReturn) { + map[curr->target].reffed = true; + } + } + + void visitRefFunc(RefFunc* curr) { map[curr->func].reffed = true; } + }; + + NameInfoMap map; + FunctionInfoScanner scanner(map); + PassRunner runner(&wasm); + scanner.setModule(&wasm); + scanner.run(&runner, &wasm); + scanner.walkModuleCode(&wasm); + + // If a function does not have its address taken, we can refine types. This is + // safe because we will still send and receive the right number of values (we + // are not changing the arity, which JS might notice). Each place we may + // refine, we are given the maximum refinement and pick a random type between + // it and the old type. + auto maybeRefine = [&](Type old, Type new_) { + if (!old.isRef()) { + return old; + } + + // If this is unreachable code, we can still refine to the bottom. + if (new_ == Type::unreachable) { + new_ = Type(old.getHeapType().getBottom(), NonNullable); + } + + // Find all heap types between the old and new, starting from new. + auto oldHeapType = old.getHeapType(); + auto newHeapType = new_.getHeapType(); + assert(HeapType::isSubType(newHeapType, oldHeapType)); + std::vector options; + while (1) { + options.push_back(newHeapType); + // We cannot look at a bottom type's supers (there can be many, and the + // getSuperType() API doesn't return them), but can use + // interestingHeapSubTypes: any subtype of old is valid. + if (newHeapType.isBottom()) { + for (auto type : interestingHeapSubTypes[oldHeapType]) { + options.push_back(type); + } + break; + } + // Continue until we reach the old type. + if (newHeapType == oldHeapType) { + break; + } + auto next = newHeapType.getSuperType(); + assert(next); + newHeapType = *next; + } + newHeapType = pick(options); + + // Pick the nullability. + auto oldNullability = old.getNullability(); + auto newNullability = new_.getNullability(); + if (newNullability != oldNullability) { + newNullability = getNullability(); + } + + // Pick the exactness. + auto oldExactness = old.getExactness(); + auto newExactness = new_.getExactness(); + // We can only be exact if we are using the new heap type: that type is + // exactly what is sent here, and no intermediate heap type would be valid. + // For example, given $A :> $B :> $C, then maybeRefine($A, exact $C) can + // return exact $C, but cannot return exact $B. + // + // Also, basic heap types cannot be exact. + if (newHeapType != new_.getHeapType() || newHeapType.isBasic()) { + newExactness = Inexact; + } else if (newExactness != oldExactness) { + // TODO: once getExactness() is fixed (see there), use that + newExactness = oneIn(2) ? Exact : Inexact; + } + + return Type(newHeapType, newNullability, newExactness); + }; + + // Given a set of types (all params or all results), and an index among them, + // refine that index if we can. It is possible that no new types exist at all, + // if the code was unreachable and we noted nothing. + auto maybeRefineIndex = [&](Type oldTypes, LUBFinder newLUB, Index index) { + auto lub = + newLUB.noted() ? newLUB.getLUB()[index] : Type(Type::unreachable); + return maybeRefine(oldTypes[index], lub); + }; + + // First, refine params sent to imports. Gather the LUB sent to each import, + // and then refine. + std::unordered_map paramLUBs; + for (auto& [_, info] : map) { + for (auto* call : info.callImports) { + auto declaredParams = wasm.getFunction(call->target)->getParams(); + std::vector sent; + for (Index i = 0; i < call->operands.size(); i++) { + auto type = call->operands[i]->type; + if (type == Type::unreachable) { + // Nothing sent here. What we refine to must still validate, even + // though this call is unreachable. Using the non-nullable bottom type + // is valid, and has the fewest restrictions. + type = declaredParams[i]; + if (type.isRef()) { + type = Type(type.getHeapType().getBottom(), NonNullable); + } + } + sent.push_back(type); + } + paramLUBs[call->target].note(Type(sent)); + } + } + + for (auto& func : wasm.functions) { + if (!func->imported()) { + continue; + } + // TODO: In the referenced case, we could consider using import/export + // wrappers and refining just there. + if (map[func->name].reffed) { + continue; + } + // Do not alter the signature of configureAll or other VM builtins. Changing + // these to something the VM does not expect will just cause it to + // immediately reject the module by trapping. + if (func->module.startsWith("wasm:")) { + continue; + } + + auto oldParams = func->getParams(); + if (oldParams == Type::none) { + continue; + } + + // Refine. + auto lub = paramLUBs[func->name]; + auto lubType = lub.getLUB(); + // Either the LUB has the right data shape, or nothing was noted (this is + // unreachable). + assert(oldParams.size() == lubType.size() || !lub.noted()); + std::vector newParams; + for (Index i = 0; i < lubType.size(); i++) { + newParams.push_back(maybeRefineIndex(oldParams, lub, i)); + } + func->setParams(Type(newParams)); + } + + // Second, refine results sent from exports. + for (auto& exp : wasm.exports) { + if (exp->kind != ExternalKind::Function) { + continue; + } + auto name = *exp->getInternalName(); + if (map[name].reffed) { + continue; + } + + auto* func = wasm.getFunction(name); + auto oldResults = func->getResults(); + if (oldResults == Type::none) { + continue; + } + + // Refine. + auto lub = LUB::getResultsLUB(func, wasm); + auto lubType = lub.getLUB(); + assert(oldResults.size() == lubType.size() || !lub.noted()); + std::vector newResults; + for (Index i = 0; i < lubType.size(); i++) { + newResults.push_back(maybeRefineIndex(oldResults, lub, i)); + } + func->setResults(Type(newResults)); + } + + // Update return types from calls to exports whose results we refined. + struct CallUpdater : public WalkerPass> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr create() override { + return std::make_unique(); + } + + void visitCall(Call* curr) { + if (curr->type != Type::unreachable) { + curr->type = getModule()->getFunction(curr->target)->getResults(); + } + } + } updater; + updater.setModule(&wasm); + updater.run(&runner, &wasm); + + // Propagate after our changes. + ReFinalize().run(&runner, &wasm); } void TranslateToFuzzReader::dropToLog(Function* func) { @@ -2559,7 +2812,11 @@ Expression* TranslateToFuzzReader::_makeConcrete(Type type) { &Self::makeStringGet); } if (type.isTuple()) { - options.add(FeatureSet::Multivalue, &Self::makeTupleMake); + if (type == Types::getI64Pair() && oneIn(2)) { + options.add(FeatureSet::WideArithmetic, &Self::makeWideIntExpression); + } else { + options.add(FeatureSet::Multivalue, &Self::makeTupleMake); + } } if (type.isRef()) { auto heapType = type.getHeapType(); @@ -3242,6 +3499,30 @@ Expression* TranslateToFuzzReader::makeTupleMake(Type type) { return builder.makeTupleMake(std::move(elements)); } +Expression* TranslateToFuzzReader::makeWideIntAddSub(Type type) { + assert(wasm.features.hasWideArithmetic()); + assert(type == Types::getI64Pair()); + auto op = oneIn(2) ? AddInt128 : SubInt128; + auto* leftLow = make(Type::i64); + auto* leftHigh = make(Type::i64); + auto* rightLow = make(Type::i64); + auto* rightHigh = make(Type::i64); + return builder.makeWideIntAddSub(op, leftLow, leftHigh, rightLow, rightHigh); +} + +Expression* TranslateToFuzzReader::makeWideIntMul(Type type) { + assert(wasm.features.hasWideArithmetic()); + assert(type == Types::getI64Pair()); + auto op = oneIn(2) ? MulWideSInt64 : MulWideUInt64; + auto* left = make(Type::i64); + auto* right = make(Type::i64); + return builder.makeWideIntMul(op, left, right); +} + +Expression* TranslateToFuzzReader::makeWideIntExpression(Type type) { + return oneIn(2) ? makeWideIntAddSub(type) : makeWideIntMul(type); +} + Expression* TranslateToFuzzReader::makeTupleExtract(Type type) { // Tuples can require locals in binary format conversions. if (!type.isDefaultable()) { @@ -3900,8 +4181,8 @@ Expression* TranslateToFuzzReader::makeBasicRef(Type type) { case HeapType::func: { // Rarely, emit a call to imported table.get (when nullable, unshared, and // where we can emit a call). - if (type.isNullable() && share == Unshared && funcContext && - tableGetImportName && !oneIn(3)) { + if (!trivialNesting && type.isNullable() && share == Unshared && + funcContext && tableGetImportName && !oneIn(3)) { return makeImportTableGet(); } return makeRefFuncConst(type); @@ -4454,7 +4735,10 @@ Expression* TranslateToFuzzReader::makeUnary(Type type) { case 1: return buildUnary({SplatVecI64x2, make(Type::i64)}); case 2: - return buildUnary({SplatVecF32x4, make(Type::f32)}); + return buildUnary({pick(FeatureOptions() + .add(FeatureSet::SIMD, SplatVecF32x4) + .add(FeatureSet::FP16, SplatVecF16x8)), + make(Type::f32)}); case 3: return buildUnary({SplatVecF64x2, make(Type::f64)}); case 4: @@ -4526,7 +4810,10 @@ Expression* TranslateToFuzzReader::makeUnary(Type type) { TruncSatSVecF16x8ToVecI16x8, TruncSatUVecF16x8ToVecI16x8, ConvertSVecI16x8ToVecF16x8, - ConvertUVecI16x8ToVecF16x8)), + ConvertUVecI16x8ToVecF16x8, + PromoteLowVecF16x8ToVecF32x4, + DemoteZeroVecF32x4ToVecF16x8, + DemoteZeroVecF64x2ToVecF16x8)), make(Type::v128)}); } WASM_UNREACHABLE("invalid value"); @@ -4785,6 +5072,14 @@ Expression* TranslateToFuzzReader::makeBinary(Type type) { // SIMD Swizzle SwizzleVecI8x16) + .add(FeatureSet::RelaxedSIMD, + RelaxedSwizzleVecI8x16, + RelaxedMinVecF32x4, + RelaxedMaxVecF32x4, + RelaxedMinVecF64x2, + RelaxedMaxVecF64x2, + RelaxedQ15MulrSVecI16x8, + RelaxedDotI8x16I7x16SToVecI16x8) .add(FeatureSet::FP16, EqVecF16x8, EqVecF16x8, @@ -4916,7 +5211,7 @@ Expression* TranslateToFuzzReader::makeAtomic(Type type) { bytes = pick(1, 2, 4); break; default: - WASM_UNREACHABLE("invalide value"); + WASM_UNREACHABLE("invalid value"); } break; } @@ -4935,7 +5230,7 @@ Expression* TranslateToFuzzReader::makeAtomic(Type type) { bytes = pick(1, 2, 4, 8); break; default: - WASM_UNREACHABLE("invalide value"); + WASM_UNREACHABLE("invalid value"); } break; } @@ -5108,13 +5403,20 @@ Expression* TranslateToFuzzReader::makeSIMDShuffle() { } Expression* TranslateToFuzzReader::makeSIMDTernary() { - // TODO: Enable qfma/qfms once it is implemented in V8 and the interpreter - // SIMDTernaryOp op = pick(Bitselect, - // QFMAF32x4, - // QFMSF32x4, - // QFMAF64x2, - // QFMSF64x2); - SIMDTernaryOp op = Bitselect; + SIMDTernaryOp op = + pick(FeatureOptions() + .add(FeatureSet::SIMD, Bitselect) + .add(FeatureSet::RelaxedSIMD, + RelaxedMaddVecF32x4, + RelaxedNmaddVecF32x4, + RelaxedMaddVecF64x2, + RelaxedNmaddVecF64x2, + RelaxedLaneselectI8x16, + RelaxedLaneselectI16x8, + RelaxedLaneselectI32x4, + RelaxedLaneselectI64x2, + RelaxedDotI8x16I7x16AddSToVecI32x4) + .add(FeatureSet::FP16, MaddVecF16x8, NmaddVecF16x8)); Expression* a = make(Type::v128); Expression* b = make(Type::v128); Expression* c = make(Type::v128); @@ -6151,9 +6453,14 @@ Type TranslateToFuzzReader::getMVPType() { } Type TranslateToFuzzReader::getTupleType() { + // Give a significant chance to an i64 pair, for wide arithmetic. + if (wasm.features.hasWideArithmetic() && oneIn(5)) { + return Types::getI64Pair(); + } + std::vector elements; - size_t maxElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 1); - for (size_t i = 0; i < maxElements; ++i) { + size_t numElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 2); + for (size_t i = 0; i < numElements; ++i) { auto type = getSingleConcreteType(); // Don't add a non-defaultable type into a tuple, as currently we can't // spill them into locals (that would require a "let"). @@ -6424,7 +6731,7 @@ bool TranslateToFuzzReader::isValidRefFuncTarget(Name func) { // reference, but in that mode we must only pass in jsCalled functions. We // handle direct calls in fixClosedWorld, but cannot handle indirect ones // easily, so just disallow taking references of those functions. - if (!closedWorld) { + if (worldMode == WorldMode::Open) { return true; } return !isCallRefImport(func); diff --git a/src/tools/fuzzing/heap-types.cpp b/src/tools/fuzzing/heap-types.cpp index 426a9b76a9c..41d3a1cf084 100644 --- a/src/tools/fuzzing/heap-types.cpp +++ b/src/tools/fuzzing/heap-types.cpp @@ -422,7 +422,7 @@ struct HeapTypeGeneratorImpl { } Type generateTupleType(Shareability share) { - std::vector types(2 + rand.upTo(params.MAX_TUPLE_SIZE - 1)); + std::vector types(2 + rand.upTo(params.MAX_TUPLE_SIZE - 2)); for (auto& type : types) { type = generateSingleType(share); } @@ -1000,8 +1000,10 @@ void Inhabitator::markNullable(FieldPos field) { // this extra `index` variable once we have C++20. It's a workaround for // lambdas being unable to capture structured bindings. const size_t index = idx; - subtypes.iterSubTypes( - curr, [&](HeapType type, Index) { nullables.insert({type, index}); }); + subtypes.iterSubTypes(curr, [&](HeapType type, Index) { + nullables.insert({type, index}); + return true; + }); break; } } diff --git a/src/tools/fuzzing/parameters.cpp b/src/tools/fuzzing/parameters.cpp index 3220f9625d3..423cad941d5 100644 --- a/src/tools/fuzzing/parameters.cpp +++ b/src/tools/fuzzing/parameters.cpp @@ -26,7 +26,7 @@ void FuzzParams::setDefaults() { MAX_GLOBALS = 30; - MAX_TUPLE_SIZE = 6; + MAX_TUPLE_SIZE = 7; MAX_STRUCT_SIZE = 6; diff --git a/src/tools/fuzzing/random.cpp b/src/tools/fuzzing/random.cpp index cfcdbdd970e..7af7b412775 100644 --- a/src/tools/fuzzing/random.cpp +++ b/src/tools/fuzzing/random.cpp @@ -66,9 +66,6 @@ float Random::getFloat() { return Literal(get32()).reinterpretf32(); } double Random::getDouble() { return Literal(get64()).reinterpretf64(); } uint32_t Random::upTo(uint32_t x) { - if (finished()) { - return 0; - } if (x == 0) { return 0; } diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index bc1ac4a1478..1e089f0b8c8 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -357,7 +357,7 @@ struct OptimizationOptions : public ToolOptions { // Pass arguments with the same name as the pass are stored per-instance on // PassInfo, while all other arguments are stored globally on - // passOptions.arguments (which is what the overriden method on ToolOptions + // passOptions.arguments (which is what the overridden method on ToolOptions // does). void addPassArg(const std::string& key, const std::string& value) override { // Scan the current pass list for the last defined instance of a pass named diff --git a/src/tools/tool-options.h b/src/tools/tool-options.h index cc39daad4fc..1f3ec266bc7 100644 --- a/src/tools/tool-options.h +++ b/src/tools/tool-options.h @@ -112,6 +112,7 @@ struct ToolOptions : public Options { .addFeature(FeatureSet::RelaxedAtomics, "acquire/release atomic memory operations") .addFeature(FeatureSet::CustomPageSizes, "custom page sizes") + .addFeature(FeatureSet::WideArithmetic, "wide arithmetic") .add("--enable-typed-function-references", "", "Deprecated compatibility flag", @@ -172,7 +173,7 @@ struct ToolOptions : public Options { ToolOptionsCategory, Options::Arguments::Zero, [this](Options*, const std::string&) { - passOptions.closedWorld = true; + passOptions.worldMode = WorldMode::Closed; }) .add( "--preserve-type-order", diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index adc5a4fe988..b503b0a818a 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -213,6 +213,13 @@ class EvallingModuleRunner : public ModuleRunnerBase { return ModuleRunnerBase::visitGlobalGet(curr); } + Flow visitGlobalSet(GlobalSet* curr) { + if (curr->value->type.isContinuation()) { + throw FailToEvalException("cannot serialize continuations to globals"); + } + return ModuleRunnerBase::visitGlobalSet(curr); + } + Flow visitTableGet(TableGet* curr) { // We support tableLoad, below, so that call_indirect works (it calls it // internally), but we want to disable table.get for now. @@ -281,6 +288,9 @@ std::unique_ptr buildEnvModule(Module& wasm) { // that there are not arguments passed to main, etc. static bool ignoreExternalInput = false; +// Whether to emit informative logging to stdout about the eval process. +static bool quiet = false; + struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { Module* wasm; EvallingModuleRunner* instance; @@ -305,10 +315,21 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { linkedInstances.swap(linkedInstances_); } + bool firstApplication = true; + // Called when we want to apply the current state of execution to the Module. // Until this is called the Module is never changed. void applyToModule() { - clearApplyState(); + if (firstApplication) { + // The first time we apply things to the module, we can remove the start + // function: we evalled it successfully, if we got to here (and we must + // not execute it again later, which would mean it runs twice). We do not + // do this after the first application because we start to build up a new + // start function with the things we need, unrelated to the original one + // (see addStartFixup). + wasm->start = Name(); + firstApplication = false; + } // If nothing was ever written to memories then there is nothing to update. if (!memories.empty()) { @@ -509,12 +530,12 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { return Bits::readLE(getMemory(address, memoryName, sizeof(T))); } +public: // Clear the state of the operation of applying the interpreter's runtime - // information into the module. - // - // This happens each time we apply contents to the module, which is basically - // once per ctor function, but can be more fine-grained also if we execute a - // line at a time. + // information into the module. This must be done before we start to serialize + // content (as the serialization uses this state - defining globals must be + // set and are latter used, etc.). After this, serialization can happen, and + // after that, a call to applyToModule() can be done. void clearApplyState() { // The process of allocating "defining globals" begins here, from scratch // each time (things live before may no longer be). @@ -526,6 +547,7 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { clearStartBlock(); } +private: void applyMemoryToModule() { // Memory must have already been flattened into the standard form: one // segment at offset 0, or none. @@ -1042,9 +1064,6 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { } }; -// Whether to emit informative logging to stdout about the eval process. -static bool quiet = false; - // The outcome of evalling a ctor is one of three states: // // 1. We failed to eval it completely (but perhaps we succeeded partially). In @@ -1107,7 +1126,7 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // the locals here. That is, we need to save the local state in the function, // which we do by setting up at the entry. We update this list of expressions // at the same time as applyToModule() - we must only do it after an entire - // atomic "chunk" has been processed succesfully, we do not want partial + // atomic "chunk" has been processed successfully, we do not want partial // updates from an item in the block that we only partially evalled. When we // construct the (partially) evalled function, we will create local.sets of // these expressions at the beginning. @@ -1183,6 +1202,10 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, break; } + // We are about to serialize content (the code paths below call + // getSerialization). Clear the state. + interface.clearApplyState(); + if (flow.breakTo == RETURN_CALL_FLOW) { // The return-called function is stored in the last value. func = wasm.getFunction(flow.values.back().getFunc()); @@ -1213,7 +1236,7 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // module. Note that we must serialize the locals now as doing so may // cause changes that must be applied to the module (e.g. GC data may // cause globals to be added). And we must apply to the module now, and - // not later, as we must do so right after a successfull partial eval + // not later, as we must do so right after a successful partial eval // (after any failure to eval, the global state is no long valid to be // applied to the module, as incomplete changes may have occurred). // @@ -1462,11 +1485,16 @@ void evalCtors(Module& wasm, } } } catch (FailToEvalException& fail) { - // that's it, we failed to even create the instance + // That's it, we failed to even create the instance. if (!quiet) { std::cout << " ...stopping since could not create module instance: " << fail.why << "\n"; } + } catch (NonconstantException& fail) { + // We can also fail during start due to a non-constant operation. + if (!quiet) { + std::cout << " ...stopping since non-constant in start\n"; + } } catch (TopologicalSort::CycleException e) { // We use a topological sort for GC globals. If there is a non-breakable // cycle there, we will hit an error (we can break cycles in nullable and diff --git a/src/tools/wasm-fuzz-lattices.cpp b/src/tools/wasm-fuzz-lattices.cpp index 4d36761c69e..4b29e8eb477 100644 --- a/src/tools/wasm-fuzz-lattices.cpp +++ b/src/tools/wasm-fuzz-lattices.cpp @@ -148,7 +148,7 @@ struct RandomLattice { bool join(Element& a, const Element& b) const noexcept; }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); static_assert(Lattice); #endif @@ -1067,7 +1067,7 @@ int main(int argc, const char* argv[]) { Options options("wasm-fuzz-lattices", "Fuzz lattices for reflexivity, transitivity, and " - "anti-symmetry, and tranfer functions for monotonicity."); + "anti-symmetry, and transfer functions for monotonicity."); std::optional seed; options.add("--seed", diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 24615804da1..66800ed08ab 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -117,6 +117,11 @@ namespace { // have it as a global rather than pass it around all the time. Module merged; +// Everything we merge is accumulated into |merged|, aside from the start +// functions. To avoid incrementally adding a call each time, which can end up +// nested, we add them here and generate a series of flat calls at the end. +std::vector startFunctions; + // Name conflicts on functions etc. are resolved by renaming things in a way // that only matters internally. Conflicting export names, however, are // observable, and so the user must decide how they want wasm-merge to handle @@ -363,28 +368,9 @@ void copyModuleContents(Module& input, Name inputName) { merged.addExport(std::move(copy)); } - // Start functions must be merged. - if (input.start.is()) { - if (!merged.start.is()) { - // No previous start; just refer to the new one. - merged.start = input.start; - } else { - // Merge them, keeping the order. We copy both functions to avoid issues - // with other references to them, and just call the second one, leaving - // inlining to the optimizer if that makes sense to do. - auto copiedOldName = - Names::getValidFunctionName(merged, "merged.start.old"); - auto copiedNewName = - Names::getValidFunctionName(merged, "merged.start.new"); - auto* copiedOld = ModuleUtils::copyFunction( - merged.getFunction(merged.start), merged, copiedOldName); - ModuleUtils::copyFunction( - merged.getFunction(input.start), merged, copiedNewName); - Builder builder(merged); - copiedOld->body = builder.makeSequence( - copiedOld->body, builder.makeCall(copiedNewName, {}, Type::none)); - merged.start = copiedOldName; - } + // Start functions are accumulated till the end. + if (input.start) { + startFunctions.push_back(input.start); } // TODO: type names, features, debug info, custom sections, dylink info, etc. @@ -596,6 +582,34 @@ void updateTypes(Module& wasm) { updater.runOnModuleCode(&runner, &wasm); } +// Merge the start functions, keeping the order. We add a new function that +// calls them in sequence (leaving proper inlining, including handling of +// control flow etc., to the optimizer). +void mergeStartFunctions() { + if (startFunctions.empty()) { + return; + } + + if (startFunctions.size() == 1) { + // Avoid adding a call here. + merged.start = startFunctions[0]; + return; + } + + auto combinedName = + Names::getValidFunctionName(merged, "merged.start.combined"); + Builder builder(merged); + std::vector calls; + for (auto start : startFunctions) { + calls.push_back(builder.makeCall(start, {}, Type::none)); + } + auto* body = builder.makeBlock(calls); + auto combined = builder.makeFunction( + combinedName, Signature(Type::none, Type::none), {}, body); + merged.addFunction(std::move(combined)); + merged.start = combinedName; +} + // Merges an input module into an existing target module. The input module can // be modified, as it will no longer be needed (so it is intentionally not // marked as const here). @@ -697,11 +711,13 @@ Input source maps can be specified by adding an -ism option right after the modu "", "Write a wasm-split manifest to the specified file. This manifest can " "be given to wasm-split to split the merged module along the lines of " - "the original modules.", + "the original modules. Implies --debuginfo to preserve function names " + "in the output module.", WasmMergeOption, Options::Arguments::One, - [&manifestFile](Options* o, const std::string& argument) { + [&](Options* o, const std::string& argument) { manifestFile = argument; + debugInfo = true; }) .add("--rename-export-conflicts", "-rec", @@ -792,6 +808,13 @@ Input source maps can be specified by adding an -ism option right after the modu for (auto& curr : merged.exports) { exportModuleMap[curr.get()] = ExportInfo{inputFileName, curr->name}; } + + // Start functions are accumulated till the end. + if (merged.start) { + startFunctions.push_back(merged.start); + merged.start = Name(); + } + } else { // This is a later module: do a full merge. mergeInto(*currModule, inputFileName); @@ -825,6 +848,9 @@ Input source maps can be specified by adding an -ism option right after the modu // Update types after combing and linking everything. updateTypes(merged); + // Merge the start functions, after everything else is set up. + mergeStartFunctions(); + { PassRunner passRunner(&merged); // We might have made some globals read from others that now appear after diff --git a/src/tools/wasm-metadce.cpp b/src/tools/wasm-metadce.cpp index 96b2063df4b..29bba15aaf8 100644 --- a/src/tools/wasm-metadce.cpp +++ b/src/tools/wasm-metadce.cpp @@ -78,7 +78,7 @@ struct MetaDCEGraph { // to be kept alive. module = ENV; } - return std::string(module.str) + " (*) " + std::string(base.str); + return std::string(module.view()) + " (*) " + std::string(base.view()); } ImportId getImportId(ModuleItemKind kind, Name name) { diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index 4dd58db122d..9bdfa4b06f7 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -87,6 +87,7 @@ int main(int argc, const char* argv[]) { bool fuzzMemory = true; bool fuzzOOB = true; bool fuzzPreserveImportsAndExports = false; + bool fuzzAgainstJS = false; std::string fuzzImport; std::string emitSpecWrapper; std::string emitWasm2CWrapper; @@ -206,12 +207,19 @@ For more on how to optimize effectively, see [&](Options* o, const std::string& arguments) { fuzzOOB = false; }) .add("--fuzz-preserve-imports-exports", "", - "don't add imports and exports in -ttf mode", + "don't add imports and exports in -ttf mode, and keep the start", WasmOptOption, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { fuzzPreserveImportsAndExports = true; }) + .add( + "--fuzz-against-js", + "", + "modify the wasm in valid ways that assume it is used only from JS", + WasmOptOption, + Options::Arguments::Zero, + [&](Options* o, const std::string& arguments) { fuzzAgainstJS = true; }) .add( "--fuzz-import", "", @@ -345,10 +353,11 @@ For more on how to optimize effectively, see } if (translateToFuzz) { TranslateToFuzzReader reader( - wasm, options.extra["infile"], options.passOptions.closedWorld); + wasm, options.extra["infile"], options.passOptions.worldMode); reader.setAllowMemory(fuzzMemory); reader.setAllowOOB(fuzzOOB); reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports); + reader.setAgainstJS(fuzzAgainstJS); if (!fuzzImport.empty()) { reader.setImportedModule(fuzzImport); } diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index bc5dabfcab0..0dd8e0dda99 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -29,12 +29,13 @@ #include "ir/branch-utils.h" #include "ir/iteration.h" -#include "ir/literal-utils.h" +#include "ir/localize.h" #include "ir/properties.h" #include "ir/utils.h" #include "pass.h" #include "support/colors.h" #include "support/command-line.h" +#include "support/delta_debugging.h" #include "support/file.h" #include "support/hash.h" #include "support/path.h" @@ -351,7 +352,7 @@ struct Reducer // @param factor how much to ignore. starting with a high factor skips through // most of the file, which is often faster than going one by one // from the start - size_t reduceDestructively(int factor_) { + size_t reduceDestructively(uint64_t factor_) { factor = factor_; // prepare loadWorking(); @@ -400,7 +401,7 @@ struct Reducer std::unique_ptr module; std::unique_ptr builder; Index funcsSeen; - int factor; + uint64_t factor; // write the module and see if the command still fails on it as expected bool writeAndTestReduction() { @@ -822,7 +823,7 @@ struct Reducer auto& data = segment->data; // when we succeed, try to shrink by more and more, similar to bisection - size_t skip = 1; + uint64_t skip = 1; for (size_t i = 0; i < data.size() && !data.empty(); i++) { if (justShrank || shouldTryToReduce(bonus)) { auto save = data; @@ -838,7 +839,7 @@ struct Reducer std::cerr << "| shrank segment from " << save.size() << " => " << data.size() << " (skip: " << skip << ")\n"; noteReduction(); - skip = std::min(size_t(factor), 2 * skip); + skip = std::min(factor, 2 * skip); } else { data = std::move(save); return false; @@ -894,70 +895,249 @@ struct Reducer } } - // Reduces entire functions at a time. Returns whether we did a significant - // amount of reduction that justifies doing even more. - bool reduceFunctions() { - // try to remove functions - std::vector functionNames; - for (auto& func : module->functions) { - functionNames.push_back(func->name); + bool isEmptyBody(Expression* body) { + if (body->is() || body->is()) { + return true; } - auto numFuncs = functionNames.size(); - if (numFuncs == 0) { - return false; + if (auto* block = body->dynCast()) { + return block->list.empty(); } - size_t skip = 1; - size_t maxSkip = 1; - // If we just removed some functions in the previous iteration, keep trying - // to remove more as this is one of the most efficient ways to reduce. - bool justReduced = true; - // Start from a new place each time. - size_t base = deterministicRandom(numFuncs); - std::cerr << "| try to remove functions (base: " << base - << ", decisionCounter: " << decisionCounter << ", numFuncs " - << numFuncs << ")\n"; - for (size_t x = 0; x < functionNames.size(); x++) { - size_t i = (base + x) % numFuncs; - if (!justReduced && functionsWeTriedToRemove.contains(functionNames[i]) && - !shouldTryToReduce(std::max((factor / 5) + 1, 20000))) { + return false; + } + + void reduceFunctionBodies() { + std::cerr << "| try to remove function bodies\n"; + // Use function indices to speed up finding the complement of the kept + // partition. + std::vector nontrivialFuncIndices; + nontrivialFuncIndices.reserve(module->functions.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + auto& func = module->functions[i]; + // Skip functions that already have trivial bodies. + if (func->imported() || isEmptyBody(func->body)) { continue; } - std::vector names; - for (size_t j = 0; names.size() < skip && i + j < functionNames.size(); - j++) { - auto name = functionNames[i + j]; - if (module->getFunctionOrNull(name)) { - names.push_back(name); - functionsWeTriedToRemove.insert(name); + nontrivialFuncIndices.push_back(i); + } + DeltaDebugger dd(std::move(nontrivialFuncIndices)); + while (!dd.finished()) { + // Stop early if the partition size is less than the square root of + // the remaining set. We don't want to waste time on very fine-grained + // partitions when we could switch to another reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(dd.working.size()); + dd.test.size() > 0 && dd.test.size() < sqrtRemaining) { + break; + } + + std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " + << dd.partitionCount() << " (size " << dd.test.size() << " / " + << dd.working.size() << ")\n"; + Index removedSize = dd.working.size() - dd.test.size(); + std::vector oldBodies(removedSize); + + // We first need to remove each non-kept function body, and later we + // might need to restore the same function bodies. Abstract the logic + // for iterating over these function bodies. `f` takes a Function* and + // Expression*& for the stashed body. + auto forEachRemovedFuncBody = [&](auto f) { + Index bodyIndex = 0; + Index workingIndex = 0; + Index testIndex = 0; + while (workingIndex < dd.working.size()) { + if (testIndex < dd.test.size() && + dd.working[workingIndex] == dd.test[testIndex]) { + // Kept, skip it. + workingIndex++; + testIndex++; + } else { + // Removed, process it + Index funcIndex = dd.working[workingIndex++]; + f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); + } } + assert(bodyIndex == removedSize); + assert(testIndex == dd.test.size()); + }; + + // Stash the bodies. + forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { + oldBody = func->body; + Builder builder(*module); + if (func->getResults() == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } + }); + + if (!writeAndTestReduction()) { + // Failure. Restore the bodies. + forEachRemovedFuncBody( + [](Function* func, Expression*& oldBody) { func->body = oldBody; }); + dd.reject(); + } else { + // Success! + noteReduction(removedSize); + dd.accept(); } - if (names.size() == 0) { - continue; + } + } + + void reduceFunctions() { + std::cerr << "| try to remove functions\n"; + + // Find functions referenced from module code (i.e. global initializers). We + // will not attempt to remove these functions because we cannot generally + // replace their references with something valid. + // TODO: Look at how the function references are used. If they can be + // nullable, we can still consider deleting the functions. + struct UnremovableFinder : public PostWalker { + std::unordered_set unremovable; + void visitRefFunc(RefFunc* curr) { unremovable.insert(curr->func); } + }; + UnremovableFinder finder; + finder.walkModuleCode(module.get()); + + // Find the indices of functions we can consider removing or must not + // remove. + std::vector unremovableIndices; + std::vector initialCandidates; + initialCandidates.reserve(module->functions.size() - + finder.unremovable.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + if (finder.unremovable.contains(module->functions[i]->name)) { + unremovableIndices.push_back(i); + } else { + initialCandidates.push_back(i); + } + } + + if (initialCandidates.empty()) { + return; + } + + // Indices will change as we remove functions. Map the original indices to + // the present indices so we can use the original indices as stable + // identifiers. (Function names are not necessarily preserved through + // round-tripping.) + std::vector> currentIndices; + currentIndices.reserve(module->functions.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + currentIndices.push_back(i); + } + + DeltaDebugger dd(std::move(initialCandidates)); + while (!dd.finished()) { + // Exit early if the test set size is less than the square root of the + // working set size. We don't want to waste time on very fine-grained + // partitions when we could switch to a different reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(dd.working.size()); + dd.test.size() > 0 && dd.test.size() < sqrtRemaining) { + break; + } + + std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " + << dd.partitionCount() << " (size " << dd.test.size() << " / " + << dd.working.size() << ")\n"; + + std::unordered_set keptIndices; + for (Index i : unremovableIndices) { + keptIndices.insert(*currentIndices[i]); + } + for (Index i : dd.test) { + keptIndices.insert(*currentIndices[i]); } - std::cerr << "| trying at i=" << i << " of size " << names.size() - << "\n"; - // Try to remove functions and/or empty them. Note that - // tryToRemoveFunctions() will reload the module if it fails, which means - // function names may change - for that reason, run it second. - justReduced = tryToEmptyFunctions(names) || tryToRemoveFunctions(names); - if (justReduced) { - noteReduction(names.size()); - // Subtract 1 since the loop increments us anyhow by one: we want to - // skip over the skipped functions, and not any more. - x += skip - 1; - skip = std::min(size_t(factor), 2 * skip); - maxSkip = std::max(skip, maxSkip); + + // Get the list of kept functions and the new index mapping we will have + // to use if this reduction works. + std::vector> newFuncs; + newFuncs.reserve(keptIndices.size()); + std::vector> newCurrentIndices; + newCurrentIndices.reserve(currentIndices.size()); + for (size_t i = 0; i < currentIndices.size(); ++i) { + if (auto currIndex = currentIndices[i]; + currIndex && keptIndices.contains(*currIndex)) { + newCurrentIndices.push_back(newFuncs.size()); + newFuncs.emplace_back(std::move(module->functions[*currIndex])); + } else { + newCurrentIndices.push_back(std::nullopt); + } + } + + module->functions = std::move(newFuncs); + module->updateFunctionsMap(); + + // Remove exports for functions we have removed. + std::vector exportsToRemove; + for (auto& exp : module->exports) { + if (exp->kind == ExternalKind::Function && + !module->getFunctionOrNull(*exp->getInternalName())) { + exportsToRemove.push_back(exp->name); + } + } + for (auto expName : exportsToRemove) { + module->removeExport(expName); + } + + // We may have removed the start function. + if (module->start && !module->getFunctionOrNull(module->start)) { + module->start = Name(); + } + + struct FunctionReplacer + : public WalkerPass> { + bool isFunctionParallel() override { return true; } + std::unique_ptr create() override { + return std::make_unique(); + }; + void visitCall(Call* curr) { + // Replace calls to functions we have removed. + if (getModule()->getFunctionOrNull(curr->target)) { + return; + } + Builder builder(*getModule()); + auto* block = + ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions()) + .getChildrenReplacement(); + auto originalType = curr->type; + auto* replacement = builder.replaceWithIdenticalType(curr); + // We may have failed to come up with a replacement (e.g. for + // non-nullable references), so manually add an `unreachable` in that + // case. + if (replacement == curr) { + replacement = builder.makeUnreachable(); + } + block->list.push_back(replacement); + block->type = originalType; + replaceCurrent(block); + } + void visitRefFunc(RefFunc* curr) { + // Replace references to functions we have removed. + if (getModule()->getFunctionOrNull(curr->func)) { + return; + } + Builder builder(*getModule()); + replaceCurrent( + builder.makeBlock({builder.makeUnreachable()}, curr->type)); + } + }; + PassRunner runner(module.get()); + runner.add(std::make_unique()); + runner.run(); + + assert(WasmValidator().validate( + *module, WasmValidator::Globally | WasmValidator::Quiet)); + if (writeAndTestReduction()) { + noteReduction(dd.working.size() - dd.test.size()); + currentIndices = std::move(newCurrentIndices); + dd.accept(); } else { - skip = std::max(skip / 2, size_t(1)); // or 1? - x += factor / 100; + loadWorking(); + dd.reject(); } } - // If maxSkip is 1 then we never reduced at all. If it is 2 then we did - // manage to reduce individual functions, but all our attempts at - // exponential growth failed. Only suggest doing a new iteration of this - // function if we did in fact manage to grow, which indicated there are lots - // of opportunities here, and it is worth focusing on this. - return maxSkip > 2; } void visitModule([[maybe_unused]] Module* curr) { @@ -967,10 +1147,8 @@ struct Reducer assert(curr == module.get()); curr = nullptr; - // Reduction of entire functions at a time is very effective, and we do it - // with exponential growth and backoff, so keep doing it while it works. - while (reduceFunctions()) { - } + reduceFunctionBodies(); + reduceFunctions(); shrinkElementSegments(); @@ -980,9 +1158,9 @@ struct Reducer for (auto& exp : module->exports) { exports.push_back(*exp); } - size_t skip = 1; + uint64_t skip = 1; for (size_t i = 0; i < exports.size(); i++) { - if (!shouldTryToReduce(std::max((factor / 100) + 1, 1000))) { + if (!shouldTryToReduce(std::max((factor / 100) + 1, uint64_t(1000)))) { continue; } std::vector currExports; @@ -999,12 +1177,12 @@ struct Reducer for (auto exp : currExports) { module->addExport(new Export(exp)); } - skip = std::max(skip / 2, size_t(1)); // or 1? + skip = std::max(skip / 2, uint64_t(1)); // or 1? } else { std::cerr << "| removed " << currExports.size() << " exports\n"; noteReduction(currExports.size()); i += skip; - skip = std::min(size_t(factor), 2 * skip); + skip = std::min(factor, 2 * skip); } } // If we are left with a single function that is not exported or used in @@ -1047,96 +1225,6 @@ struct Reducer } } - // Try to empty out the bodies of some functions. - bool tryToEmptyFunctions(std::vector names) { - std::vector oldBodies; - size_t actuallyEmptied = 0; - for (auto name : names) { - auto* func = module->getFunction(name); - auto* oldBody = func->body; - oldBodies.push_back(oldBody); - // Nothing to do for imported functions (body is nullptr) or for bodies - // that have already been as reduced as we can make them. - if (func->imported() || oldBody->is() || - oldBody->is()) { - continue; - } - actuallyEmptied++; - bool useUnreachable = func->getResults() != Type::none; - if (useUnreachable) { - func->body = builder->makeUnreachable(); - } else { - func->body = builder->makeNop(); - } - } - if (actuallyEmptied > 0 && writeAndTestReduction()) { - std::cerr << "| emptied " << actuallyEmptied << " / " - << names.size() << " functions\n"; - return true; - } else { - // Restore the bodies. - for (size_t i = 0; i < names.size(); i++) { - module->getFunction(names[i])->body = oldBodies[i]; - } - return false; - } - } - - // Try to actually remove functions. If they are somehow referred to, we will - // get a validation error and undo it. - bool tryToRemoveFunctions(std::vector names) { - for (auto name : names) { - module->removeFunction(name); - } - - // remove all references to them - struct FunctionReferenceRemover - : public PostWalker { - std::unordered_set names; - std::vector exportsToRemove; - - FunctionReferenceRemover(std::vector& vec) { - for (auto name : vec) { - names.insert(name); - } - } - void visitCall(Call* curr) { - if (names.contains(curr->target)) { - replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr)); - } - } - void visitRefFunc(RefFunc* curr) { - if (names.contains(curr->func)) { - replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr)); - } - } - void visitExport(Export* curr) { - if (auto* name = curr->getInternalName(); - name && names.contains(*name)) { - exportsToRemove.push_back(curr->name); - } - } - void doWalkModule(Module* module) { - PostWalker::doWalkModule(module); - for (auto name : exportsToRemove) { - module->removeExport(name); - } - } - }; - FunctionReferenceRemover referenceRemover(names); - referenceRemover.walkModule(module.get()); - - if (WasmValidator().validate( - *module, WasmValidator::Globally | WasmValidator::Quiet) && - writeAndTestReduction()) { - std::cerr << "| removed " << names.size() << " functions\n"; - return true; - } else { - loadWorking(); // restore it from orbit - return false; - } - } - // helpers // try to replace condition with always true and always false @@ -1497,17 +1585,28 @@ More documentation can be found at std::cerr << "|starting reduction!\n"; - int factor = binary ? workingSize * 2 : workingSize / 10; + uint64_t factor = binary ? uint64_t(workingSize) * 2 : workingSize / 10; size_t lastDestructiveReductions = 0; size_t lastPostPassesSize = 0; bool stopping = false; + bool first = true; while (1) { Reducer reducer( command, test, working, binary, deNan, verbose, debugInfo, options); + // For extremely large modules with slow reproduction commands, reducing + // function bodies first can be more effective than running passes. TODO: + // clean this up and reconsider the order of reducers. + if (first) { + reducer.loadWorking(); + reducer.reduceFunctionBodies(); + reducer.reduceFunctions(); + first = false; + } + // run binaryen optimization passes to reduce. passes are fast to run // and can often reduce large amounts of code efficiently, as opposed // to detructive reduction (i.e., that doesn't preserve correctness as @@ -1550,7 +1649,7 @@ More documentation can be found at // we get "stuck" cycling through them. In that case we simply need to do // more destructive reductions to make real progress. For that reason, // decrease the factor by some small percentage. - factor = std::max(1, (factor * 9) / 10); + factor = std::max(uint64_t(1), uint64_t(factor * 0.9)); } else { if (factor > 10) { factor = (factor / 3) + 1; @@ -1559,9 +1658,9 @@ More documentation can be found at } } - // no point in a factor lorger than the size + // no point in a factor larger than the size assert(newSize > 4); // wasm modules are >4 bytes anyhow - factor = std::min(factor, int(newSize) / 4); + factor = std::min(factor, uint64_t(newSize / 4)); // try to reduce destructively. if a high factor fails to find anything, // quickly try a lower one (no point in doing passes until we reduce @@ -1577,8 +1676,8 @@ More documentation can be found at stopping = true; break; } - factor = std::max( - 1, factor / 4); // quickly now, try to find *something* we can reduce + // Quickly try to find *something* we can reduce. + factor = std::max(uint64_t(1), factor / 4); } std::cerr << "| destructive reduction led to size: " << file_size(working) diff --git a/src/tools/wasm-split/split-options.cpp b/src/tools/wasm-split/split-options.cpp index dcea3bcf227..d62d22ad765 100644 --- a/src/tools/wasm-split/split-options.cpp +++ b/src/tools/wasm-split/split-options.cpp @@ -358,6 +358,16 @@ WasmSplitOptions::WasmSplitOptions() {Mode::Split, Mode::MultiSplit, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { stripDebug = true; }) + .add("--traps-never-happen", + "-tnh", + "Split under the helpful assumption that no trap is reached at " + "runtime (from load, div/mod, etc.)", + WasmSplitOption, + {Mode::Split, Mode::MultiSplit}, + Options::Arguments::Zero, + [&](Options* o, const std::string& arguments) { + passOptions.trapsNeverHappen = true; + }) .add("--output", "-o", "Output file.", diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index fd53dd18b5e..14293b779af 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -229,6 +229,7 @@ void writePlaceholderMap( void setCommonSplitConfigs(ModuleSplitting::Config& config, const WasmSplitOptions& options) { + config.passOptions = options.passOptions; config.usePlaceholders = options.usePlaceholders; config.minimizeNewExportNames = !options.passOptions.debugInfo; if (options.importNamespace) { @@ -242,6 +243,34 @@ void setCommonSplitConfigs(ModuleSplitting::Config& config, } } +// Returns whether it is valid to split a function out from the main module. +bool canSplitFunc(Name funcName, + const Module& wasm, + const WasmSplitOptions& options) { + auto* func = wasm.getFunctionOrNull(funcName); + if (!func) { + if (!options.quiet) { + std::cerr << "warning: function " << funcName << " does not exist\n"; + } + return false; + } + if (func->imported()) { + if (!options.quiet) { + std::cerr << "warning: cannot split out imported function " << funcName + << "\n"; + } + return false; + } + if (func->name == wasm.start) { + if (!options.quiet) { + std::cerr << "warning: cannot split out start function " << funcName + << "\n"; + } + return false; + } + return true; +} + void splitModule(const WasmSplitOptions& options) { Module wasm; parseInput(wasm, options); @@ -282,18 +311,7 @@ void splitModule(const WasmSplitOptions& options) { // Use the explicitly provided `splitFuncs`. for (auto& func : options.splitFuncs) { - auto* function = wasm.getFunctionOrNull(func); - if (!function) { - if (!options.quiet) { - std::cerr << "warning: function " << func << " does not exist\n"; - } - continue; - } - if (function->imported()) { - if (!options.quiet) { - std::cerr << "warning: cannot split out imported function " << func - << "\n"; - } + if (!canSplitFunc(func, wasm, options)) { continue; } if (!options.quiet && options.keepFuncs.contains(func)) { @@ -435,6 +453,9 @@ void multiSplitModule(const WasmSplitOptions& options) { continue; } assert(currFuncs); + if (!canSplitFunc(name, wasm, options)) { + continue; + } currFuncs->insert(name); auto [it, inserted] = funcModules.insert({name, currModule}); if (!inserted && it->second != currModule) { @@ -442,9 +463,6 @@ void multiSplitModule(const WasmSplitOptions& options) { << currModule << "; it is already assigned to module " << it->second << '\n'; } - if (inserted && !options.quiet && !wasm.getFunctionOrNull(name)) { - std::cerr << "warning: Function " << name << " does not exist\n"; - } } if (options.emitModuleNames && !wasm.name) { diff --git a/src/tools/wasm2c-wrapper.h b/src/tools/wasm2c-wrapper.h index 242442a1d8a..e39bf54a3d2 100644 --- a/src/tools/wasm2c-wrapper.h +++ b/src/tools/wasm2c-wrapper.h @@ -30,7 +30,7 @@ namespace wasm { inline std::string wasm2cMangle(Name name, Signature sig) { const char escapePrefix = 'Z'; std::string mangled = "Z_"; - for (unsigned char c : name.str) { + for (unsigned char c : name.view()) { if ((isalnum(c) && c != escapePrefix) || c == '_') { // This character is ok to emit as it is. mangled += c; diff --git a/src/wasm-binary.h b/src/wasm-binary.h index c6fa761a2bd..92bb588d53c 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -474,6 +474,7 @@ extern const char* CustomDescriptorsFeature; extern const char* RelaxedAtomicsFeature; extern const char* MultibyteFeature; extern const char* CustomPageSizesFeature; +extern const char* WideArithmeticFeature; enum Subsection { NameModule = 0, @@ -1126,6 +1127,9 @@ enum ASTNodes { I16x8TruncSatF16x8U = 0x146, F16x8ConvertI16x8S = 0x147, F16x8ConvertI16x8U = 0x148, + F16x8DemoteF32x4Zero = 0x149, + F16x8DemoteF64x2Zero = 0x14a, + F32x4PromoteLowF16x8 = 0x14b, // bulk memory opcodes @@ -1134,6 +1138,13 @@ enum ASTNodes { MemoryCopy = 0x0a, MemoryFill = 0x0b, + // wide arithmetic opcodes + + I64Add128 = 0x13, + I64Sub128 = 0x14, + I64MulWideS = 0x15, + I64MulWideU = 0x16, + // reference types opcodes TableGrow = 0x0f, diff --git a/src/wasm-builder.h b/src/wasm-builder.h index 30465e9e128..0bf270ab8ef 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -151,15 +151,13 @@ class Builder { static std::unique_ptr makeDataSegment(Name name = "", - Name memory = "", - bool isPassive = false, + Name memory = Name(), Expression* offset = nullptr, const char* init = "", Address size = 0) { auto seg = std::make_unique(); seg->name = name; seg->memory = memory; - seg->isPassive = isPassive; seg->offset = offset; seg->data.resize(size); std::copy_n(init, size, seg->data.begin()); @@ -660,6 +658,30 @@ class Builder { ret->finalize(); return ret; } + WideIntAddSub* makeWideIntAddSub(WideIntAddSubOp op, + Expression* leftLow, + Expression* leftHigh, + Expression* rightLow, + Expression* rightHigh) { + auto* ret = wasm.allocator.alloc(); + ret->op = op; + ret->leftLow = leftLow; + ret->leftHigh = leftHigh; + ret->rightLow = rightLow; + ret->rightHigh = rightHigh; + ret->finalize(); + return ret; + } + + WideIntMul* + makeWideIntMul(WideIntMulOp op, Expression* left, Expression* right) { + auto* ret = wasm.allocator.alloc(); + ret->op = op; + ret->left = left; + ret->right = right; + ret->finalize(); + return ret; + } Select* makeSelect(Expression* condition, Expression* ifTrue, Expression* ifFalse) { auto* ret = wasm.allocator.alloc