diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3513897b1b..642034e0691 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,8 @@ jobs: lint: name: lint if: ${{ github.event_name == 'pull_request' }} - runs-on: ubuntu-latest + # Use Ubuntu 26.04 to get clang-21 (not present in 24.04) + runs-on: ubuntu-26.04 env: # Keep this in sync with clang-format-diff.sh LLVM_VERSION: 21 @@ -34,10 +35,7 @@ jobs: run: | pip3 install -r requirements-dev.txt sudo apt install lsb-release wget software-properties-common gnupg - wget https://apt.llvm.org/llvm.sh - sudo chmod +x llvm.sh - sudo ./llvm.sh ${LLVM_VERSION} - sudo apt-get install clang-format clang-format-${LLVM_VERSION} clang-tidy-${LLVM_VERSION} + sudo apt-get install clang-${LLVM_VERSION} clang-format-${LLVM_VERSION} clang-tidy-${LLVM_VERSION} - run: ruff check - run: ./scripts/clang-format-diff.sh - name: clang-tidy @@ -199,11 +197,6 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: install clang 18 - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 18 - name: install ninja run: sudo apt-get install ninja-build - name: install v8 @@ -322,11 +315,6 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: install clang 18 - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 18 - name: install ninja run: sudo apt-get install ninja-build - name: install v8 diff --git a/CHANGELOG.md b/CHANGELOG.md index e26794db203..3015ac5125a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ Current Trunk - Replace the `BINARYEN_ROOT` environment variable (used by developers who are doing out-of-tree builds of binaryen) with `BINARYEN_BIN` (#9023) - Reject non-natural alignment for atomic memory operations at parse time (#8962) +- Note that fast-math mode can ignore the difference between negative zero and + zero (like clang and gcc). (#9056) +- [JS API] **(breaking)** Organize types into enums (#9098) v132 ---- diff --git a/check.py b/check.py index 3d1ca8aef46..b71f74e3888 100755 --- a/check.py +++ b/check.py @@ -16,13 +16,10 @@ import functools import glob -import io import os import subprocess import sys import unittest -from contextlib import contextmanager -from multiprocessing.pool import ThreadPool from pathlib import Path from scripts.test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt @@ -140,6 +137,32 @@ def run_wasm_metadce_tests(): shared.fail_if_not_identical_to_file(stdout, expected + '.stdout') +def run_one_wasm_reduce_test(t, stdout=None): + base_name = os.path.splitext(os.path.basename(t))[0] + print('..', os.path.basename(t), file=stdout) + a_wasm = f'reduce_{base_name}_a.wasm' + b_wasm = f'reduce_{base_name}_b.wasm' + c_wasm = f'reduce_{base_name}_c.wasm' + a_wat = f'reduce_{base_name}_a.wat' + try: + support.run_command(shared.WASM_AS + [t, '-o', a_wasm, '-all'], stdout=stdout) + cmd = shared.WASM_OPT[0] + support.run_command( + shared.WASM_REDUCE + [a_wasm, f'--command={cmd} {b_wasm} --fuzz-exec -all', '-t', b_wasm, '-w', c_wasm, '--timeout=4'], + stdout=stdout, + stderr=subprocess.PIPE, + ) + expected = t + '.txt' + support.run_command(shared.WASM_DIS + [c_wasm, '-o', a_wat], stdout=stdout) + with open(a_wat) as seen: + shared.fail_if_not_identical_to_file(seen.read(), expected) + finally: + shared.delete_from_orbit(a_wasm) + shared.delete_from_orbit(b_wasm) + shared.delete_from_orbit(c_wasm) + shared.delete_from_orbit(a_wat) + + def run_wasm_reduce_tests(): if not shared.has_shell_timeout(): print_heading('skipping wasm-reduce testcases') @@ -148,16 +171,8 @@ def run_wasm_reduce_tests(): print_heading('checking wasm-reduce testcases') # fixed testcases - for t in shared.get_tests(shared.get_test_dir('reduce'), ['.wast']): - print('..', os.path.basename(t)) - # convert to wasm - support.run_command(shared.WASM_AS + [t, '-o', 'a.wasm', '-all']) - cmd = shared.WASM_OPT[0] - support.run_command(shared.WASM_REDUCE + ['a.wasm', f'--command={cmd} b.wasm --fuzz-exec -all ', '-t', 'b.wasm', '-w', 'c.wasm', '--timeout=4']) - expected = t + '.txt' - support.run_command(shared.WASM_DIS + ['c.wasm', '-o', 'a.wat']) - with open('a.wat') as seen: - shared.fail_if_not_identical_to_file(seen.read(), expected) + tests = shared.get_tests(shared.get_test_dir('reduce'), ['.wast']) + shared.run_parallel_tests(run_one_wasm_reduce_test, tests) # run on a nontrivial fuzz testcase, for general coverage # this is very slow in ThreadSanitizer, so avoid it there @@ -232,7 +247,6 @@ def run_one_spec_test(wast: Path, stdout=None): 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)) raise check_expected(actual, expected, stdout=stdout) @@ -265,67 +279,10 @@ def run_one_spec_test(wast: Path, stdout=None): check_expected(actual, os.path.join(shared.get_test_dir('spec'), 'expected-output', test_name + '.log'), stdout=stdout) -def run_spec_test_with_wrapped_stdout(wast: Path): - """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() - try: - run_one_spec_test(wast, stdout=out) - except Exception as e: - shared.num_failures += 1 - # Serialize exceptions into the output string buffer - # so they can be reported on the main thread. - print(e, file=out) - return False, out.getvalue() - return True, out.getvalue() - - -@contextmanager -def red_output(file=sys.stdout): - print("\033[31m", end="", file=file) - try: - yield - finally: - print("\033[0m", end="", file=file) - - -def red_stderr(): - return red_output(file=sys.stderr) - - def run_spec_tests(): print_heading('checking wasm-shell spec testcases...') - - worker_count = os.cpu_count() - print("Running with", worker_count, "workers") test_paths = (Path(x) for x in shared.options.spec_tests) - - failed_stdouts = [] - with ThreadPool(processes=worker_count) as pool: - try: - for success, stdout in pool.imap_unordered(run_spec_test_with_wrapped_stdout, test_paths): - if success: - print(stdout, end="") - continue - - failed_stdouts.append(stdout) - if shared.options.abort_on_first_failure: - with red_stderr(): - print("Aborted spec test suite execution after first failure. Set --no-fail-fast to disable this.", file=sys.stderr) - break - except KeyboardInterrupt: - # Hard exit to avoid threads continuing to run after Ctrl-C. - # There's no concern of deadlocking during shutdown here. - os._exit(1) - - if failed_stdouts: - with red_stderr(): - print("Failed tests:", file=sys.stderr) - for failed in failed_stdouts: - print(failed, end="", file=sys.stderr) + shared.run_parallel_tests(run_one_spec_test, test_paths) def run_validator_tests(): @@ -345,6 +302,44 @@ def run_validator_tests(): support.run_command(cmd, expected_status=1) +def run_one_example_test(t, stdout=None): + src = os.path.join(shared.get_test_dir('example'), t) + expected = os.path.join(shared.get_test_dir('example'), '.'.join(t.split('.')[:-1]) + '.txt') + # build the C file separately + libpath = shared.options.binaryen_lib + output_file = os.path.basename(os.path.splitext(t)[0]) + objfile = output_file + '.o' + compile = [shared.NATIVECC, src, '-c', '-o', objfile, + '-I' + os.path.join(shared.options.binaryen_root, 'src'), '-g', '-L' + libpath, '-pthread'] + if src.endswith('.cpp'): + compile += ['-std=c++' + str(shared.cxx_standard)] + if os.environ.get('COMPILER_FLAGS'): + for f in os.environ.get('COMPILER_FLAGS').split(' '): + compile.append(f) + print('build: ', ' '.join(compile), file=stdout) + proc = subprocess.run(compile, capture_output=True, text=True) + if proc.returncode != 0: + raise Exception(f"Failed to compile {src}:\n{proc.stderr or proc.stdout}") + + cmd = ['-I' + os.path.join(shared.options.binaryen_root, 't'), '-g', '-pthread', '-o', output_file] + # Link against the binaryen C library DSO, using an executable-relative rpath + cmd = [objfile, '-L' + libpath, '-lbinaryen'] + cmd + ['-Wl,-rpath,' + libpath] + print(' ', t, src, expected, file=stdout) + if os.environ.get('COMPILER_FLAGS'): + for f in os.environ.get('COMPILER_FLAGS').split(' '): + cmd.append(f) + cmd = [shared.NATIVEXX, '-std=c++' + str(shared.cxx_standard)] + cmd + print('link: ', ' '.join(cmd), file=stdout) + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode != 0: + raise Exception(f"Failed to link {output_file}:\n{proc.stderr or proc.stdout}") + print('run...', output_file, file=stdout) + proc = subprocess.run([os.path.abspath(output_file)], capture_output=True, text=True) + if proc.returncode != 0: + raise Exception(f"Failed to run {output_file}:\n{proc.stderr or proc.stdout}") + shared.fail_if_not_identical_to_file(proc.stdout, expected) + + def run_example_tests(): print_heading('checking native example testcases...') if not shared.NATIVECC or not shared.NATIVEXX: @@ -354,38 +349,8 @@ def run_example_tests(): if shared.skip_if_on_windows('example'): return - for t in shared.get_tests(shared.get_test_dir('example')): - if not t.endswith(('.c', '.cpp')): - continue - src = os.path.join(shared.get_test_dir('example'), t) - expected = os.path.join(shared.get_test_dir('example'), '.'.join(t.split('.')[:-1]) + '.txt') - # build the C file separately - libpath = shared.options.binaryen_lib - output_file = os.path.basename(os.path.splitext(t)[0]) - objfile = output_file + '.o' - compile = [shared.NATIVECC, src, '-c', '-o', objfile, - '-I' + os.path.join(shared.options.binaryen_root, 'src'), '-g', '-L' + libpath, '-pthread'] - if src.endswith('.cpp'): - compile += ['-std=c++' + str(shared.cxx_standard)] - if os.environ.get('COMPILER_FLAGS'): - for f in os.environ.get('COMPILER_FLAGS').split(' '): - compile.append(f) - print('build: ', ' '.join(compile)) - subprocess.check_call(compile) - - cmd = ['-I' + os.path.join(shared.options.binaryen_root, 't'), '-g', '-pthread', '-o', output_file] - # Link against the binaryen C library DSO, using an executable-relative rpath - cmd = [objfile, '-L' + libpath, '-lbinaryen'] + cmd + ['-Wl,-rpath,' + libpath] - print(' ', t, src, expected) - if os.environ.get('COMPILER_FLAGS'): - for f in os.environ.get('COMPILER_FLAGS').split(' '): - cmd.append(f) - cmd = [shared.NATIVEXX, '-std=c++' + str(shared.cxx_standard)] + cmd - print('link: ', ' '.join(cmd)) - subprocess.check_call(cmd) - print('run...', output_file) - actual = subprocess.check_output([os.path.abspath(output_file)], text=True) - shared.fail_if_not_identical_to_file(actual, expected) + tests = shared.get_tests(shared.get_test_dir('example'), ['.c', '.cpp']) + shared.run_parallel_tests(run_one_example_test, tests) def run_unittest(): diff --git a/requirements-dev.txt b/requirements-dev.txt index 500c11b34e0..d33bb82c75d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,5 +4,5 @@ # Install with `pip3 install -r requirements-dev.txt` ruff==0.16.0 -filecheck==0.0.22 +filecheck==1.0.5 lit==0.11.0.post1 diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py index 941384cfc40..96940af3c47 100755 --- a/scripts/bundle_clusterfuzz.py +++ b/scripts/bundle_clusterfuzz.py @@ -115,7 +115,8 @@ '--disable-relaxed-atomics', ] -with tarfile.open(output_file, "w:gz") as tar: +# Use fast compression (level 1) to speed up bundling with only a modest size increase. +with tarfile.open(output_file, 'w:gz', compresslevel=1) as tar: # run.py run = os.path.join(shared.options.binaryen_root, 'scripts', 'clusterfuzz', 'run.py') print(f' .. run: {run}') diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 77652a53971..67b00e7b94d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -272,12 +272,12 @@ def auto_select_recent_initial_contents(): # commit time of HEAD. The reason we use the commit time of HEAD instead # of the current system time is to make the results deterministic given # the Binaryen HEAD commit. - head_ts_str = run(['git', 'log', '-1', '--format=%cd', '--date=raw'], + head_ts_str = run(['git', '-C', shared.options.binaryen_root, 'log', '-1', '--format=%cd', '--date=raw'], silent=True).split()[0] head_dt = datetime.utcfromtimestamp(int(head_ts_str)) start_dt = head_dt - timedelta(days=RECENT_DAYS) start_ts = start_dt.replace(tzinfo=timezone.utc).timestamp() - log = run(['git', 'log', '--name-status', '--format=', '--date=raw', '--no-renames', f'--since={start_ts}'], silent=True).splitlines() + log = run(['git', '-C', shared.options.binaryen_root, 'log', '--name-status', '--format=', '--date=raw', '--no-renames', f'--since={start_ts}'], silent=True).splitlines() # Pick up lines in the form of # A test/../something.wast # M test/../something.wast @@ -290,7 +290,7 @@ def auto_select_recent_initial_contents(): def is_git_repo(): try: - ret = run(['git', 'rev-parse', '--is-inside-work-tree'], + ret = run(['git', '-C', shared.options.binaryen_root, 'rev-parse', '--is-inside-work-tree'], silent=True, stderr=subprocess.DEVNULL) return ret == 'true\n' except subprocess.CalledProcessError: @@ -1008,7 +1008,7 @@ def __init__(self): D8(), D8Liftoff(), D8Turboshaft(), - # FIXME: Temprorary disable. See issue #4741 for more details + # FIXME: Temporary disable. See issue #4741 for more details # Wasm2C(), # Wasm2C2Wasm() ] @@ -1372,7 +1372,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # "[fuzz-exec] export bar". call_start = before.rfind(FUZZ_EXEC_EXPORT_PREFIX, 0, trap_index) if call_start < 0: - # the trap happened before we called an export, so it occured + # the trap happened before we called an export, so it occurred # during startup (the start function, or memory segment # operations, etc.). in that case there is nothing for us to # compare here; just leave. @@ -2336,6 +2336,10 @@ def do_run(vm, js, wasm): # # Ignore it, as details of traces differ based on optimizations. continue + elif not line: + # V8 may print blank lines before stack traces when the top + # frame has no script location (e.g. after a return_call to JS). + continue cleaned.append(line) cleaned = '\n'.join(cleaned) @@ -2575,7 +2579,7 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), -# Split(), # Will reenable after stabilized +# Split(), # Will re-enable after stabilized RoundtripText(), ClusterFuzz(), Two(), @@ -2765,6 +2769,7 @@ def write_commands(commands, filename): ("--simplify-locals-notee",), ("--simplify-locals-notee-nostructure",), ("--ssa",), + ("--tail-call",), ("--tuple-optimization",), ("--type-finalizing",), ("--type-refining",), @@ -3014,6 +3019,7 @@ def get_random_opts(): working_wasm = abspath('w.wasm') wasm_reduce = in_bin('wasm-reduce') reduce_sh = abspath('reduce.sh') + fuzz_opt = in_binaryen('scripts', 'fuzz_opt.py') features = ' '.join(FEATURE_OPTS) with open('reduce.sh', 'w') as f: f.write(f'''\ @@ -3026,12 +3032,12 @@ def get_random_opts(): if [ -z "$BINARYEN_FIRST_WASM" ]; then # run the command normally - ./scripts/fuzz_opt.py {auto_init} --binaryen-bin {binaryen_bin} {seed} {temp_wasm} > o 2> e + {fuzz_opt} {auto_init} --binaryen-bin {binaryen_bin} {seed} {temp_wasm} > o 2> e else # BINARYEN_FIRST_WASM was provided so we should actually reduce the *second* # file. pass the first one in as the main file, and use the env var for the # second. - BINARYEN_SECOND_WASM={temp_wasm} ./scripts/fuzz_opt.py {auto_init} --binaryen-bin {binaryen_bin} {seed} $BINARYEN_FIRST_WASM > o 2> e + BINARYEN_SECOND_WASM={temp_wasm} {fuzz_opt} {auto_init} --binaryen-bin {binaryen_bin} {seed} $BINARYEN_FIRST_WASM > o 2> e fi echo " " $? diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 93c70b7d755..3012c7dd7ab 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -219,6 +219,7 @@ ("memory.atomic.wait64", "makeAtomicWait(Type::i64)"), ("atomic.fence", "makeAtomicFence()"), ("pause", "makePause()"), + ("publish", "makePublish()"), ("i32.atomic.load8_u", "makeLoad(Type::i32, /*signed=*/false, 1, /*isAtomic=*/true)"), ("i32.atomic.load16_u", "makeLoad(Type::i32, /*signed=*/false, 2, /*isAtomic=*/true)"), ("i32.atomic.load", "makeLoad(Type::i32, /*signed=*/false, 4, /*isAtomic=*/true)"), diff --git a/scripts/monitor_fuzz.py b/scripts/monitor_fuzz.py new file mode 100755 index 00000000000..40b088ebd5a --- /dev/null +++ b/scripts/monitor_fuzz.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 + +# 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. + +"""Run and monitor the Binaryen fuzzer (fuzz_opt.py). + +Monitors progress, manages log file truncation, stops at iteration limits, +and reports bugs found. +""" + +import argparse +import collections +import os +import re +import signal +import subprocess +import sys +import threading +import time + + +class FuzzMonitor: + """Monitors fuzzer output stream, manages log files, and tracks state.""" + + def __init__(self, log_path, max_lines, keep_lines, truncate_interval): + self.log_path = log_path + self.max_lines = max_lines + self.keep_lines = keep_lines + self.truncate_interval = truncate_interval + + self.lock = threading.Lock() + self.latest_iteration = 0 + self.latest_seed = 'unknown' + self.bug_found = False + self.recent_lines = collections.deque(maxlen=20) + + self.deque = collections.deque(maxlen=keep_lines) + self.lines_written = 0 + + if os.path.isfile(log_path): + try: + with open(log_path, encoding='utf-8', errors='replace') as f: + for line in f: + self.deque.append(line) + self.lines_written += 1 + except Exception: + pass + + def _parse_line(self, line): + iter_match = re.search(r'ITERATION:\s*(\d+)', line) + if iter_match: + self.latest_iteration = int(iter_match.group(1)) + + seed_match = re.search(r'seed:\s*(\d+)', line) + if seed_match: + self.latest_seed = seed_match.group(1) + + if re.search(r'You found a bug', line, re.IGNORECASE): + self.bug_found = True + + def run(self, stdout_stream): + last_truncate = time.time() + try: + with open(self.log_path, 'a', encoding='utf-8') as f: + for line in stdout_stream: + with self.lock: + self._parse_line(line) + self.deque.append(line) + self.recent_lines.append(line) + self.lines_written += 1 + + f.write(line) + f.flush() + + now = time.time() + if ( + self.lines_written >= self.max_lines + and (now - last_truncate) >= self.truncate_interval + ): + f.close() + with open(self.log_path, 'w', encoding='utf-8') as wf: + with self.lock: + wf.writelines(self.deque) + self.lines_written = len(self.deque) + f = open(self.log_path, 'a', encoding='utf-8') + last_truncate = now + except Exception as e: + print(f'Error writing to log file: {e}', file=sys.stderr) + + def get_progress(self): + with self.lock: + return self.latest_iteration + + def get_status(self): + with self.lock: + return ( + self.bug_found, + self.latest_iteration, + self.latest_seed, + list(self.recent_lines), + ) + + +class FuzzerWorker: + """Manages a single fuzzer subprocess and its monitor.""" + + def __init__( + self, + worker_id, + work_dir, + cmd, + env, + max_lines, + keep_lines, + truncate_interval, + ): + self.id = worker_id + self.work_dir = work_dir + os.makedirs(work_dir, exist_ok=True) + self.log_path = os.path.join(work_dir, 'fuzz.log') + self.monitor = FuzzMonitor( + log_path=self.log_path, + max_lines=max_lines, + keep_lines=keep_lines, + truncate_interval=truncate_interval, + ) + worker_env = env.copy() + worker_env['BINARYEN_OUT_DIR'] = work_dir + self.proc = subprocess.Popen( + cmd, + cwd=work_dir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + env=worker_env, + errors='replace', + start_new_session=True, + ) + self.reader_thread = threading.Thread( + target=self.monitor.run, + args=(self.proc.stdout,), + daemon=True, + ) + self.reader_thread.start() + + +def parse_args(): + # N.B. We could alternatively `import shared from test`, which has the side + # effect of changing the current directory to /out/test, but + # this is less magical. + binaryen_root = os.path.dirname( + os.path.dirname(os.path.abspath(__file__))) + default_log_dir = os.path.join(binaryen_root, 'out', 'test') + cores = os.cpu_count() or 1 + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '-j', + '--jobs', + type=int, + nargs='?', + const=cores, + default=int(os.environ.get('JOBS', '1')), + help=( + f'Number of parallel fuzzers to run (default: $JOBS or 1; ' + f'defaults to {cores} if passed without an argument)' + ), + ) + parser.add_argument( + '--log-dir', + default=os.environ.get('LOG_DIR', default_log_dir), + help='Directory to save fuzz logs (default: $LOG_DIR or out/test)', + ) + parser.add_argument( + '--max-iters', + type=int, + default=int(os.environ.get('MAX_ITERS', '0')), + help='Stop after N total iterations across all fuzzers (0 for infinite, default: $MAX_ITERS or 0)', + ) + parser.add_argument( + '--truncate-interval', + type=float, + default=30.0, + help='Seconds between log truncation checks (default: 30)', + ) + parser.add_argument( + '--max-lines', + type=int, + default=10000, + help='Maximum lines in log before truncation (default: 10000)', + ) + parser.add_argument( + '--keep-lines', + type=int, + default=5000, + help='Lines to keep when truncating (default: 5000)', + ) + parser.add_argument( + 'cmd', + nargs=argparse.REMAINDER, + help='Fuzzer command to run (default: ./scripts/fuzz_opt.py)', + ) + args = parser.parse_args() + if args.jobs < 1: + parser.error('--jobs must be at least 1') + return args + + +def main(): + args = parse_args() + + cmd = list(args.cmd) + if cmd and cmd[0] == '--': + cmd.pop(0) + if not cmd: + default_fuzzer = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'fuzz_opt.py', + ) + cmd = [sys.executable, default_fuzzer] + else: + cmd = [ + os.path.abspath(arg) if os.path.exists(arg) else arg for arg in cmd + ] + + env = os.environ.copy() + env['PYTHONUNBUFFERED'] = '1' + + workers = [] + for i in range(args.jobs): + work_dir = os.path.join(args.log_dir, str(i)) + workers.append( + FuzzerWorker( + worker_id=i, + work_dir=work_dir, + cmd=cmd, + env=env, + max_lines=args.max_lines, + keep_lines=args.keep_lines, + truncate_interval=args.truncate_interval, + ), + ) + + if len(workers) == 1: + print( + f'Fuzzer started with PID {workers[0].proc.pid}. Monitoring...', + flush=True, + ) + else: + pids = ', '.join(str(w.proc.pid) for w in workers) + print( + f'Started {len(workers)} fuzzers with PIDs {pids}. Monitoring...', + flush=True, + ) + + def stop_children(): + for w in workers: + if w.proc.poll() is None: + try: + os.killpg(w.proc.pid, signal.SIGTERM) + except ProcessLookupError: + pass + deadline = time.time() + 5.0 + for w in workers: + if w.proc.poll() is None: + remaining = max(0.0, deadline - time.time()) + try: + w.proc.wait(timeout=remaining) + except subprocess.TimeoutExpired: + try: + os.killpg(w.proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass + w.proc.wait() + + def signal_handler(signum, _frame): + stop_children() + for w in workers: + w.reader_thread.join(timeout=2.0) + sys.exit(128 + signum) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + start_time = time.time() + last_report = 0 + limit_reached = False + stopped_worker = None + + try: + while any( + w.reader_thread.is_alive() or w.proc.poll() is None for w in workers + ): + time.sleep(0.2) + now = time.time() + elapsed = int(now - start_time) + + minute = elapsed // 60 + total_iters = sum(w.monitor.get_progress() for w in workers) + + if minute > last_report: + last_report = minute + timestamp = time.strftime('%H:%M:%S') + print( + f'[{timestamp}] Runtime: {last_report} min,' + f' Iterations: {total_iters}', + flush=True, + ) + + if args.max_iters > 0 and total_iters >= args.max_iters: + fuzzer_str = 'fuzzer' if len(workers) == 1 else 'fuzzers' + print( + f'Reached max iterations ({args.max_iters}). Stopping' + f' {fuzzer_str}...', + flush=True, + ) + limit_reached = True + stop_children() + break + + should_stop = False + for w in workers: + if w.monitor.get_status()[0]: + try: + w.proc.wait(timeout=2.0) + except subprocess.TimeoutExpired: + pass + w.reader_thread.join(timeout=2.0) + stopped_worker = w + should_stop = True + break + if w.proc.poll() is not None: + w.reader_thread.join(timeout=2.0) + stopped_worker = w + should_stop = True + break + + if should_stop: + stop_children() + break + finally: + stop_children() + for w in workers: + w.reader_thread.join(timeout=5.0) + + for w in workers: + bug_found, iteration, seed, _ = w.monitor.get_status() + if bug_found: + print('SUCCESS: Bug found!') + if len(workers) > 1: + print(f'Fuzzer: {w.id}') + print(f'Directory: {w.work_dir}') + print(f'Iteration: {iteration}') + print(f'Seed: {seed}') + print(f'Exit code: {w.proc.returncode}') + return 1 + + if limit_reached: + print( + f'SUCCESS: Reached max iterations ({args.max_iters}) without finding' + ' a bug.', + ) + return 0 + + failed_worker = stopped_worker or workers[0] + _, _, _, recent_lines = failed_worker.monitor.get_status() + + print('FAILURE: Fuzzer stopped unexpectedly without finding a bug.') + if len(workers) > 1: + print(f'Fuzzer: {failed_worker.id}') + print(f'Directory: {failed_worker.work_dir}') + print(f'Exit code: {failed_worker.proc.returncode}') + if recent_lines: + print('Last 20 lines of log:') + for line in recent_lines: + print(line.rstrip('\r\n')) + return 2 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 1dafecfe570..a091689f7e2 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -93,7 +93,7 @@ 'string-lifting-section.wast', # TODO: fuzzer support for uninhabitable imported globals 'exact-references.wast', - # We do not have full suppor for these imports in all parts of the fuzzer. + # We do not have full support for these imports in all parts of the fuzzer. 'instrument-branch-hints.wast', # Contains a subtype chain that exceeds depth limits. 'reorder-types-real.wast', @@ -117,6 +117,9 @@ # Not fully implemented. 'waitqueue.wast', 'gufa-waitqueue.wast', + 'optimize-instructions-waitqueue.wast', + 'publish.wast', + 'optimize-instructions-publish.wast', # TODO: fix handling of the non-utf8 names here 'name-high-bytes.wast', # JS interop testcases have complex js-wasm interactions diff --git a/scripts/test/shared.py b/scripts/test/shared.py index bd5bd9dc43b..0e65e159056 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -17,12 +17,14 @@ import difflib import fnmatch import glob +import io import os import shutil import stat import subprocess import sys from contextlib import contextmanager +from multiprocessing.pool import ThreadPool from pathlib import Path # The C++ standard whose features are required to build Binaryen. @@ -126,6 +128,63 @@ def verbose_log(*args, **kwargs): print(*args, **kwargs) +@contextmanager +def red_output(file=sys.stderr): + print("\033[31m", end="", file=file) + try: + yield + finally: + print("\033[0m", end="", file=file) + + +def red_stderr(): + return red_output(file=sys.stderr) + + +def run_parallel_tests(run_one_test_func, tests, show_worker_count=True): + global num_failures + tests = list(tests) + if not tests: + return + worker_count = min(os.cpu_count() or 1, len(tests)) + if show_worker_count: + print(f"Running with {worker_count} workers") + + def run_test_with_wrapped_stdout(test): + out = io.StringIO() + try: + run_one_test_func(test, stdout=out) + except Exception as e: + print(e, file=out) + return False, out.getvalue() + return True, out.getvalue() + + failed_stdouts = [] + with ThreadPool(processes=worker_count) as pool: + try: + for success, stdout in pool.imap_unordered(run_test_with_wrapped_stdout, tests): + if success: + print(stdout, end="") + continue + + num_failures += 1 + failed_stdouts.append(stdout) + if options.abort_on_first_failure: + with red_stderr(): + print("Aborted test execution after first failure. Set --no-fail-fast to disable this.", file=sys.stderr) + break + except KeyboardInterrupt: + # Hard exit to avoid threads continuing to run after Ctrl-C. + # There's no concern of deadlocking during shutdown here. + os._exit(1) + + if failed_stdouts: + with red_stderr(): + print("Failed tests:", file=sys.stderr) + for failed in failed_stdouts: + print(failed, end="", file=sys.stderr) + + # setup # Locate Binaryen source directory if not specified. @@ -157,7 +216,8 @@ def verbose_log(*args, **kwargs): options.binaryen_test = os.path.join(options.binaryen_root, 'test') if not options.out_dir: - options.out_dir = os.path.join(options.binaryen_root, 'out', 'test') + default_out_dir = os.path.join(options.binaryen_root, 'out', 'test') + options.out_dir = os.environ.get('BINARYEN_OUT_DIR', default_out_dir) if not os.path.exists(options.out_dir): os.makedirs(options.out_dir) @@ -500,6 +560,12 @@ def binary_format_check(wast, verify_final_result=True, base_name=None, stdout=N return disassembled_file +def pass_debug_env(): + env = os.environ.copy() + env['BINARYEN_PASS_DEBUG'] = '1' + return env + + @contextmanager def with_pass_debug(): old_pass_debug = os.environ.get('BINARYEN_PASS_DEBUG') diff --git a/scripts/test/support.py b/scripts/test/support.py index eab86044889..6d06999e944 100644 --- a/scripts/test/support.py +++ b/scripts/test/support.py @@ -136,7 +136,8 @@ 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): + expected_err=None, err_contains=False, err_ignore=None, + *, env=None): """Run a subprocess, returning its output. stderr - None, subprocess.PIPE, subprocess.STDOUT or a file handle / io.StringIO to write stdout to @@ -149,7 +150,7 @@ def run_command(cmd, expected_status=0, stdout=None, stderr=None, stderr = subprocess.PIPE print('executing: ', ' '.join(cmd), file=stdout) - out, err, code = _subprocess_run(cmd, stdout=subprocess.PIPE, stderr=stderr, encoding='UTF-8') + out, err, code = _subprocess_run(cmd, stdout=subprocess.PIPE, stderr=stderr, encoding='UTF-8', env=env) if expected_status is not None and code != expected_status: raise Exception(f"run_command `{' '.join(cmd)}` failed ({code}) {err or ''}") diff --git a/scripts/test/wasm2js.py b/scripts/test/wasm2js.py index 76b788841e9..541ef222d82 100644 --- a/scripts/test/wasm2js.py +++ b/scripts/test/wasm2js.py @@ -57,7 +57,86 @@ def check_for_stale_files(): shared.fail_with_error(f'orphan test output: {f}') +def run_one_wasm2js_test(item, stdout=None): + t, opt, expected_file = item + basename = os.path.basename(t) + print('..', basename, file=stdout) + + # Include the path to avoid collisions between test suites (e.g. lit/basic, + # spec, and wasm2js) when running in parallel. + # /path/to/binaryen/test/wasm2js/foo.wast -> wasm2js-foo + rel = os.path.relpath(t, shared.options.binaryen_test) + base_name = os.path.splitext(rel)[0].replace(os.sep, '-') + split_file = f'split_{base_name}_{opt}.wast' + mjs_file = f'a_{base_name}_{opt}.2asm.mjs' + asserts_mjs_file = f'a_{base_name}_{opt}.2asm.asserts.mjs' + + all_js = [] + all_out = '' + + try: + for module, asserts in support.split_wast(t): + support.write_wast(split_file, module, asserts) + + # wasm2js does not yet support EH or stack switching, and + # enabling them can reduce optimization opportunities + cmd = shared.WASM2JS + [split_file, '-all', + '--disable-exception-handling', + '--disable-stack-switching'] + if opt: + cmd += ['-O'] + if 'emscripten' in basename: + cmd += ['--emscripten'] + if 'deterministic' in basename: + cmd += ['--deterministic'] + js = support.run_command(cmd, stdout=stdout) + all_js.append(js) + + if not shared.NODEJS and not shared.MOZJS: + print('No JS interpreters. Skipping spec tests.', file=stdout) + continue + + with open(mjs_file, 'w') as f: + f.write(js) + + cmd_asserts = cmd + ['--allow-asserts'] + js_asserts = support.run_command(cmd_asserts, stdout=stdout) + # also verify it passes pass-debug verifications + support.run_command(cmd_asserts, stderr=subprocess.PIPE, stdout=stdout, env=shared.pass_debug_env()) + + with open(asserts_mjs_file, 'w') as f: + f.write(js_asserts) + + # verify asm.js is valid js, note that we're using --experimental-modules + # to enable ESM syntax and we're also passing a custom loader to handle the + # `spectest` and `env` modules in our tests. + if shared.NODEJS: + loader = os.path.join(shared.options.binaryen_root, 'scripts', 'test', 'node-esm-loader.mjs') + node = [shared.NODEJS, '--experimental-modules', '--no-warnings', '--loader', loader] + cmd_node = node[:] + cmd_node.append(mjs_file) + out = support.run_command(cmd_node, stdout=stdout) + shared.fail_if_not_identical(out, '') + cmd_node = node[:] + cmd_node.append(asserts_mjs_file) + out = support.run_command(cmd_node, expected_err='', err_ignore='ExperimentalWarning', stdout=stdout) + all_out += out + + shared.fail_if_not_identical_to_file(''.join(all_js), expected_file) + expected_out = os.path.join(shared.get_test_dir('spec'), 'expected-output', basename + '.log') + if os.path.exists(expected_out): + expected_out_text = open(expected_out).read() + else: + expected_out_text = '' + shared.fail_if_not_identical(all_out, expected_out_text) + finally: + shared.delete_from_orbit(split_file) + shared.delete_from_orbit(mjs_file) + shared.delete_from_orbit(asserts_mjs_file) + + def test_wasm2js_output(): + tests = [] for opt in (0, 1): for t in basic_tests + spec_tests + wasm2js_tests: basename = os.path.basename(t) @@ -80,63 +159,9 @@ def test_wasm2js_output(): else: raise Exception(f'missing expected file {expected_file}') - print('..', os.path.basename(t)) - - all_js = [] - all_out = '' - - for module, asserts in support.split_wast(t): - support.write_wast('split.wast', module, asserts) - - # wasm2js does not yet support EH, and enabling it can reduce - # optimization opportunities - cmd = shared.WASM2JS + ['split.wast', '-all', - '--disable-exception-handling'] - if opt: - cmd += ['-O'] - if 'emscripten' in basename: - cmd += ['--emscripten'] - if 'deterministic' in basename: - cmd += ['--deterministic'] - js = support.run_command(cmd) - all_js.append(js) - - if not shared.NODEJS and not shared.MOZJS: - print('No JS interpreters. Skipping spec tests.') - continue + tests.append((t, opt, expected_file)) - open('a.2asm.mjs', 'w').write(js) - - cmd += ['--allow-asserts'] - js = support.run_command(cmd) - # also verify it passes pass-debug verifications - with shared.with_pass_debug(): - support.run_command(cmd, stderr=subprocess.PIPE) - - open('a.2asm.asserts.mjs', 'w').write(js) - - # verify asm.js is valid js, note that we're using --experimental-modules - # to enable ESM syntax and we're also passing a custom loader to handle the - # `spectest` and `env` modules in our tests. - if shared.NODEJS: - loader = os.path.join(shared.options.binaryen_root, 'scripts', 'test', 'node-esm-loader.mjs') - node = [shared.NODEJS, '--experimental-modules', '--no-warnings', '--loader', loader] - cmd = node[:] - cmd.append('a.2asm.mjs') - out = support.run_command(cmd) - shared.fail_if_not_identical(out, '') - cmd = node[:] - cmd.append('a.2asm.asserts.mjs') - out = support.run_command(cmd, expected_err='', err_ignore='ExperimentalWarning') - all_out += out - - shared.fail_if_not_identical_to_file(''.join(all_js), expected_file) - expected_out = os.path.join(shared.get_test_dir('spec'), 'expected-output', os.path.basename(t) + '.log') - if os.path.exists(expected_out): - expected_out = open(expected_out).read() - else: - expected_out = '' - shared.fail_if_not_identical(all_out, expected_out) + shared.run_parallel_tests(run_one_wasm2js_test, tests) def test_asserts_output(): @@ -150,7 +175,8 @@ def test_asserts_output(): wasm = os.path.join(shared.get_test_dir('wasm2js'), wasm) cmd = shared.WASM2JS + [wasm, '--allow-asserts', '-all', - '--disable-exception-handling'] + '--disable-exception-handling', + '--disable-stack-switching'] out = support.run_command(cmd) shared.fail_if_not_identical_to_file(out, asserts_expected_file) @@ -201,10 +227,11 @@ def update_wasm2js_tests(): for module, asserts in support.split_wast(t): support.write_wast('split.wast', module, asserts) - # wasm2js does not yet support EH, and enable it can reduce - # optimization opportunities + # wasm2js does not yet support EH or stack switching, and + # enabling them can reduce optimization opportunities cmd = shared.WASM2JS + ['split.wast', '-all', - '--disable-exception-handling'] + '--disable-exception-handling', + '--disable-stack-switching'] if opt: cmd += ['-O'] if 'emscripten' in basename: @@ -225,7 +252,7 @@ def update_wasm2js_tests(): asserts_expected_file = os.path.join(shared.options.binaryen_test, 'wasm2js', asserts) traps_expected_file = os.path.join(shared.options.binaryen_test, 'wasm2js', traps) - cmd = shared.WASM2JS + [os.path.join(shared.get_test_dir('wasm2js'), wasm), '--allow-asserts', '-all', '--disable-exception-handling'] + cmd = shared.WASM2JS + [os.path.join(shared.get_test_dir('wasm2js'), wasm), '--allow-asserts', '-all', '--disable-exception-handling', '--disable-stack-switching'] out = support.run_command(cmd) with open(asserts_expected_file, 'w') as o: o.write(out) diff --git a/scripts/test/wasm_opt.py b/scripts/test/wasm_opt.py index de51497139c..7b2ea147371 100644 --- a/scripts/test/wasm_opt.py +++ b/scripts/test/wasm_opt.py @@ -15,12 +15,74 @@ import os import shutil import subprocess -import sys from . import shared, support from .shared import print_heading +def run_one_pass_test(t, stdout=None): + # windows has some failures that need to be investigated: + # * ttf tests have different outputs - order of execution of params? + # * dwarf tests print windows slashes instead of unix + if ('translate-to-fuzz' in t or 'dwarf' in t) and \ + shared.skip_if_on_windows('fuzz translation tests'): + return + print('..', os.path.basename(t), file=stdout) + binary = t.endswith('.wasm') + base = os.path.basename(t).replace('.wast', '').replace('.wasm', '') + passname = base + passes_file = os.path.join(shared.get_test_dir('passes'), passname + '.passes') + if os.path.exists(passes_file): + passname = open(passes_file).read().strip() + passes = [p for p in passname.split('_') if p != 'noprint'] + opts = [('--' + p if not p.startswith('O') and p != 'g' else '-' + p) for p in passes] + actual = '' + split_wast = f'split_{base}.wast' + try: + for module, asserts in support.split_wast(t): + assert len(asserts) == 0 + support.write_wast(split_wast, module) + cmd = shared.WASM_OPT + opts + [split_wast, '-q'] + if 'noprint' not in t: + cmd.append('--print') + curr = support.run_command(cmd, stdout=stdout) + actual += curr + # also check debug mode output is valid + debugged = support.run_command(cmd + ['--debug'], stderr=subprocess.PIPE, stdout=stdout) + shared.fail_if_not_contained(actual, debugged) + + # also check pass-debug mode + # ignore stderr, as the pass-debug output is very verbose in CI + pass_debug = support.run_command(cmd, stderr=subprocess.PIPE, stdout=stdout, env=shared.pass_debug_env()) + shared.fail_if_not_identical(curr, pass_debug) + + expected_file = os.path.join(shared.get_test_dir('passes'), base + ('.bin' if binary else '') + '.txt') + shared.fail_if_not_identical_to_file(actual, expected_file) + + if 'emit-js-wrapper' in t: + with open('a.js') as actual_wrapper: + shared.fail_if_not_identical_to_file(actual_wrapper.read(), t + '.js') + if 'emit-spec-wrapper' in t: + with open('a.wat') as actual_wrapper: + shared.fail_if_not_identical_to_file(actual_wrapper.read(), t + '.wat') + finally: + shared.delete_from_orbit(split_wast) + + +def run_one_print_test(t, stdout=None): + print('..', os.path.basename(t), file=stdout) + wasm = os.path.basename(t).replace('.wast', '') + cmd = shared.WASM_OPT + [t, '--print', '-all'] + print(' ', ' '.join(cmd), file=stdout) + proc = subprocess.run(cmd, capture_output=True, text=True) + expected_file = os.path.join(shared.get_test_dir('print'), wasm + '.txt') + shared.fail_if_not_identical_to_file(proc.stdout, expected_file) + cmd = shared.WASM_OPT + [os.path.join(shared.get_test_dir('print'), t), '--print-minified', '-all'] + print(' ', ' '.join(cmd), file=stdout) + proc = subprocess.run(cmd, capture_output=True, text=True) + shared.fail_if_not_identical(proc.stdout.strip(), open(os.path.join(shared.get_test_dir('print'), wasm + '.minified.txt')).read().strip()) + + def test_wasm_opt(): print_heading('checking wasm-opt -o notation...') @@ -43,72 +105,12 @@ def test_wasm_opt(): assert open('b.wast', 'rb').read()[0] != 0, 'we emit text with -S' print_heading('checking 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: - # * ttf tests have different outputs - order of execution of params? - # * dwarf tests print windows slashes instead of unix - if ('translate-to-fuzz' in t or 'dwarf' in t) and \ - shared.skip_if_on_windows('fuzz translation tests'): - continue - binary = t.endswith('.wasm') - base = os.path.basename(t).replace('.wast', '').replace('.wasm', '') - passname = base - passes_file = os.path.join(shared.get_test_dir('passes'), passname + '.passes') - if os.path.exists(passes_file): - passname = open(passes_file).read().strip() - passes = [p for p in passname.split('_') if p != 'noprint'] - 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'] - if 'noprint' not in t: - cmd.append('--print') - curr = support.run_command(cmd) - actual += curr - # also check debug mode output is valid - debugged = support.run_command(cmd + ['--debug'], stderr=subprocess.PIPE) - shared.fail_if_not_contained(actual, debugged) - - # also check pass-debug mode - with shared.with_pass_debug(): - # ignore stderr, as the pass-debug output is very verbose in CI - pass_debug = support.run_command(cmd, stderr=subprocess.PIPE) - shared.fail_if_not_identical(curr, pass_debug) - - expected_file = os.path.join(shared.get_test_dir('passes'), base + ('.bin' if binary else '') + '.txt') - shared.fail_if_not_identical_to_file(actual, expected_file) - - if 'emit-js-wrapper' in t: - with open('a.js') as actual: - shared.fail_if_not_identical_to_file(actual.read(), t + '.js') - if 'emit-spec-wrapper' in t: - with open('a.wat') as actual: - shared.fail_if_not_identical_to_file(actual.read(), t + '.wat') + passes_tests = shared.get_tests(shared.get_test_dir('passes'), ['.wast', '.wasm']) + shared.run_parallel_tests(run_one_pass_test, passes_tests) print_heading('checking wasm-opt parsing & printing...') - - for t in shared.get_tests(shared.get_test_dir('print'), ['.wast']): - print('..', os.path.basename(t)) - wasm = os.path.basename(t).replace('.wast', '') - cmd = shared.WASM_OPT + [t, '--print', '-all'] - print(' ', ' '.join(cmd)) - proc = subprocess.run(cmd, capture_output=True, text=True) - expected_file = os.path.join(shared.get_test_dir('print'), wasm + '.txt') - shared.fail_if_not_identical_to_file(proc.stdout, expected_file) - cmd = shared.WASM_OPT + [os.path.join(shared.get_test_dir('print'), t), '--print-minified', '-all'] - print(' ', ' '.join(cmd)) - proc = subprocess.run(cmd, capture_output=True, text=True) - shared.fail_if_not_identical(proc.stdout.strip(), open(os.path.join(shared.get_test_dir('print'), wasm + '.minified.txt')).read().strip()) + print_tests = shared.get_tests(shared.get_test_dir('print'), ['.wast']) + shared.run_parallel_tests(run_one_print_test, print_tests) def update_wasm_opt_tests(): diff --git a/scripts/update_help_checks.py b/scripts/update_help_checks.py index 5471f3533a9..e7e3e0deb21 100755 --- a/scripts/update_help_checks.py +++ b/scripts/update_help_checks.py @@ -39,11 +39,13 @@ def main(): out.write(f';; RUN: {tool} --help | filecheck %s' + os.linesep) first = True for line in output.splitlines(): - if first: - out.write(f';; CHECK: {line}'.strip() + os.linesep) + if not line: + out.write(';; CHECK-EMPTY:' + os.linesep) + elif first: + out.write(f';; CHECK: {line}' + os.linesep) first = False else: - out.write(f';; CHECK-NEXT: {line}'.strip() + os.linesep) + out.write(f';; CHECK-NEXT: {line}' + os.linesep) if __name__ == '__main__': diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index d399fae8493..0e6801e663c 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -167,7 +167,7 @@ def find_annotations(module, start): else: # Found something that isn't an annotation. break - # Look for the start of the line containin the first annoation. + # Look for the start of the line containing the first annotation. for i in range(annotation - 1, -1, -1): if module[i] == '\n': return i + 1 @@ -331,7 +331,7 @@ def update_test(args, test, lines, tmp): prefixes = {prefix for module_output in command_output for prefix in module_output.keys()} check_line_re = re.compile(r'^\s*;;\s*(' + '|'.join(prefixes) + - r')(?:-NEXT|-LABEL|-NOT)?:.*$') + r')(?:-[A-Z0-9]+)?:.*$') # Filter out whitespace between check blocks if lines: @@ -353,8 +353,14 @@ def update_test(args, test, lines, tmp): def emit_checks(indent, prefix, lines): def pad(line): return line if not line or line.startswith(' ') else ' ' + line - output_lines.append(f'{indent};; {prefix}: {pad(lines[0])}') - output_lines.extend(f'{indent};; {prefix}-NEXT:{pad(line)}' for line in lines[1:]) + + for i, line in enumerate(lines): + if not line.strip(): + output_lines.append(f'{indent};; {prefix}-EMPTY:') + elif i == 0: + output_lines.append(f'{indent};; {prefix}: {pad(line)}') + else: + output_lines.append(f'{indent};; {prefix}-NEXT:{pad(line)}') input_modules = [m.split('\n') for m in split_modules('\n'.join(lines))] if len(input_modules) > len(command_output): diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 2f2f0d3ddac..3d2f776b20a 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -1949,6 +1949,10 @@ BinaryenExpressionRef BinaryenWaitqueueNotify(BinaryenModuleRef module, return Builder(*(Module*)module) .makeWaitqueueNotify((Expression*)waitqueue, (Expression*)count); } +BinaryenExpressionRef BinaryenPublish(BinaryenModuleRef module, + BinaryenExpressionRef ref) { + return Builder(*(Module*)module).makePublish((Expression*)ref); +} BinaryenExpressionRef BinaryenArrayNew(BinaryenModuleRef module, BinaryenHeapType type, BinaryenExpressionRef size, @@ -4788,6 +4792,20 @@ void BinaryenWaitqueueNotifySetCount(BinaryenExpressionRef expr, static_cast(expression)->count = (Expression*)countExpr; } +// Publish +BinaryenExpressionRef BinaryenPublishGetRef(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->ref; +} +void BinaryenPublishSetRef(BinaryenExpressionRef expr, + BinaryenExpressionRef refExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(refExpr); + static_cast(expression)->ref = (Expression*)refExpr; +} + // ArrayNew BinaryenExpressionRef BinaryenArrayNewGetInit(BinaryenExpressionRef expr) { auto* expression = (Expression*)expr; @@ -6846,6 +6864,10 @@ BinaryenSideEffects BinaryenSideEffectDanglingPop(void) { return static_cast( EffectAnalyzer::SideEffects::DanglingPop); } +BinaryenSideEffects BinaryenSideEffectSuspends(void) { + return static_cast( + EffectAnalyzer::SideEffects::Suspends); +} BinaryenSideEffects BinaryenSideEffectAny(void) { return static_cast(EffectAnalyzer::SideEffects::Any); } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index d550e42d04e..1754e60dde6 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -1124,6 +1124,8 @@ BINARYEN_API BinaryenExpressionRef BinaryenWaitqueueNotify(BinaryenModuleRef module, BinaryenExpressionRef waitqueue, BinaryenExpressionRef count); +BINARYEN_API BinaryenExpressionRef BinaryenPublish(BinaryenModuleRef module, + BinaryenExpressionRef ref); BINARYEN_API BinaryenExpressionRef BinaryenArrayNew(BinaryenModuleRef module, BinaryenHeapType type, BinaryenExpressionRef size, @@ -2702,6 +2704,13 @@ BINARYEN_API void BinaryenWaitqueueNotifySetCount(BinaryenExpressionRef expr, BinaryenExpressionRef countExpr); +// Publish + +BINARYEN_API BinaryenExpressionRef +BinaryenPublishGetRef(BinaryenExpressionRef expr); +BINARYEN_API void BinaryenPublishSetRef(BinaryenExpressionRef expr, + BinaryenExpressionRef refExpr); + // ArrayNew BINARYEN_API BinaryenExpressionRef @@ -3753,6 +3762,7 @@ BINARYEN_API BinaryenSideEffects BinaryenSideEffectTrapsNeverHappen(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectIsAtomic(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectThrows(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectDanglingPop(void); +BINARYEN_API BinaryenSideEffects BinaryenSideEffectSuspends(void); BINARYEN_API BinaryenSideEffects BinaryenSideEffectAny(void); BINARYEN_API BinaryenSideEffects BinaryenExpressionGetSideEffects( @@ -3808,10 +3818,10 @@ BINARYEN_API void RelooperAddBranchForSwitch(RelooperBlockRef from, BinaryenIndex numIndexes, BinaryenExpressionRef code); -// Generate structed wasm control flow from the CFG of blocks and branches that -// were created on this relooper instance. This returns the rendered output, and -// also disposes of the relooper and its blocks and branches, as they are no -// longer needed. +// Generate structured wasm control flow from the CFG of blocks and branches +// that were created on this relooper instance. This returns the rendered +// output, and also disposes of the relooper and its blocks and branches, as +// they are no longer needed. // @param labelHelper To render irreducible control flow, we may need a helper // variable to guide us to the right target label. This value should be // an index of an i32 local variable that is free for us to use. diff --git a/src/dataflow/node.h b/src/dataflow/node.h index d88134a2feb..c529f2040c2 100644 --- a/src/dataflow/node.h +++ b/src/dataflow/node.h @@ -55,7 +55,7 @@ struct Node { // in wasm) Expr, // a value represented by a Binaryen Expression Phi, // a phi from converging control flow - Cond, // a blockpc, representing one of the branchs for a Block + Cond, // a blockpc, representing one of the branches for a Block Block, // a source of phis Zext, // zero-extend an i1 (from an op where Souper returns i1 but wasm does // not, and so we need a special way to get back to an i32/i64 if we diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index 6288b5b2734..7d7e5887b2c 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -4948,6 +4948,12 @@ switch (buf[0]) { return Ok{}; } goto parse_error; + case 'u': + if (op == "publish"sv) { + CHECK_ERR(makePublish(ctx, pos, annotations)); + return Ok{}; + } + goto parse_error; default: goto parse_error; } } diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index d7f31a6ddf0..878752e38e0 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -259,6 +259,7 @@ struct ExpressionInterpreter : OverriddenVisitor { Flow visitStructWait(StructWait* curr) { WASM_UNREACHABLE("TODO"); } Flow visitWaitqueueNew(WaitqueueNew* curr) { WASM_UNREACHABLE("TODO"); } Flow visitWaitqueueNotify(WaitqueueNotify* curr) { WASM_UNREACHABLE("TODO"); } + Flow visitPublish(Publish* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayNew(ArrayNew* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayNewData(ArrayNewData* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayNewElem(ArrayNewElem* curr) { WASM_UNREACHABLE("TODO"); } @@ -311,7 +312,7 @@ Result<> Interpreter::instantiate(Instance& instance) { return Ok{}; } -// This is a temporary convenience while stil using gTests to validate this +// This is a temporary convenience while still using gTests to validate this // interpreter. Once spec tests can run, this shall be deleted. std::vector Interpreter::runTest(Expression* root) { static std::shared_ptr dummyModule = std::make_shared(); diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index 8e5b6c801ee..32fb39f314c 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -170,6 +170,7 @@ void ReFinalize::visitWaitqueueNew(WaitqueueNew* curr) { curr->finalize(); } void ReFinalize::visitWaitqueueNotify(WaitqueueNotify* curr) { curr->finalize(); } +void ReFinalize::visitPublish(Publish* curr) { curr->finalize(); } void ReFinalize::visitArrayNew(ArrayNew* curr) { curr->finalize(); } void ReFinalize::visitArrayNewData(ArrayNewData* curr) { curr->finalize(); } void ReFinalize::visitArrayNewElem(ArrayNewElem* curr) { curr->finalize(); } diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 27499b785fb..21d59a88bfb 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -83,7 +83,7 @@ template struct ChildTyper : OverriddenVisitor { } } - // Disambiguate betwween Type and VarType. + // Disambiguate between Type and VarType. void note(Expression** childp, Type::BasicType type) { note(childp, VarType{Type(type)}); } @@ -1056,6 +1056,11 @@ template struct ChildTyper : OverriddenVisitor { note(&curr->count, Type(Type::BasicType::i32)); } + void visitPublish(Publish* curr) { + // Polymorphic over heap types. + note(&curr->ref, VarRef{Nullable, VarHeapType{0u}}); + } + void visitArrayNew(ArrayNew* curr) { if (!curr->isWithDefault()) { if (!curr->type.isRef()) { diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index c38c97162b6..0c9cc14fefe 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -17,6 +17,7 @@ #include #include "ir/constraint.h" +#include "ir/find_all.h" #include "ir/properties.h" #include "wasm.h" @@ -255,6 +256,66 @@ Result provesConstantPair(Abstract::Op aOp, return Unknown; } +// Evaluate whether a => b, where a and b are operations on identical terms. +Result provesTermEqualPair(Abstract::Op aOp, Abstract::Op bOp) { + using namespace Abstract; + + // Trivial cases where aOp == bOp or aOp == !bOp are taken care of elsewhere. + assert(aOp != bOp && aOp != Abstract::negateRelational(bOp)); + + switch (aOp) { + case Eq: + // == proves >= etc. true, and > (without =) false + if (bOp == LeU || bOp == LeS || bOp == GeU || bOp == GeS) { + return True; + } + if (bOp == LtU || bOp == LtS || bOp == GtU || bOp == GtS) { + return False; + } + break; + case LtS: + // < proves <=, != true and ==, > false + if (bOp == LeS || bOp == Ne) { + return True; + } + if (bOp == Eq || bOp == GtS) { + return False; + } + break; + case GtS: + // Ditto, with G instead of L. + if (bOp == GeS || bOp == Ne) { + return True; + } + if (bOp == Eq || bOp == LtS) { + return False; + } + break; + case LtU: + // Ditto, with unsigned. + if (bOp == LeU || bOp == Ne) { + return True; + } + if (bOp == Eq || bOp == GtU) { + return False; + } + break; + case GtU: + // Ditto, with G instead of L. + if (bOp == GeU || bOp == Ne) { + return True; + } + if (bOp == Eq || bOp == LtU) { + return False; + } + break; + default: { + } + } + + return Unknown; +} + // Core comparison of two constraints: whether a => b Result provesPair(const Constraint& a, const Constraint& b) { // A thing always implies itself. @@ -309,6 +370,10 @@ Result provesPair(const Constraint& a, const Constraint& b) { } } + if (a.term == b.term) { + return provesTermEqualPair(a.op, b.op); + } + return Unknown; } @@ -669,22 +734,130 @@ bool AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { return changed; } -std::optional LocalConstraint::parse(Expression* curr) { +namespace { + +// Internal parsing, utilizing a map of tees. We can handle tees in an +// expression, so long as they do not interfere with each other: +// +// (i32.eq +// (local.tee $x ..) +// (i32.const 10) +// ) +// +// We can parse this into $x == 10, just as if we saw a local.get $x there. But +// we cannot handle this: +// +// (i32.eq +// (local.get $x) +// (local.tee $x ..) +// ) +// +// Parsing this into $x == $x would be wrong: the first $x is the old value. +// That is, fully handling tees requires a more SSA-like IR. To handle the +// common cases we want to, we parse the code in the natural order of execution, +// and maintain a list of local operations. A get before a tee indicates +// possible interference. +struct LocalOperations { + SmallVector vec; + + // Check if an Expression returns a local's value: it is either a get or a + // tee. Returns the index and type if so, and notes it in our vector. + struct LocalOperation { + Index index; + }; + std::optional parse(Expression* curr) { + auto handleNestedSets = [&](Expression* from) { + for (auto* nested : FindAll(from).list) { + vec.push_back(nested); + } + }; + + if (auto* get = curr->dynCast()) { + vec.push_back(get); + return LocalOperation{get->index}; + } + if (auto* set = curr->dynCast()) { + // We are parsing expressions in a tree, not none-typed items in a block. + assert(set->isTee()); + + // We know the value of this expression - the local the tee writes to - + // but further sets may be nested in the value, affecting other locals. + handleNestedSets(set->value); + + // Ignore unreachable code, so the callers don't need to handle it. + if (set->type == Type::unreachable) { + return {}; + } + + vec.push_back(set); + return LocalOperation{set->index}; + } + // Unrecognized. As above, we must scan for nested sets. + handleNestedSets(curr); + return {}; + } + + // Check for any possible interference between locals, which would tell the + // caller that whatever was parsed is not valid. + bool hasLocalInterference() const { + if (vec.size() <= 1) { + return false; + } + + // Process the list in detail, as interference - a get before a set of the + // same local - is possible. We track the read locals, and if we see a + // later write, that shows a problem. + std::unordered_set read; + for (auto* curr : vec) { + if (auto* get = curr->dynCast()) { + read.insert(get->index); + } else if (auto* set = curr->dynCast()) { + if (read.contains(set->index)) { + return true; + } + // Insert a read, because the tee does both a write and a read. + if (set->isTee()) { + read.insert(set->index); + } + } else { + WASM_UNREACHABLE("invalid local op"); + } + } + return false; + } +}; + +std::optional +localConstraintParseInternal(Expression* curr, + LocalOperations& localOperations) { + using namespace Match; + auto parseEqZArgument = [&](Expression* value) -> std::optional { - if (auto* get = value->dynCast()) { + if (auto localOp = localOperations.parse(value)) { // Canonicalize EqZ to Eq of 0. - auto value = Literal::makeZero(get->type); - return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}}; + auto zero = Literal::makeZero(value->type); + return LocalConstraint{localOp->index, Constraint{Abstract::Eq, {zero}}}; } // TODO: Recursively parse and reverse a constraint return {}; }; - if (auto* unary = curr->dynCast()) { - if (Abstract::getUnary(unary->value->type, Abstract::EqZ) == unary->op) { - return parseEqZArgument(unary->value); + if (auto* u = curr->dynCast()) { + if (Abstract::getUnary(u->value->type, Abstract::EqZ) == u->op) { + // EqZ of EqZ means a check that the value is *not* zero. + Expression* nested; + if (matches(u->value, unary(Abstract::EqZ, any(&nested)))) { + if (auto localOp = localOperations.parse(nested)) { + auto value = Literal::makeZero(nested->type); + return LocalConstraint{localOp->index, + Constraint{Abstract::Ne, {value}}}; + } + } + + return parseEqZArgument(u->value); } + return {}; } @@ -692,10 +865,10 @@ std::optional LocalConstraint::parse(Expression* curr) { return parseEqZArgument(refIsNull->value); } - // Parse a get or a constant. + // Parse a get, tee, or a constant. auto parseTerm = [&](Expression* expr) -> std::optional { - if (auto* get = expr->dynCast()) { - return Term{get->index}; + if (auto localOp = localOperations.parse(expr)) { + return Term{localOp->index}; } if (Properties::isSingleConstantExpression(expr)) { return Term{Properties::getLiteral(expr)}; @@ -707,11 +880,11 @@ std::optional LocalConstraint::parse(Expression* curr) { [&](Abstract::Op op, Expression* left, Expression* right) -> std::optional { - // The left must be a get. - if (auto* get = left->dynCast()) { + // The left must be a get or a tee. + if (auto localOp = localOperations.parse(left)) { // The right can be any term. if (auto value = parseTerm(right)) { - return LocalConstraint{get->index, Constraint{op, *value}}; + return LocalConstraint{localOp->index, Constraint{op, *value}}; } } return {}; @@ -743,17 +916,129 @@ std::optional LocalConstraint::parse(Expression* curr) { return {}; } -std::optional -LocalConstraint::parseCondition(Expression* curr) { - // A get by itself is a check for not being null. - if (auto* get = curr->dynCast()) { - auto value = Literal::makeZero(get->type); - return LocalConstraint{get->index, Constraint{Abstract::Ne, {value}}}; +} // anonymous namespace + +std::optional LocalConstraint::parse(Expression* curr) { + LocalOperations localOperations; + auto ret = localConstraintParseInternal(curr, localOperations); + if (localOperations.hasLocalInterference()) { + return {}; + } + return ret; +} + +ParsedAndedConstraints ParsedAndedConstraints::parse(Expression* curr) { + using namespace Match; + + // The final return value. + ParsedAndedConstraints ret; + + // We will track local operations over the entire tree we parse. + LocalOperations localOperations; + + // Starting from |curr|, parse and recurse into sub-trees: when we see an AND, + // we push both children as further work. + SmallVector work; + work.push_back(curr); + while (!work.empty()) { + auto* curr = work.back(); + work.pop_back(); + + auto parsed = localConstraintParseInternal(curr, localOperations); + if (parsed) { + ret.push_back(*parsed); + continue; + } + + Binary* b; + if (matches(curr, binary(&b, Abstract::And, any(), any()))) { + // An AND can be recursively processed: both sides must be true. Push them + // in reverse order, so we process them in the natural order of execution. + work.push_back(b->right); + work.push_back(b->left); + continue; + } + // TODO: support OR + + // We failed to parse this as constraints. We do still need to check for + // local operations that might interfere with the things we did parse, + // otherwise. + localOperations.parse(curr); + ret.hasUnknown = true; + } + + if (localOperations.hasLocalInterference()) { + return {}; + } + return ret; +} + +ParsedAndedConstraints +ParsedAndedConstraints::parseCondition(Expression* curr) { + // A get or tee by itself is a check for not being null. + LocalOperations localOperations; + if (auto localOp = localOperations.parse(curr)) { + auto value = Literal::makeZero(curr->type); + return {LocalConstraint{localOp->index, Constraint{Abstract::Ne, {value}}}}; } // Otherwise, parse normally. return parse(curr); -}; +} + +void ParsedAndedConstraints::negate() { + if (empty()) { + return; + } + + if (hasUnknown) { + // This includes things we don't know about, and don't know how to negate. + clear(); + return; + } + + // The input is a list of constraints all applying at once, A & B & C. The + // negation is !A | !B | !C, but we cannot express a general OR like that, + // except in the simple case where they all talk about the same local: then + // we can at least approximateOr them all into one constraint. + auto& self = *this; + for (Index i = 1; i < size(); i++) { + if (self[i].local != self[0].local) { + // They refer to different locals. Give up. + clear(); + return; + } + } + + // Negate them before the OR. + for (auto& pair : self) { + pair.constraint = pair.constraint.negate(); + } + + if (size() == 1) { + // The simple case of 1 doesn't need any more work. + return; + } + + // Do the OR. + AndedConstraintSet anded; + anded.set(self[0].constraint); + for (Index i = 1; i < size(); i++) { + anded.approximateOr({self[i].constraint}); + if (anded.provesNothing()) { + // We have nothing useful here. + clear(); + return; + } + } + + // Return only the OR'ed result. + auto local = self[0].local; + clear(); + for (auto& c : anded) { + emplace_back(local, c); + } +} void LocalConstraint::flip() { auto other = std::get(constraint.term); @@ -1088,6 +1373,11 @@ std::ostream& operator<<(std::ostream& o, const Constraint& c) { return o; } +std::ostream& operator<<(std::ostream& o, const LocalConstraint& c) { + o << "LocalConstraint{$" << c.local << ", " << c.constraint << '}'; + return o; +} + std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set) { if (set.provesEverything()) { o << "AndedConstraintSet(contradiction)"; diff --git a/src/ir/constraint.h b/src/ir/constraint.h index d699fc4c424..2e15bec5ff0 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -32,6 +32,7 @@ #include "ir/abstract.h" #include "support/inplace_vector.h" +#include "support/small_vector.h" #include "support/span.h" #include "support/utilities.h" #include "wasm.h" @@ -235,6 +236,12 @@ struct LocalConstraint { Index local; Constraint constraint; + LocalConstraint() = default; + LocalConstraint(Index local, Constraint constraint) + : local(local), constraint(std::move(constraint)) {} + + bool operator==(const LocalConstraint&) const = default; + // Try to parse BinaryenIR into a local to which a constraint is applied. For // example // @@ -246,15 +253,43 @@ struct LocalConstraint { // static std::optional parse(Expression* curr); - // Parse in a condition context, i.e., where (local.get $x) is the same as - // $x != 0 (e.g., in an if condition, or a br_on ref). - static std::optional parseCondition(Expression* curr); - // Reverse the constraint. The constraint's term must, of course, be another // local. void flip(); }; +// A utility to parse BinaryenIR into locals and constraints on them. This is +// similar to LocalConstraint::parse, but that parses a single constraint, while +// this can handle a list of ANDed ones: +// +// (i32.and (..A..) (..B..)) +// +// parses into [ A, B ]. +// +// We also set a field |hasUnknown| if we saw things we could not parse. E.g. +// +// (i32.and (call $unknown) (i32.eqz (local.get $x))) +// +// This parses into [ $x == 0 ] and sets hasUnknown=true. Even if there are +// unknown things, we do know that definitely $x == 0 at least, which is useful +// in some cases. +struct ParsedAndedConstraints : public SmallVector { + using SmallVector::SmallVector; + + bool hasUnknown = false; + + static ParsedAndedConstraints parse(Expression* curr); + + // Parse in a condition context, i.e., where (local.get $x) is the same as + // $x != 0 (e.g., in an if condition, or a br_on ref). + static ParsedAndedConstraints parseCondition(Expression* curr); + + // Negate the entire list of constraints. If we fail to generate something + // that can be represented as a list of ANDed constraints, the list will be + // empty (i.e., we can prove nothing). + void negate(); +}; + // A map of locals and their constraints, representing the state at a basic // block. We use the following representation: // @@ -366,6 +401,7 @@ struct BasicBlockConstraintMap { }; std::ostream& operator<<(std::ostream& o, const Constraint& c); +std::ostream& operator<<(std::ostream& o, const LocalConstraint& c); std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set); } // namespace wasm::constraint diff --git a/src/ir/cost.h b/src/ir/cost.h index 13fa7cb8b13..f2789666369 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -128,6 +128,7 @@ struct CostAnalyzer : public OverriddenVisitor { return AtomicCost + nullCheckCost(curr->waitqueue) + visit(curr->waitqueue) + visit(curr->count); } + CostType visitPublish(Publish* curr) { return AtomicCost + visit(curr->ref); } CostType visitAtomicNotify(AtomicNotify* curr) { return AtomicCost + visit(curr->ptr) + visit(curr->notifyCount); } diff --git a/src/ir/effects.cpp b/src/ir/effects.cpp index 017057a1349..a8d36085f2b 100644 --- a/src/ir/effects.cpp +++ b/src/ir/effects.cpp @@ -96,6 +96,9 @@ std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects) { if (effects.throws_) { o << "throws_\n"; } + if (effects.suspends) { + o << "suspends_\n"; + } if (effects.tryDepth) { o << "tryDepth\n"; } diff --git a/src/ir/effects.h b/src/ir/effects.h index 40bd93b6ba2..24e50e5d28c 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -23,7 +23,6 @@ #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" @@ -44,8 +43,8 @@ class EffectAnalyzer { readsMutableArray(false), writesArray(false), readsSharedMutableArray(false), writesSharedArray(false), trap(false), implicitTrap(false), throws_(false), danglingPop(false), - mayNotReturn(false), hasReturnCallThrow(false), module(module), - features(module.features) {} + mayNotReturn(false), hasReturnCallThrow(false), suspends(false), + module(module), features(module.features) {} EffectAnalyzer(const PassOptions& passOptions, const Module& module, @@ -138,6 +137,8 @@ class EffectAnalyzer { // more here.) bool hasReturnCallThrow : 1; + bool suspends : 1; + const Module& module; FeatureSet features; @@ -228,15 +229,17 @@ class EffectAnalyzer { return calls || readsSharedMutableArray || writesSharedArray; } bool throws() const { return throws_ || !delegateTargets.empty(); } + // Check whether this may transfer control flow to somewhere outside of this - // expression (aside from just flowing out normally). That includes a break - // or a throw (if the throw is not known to be caught inside this expression; + // expression (aside from just flowing out normally). That includes a break, + // a throw (if the throw is not known to be caught inside this expression; // note that if the throw is not caught in this expression then it might be // caught in this function but outside of this expression, or it might not be // caught in the function at all, which would mean control flow cannot be - // transferred inside the function, but this expression does not know that). + // transferred inside the function, but this expression does not know that), + // or a suspension. bool transfersControlFlow() const { - return branchesOut || throws() || hasExternalBreakTargets(); + return branchesOut || suspends || throws() || hasExternalBreakTargets(); } // Changes something in globally-stored state. @@ -480,6 +483,7 @@ class EffectAnalyzer { danglingPop = danglingPop || other.danglingPop; mayNotReturn = mayNotReturn || other.mayNotReturn; hasReturnCallThrow = hasReturnCallThrow || other.hasReturnCallThrow; + suspends = suspends || other.suspends; readOrder = std::max(readOrder, other.readOrder); writeOrder = std::max(writeOrder, other.writeOrder); @@ -1104,6 +1108,27 @@ class EffectAnalyzer { parent.writesSharedStruct = true; parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; } + void visitPublish(Publish* curr) { + // Publish is a no-op on anything besides shared structs and arrays. + if (!curr->ref->type.isRef()) { + return; + } + auto heapType = curr->ref->type.getHeapType(); + if (!heapType.isShared()) { + return; + } + if (!heapType.isStruct() && !heapType.isMaybeShared(HeapType::struct_) && + !heapType.isArray() && !heapType.isMaybeShared(HeapType::array) && + !heapType.isMaybeShared(HeapType::eq) && + !heapType.isMaybeShared(HeapType::any)) { + return; + } + // TODO: Modeling `publish` as an arbitrary call is overly conservative. + // We need to prevent writes to the published object from being moved + // after the publish and we need to prevent writes of the published object + // to anywhere else from being moved before the publish. + parent.calls = true; + } void visitArrayNew(ArrayNew* curr) {} void visitArrayNewData(ArrayNewData* curr) { // Traps on out of bounds access to segments or access to dropped @@ -1271,8 +1296,9 @@ class EffectAnalyzer { parent.calls = true; } void visitSuspend(Suspend* curr) { - // Similar to resume/call: Suspending means that we execute arbitrary - // other code before we may resume here. + // Suspending transfers control to an enclosing handler and executes + // arbitrary other code before we may resume here. + parent.suspends = true; parent.calls = true; if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { parent.throws_ = true; @@ -1370,12 +1396,27 @@ class EffectAnalyzer { parent.throws_ = true; } } + // If stack switching is enabled and we don't have global effects + // information, assume that the call target may suspend. + if (parent.features.hasStackSwitching()) { + parent.suspends = true; + } } }; public: // Helpers + // See comment on orderedBefore() for the assumptions on the inputs here. + static bool orderedBefore(const PassOptions& passOptions, + Module& module, + Expression* a, + Expression* b) { + EffectAnalyzer aEffects(passOptions, module, a); + EffectAnalyzer bEffects(passOptions, module, b); + return aEffects.orderedBefore(bEffects); + } + // See comment on orderedBefore() for the assumptions on the inputs here. // TODO: Update users so we can check just one direction here. static bool canReorder(const PassOptions& passOptions, @@ -1407,7 +1448,8 @@ class EffectAnalyzer { Throws = 1 << 12, DanglingPop = 1 << 13, TrapsNeverHappen = 1 << 14, - Any = (1 << 15) - 1 + Suspends = 1 << 15, + Any = (1 << 16) - 1 }; uint32_t getSideEffects() const { uint32_t effects = 0; @@ -1459,12 +1501,15 @@ class EffectAnalyzer { if (danglingPop) { effects |= SideEffects::DanglingPop; } + if (suspends) { + effects |= SideEffects::Suspends; + } return effects; } - // Ignores all forms of control flow transfers: breaks, returns, and - // exceptions. (Note that traps are not considered relevant here - a trap does - // not just transfer control flow, but can be seen as halting the entire + // Ignores all forms of control flow transfers: breaks, returns, exceptions, + // and suspensions. (Note that traps are not considered relevant here - a trap + // does not just transfer control flow, but can be seen as halting the entire // program.) // // This function matches transfersControlFlow(), that is, after calling this @@ -1474,6 +1519,7 @@ class EffectAnalyzer { breakTargets.clear(); throws_ = false; delegateTargets.clear(); + suspends = false; assert(!transfersControlFlow()); } diff --git a/src/ir/js-utils.h b/src/ir/js-utils.h index 105dea499cf..99d71e2388e 100644 --- a/src/ir/js-utils.h +++ b/src/ir/js-utils.h @@ -23,6 +23,18 @@ namespace wasm::JSUtils { +// Whether a field is immutable and a reference to a subtype of externref that +// could hold a JS prototype. +inline bool isPossibleJSPrototypeField(const Field& field) { + if (field.mutable_ != Immutable) { + return false; + } + if (!field.type.isRef()) { + return false; + } + return field.type.getHeapType().isMaybeShared(HeapType::ext); +} + // Whether this is a descriptor struct type whose first field is immutable and a // subtype of externref. inline bool hasPossibleJSPrototypeField(HeapType type) { @@ -34,13 +46,7 @@ inline bool hasPossibleJSPrototypeField(HeapType type) { if (fields.empty()) { return false; } - if (fields[0].mutable_ == Mutable) { - return false; - } - if (!fields[0].type.isRef()) { - return false; - } - return fields[0].type.getHeapType().isMaybeShared(HeapType::ext); + return isPossibleJSPrototypeField(fields[0]); } // Calls flowIn and flowOut on all types that may flow in from or out to JS. diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 9e69405ff7c..905160961c1 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -251,6 +251,7 @@ struct LinearExecutionWalker : public PostWalker { if (!self->connectAdjacentBlocks) { self->pushTask(SubType::doNoteNonLinear, currp); } + self->maybePushTask(SubType::scan, &curr->cast()->desc); self->pushTask(SubType::scan, &curr->cast()->ref); break; } diff --git a/src/ir/manipulation.h b/src/ir/manipulation.h index dbb34edbe66..5058f82b972 100644 --- a/src/ir/manipulation.h +++ b/src/ir/manipulation.h @@ -21,7 +21,7 @@ namespace wasm::ExpressionManipulator { -// Re-use a node's memory. This helps avoid allocation when optimizing. +// Reuse a node's memory. This helps avoid allocation when optimizing. template inline OutputType* convert(InputType* input) { static_assert(sizeof(OutputType) <= sizeof(InputType), diff --git a/src/ir/match.h b/src/ir/match.h index a60bad43235..4edba55ec4e 100644 --- a/src/ir/match.h +++ b/src/ir/match.h @@ -896,6 +896,9 @@ inline decltype(auto) local() { inline decltype(auto) local(Index* binder) { return Internal::LocalGetMatcher(nullptr, Internal::Any(binder)); } +inline decltype(auto) local(LocalGet** binder) { + return Internal::LocalGetMatcher(binder, Internal::Any(nullptr)); +} } // namespace wasm::Match diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index b340ad5f874..67b361fb946 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -156,7 +156,7 @@ 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 + // calls lazily. Do not reuse slots for functions that will already be in // existing tables, since that is not correct in the face of table mutations. // However, do not do this for emscripten; its loader code (and dynamic // loading in particular) do not support this yet. diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 8f3ee597c08..46dd332e102 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -358,7 +358,7 @@ struct TypeInfos { // Multivalue control flow structures need a function type, but the identity // of the function type (i.e. what recursion group it is in or whether it is - // final) doesn't matter. Save them for the end to see if we can re-use an + // final) doesn't matter. Save them for the end to see if we can reuse an // existing function type with the necessary signature. InsertOrderedMap controlFlowSignatures; diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index f752fa10172..381d4e49441 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -1098,6 +1098,7 @@ struct InfoCollector void visitStructWait(StructWait* curr) { addRoot(curr); } void visitWaitqueueNew(WaitqueueNew* curr) { addRoot(curr); } void visitWaitqueueNotify(WaitqueueNotify* curr) { addRoot(curr); } + void visitPublish(Publish* curr) { receiveChildValue(curr->ref, curr); } // Array operations access the array's location, parallel to how structs work. void visitArrayGet(ArrayGet* curr) { if (!isRelevant(curr->ref)) { @@ -1410,8 +1411,12 @@ struct InfoCollector // continuation values. auto numTags = curr->handlerTags.size(); for (Index tagIndex = 0; tagIndex < numTags; tagIndex++) { - auto tag = curr->handlerTags[tagIndex]; auto target = curr->handlerBlocks[tagIndex]; + if (!target) { + // A switch handler does not branch to a target block. + continue; + } + auto tag = curr->handlerTags[tagIndex]; auto params = getModule()->getTag(tag)->params(); // Add the values from the tag. @@ -1771,6 +1776,7 @@ void TNHOracle::scan(Function* func, void visitStructCmpxchg(StructCmpxchg* curr) { notePossibleTrap(curr->ref); } + // TODO: visitStructWait void visitArrayGet(ArrayGet* curr) { notePossibleTrap(curr->ref); } void visitArraySet(ArraySet* curr) { notePossibleTrap(curr->ref); } void visitArrayLoad(ArrayLoad* curr) { notePossibleTrap(curr->ref); } diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index 535f50dace4..a3dc9c45dc9 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -47,9 +47,23 @@ struct GenerativityScanner : public PostWalker { void visitArrayNewElem(ArrayNewElem* curr) { generative = true; } void visitArrayNewFixed(ArrayNewFixed* curr) { generative = true; } void visitContNew(ContNew* curr) { generative = true; } + + // Notifications/waits depend on events on other threads. + void visitAtomicNotify(AtomicNotify* curr) { generative = true; } + void visitWaitqueueNotify(WaitqueueNotify* curr) { generative = true; } + void visitAtomicWait(AtomicWait* curr) { generative = true; } + void visitStructWait(StructWait* curr) { generative = true; } void visitWaitqueueNew(WaitqueueNew* curr) { generative = true; } - // TODO: waitqueue.notify, struct.wait, atomic.notify, and atomic.wait should - // also be generative. + + // Instructions that both read and write memory are generative (as they + // themselves can lead to a different value being returned from identical- + // looking instructions; no other instruction in the middle is needed). + void visitAtomicRMW(AtomicRMW* curr) { generative = true; } + void visitAtomicCmpxchg(AtomicCmpxchg* curr) { generative = true; } + void visitStructRMW(StructRMW* curr) { generative = true; } + void visitStructCmpxchg(StructCmpxchg* curr) { generative = true; } + void visitArrayRMW(ArrayRMW* curr) { generative = true; } + void visitArrayCmpxchg(ArrayCmpxchg* curr) { generative = true; } }; } // anonymous namespace diff --git a/src/ir/properties.h b/src/ir/properties.h index 4b4126d750d..59c35cf9cae 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -316,8 +316,8 @@ inline Expression** getImmediateFallthroughPtr( // know the fallthrough in that case. if (br->condition && br->value && behavior == FallthroughBehavior::AllowTeeBrIf && - EffectAnalyzer::canReorder( - passOptions, module, br->condition, br->value)) { + !EffectAnalyzer::orderedBefore( + passOptions, module, br->value, br->condition)) { return &br->value; } } else if (auto* tryy = curr->dynCast()) { @@ -325,7 +325,10 @@ inline Expression** getImmediateFallthroughPtr( return &tryy->body; } } else if (auto* as = curr->dynCast()) { - return &as->ref; + if (!as->desc || (!EffectAnalyzer::orderedBefore( + passOptions, module, as->ref, as->desc))) { + return &as->ref; + } } else if (auto* as = curr->dynCast()) { // Extern conversions are not casts and actually produce new values. // Treating them as fallthroughs would lead to misoptimizations of @@ -334,7 +337,12 @@ inline Expression** getImmediateFallthroughPtr( return &as->value; } } else if (auto* br = curr->dynCast()) { - return &br->ref; + if (!br->desc || (!EffectAnalyzer::orderedBefore( + passOptions, module, br->ref, br->desc))) { + return &br->ref; + } + } else if (auto* pub = curr->dynCast()) { + return &pub->ref; } return currp; } diff --git a/src/ir/struct-utils.h b/src/ir/struct-utils.h index 471a580a9d4..c90fbb85348 100644 --- a/src/ir/struct-utils.h +++ b/src/ir/struct-utils.h @@ -287,6 +287,8 @@ struct StructScanner : public WalkerPass> { noteExpressionOrCopy(curr->replacement, type, index, info); } + // TODO: visitStructWait + void visitRefCast(RefCast* curr) { if (curr->desc) { // We may try to read a descriptor from anything arriving in |curr->ref|, diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index 2394fdd114a..603a5a6ecfd 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -50,7 +50,7 @@ namespace wasm { // must be a subtype of the signature's // param. // * noteSubtype(Expression, Expression) - An expression's type must be a -// subtype of anothers, for example, +// subtype of another's, for example, // a block and its last child. // // * noteCast(HeapType, Type) - A fixed type is cast to another, for example, @@ -397,6 +397,7 @@ struct SubtypingDiscoverer : public OverriddenVisitor { self()->noteSubtype(curr->waitqueue, Type(HeapTypes::sharedWaitqueue, Nullable)); } + void visitPublish(Publish* curr) {} void visitArrayNew(ArrayNew* curr) { if (!curr->type.isArray() || curr->isWithDefault()) { return; diff --git a/src/ir/trapping.h b/src/ir/trapping.h deleted file mode 100644 index 6272bbf0f3c..00000000000 --- a/src/ir/trapping.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2017 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_ir_trapping_h -#define wasm_ir_trapping_h - -#include - -#include "pass.h" - -namespace wasm { - -enum class TrapMode { Allow, Clamp, JS }; - -inline void addTrapModePass(PassRunner& runner, TrapMode trapMode) { - if (trapMode == TrapMode::Clamp) { - runner.add("trap-mode-clamp"); - } else if (trapMode == TrapMode::JS) { - runner.add("trap-mode-js"); - } -} - -class TrappingFunctionContainer { -public: - TrappingFunctionContainer(TrapMode mode, Module& wasm, bool immediate = false) - : mode(mode), wasm(wasm), immediate(immediate) {} - - bool hasFunction(Name name) { - return functions.find(name) != functions.end(); - } - bool hasImport(Name name) { return imports.find(name) != imports.end(); } - - void addFunction(Function* function) { - functions[function->name] = function; - if (immediate) { - wasm.addFunction(function); - } - } - void addImport(Function* import) { - imports[import->name] = import; - if (immediate) { - wasm.addFunction(import); - } - } - - void addToModule() { - if (!immediate) { - for (auto& [_, func] : functions) { - wasm.addFunction(func); - } - for (auto& [_, func] : imports) { - wasm.addFunction(func); - } - } - functions.clear(); - imports.clear(); - } - - TrapMode getMode() { return mode; } - - Module& getModule() { return wasm; } - - std::map& getFunctions() { return functions; } - -private: - std::map functions; - std::map imports; - - TrapMode mode; - Module& wasm; - bool immediate; -}; - -Expression* makeTrappingBinary(Binary* curr, - TrappingFunctionContainer& trappingFunctions); -Expression* makeTrappingUnary(Unary* curr, - TrappingFunctionContainer& trappingFunctions); - -inline TrapMode trapModeFromString(std::string const& str) { - if (str == "allow") { - return TrapMode::Allow; - } else if (str == "clamp") { - return TrapMode::Clamp; - } else if (str == "js") { - return TrapMode::JS; - } else { - throw std::invalid_argument( - "Unsupported trap mode \"" + str + - "\". " - "Valid modes are \"allow\", \"js\", and \"clamp\""); - } -} - -} // namespace wasm - -#endif // wasm_ir_trapping_h diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index d325e539a9a..2643e3ea3c6 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -78,7 +78,7 @@ updateIndirectCallEffects( // oldType has no entry, which means its effects are explicitly unknown. // Why? It's a source type in `typeMap`, so it must have appeared in // the module at some point, but GlobalEffects were never computed for it, - // or GlobalEffects intentionally ommitted its entry because it couldn't + // or GlobalEffects intentionally omitted its entry because it couldn't // determine its effects (e.g. if an import has that type). newTypes.insert(destType); newTypeEffects.erase(destType); diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index b04996952eb..f9a6240e9b8 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -29,7 +29,9 @@ function i8sToStack(i8s) { function initializeConstants() { // Types - [ ['none', 'None'], + Module['Type'] = {}; + [ + ['none', 'None'], ['i32', 'Int32'], ['i64', 'Int64'], ['f32', 'Float32'], @@ -51,9 +53,11 @@ function initializeConstants() { ['unreachable', 'Unreachable'], ['auto', 'Auto'] ].forEach(entry => { - Module[entry[0]] = Module['_BinaryenType' + entry[1]](); + Module['Type'][entry[0]] = Module['_BinaryenType' + entry[1]](); }); + // Heap types + Module['HeapType'] = {}; [ ['func', 'Func'], ['extern', 'Ext'], @@ -63,24 +67,23 @@ function initializeConstants() { ['struct', 'Struct'], ['array', 'Array'], ['string', 'String'], - /* - TODO: Reconcile with `none` above (line 32). - Maybe keep this as 'none' and change the above to 'void'? ['none', 'None'], - */ ['noextern', 'Noext'], ['nofunc', 'Nofunc'], ['exn', 'Exn'], ['noexn', 'Noexn'], ].forEach(entry => { - Module[entry[0]] = Module['_BinaryenHeapType' + entry[1]](); + Module['HeapType'][entry[0]] = Module['_BinaryenHeapType' + entry[1]](); }); - [ ['notPacked', 'NotPacked'], + // Packed types + Module['PackedType'] = {}; + [ + ['notPacked', 'NotPacked'], ['i8', 'Int8'], ['i16', 'Int16'] ].forEach(entry => { - Module[entry[0]] = Module['_BinaryenPackedType' + entry[1]](); + Module['PackedType'][entry[0]] = Module['_BinaryenPackedType' + entry[1]](); }); // Expression ids @@ -153,6 +156,7 @@ function initializeConstants() { 'StructWait', 'WaitqueueNew', 'WaitqueueNotify', + 'Publish', 'ArrayNew', 'ArrayNewFixed', 'ArrayNewData', @@ -672,6 +676,7 @@ function initializeConstants() { 'Throws', 'DanglingPop', 'TrapsNeverHappen', + 'Suspends', 'Any' ].forEach(name => { Module['SideEffects'][name] = Module['_BinaryenSideEffect' + name](); @@ -712,7 +717,7 @@ function wrapModule(module, self = {}) { return preserveStack(() => Module['_BinaryenBlock'](module, name ? strToStack(name) : 0, i32sToStack(children), children.length, - typeof type !== 'undefined' ? type : Module['none']) + typeof type !== 'undefined' ? type : Module['Type']['none']) ); }; self['if'] = function(condition, ifTrue, ifFalse) { @@ -815,10 +820,10 @@ function wrapModule(module, self = {}) { return preserveStack(() => Module['_BinaryenAtomicNotify'](module, ptr, notifyCount, strToStack(name))); }, 'wait32'(ptr, expected, timeout, name) { - return preserveStack(() => Module['_BinaryenAtomicWait'](module, ptr, expected, timeout, Module['i32'], strToStack(name))); + return preserveStack(() => Module['_BinaryenAtomicWait'](module, ptr, expected, timeout, Module['Type']['i32'], strToStack(name))); }, 'wait64'(ptr, expected, timeout, name) { - return preserveStack(() => Module['_BinaryenAtomicWait'](module, ptr, expected, timeout, Module['i64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenAtomicWait'](module, ptr, expected, timeout, Module['Type']['i64'], strToStack(name))); } } } @@ -831,28 +836,28 @@ function wrapModule(module, self = {}) { self['i32'] = { 'load'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['i32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['Type']['i32'], ptr, strToStack(name))); }, 'load8_s'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 1, true, offset, align, Module['i32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 1, true, offset, align, Module['Type']['i32'], ptr, strToStack(name))); }, 'load8_u'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 1, false, offset, align, Module['i32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 1, false, offset, align, Module['Type']['i32'], ptr, strToStack(name))); }, 'load16_s'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 2, true, offset, align, Module['i32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 2, true, offset, align, Module['Type']['i32'], ptr, strToStack(name))); }, 'load16_u'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 2, false, offset, align, Module['i32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 2, false, offset, align, Module['Type']['i32'], ptr, strToStack(name))); }, 'store'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['i32'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['Type']['i32'], strToStack(name))); }, 'store8'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 1, offset, align, ptr, value, Module['i32'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 1, offset, align, ptr, value, Module['Type']['i32'], strToStack(name))); }, 'store16'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 2, offset, align, ptr, value, Module['i32'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 2, offset, align, ptr, value, Module['Type']['i32'], strToStack(name))); }, 'const'(x) { return preserveStack(() => { @@ -994,152 +999,152 @@ function wrapModule(module, self = {}) { }, 'atomic': { 'load'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 4, offset, Module['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 4, offset, Module['Type']['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'load8_u'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 1, offset, Module['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 1, offset, Module['Type']['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'load16_u'(offset, ptr, name) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 2, offset, Module['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 2, offset, Module['Type']['i32'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store8'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store16'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'rmw': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 4, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 4, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 4, offset, ptr, expected, replacement, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 4, offset, ptr, expected, replacement, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, 'rmw8_u': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 1, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 1, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 1, offset, ptr, expected, replacement, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 1, offset, ptr, expected, replacement, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, 'rmw16_u': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 2, offset, ptr, value, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 2, offset, ptr, value, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 2, offset, ptr, expected, replacement, Module['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 2, offset, ptr, expected, replacement, Module['Type']['i32'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, }, 'pop'() { - return Module['_BinaryenPop'](module, Module['i32']); + return Module['_BinaryenPop'](module, Module['Type']['i32']); } }; self['i64'] = { 'load'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 8, true, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 8, true, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load8_s'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 1, true, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 1, true, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load8_u'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 1, false, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 1, false, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load16_s'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 2, true, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 2, true, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load16_u'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 2, false, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 2, false, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load32_s'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'load32_u'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 4, false, offset, align, Module['i64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 4, false, offset, align, Module['Type']['i64'], ptr, strToStack(name))); }, 'store'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 8, offset, align, ptr, value, Module['i64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 8, offset, align, ptr, value, Module['Type']['i64'], strToStack(name))); }, 'store8'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 1, offset, align, ptr, value, Module['i64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 1, offset, align, ptr, value, Module['Type']['i64'], strToStack(name))); }, 'store16'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 2, offset, align, ptr, value, Module['i64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 2, offset, align, ptr, value, Module['Type']['i64'], strToStack(name))); }, 'store32'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['i64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['Type']['i64'], strToStack(name))); }, 'const'(x, y = undefined) { return preserveStack(() => { @@ -1306,161 +1311,161 @@ function wrapModule(module, self = {}) { }, 'atomic': { 'load'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 8, offset, Module['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 8, offset, Module['Type']['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'load8_u'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 1, offset, Module['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 1, offset, Module['Type']['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'load16_u'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 2, offset, Module['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 2, offset, Module['Type']['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'load32_u'(offset, ptr, name, order) { - return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 4, offset, Module['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicLoad'](module, 4, offset, Module['Type']['i64'], ptr, strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store8'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store16'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'store32'(offset, ptr, value, name, order) { - return preserveStack(() => Module['_BinaryenAtomicStore'](module, 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + return preserveStack(() => Module['_BinaryenAtomicStore'](module, 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'rmw': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 8, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 8, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 8, offset, ptr, expected, replacement, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 8, offset, ptr, expected, replacement, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, 'rmw8_u': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 1, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 1, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 1, offset, ptr, expected, replacement, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 1, offset, ptr, expected, replacement, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, 'rmw16_u': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 2, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 2, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 2, offset, ptr, expected, replacement, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 2, offset, ptr, expected, replacement, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, 'rmw32_u': { 'add'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAdd'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'sub'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWSub'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'and'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWAnd'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'or'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWOr'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xor'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXor'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'xchg'(offset, ptr, value, name, order) { return preserveStack(() => - Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 4, offset, ptr, value, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicRMW'](module, Module['AtomicRMWXchg'], 4, offset, ptr, value, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, 'cmpxchg'(offset, ptr, expected, replacement, name, order) { return preserveStack(() => - Module['_BinaryenAtomicCmpxchg'](module, 4, offset, ptr, expected, replacement, Module['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); + Module['_BinaryenAtomicCmpxchg'](module, 4, offset, ptr, expected, replacement, Module['Type']['i64'], strToStack(name), typeof order !== 'undefined' ? order : Module['MemoryOrder']['seqcst'])); }, }, }, 'pop'() { - return Module['_BinaryenPop'](module, Module['i64']); + return Module['_BinaryenPop'](module, Module['Type']['i64']); } }; self['f32'] = { 'load'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['f32'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 4, true, offset, align, Module['Type']['f32'], ptr, strToStack(name))); }, 'store'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['f32'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 4, offset, align, ptr, value, Module['Type']['f32'], strToStack(name))); }, 'const'(x) { return preserveStack(() => { @@ -1559,16 +1564,16 @@ function wrapModule(module, self = {}) { return Module['_BinaryenBinary'](module, Module['GeFloat32'], left, right); }, 'pop'() { - return Module['_BinaryenPop'](module, Module['f32']); + return Module['_BinaryenPop'](module, Module['Type']['f32']); } }; self['f64'] = { 'load'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 8, true, offset, align, Module['f64'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 8, true, offset, align, Module['Type']['f64'], ptr, strToStack(name))); }, 'store'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 8, offset, align, ptr, value, Module['f64'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 8, offset, align, ptr, value, Module['Type']['f64'], strToStack(name))); }, 'const'(x) { return preserveStack(() => { @@ -1672,13 +1677,13 @@ function wrapModule(module, self = {}) { return Module['_BinaryenBinary'](module, Module['GeFloat64'], left, right); }, 'pop'() { - return Module['_BinaryenPop'](module, Module['f64']); + return Module['_BinaryenPop'](module, Module['Type']['f64']); } }; self['v128'] = { 'load'(offset, align, ptr, name) { - return preserveStack(() => Module['_BinaryenLoad'](module, 16, false, offset, align, Module['v128'], ptr, strToStack(name))); + return preserveStack(() => Module['_BinaryenLoad'](module, 16, false, offset, align, Module['Type']['v128'], ptr, strToStack(name))); }, 'load8_splat'(offset, align, ptr, name) { return preserveStack(() => Module['_BinaryenSIMDLoad'](module, Module['Load8SplatVec128'], offset, align, ptr, strToStack(name))); @@ -1749,7 +1754,7 @@ function wrapModule(module, self = {}) { Module['_BinaryenSIMDLoadStoreLane'](module, Module['Store64LaneVec128'], offset, align, index, ptr, vec, strToStack(name))); }, 'store'(offset, align, ptr, value, name) { - return preserveStack(() => Module['_BinaryenStore'](module, 16, offset, align, ptr, value, Module['v128'], strToStack(name))); + return preserveStack(() => Module['_BinaryenStore'](module, 16, offset, align, ptr, value, Module['Type']['v128'], strToStack(name))); }, 'const'(i8s) { return preserveStack(() => { @@ -1780,7 +1785,7 @@ function wrapModule(module, self = {}) { return Module['_BinaryenSIMDTernary'](module, Module['BitselectVec128'], left, right, cond); }, 'pop'() { - return Module['_BinaryenPop'](module, Module['v128']); + return Module['_BinaryenPop'](module, Module['Type']['v128']); } }; @@ -2422,49 +2427,49 @@ function wrapModule(module, self = {}) { self['funcref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['funcref']); + return Module['_BinaryenPop'](module, Module['Type']['funcref']); } }; self['externref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['externref']); + return Module['_BinaryenPop'](module, Module['Type']['externref']); } }; self['anyref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['anyref']); + return Module['_BinaryenPop'](module, Module['Type']['anyref']); } }; self['eqref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['eqref']); + return Module['_BinaryenPop'](module, Module['Type']['eqref']); } }; self['i31ref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['i31ref']); + return Module['_BinaryenPop'](module, Module['Type']['i31ref']); } }; self['structref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['structref']); + return Module['_BinaryenPop'](module, Module['Type']['structref']); } }; self['arrayref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['arrayref']); + return Module['_BinaryenPop'](module, Module['Type']['arrayref']); } }; self['stringref'] = { 'pop'() { - return Module['_BinaryenPop'](module, Module['stringref']); + return Module['_BinaryenPop'](module, Module['Type']['stringref']); } }; @@ -2583,11 +2588,11 @@ function wrapModule(module, self = {}) { }; self['br_on_null'] = function(name, value) { - return preserveStack(() => Module['_BinaryenBrOn'](module, Module['BrOnNull'], strToStack(name), value, Module['unreachable'])); + return preserveStack(() => Module['_BinaryenBrOn'](module, Module['BrOnNull'], strToStack(name), value, Module['Type']['unreachable'])); }; self['br_on_non_null'] = function(name, value) { - return preserveStack(() => Module['_BinaryenBrOn'](module, Module['BrOnNonNull'], strToStack(name), value, Module['unreachable'])); + return preserveStack(() => Module['_BinaryenBrOn'](module, Module['BrOnNonNull'], strToStack(name), value, Module['Type']['unreachable'])); }; self['br_on_cast'] = function(name, value, castType) { @@ -2627,6 +2632,10 @@ function wrapModule(module, self = {}) { } }; + self['publish'] = function(ref) { + return Module['_BinaryenPublish'](module, ref); + }; + self['array'] = { 'new'(type, size, init) { return Module['_BinaryenArrayNew'](module, type, size, init); @@ -3236,11 +3245,11 @@ Module['getExpressionInfo'] = function(expr) { switch (id) { case Module['ConstId']: switch (type) { - case Module['i32']: info.value = Module['_BinaryenConstGetValueI32'](expr); break; - case Module['i64']: info.value = Module['_BinaryenConstGetValueI64'](expr); break; - case Module['f32']: info.value = Module['_BinaryenConstGetValueF32'](expr); break; - case Module['f64']: info.value = Module['_BinaryenConstGetValueF64'](expr); break; - case Module['v128']: { + case Module['Type']['i32']: info.value = Module['_BinaryenConstGetValueI32'](expr); break; + case Module['Type']['i64']: info.value = Module['_BinaryenConstGetValueI64'](expr); break; + case Module['Type']['f32']: info.value = Module['_BinaryenConstGetValueF32'](expr); break; + case Module['Type']['f64']: info.value = Module['_BinaryenConstGetValueF64'](expr); break; + case Module['Type']['v128']: { preserveStack(() => { const tempBuffer = stackAlloc(16); Module['_BinaryenConstGetValueV128'](expr, tempBuffer); @@ -5012,6 +5021,15 @@ Module['WaitqueueNotify'] = makeExpressionWrapper(Module['_BinaryenWaitqueueNoti } }); +Module['Publish'] = makeExpressionWrapper(Module['_BinaryenPublishId'](), { + 'getRef'(expr) { + return Module['_BinaryenPublishGetRef'](expr); + }, + 'setRef'(expr, refExpr) { + Module['_BinaryenPublishSetRef'](expr, refExpr); + } +}); + Module['ArrayNew'] = makeExpressionWrapper(Module['_BinaryenArrayNewId'](), { 'getInit'(expr) { return Module['_BinaryenArrayNewGetInit'](expr); diff --git a/src/literal.h b/src/literal.h index adb8e2f171e..335e117c147 100644 --- a/src/literal.h +++ b/src/literal.h @@ -19,6 +19,7 @@ #include #include +#include #include "support/bits.h" #include "support/hash.h" @@ -212,7 +213,7 @@ class Literal { } } - static Literal makeFromMemory(void* p, Type type); + static Literal makeFromMemory(const void* p, Type type); static Literal makeSignedMin(Type type) { switch (type.getBasic()) { @@ -312,6 +313,12 @@ class Literal { Name getFunc() const; std::shared_ptr getFuncData() const; std::shared_ptr getGCData() const; + size_t getNumElements() const; + Literal getElement(size_t index, bool signed_ = false) const; + void setElement(size_t index, Literal value); + bool isRawBytes() const; + const std::vector& getRawBytes() const; + std::vector& getRawBytes(); std::shared_ptr getExnData() const; std::shared_ptr getContData() const; @@ -784,28 +791,65 @@ std::ostream& operator<<(std::ostream& o, wasm::Literals literals); // A GC Struct, Array, or String is a set of values with a type saying how it // should be interpreted. struct GCData { - // The element or field values. - Literals values; + // The element or field values. Primitive numeric arrays use raw byte buffers + // (std::vector), while reference arrays, structs, strings, and other + // reference allocations use Literals. + std::variant, Literals> storage; // The descriptor, if it exists, or null. Literal desc; GCData(Literals&& values, const Literal& desc = Literal::makeNull(HeapType::none)) - : values(std::move(values)), desc(desc) {} + : storage(std::move(values)), desc(desc) {} + + GCData(std::vector&& data, + const Literal& desc = Literal::makeNull(HeapType::none)) + : storage(std::move(data)), desc(desc) {} + + bool isRawBytes() const { + return std::holds_alternative>(storage); + } + + const std::vector& getRawBytes() const { + return std::get>(storage); + } + + std::vector& getRawBytes() { + return std::get>(storage); + } + + const Literals& getLiterals() const { return std::get(storage); } + + Literals& getLiterals() { return std::get(storage); } }; +inline bool Literal::isRawBytes() const { + assert(isData()); + return gcData->isRawBytes(); +} + +inline const std::vector& Literal::getRawBytes() const { + assert(isData()); + return gcData->getRawBytes(); +} + +inline std::vector& Literal::getRawBytes() { + assert(isData()); + return gcData->getRawBytes(); +} + inline bool Literal::hasExternPayload() const { if (isNull()) { return false; } assert(type.getHeapType().isMaybeShared(HeapType::ext)); - return gcData->values[0].type == Type::i32; + return gcData->getLiterals()[0].type == Type::i32; } inline int32_t Literal::getExternPayload() const { assert(hasExternPayload()); - return gcData->values[0].geti32(); + return gcData->getLiterals()[0].geti32(); } } // namespace wasm @@ -869,7 +913,7 @@ template<> struct hash { return digest; } if (a.type.isString()) { - auto& values = a.getGCData()->values; + auto& values = a.getGCData()->getLiterals(); wasm::rehash(digest, values.size()); for (auto c : values) { wasm::rehash(digest, c.getInteger()); diff --git a/src/parser/contexts.h b/src/parser/contexts.h index 6f0d38f9cd4..a0adea5708d 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -638,6 +638,7 @@ struct NullInstrParserCtx { return Ok{}; } Result<> makePause(Index, const std::vector&) { return Ok{}; } + Result<> makePublish(Index, const std::vector&) { return Ok{}; } Result<> makeSIMDExtract(Index, const std::vector&, SIMDExtractOp, @@ -2506,6 +2507,10 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { return withLoc(pos, irBuilder.makePause()); } + Result<> makePublish(Index pos, const std::vector& annotations) { + return withLoc(pos, irBuilder.makePublish()); + } + Result<> makeSIMDExtract(Index pos, const std::vector& annotations, SIMDExtractOp op, diff --git a/src/parser/parsers.h b/src/parser/parsers.h index 090775647f7..2bf3acbf951 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -155,6 +155,8 @@ makeAtomicFence(Ctx&, Index, const std::vector&, MemoryOrder); template Result<> makePause(Ctx&, Index, const std::vector&); template +Result<> makePublish(Ctx&, Index, const std::vector&); +template Result<> makeSIMDExtract( Ctx&, Index, const std::vector&, SIMDExtractOp op, size_t lanes); template @@ -1995,6 +1997,12 @@ makePause(Ctx& ctx, Index pos, const std::vector& annotations) { return ctx.makePause(pos, annotations); } +template +Result<> +makePublish(Ctx& ctx, Index pos, const std::vector& annotations) { + return ctx.makePublish(pos, annotations); +} + template Result<> makeSIMDExtract(Ctx& ctx, Index pos, diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 41ccdef82bd..ab08d3a410b 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -110,6 +110,7 @@ set(passes_SOURCES TraceCalls.cpp RandomizeBranchHints.cpp RedundantSetElimination.cpp + RemoveEmptyFunctionExports.cpp RemoveExports.cpp RemoveImports.cpp RemoveMemoryInit.cpp @@ -124,7 +125,6 @@ set(passes_SOURCES ReorderLocals.cpp ReorderTypes.cpp ReReloop.cpp - TrapMode.cpp TypeGeneralizing.cpp TypeRefining.cpp TypeMerging.cpp @@ -138,6 +138,7 @@ set(passes_SOURCES StackCheck.cpp StripEH.cpp SSAify.cpp + TailCall.cpp TupleOptimization.cpp TranslateEH.cpp TypeFinalizing.cpp diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp index fff4e4b8ed3..9e275665e9e 100644 --- a/src/passes/ConstraintAnalysis.cpp +++ b/src/passes/ConstraintAnalysis.cpp @@ -66,6 +66,8 @@ // function analysis). // +#include + #include "cfg/cfg-traversal.h" #include "ir/constraint.h" #include "ir/drop.h" @@ -146,11 +148,12 @@ struct ConstraintAnalysis void maybeMarkRelevant(Expression* curr) { // If this parses into a constraint on a local, that local is relevant. - if (auto parsed = LocalConstraint::parseCondition(curr); - parsed && isRelevantType(getFunction()->getLocalType(parsed->local))) { - relevantLocals[parsed->local] = true; - if (auto* other = std::get_if(&parsed->constraint.term)) { - relevantLocals[*other] = true; + for (auto& pair : ParsedAndedConstraints::parseCondition(curr)) { + if (isRelevantType(getFunction()->getLocalType(pair.local))) { + relevantLocals[pair.local] = true; + if (auto* other = std::get_if(&pair.constraint.term)) { + relevantLocals[*other] = true; + } } } } @@ -371,9 +374,9 @@ struct ConstraintAnalysis // Find the constraints sent to this specific successor, if there is a // branch, and use them. if (auto branch = getBranchConstraints(block, out); - branch && checkRelevancy(*branch)) { + filterRelevant(branch), !branch.empty()) { auto sentConstraints = constraints; - applyBranchConstraints(*branch, sentConstraints); + applyBranchConstraints(branch, sentConstraints); #if CONSTRAINT_DEBUG std::cout << block << " sending branch to " << out << " with sent constraints: " << sentConstraints << '\n'; @@ -441,6 +444,9 @@ struct ConstraintAnalysis void optimizeExpression(Expression** currp, const BasicBlockConstraintMap& constraints) { auto* curr = *currp; + // Note that we don't need to try to parse a series of constraints with + // ParsedAndedConstraints: if there is a tree of ANDed things, we will + // simply optimize it as we walk it, each time handling one. auto parsed = LocalConstraint::parse(curr); if (!parsed) { return; @@ -472,8 +478,8 @@ struct ConstraintAnalysis // Given a predecessor and one of its successors, find new constraints that // can be added due to the flow to that specific successor. - std::optional getBranchConstraints(BasicBlock* pred, - BasicBlock* succ) { + ParsedAndedConstraints getBranchConstraints(BasicBlock* pred, + BasicBlock* succ) { auto* brancher = pred->contents.brancher; if (!brancher) { return {}; @@ -502,32 +508,31 @@ struct ConstraintAnalysis return {}; } - std::optional getConstraintsFromIf(If* iff, - bool physicalSuccessor) { - auto parsed = LocalConstraint::parseCondition(iff->condition); - if (parsed && !physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromIf(If* iff, bool physicalSuccessor) { + auto parsed = ParsedAndedConstraints::parseCondition(iff->condition); + if (!physicalSuccessor) { // We are in the ifFalse, so negate the condition. - parsed->constraint = parsed->constraint.negate(); + parsed.negate(); } return parsed; } - std::optional - getConstraintsFromBreak(Break* br, bool physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromBreak(Break* br, + bool physicalSuccessor) { // We get here when there is more than one successor, so there must be a // condition. assert(br->condition); - auto parsed = LocalConstraint::parseCondition(br->condition); - if (parsed && physicalSuccessor) { + auto parsed = ParsedAndedConstraints::parseCondition(br->condition); + if (physicalSuccessor) { // The branch was not taken, so negate the condition. - parsed->constraint = parsed->constraint.negate(); + parsed.negate(); } return parsed; } - std::optional - getConstraintsFromBrOn(BrOn* brOn, bool physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromBrOn(BrOn* brOn, + bool physicalSuccessor) { // The constraint on that local depends on the op. // TODO: Handle BrOnCast* etc using subtyping operations. if (brOn->op != BrOnNull && brOn->op != BrOnNonNull) { @@ -537,10 +542,10 @@ struct ConstraintAnalysis // parseCondition can parse more things than a local.get, which is all we // handle here, but there is no other valid IR that can appear there, so we // can reuse it. - auto parsed = LocalConstraint::parseCondition(brOn->ref); + auto parsed = ParsedAndedConstraints::parseCondition(brOn->ref); // Negate depending on the op and (similar to Break) the successor. - if (parsed && ((brOn->op == BrOnNull) ^ physicalSuccessor)) { - parsed->constraint = parsed->constraint.negate(); + if ((brOn->op == BrOnNull) ^ physicalSuccessor) { + parsed.negate(); } return parsed; } @@ -568,14 +573,6 @@ struct ConstraintAnalysis return; } - // See above on binary action counting limits. - if (auto* binary = set->value->dynCast()) { - if (binaryActionCounts[binary]++ >= MaxBinaryActions) { - constraints.setProvesNothing(set->index); - return; - } - } - // Look at the fallthrough. It is valid to do so, because our constraints // only track two things, constants and locals. For a constant, it does // not change while falling through. For a local, the only way for the @@ -600,8 +597,47 @@ struct ConstraintAnalysis // opportunity to write any other value while falling through. (And, any // local.tee appearing here would have been reached earlier in the // traversal, and handled.) - auto* value = - Properties::getFallthrough(set->value, getPassOptions(), *getModule()); + auto* value = set->value; + while (1) { + if (value->is()) { + // We stop at the first tee: we don't need to look any further, and + // will just apply that local's values to ourselves, saving repeated + // work. + break; + } + auto* next = Properties::getImmediateFallthrough( + value, getPassOptions(), *getModule()); + if (value == next) { + break; + } else { + value = next; + } + } + + // Now that we know the value, check binary action counting limits (see + // above). + if (auto* binary = value->dynCast()) { + // The code below will stop calculating this binary once we pass + // MaxBinaryActions operations on it. That is enough to prevent + // unbounded work on this binary, however, we may end up reaching this + // basic block an even larger number of times for other reasons, i.e., + // just because of a very complex CFG. That should be very rare, but can + // happen. In debug builds we check we do not exceed a very high limit + // there, intending to throw an assert rather than just hang in the case + // of a bug (as assert is easier to diagnose, even if it happens after a + // long delay). + auto& count = binaryActionCounts[binary]; +#ifndef NDEBUG + static const Index MaxBasicBlockActions = 1024 * 1024; + assert(count < MaxBasicBlockActions); +#endif + count++; + if (count >= MaxBinaryActions) { + constraints.setProvesNothing(set->index); + return; + } + } + constraints.set(set->index, value); } } @@ -633,17 +669,31 @@ struct ConstraintAnalysis return true; } + // Filters out constraints on irrelevant locals. + void filterRelevant(ParsedAndedConstraints& parsed) { + parsed.erase(std::remove_if(parsed.begin(), + parsed.end(), + [&](const LocalConstraint& pair) { + return !checkRelevancy(pair); + }), + parsed.end()); + } + // Apply branch constraints to the current set of constraints. - void applyBranchConstraints(const LocalConstraint& branch, + void applyBranchConstraints(const ParsedAndedConstraints& branch, BasicBlockConstraintMap& constraints) { - // Extend the range of values in the "jump ahead" manner described in the - // top-level comment. - if (applyBranchRangeExtensionToConstraints(branch, constraints)) { - return; - } + for (auto& pair : branch) { + // Extend the range of values in the "jump ahead" manner described in the + // top-level comment. + if (!applyBranchRangeExtensionToConstraints(pair, constraints)) { + // Otherwise, apply the constraint normally. + constraints.approximateAnd(pair.local, pair.constraint); + } - // Otherwise, apply the constraint normally. - constraints.approximateAnd(branch.local, branch.constraint); + if (constraints.unreachable) { + return; + } + } } bool diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 4793789373a..4597ae76ef5 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -158,7 +158,7 @@ struct FunctionInfo { std::unordered_map forwardedToIndirectParams; // Locations forwarded to this function's result. These locations will become - // used if the result turns out ot be used. + // used if the result turns out to be used. std::vector resultSources; // For each parameter of this function, the list of locations that will become diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index f3313700e8e..e4e95a833f4 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -138,10 +138,12 @@ std::map analyzeFuncs(Module& module, // 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. + // Clear throws and suspends as well, as we are "forgetting" calls right + // now, and want to forget their throwing and suspending effects as + // well. If we see something else that throws or suspends, below, then + // we'll note that there. funcInfo.effects->throws_ = false; + funcInfo.effects->suspends = false; struct CallScanner : public PostWalker analyzeFuncs(Module& module, 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.) + // No call here, but update throwing and suspending 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; } + if (effects.suspends && funcInfo.effects) { + funcInfo.effects->suspends = true; + } } } }; diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 46eb698eaa4..b0bb31edbd9 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -70,16 +70,20 @@ struct FieldInfo { struct FieldInfoScanner : public StructUtils::StructScanner { + std::unordered_map>& jsExposedTypes; + std::unique_ptr create() override { - return std::make_unique(functionNewInfos, - functionSetGetInfos); + return std::make_unique( + functionNewInfos, functionSetGetInfos, jsExposedTypes); } FieldInfoScanner( StructUtils::FunctionStructValuesMap& functionNewInfos, - StructUtils::FunctionStructValuesMap& functionSetGetInfos) + StructUtils::FunctionStructValuesMap& functionSetGetInfos, + std::unordered_map>& jsExposedTypes) : StructUtils::StructScanner( - functionNewInfos, functionSetGetInfos) {} + functionNewInfos, functionSetGetInfos), + jsExposedTypes(jsExposedTypes) {} void noteExpression(Expression* expr, HeapType type, @@ -116,16 +120,8 @@ struct FieldInfoScanner // Converting a reference to externref makes the prototype field on its // descriptor available to be read by JS, if such a field exists. void visitRefAs(RefAs* curr) { - if (curr->op != ExternConvertAny) { - return; - } - if (!curr->value->type.isRef()) { - return; - } - if (auto desc = curr->value->type.getHeapType().getDescriptorType(); - desc && JSUtils::hasPossibleJSPrototypeField(*desc)) { - auto exact = curr->value->type.getExactness(); - functionSetGetInfos[getFunction()][{*desc, exact}][0].noteRead(); + if (curr->op == ExternConvertAny && curr->value->type.isRef()) { + jsExposedTypes.at(getFunction()).push_back(curr->value->type); } } }; @@ -139,6 +135,11 @@ struct GlobalTypeOptimization : public Pass { // rare). std::unordered_map> canBecomeImmutable; + // Descriptor types that are exposed to JS but do _not_ configure prototypes + // for their described types. We must avoid optimizing these types such that + // they start configuring prototypes. + std::unordered_set exposedNoProtoDescs; + // Maps each field to its new index after field removals. That is, this // takes into account that fields before this one may have been removed, // which would then reduce this field's index. If a field itself is removed, @@ -149,6 +150,32 @@ struct GlobalTypeOptimization : public Pass { static const Index RemovedField = Index(-1); std::unordered_map> indexesAfterRemovals; + struct IndexAnalysis { + // The size after removing fields and possibly adding a placeholder. + Index newSize = 0; + bool hasPlaceholder = false; + + // `indexes` is the mapping from old to new indices. + IndexAnalysis(const std::vector& indexes) { + Index maxIndex = 0; + bool hasKept = false; + bool hasIndexZero = false; + for (auto idx : indexes) { + if (idx != RemovedField) { + hasKept = true; + maxIndex = std::max(maxIndex, idx); + if (idx == 0) { + hasIndexZero = true; + } + } + } + newSize = hasKept ? maxIndex + 1 : 0; + // We know there is a placeholder if we have fields but none of them are + // at index 0. + hasPlaceholder = hasKept && !hasIndexZero; + } + }; + void run(Module* module) override { if (!module->features.hasGC()) { return; @@ -157,21 +184,32 @@ struct GlobalTypeOptimization : public Pass { Fatal() << "GTO requires --closed-world"; } + std::unordered_map> jsExposedTypesByFunction; + jsExposedTypesByFunction[nullptr]; + for (auto& func : module->functions) { + jsExposedTypesByFunction[func.get()]; + } + // Find and analyze struct operations inside each function. StructUtils::FunctionStructValuesMap functionNewInfos(*module), functionSetGetInfos(*module); - FieldInfoScanner scanner(functionNewInfos, functionSetGetInfos); + FieldInfoScanner scanner( + functionNewInfos, functionSetGetInfos, jsExposedTypesByFunction); scanner.run(getPassRunner(), module); scanner.runOnModuleCode(getPassRunner(), module); // Combine the data from the functions. functionSetGetInfos.combineInto(combinedSetGetInfos); + std::vector jsExposedTypes; + for (auto& [_, types] : jsExposedTypesByFunction) { + jsExposedTypes.insert(jsExposedTypes.end(), types.begin(), types.end()); + } SubTypes subTypes(*module); // Analyze the JS interface to find fields holding configured prototypes // that cannot be removed. - analyzeJSInterface(*module, subTypes); + analyzeJSInterface(*module, subTypes, jsExposedTypes); // Propagate information to super and subtypes on set/get infos: // @@ -291,15 +329,18 @@ struct GlobalTypeOptimization : public Pass { } // We need to compute the new set of indexes if we are removing fields, or - // if our parent removed fields. In the latter case, our parent may have - // reordered fields even if we ourselves are not removing anything, and we - // must update to match the parent's order. + // if our parent removed fields, or if we might need a placeholder because + // this type is exposed outside the module and does not configure a JS + // prototype. If we have a parent, it may have reordered fields even if we + // ourselves are not removing anything, and we must update to match the + // parent's order. auto super = type.getDeclaredSuperType(); auto superHasUpdates = super && indexesAfterRemovals.contains(*super); - if (!removableIndexes.empty() || superHasUpdates) { - // We are removing fields. Reorder them to allow that, as in the general - // case we can only remove fields from the end, so that if our subtypes - // still need the fields they can append them. For example: + bool isExposedNoProto = exposedNoProtoDescs.contains(type); + if (!removableIndexes.empty() || superHasUpdates || isExposedNoProto) { + // We might be removing fields. Reorder them to allow that, as in the + // general case we can only remove fields from the end, so that if our + // subtypes still need the fields they can append them. For example: // // type A = { x: i32, y: f64 }; // type B : A = { x: 132, y: f64, z: v128 }; @@ -392,6 +433,38 @@ struct GlobalTypeOptimization : public Pass { } } + // If the type has no supertype (or its supertype has no fields), check + // if its first field becomes prototype-exposing. If so, add a + // placeholder at index 0 and shift all computed indices. + if (isExposedNoProto && (!super || super->getStruct().fields.empty())) { + // Find the field that will become field 0. + Index i = 0; + for (; i < fields.size(); ++i) { + if (indexesAfterRemoval[i] == 0) { + break; + } + } + // Check whether that field would expose a prototype. + if (i < fields.size()) { + Field optimizedField = fields[i]; + if (auto it = canBecomeImmutable.find(type); + it != canBecomeImmutable.end() && i < it->second.size() && + it->second[i]) { + optimizedField.mutable_ = Immutable; + } + if (JSUtils::isPossibleJSPrototypeField(optimizedField)) { + // The field exposes a prototype. Increment all field indices to + // make room for a placeholder first field (which will be + // materialized as an i8 field later). + for (auto& idx : indexesAfterRemoval) { + if (idx != RemovedField) { + ++idx; + } + } + } + } + } + // Only store the new indexes we computed if we found something // interesting. We might not, if e.g. our parent removes fields and we // add them back in the exact order we started with. In such cases, @@ -416,7 +489,9 @@ struct GlobalTypeOptimization : public Pass { } } - void analyzeJSInterface(Module& wasm, const SubTypes& subTypes) { + void analyzeJSInterface(Module& wasm, + const SubTypes& subTypes, + const std::vector& jsExposedTypes) { if (!wasm.features.hasCustomDescriptors()) { return; } @@ -426,10 +501,16 @@ struct GlobalTypeOptimization : public Pass { // Mark the relevant prototype field as read and return true iff we newly // 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)) { - // This field holds a JS-visible prototype. Do not remove it. - combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead(); + if (auto desc = type.getDescriptorType()) { + if (JSUtils::hasPossibleJSPrototypeField(*desc)) { + // This descriptor configures a JS-visible prototype. Do not remove + // it. + combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead(); + } else { + // This descriptor does _not_ configure a JS prototype. Do not add + // one. + exposedNoProtoDescs.insert(*desc); + } } if (exact == Inexact) { return subtypesExposed.insert(type).second; @@ -449,6 +530,12 @@ struct GlobalTypeOptimization : public Pass { JSUtils::iterJSInterface(wasm, flowIn, flowOut); + for (auto type : jsExposedTypes) { + if (type.isRef()) { + noteExposed(type.getHeapType(), type.getExactness()); + } + } + // Any type that is a subtype of an exposed type is also exposed. Propagate // from supertypes to subtypes. std::vector work(subtypesExposed.begin(), subtypesExposed.end()); @@ -471,6 +558,20 @@ struct GlobalTypeOptimization : public Pass { } } } + + // Also propagate lack of exposed descriptors to supertypes so that + // descriptor hierarchies have consistent layouts. Do not propagate to + // supertypes that actually expose a prototype, which can happen when the + // subtype has refined an externref field to a nullexternref. + for (auto type : subTypes.types) { + if (exposedNoProtoDescs.contains(type)) { + auto curr = type.getDeclaredSuperType(); + while (curr && !JSUtils::hasPossibleJSPrototypeField(*curr)) { + exposedNoProtoDescs.insert(*curr); + curr = curr->getDeclaredSuperType(); + } + } + } } void updateTypes(Module& wasm) { @@ -499,17 +600,19 @@ struct GlobalTypeOptimization : public Pass { auto remIter = parent.indexesAfterRemovals.find(oldStructType); if (remIter != parent.indexesAfterRemovals.end()) { auto& indexesAfterRemoval = remIter->second; - Index removed = 0; + IndexAnalysis analysis(indexesAfterRemoval); auto copy = newFields; - for (Index i = 0; i < newFields.size(); i++) { + newFields.resize(analysis.newSize); + if (analysis.hasPlaceholder) { + newFields[0] = Field(Field::i8, Immutable); + } + for (Index i = 0; i < copy.size(); i++) { auto newIndex = indexesAfterRemoval[i]; if (newIndex != RemovedField) { + assert(newIndex < newFields.size()); newFields[newIndex] = copy[i]; - } else { - removed++; } } - newFields.resize(newFields.size() - removed); // Update field names as well. The Type Rewriter cannot do this for // us, as it does not know which old fields map to which new ones (it @@ -540,8 +643,26 @@ struct GlobalTypeOptimization : public Pass { TypeRewriter(wasm, *this).update(); } + Index getNewIndex(HeapType type, Index index) { + auto iter = indexesAfterRemovals.find(type); + if (iter == indexesAfterRemovals.end()) { + return index; + } + auto& indexesAfterRemoval = iter->second; + auto newIndex = indexesAfterRemoval[index]; + assert(newIndex < IndexAnalysis(indexesAfterRemoval).newSize || + newIndex == RemovedField); + return newIndex; + } + // After updating the types to remove certain fields, we must also remove - // them from struct instructions. + // them from struct instructions and update field indices. We do this in two + // passes: first FieldRemover removes/reorders StructNew operands and replaces + // removed StructSets (which invokes EffectAnalyzer via ChildLocalizer and + // getResultOfFirst), and then IndexUpdater updates the field indices on + // remaining struct instructions. Keeping the old indices during FieldRemover + // is essential because EffectAnalyzer inspects type.getStruct().fields[index] + // on the old HeapTypes, which requires old indices. void updateInstructions(Module& wasm) { struct FieldRemover : public WalkerPass> { bool isFunctionParallel() override { return true; } @@ -595,26 +716,31 @@ struct GlobalTypeOptimization : public Pass { auto& operands = curr->operands; assert(indexesAfterRemoval.size() == operands.size()); - Index removed = 0; + IndexAnalysis analysis(indexesAfterRemoval); std::vector old(operands.begin(), operands.end()); for (Index i = 0; i < operands.size(); ++i) { - auto newIndex = indexesAfterRemoval[i]; - if (newIndex != RemovedField) { - assert(newIndex < operands.size()); - operands[newIndex] = old[i]; - } else { - ++removed; + if (indexesAfterRemoval[i] == RemovedField) { if (!func && EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) { removedTrappingInits.push_back(old[i]); } } } - if (removed) { - operands.resize(operands.size() - removed); - } else { - // If we didn't remove anything then we must have reordered (or else - // we have done pointless work). + operands.resize(analysis.newSize); + if (analysis.hasPlaceholder) { + // The value we put in the i8 placeholder does not matter. + operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0))); + } + for (Index i = 0; i < old.size(); ++i) { + auto newIndex = indexesAfterRemoval[i]; + if (newIndex != RemovedField) { + assert(newIndex < operands.size()); + operands[newIndex] = old[i]; + } + } + if (analysis.newSize == old.size() && !analysis.hasPlaceholder) { + // If we didn't remove or insert anything then we must have reordered + // (or else we have done pointless work). assert(indexesAfterRemoval != makeIdentity(indexesAfterRemoval.size())); } @@ -625,11 +751,9 @@ struct GlobalTypeOptimization : public Pass { return; } - auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index); - if (newIndex != RemovedField) { - // Map to the new index. - curr->index = newIndex; - } else { + auto newIndex = + parent.getNewIndex(curr->ref->type.getHeapType(), curr->index); + if (newIndex == RemovedField) { // This field was removed, so just emit drops of our children, plus a // trap if the ref is null. Note that we must preserve the order of // operations here: the trap on a null ref happens after the value, @@ -650,12 +774,42 @@ struct GlobalTypeOptimization : public Pass { } } + void visitFunction(Function* curr) { + if (needEHFixups) { + EHUtils::handleBlockNestedPops(curr, *getModule()); + } + } + }; + + struct IndexUpdater : public WalkerPass> { + bool isFunctionParallel() override { return true; } + + GlobalTypeOptimization& parent; + + IndexUpdater(GlobalTypeOptimization& parent) : parent(parent) {} + + std::unique_ptr create() override { + return std::make_unique(parent); + } + + void visitStructSet(StructSet* curr) { + if (curr->ref->type == Type::unreachable) { + return; + } + + auto newIndex = + parent.getNewIndex(curr->ref->type.getHeapType(), curr->index); + assert(newIndex != RemovedField); + curr->index = newIndex; + } + void visitStructGet(StructGet* curr) { if (curr->ref->type == Type::unreachable) { return; } - auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index); + auto newIndex = + parent.getNewIndex(curr->ref->type.getHeapType(), curr->index); // We must not remove a field that is read from. assert(newIndex != RemovedField); curr->index = newIndex; @@ -666,7 +820,8 @@ struct GlobalTypeOptimization : public Pass { return; } - auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index); + auto newIndex = + parent.getNewIndex(curr->ref->type.getHeapType(), curr->index); // We must not remove a field that is read from. assert(newIndex != RemovedField); curr->index = newIndex; @@ -677,45 +832,36 @@ struct GlobalTypeOptimization : public Pass { return; } - auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index); + auto newIndex = + parent.getNewIndex(curr->ref->type.getHeapType(), curr->index); // We must not remove a field that is read from. assert(newIndex != RemovedField); curr->index = newIndex; } - void visitFunction(Function* curr) { - if (needEHFixups) { - EHUtils::handleBlockNestedPops(curr, *getModule()); - } - } - - private: - Index getNewIndex(HeapType type, Index index) { - auto iter = parent.indexesAfterRemovals.find(type); - if (iter == parent.indexesAfterRemovals.end()) { - return index; - } - auto& indexesAfterRemoval = iter->second; - auto newIndex = indexesAfterRemoval[index]; - assert(newIndex < indexesAfterRemoval.size() || - newIndex == RemovedField); - return newIndex; - } + // TODO: visitStructWait }; - FieldRemover remover(*this); - remover.run(getPassRunner(), &wasm); - remover.runOnModuleCode(getPassRunner(), &wasm); + PassRunner runner(getPassRunner()); + runner.add(std::make_unique(*this)); + runner.add(std::make_unique(*this)); + runner.run(); + + FieldRemover moduleRemover(*this); + moduleRemover.runOnModuleCode(getPassRunner(), &wasm); // Insert globals necessary to preserve instantiation-time trapping of // removed expressions. - for (Index i = 0; i < remover.removedTrappingInits.size(); ++i) { - auto* curr = remover.removedTrappingInits[i]; + for (Index i = 0; i < moduleRemover.removedTrappingInits.size(); ++i) { + auto* curr = moduleRemover.removedTrappingInits[i]; auto name = Names::getValidGlobalName( wasm, std::string("gto-removed-") + std::to_string(i)); wasm.addGlobal( Builder::makeGlobal(name, curr->type, curr, Builder::Immutable)); } + + IndexUpdater moduleUpdater(*this); + moduleUpdater.runOnModuleCode(getPassRunner(), &wasm); } }; diff --git a/src/passes/HeapStoreOptimization.cpp b/src/passes/HeapStoreOptimization.cpp index 6c6e729e744..118c44998d9 100644 --- a/src/passes/HeapStoreOptimization.cpp +++ b/src/passes/HeapStoreOptimization.cpp @@ -165,8 +165,8 @@ struct HeapStoreOptimization bool trySwap(ExpressionList& list, Index i, Index j) { if (j == list.size() - 1) { // There is no reason to swap with the last element of the list as it - // won't match the pattern because there wont be anything after. This also - // avoids swapping an instruction that does not leave anything in the + // won't match the pattern because there won't be anything after. This + // also avoids swapping an instruction that does not leave anything in the // stack by one that could leave something, and that which would be // incorrect. return false; diff --git a/src/passes/Inlining.cpp b/src/passes/Inlining.cpp index eeee2d8b8b8..18fe7069038 100644 --- a/src/passes/Inlining.cpp +++ b/src/passes/Inlining.cpp @@ -837,9 +837,9 @@ struct FunctionSplitter { // // Note that to avoid wasteful work, this function may return "Full" inlining // mode instead of a split inlining. That is, if it detects that a partial - // inlining will trigger a follow up full inline of the splitted function - // then it will instead return "InliningMode::Full" directly. In more detail, - // imagine we have + // inlining will trigger a follow up full inline of the split function then it + // will instead return "InliningMode::Full" directly. In more detail, imagine + // we have // // foo(10); // diff --git a/src/passes/Intrinsics.cpp b/src/passes/Intrinsics.cpp index faba23816bb..97a7936e987 100644 --- a/src/passes/Intrinsics.cpp +++ b/src/passes/Intrinsics.cpp @@ -39,9 +39,11 @@ struct IntrinsicLowering : public WalkerPass> { // forgets to optimize. Builder builder(*getModule()); if (auto* refFunc = target->dynCast()) { - replaceCurrent(builder.makeCall(refFunc->func, operands, curr->type)); + replaceCurrent(builder.makeCall( + refFunc->func, operands, curr->type, curr->isReturn)); } else { - replaceCurrent(builder.makeCallRef(target, operands, curr->type)); + replaceCurrent( + builder.makeCallRef(target, operands, curr->type, curr->isReturn)); } } } diff --git a/src/passes/MakeSharedObjects.cpp b/src/passes/MakeSharedObjects.cpp index 71af94ac74b..8a52fb79948 100644 --- a/src/passes/MakeSharedObjects.cpp +++ b/src/passes/MakeSharedObjects.cpp @@ -31,6 +31,17 @@ // accordingly. Use i31 references to represent the table indices to avoid // further complications from mapping function references to non-reference // values. +// +// Although the shared objects prototype supports shared externrefs in general, +// it is not the case that arbitrary unshared externrefs can be made shared. To +// work around this, also lower unshared externrefs to i31ref table indices. +// Unlike function references, which we assume form a closed set whose table +// indices are meaningful across threads, there can be an arbitrary number of +// externrefs at runtime and only those that are imported as globals can be +// assumed to be meaningful across different threads. As a result, the externref +// table supports growing over time and externrefs rather than their table +// indices are still passed at the module boundary, unlike for function +// references. #include "ir/drop.h" #include "ir/module-utils.h" @@ -51,20 +62,229 @@ namespace wasm { +// Track several components used to store references in a table: +// - The table itself +// - Runtime functions for converting between references and table indices +// - Utilities for inserting calls to these runtime functions as necessary. +// The table and runtime functions are only added to the module if they are +// used. +struct LazyTable { + Module* wasm = nullptr; + // The desired table name. + Name base; + // The non-conflicting version of `base`, if ever accessed. + Name name; + // The table type. + Type type; + // The names of conversion functions, if ever used. + Name refToIndexName; + Name indexToRefName; + + LazyTable(Name base, Type type) : base(base), type(type) {} + + Name getName() { + assert(wasm); + if (!name) { + name = Names::getValidTableName(*wasm, base); + } + return name; + } + + Name getRefToIndexName() { + assert(wasm); + if (!refToIndexName) { + std::string funcName = type.getHeapType().toString() + "_to_index"; + refToIndexName = Names::getValidFunctionName(*wasm, funcName); + getName(); + } + return refToIndexName; + } + + Name getIndexToRefName() { + assert(wasm); + if (!indexToRefName) { + std::string funcName = "index_to_" + type.getHeapType().toString(); + indexToRefName = Names::getValidFunctionName(*wasm, funcName); + getName(); + } + return indexToRefName; + } + + void addRefToIndexFunction() { + // (func $_to_index (param $ref ) (result (ref null (shared i31))) + // (local $idx i32) + // (if (result (ref null (shared i31))) + // (ref.is_null (local.get $ref)) + // (then + // (ref.null (shared none)) + // ) + // (else + // (if (result (ref (shared i31))) + // (i32.ge_s + // (local.tee $idx + // (table.grow $ (local.get $ref) (i32.const 1)) + // ) + // (i32.const 0) + // ) + // (then + // (ref.i31_shared (local.get $idx)) + // ) + // (else + // (unreachable) + // ) + // ) + // ) + // ) + // ) + Builder builder(*wasm); + + Type sharedI31Nullable = Type(HeapTypes::i31.getBasic(Shared), Nullable); + Type sharedI31NonNull = Type(HeapTypes::i31.getBasic(Shared), NonNullable); + auto* isNull = builder.makeRefIsNull(builder.makeLocalGet(0, type)); + auto* retNull = builder.makeRefNull(HeapTypes::none.getBasic(Shared)); + auto* grow = builder.makeTableGrow(getName(), + builder.makeLocalGet(0, type), + builder.makeConst(Literal(int32_t(1)))); + auto* tee = builder.makeLocalTee(1, grow, Type::i32); + auto* geZero = + builder.makeBinary(GeSInt32, tee, builder.makeConst(Literal(int32_t(0)))); + auto* retIndex = + builder.makeRefI31(builder.makeLocalGet(1, Type::i32), Shared); + auto* checkGrow = builder.makeIf( + geZero, retIndex, builder.makeUnreachable(), sharedI31NonNull); + + auto* body = builder.makeIf(isNull, retNull, checkGrow, sharedI31Nullable); + auto func = Builder::makeFunction( + refToIndexName, Signature(type, sharedI31Nullable), {Type::i32}, body); + func->hasExplicitName = true; + wasm->addFunction(std::move(func)); + } + + void addIndexToRefFunction() { + // (func $index_to_ (param $idx (ref null (shared i31))) (result ) + // (if (result ) + // (ref.is_null (local.get $idx)) + // (then + // (ref.null ) + // ) + // (else + // (table.get $
(i31.get_u (local.get $idx))) + // ) + // ) + // ) + Builder builder(*wasm); + + Type sharedI31Nullable = Type(HeapTypes::i31.getBasic(Shared), Nullable); + auto* isNull = + builder.makeRefIsNull(builder.makeLocalGet(0, sharedI31Nullable)); + auto* retNull = builder.makeRefNull(type.getHeapType().getBottom()); + auto* getRef = builder.makeTableGet( + getName(), + builder.makeI31Get(builder.makeLocalGet(0, sharedI31Nullable), false), + type); + + auto* body = builder.makeIf(isNull, retNull, getRef, type); + auto func = Builder::makeFunction( + indexToRefName, Signature(sharedI31Nullable, type), {}, body); + func->hasExplicitName = true; + wasm->addFunction(std::move(func)); + } + + // Returns true if the table was added. (If it is never used, it will not be + // added.) + bool maybeAdd(Address initial, Address max) { + assert(wasm); + if (!name) { + return false; + } + Table* table = wasm->addTable(Builder::makeTable(name)); + table->type = type; + table->initial = initial; + table->max = max; + if (refToIndexName) { + addRefToIndexFunction(); + } + if (indexToRefName) { + addIndexToRefFunction(); + } + return true; + } + + bool isTableType(Type t) const { + return t.isRef() && Type::isSubType(t, type); + } + + bool hasTableType(Type t) const { + if (t.isTuple()) { + for (Type elem : t) { + if (hasTableType(elem)) { + return true; + } + } + return false; + } + return isTableType(t); + } + + bool funcHasTableType(Function* func) const { + Signature sig = func->type.getHeapType().getSignature(); + return hasTableType(sig.params) || hasTableType(sig.results); + } + + Expression* convertToRef(Expression* arg, Type origType) { + if (!isTableType(origType)) { + return arg; + } + Builder builder(*wasm); + Expression* res = builder.makeCall(getIndexToRefName(), {arg}, type); + if (origType != type) { + res = builder.makeRefCast(res, origType); + } + return res; + } + + Expression* convertToIndex(Expression* arg, Type origType, Type targetType) { + if (!isTableType(origType)) { + return arg; + } + Builder builder(*wasm); + Type sharedI31Nullable = Type(HeapTypes::i31.getBasic(Shared), Nullable); + Expression* res = + builder.makeCall(getRefToIndexName(), {arg}, sharedI31Nullable); + if (targetType.isNonNullable()) { + res = builder.makeRefAs(RefAsNonNull, res); + } + return res; + } +}; + struct MakeSharedObjects : WalkerPass>> { - Name tableName; + Type funcref = Type(HeapTypes::func, Nullable); + Type externref = Type(HeapTypes::ext, Nullable); + + LazyTable funcTable{"funcs", funcref}; + LazyTable externTable{"externs", externref}; std::vector funcs; std::unordered_map funcIndices; + Name anyToExternName; + Name externToAnyName; - Type funcref = Type(HeapTypes::func, Nullable); + Name getAnyToExternName() { + if (!anyToExternName) { + anyToExternName = + Names::getValidFunctionName(*getModule(), "any_to_extern"); + } + return anyToExternName; + } - Name getTable() { - if (!tableName) { - tableName = Names::getValidTableName(*getModule(), "funcs"); + Name getExternToAnyName() { + if (!externToAnyName) { + externToAnyName = + Names::getValidFunctionName(*getModule(), "extern_to_any"); } - return tableName; + return externToAnyName; } Index getIndex(Name func) { @@ -82,6 +302,12 @@ struct MakeSharedObjects if (type.isMaybeShared(HeapType::nofunc)) { return HeapTypes::none.getBasic(Shared); } + if (type == HeapType::ext || type == HeapType::string) { + return HeapTypes::i31.getBasic(Shared); + } + if (type == HeapType::noext) { + return HeapTypes::none.getBasic(Shared); + } if (type.isBasic()) { return type.getBasic(Shared); } @@ -109,6 +335,157 @@ struct MakeSharedObjects void updateType(Type& type) { type = updatedType(type); } + Type getBoundaryType(Type origType, Type rewrittenType) { + Signature origSig = origType.getHeapType().getSignature(); + Signature rewrittenSig = rewrittenType.getHeapType().getSignature(); + + std::vector params; + Index i = 0; + for (Type param : origSig.params) { + if (externTable.isTableType(param)) { + params.push_back(param); + } else { + params.push_back(rewrittenSig.params[i]); + } + ++i; + } + + std::vector results; + Index j = 0; + for (Type result : origSig.results) { + if (externTable.isTableType(result)) { + results.push_back(result); + } else { + results.push_back(rewrittenSig.results[j]); + } + ++j; + } + + return Type(Signature(Type(params), Type(results)), + NonNullable, + origType.getExactness()); + } + + void wrapImport(Function* func, Type origType) { + // Create a new imported function with the boundary type. The original + // function (which has lowered param and result types) is no longer an + // import and is given a body that calls the new import and converts the + // externrefs to and from indices. + Builder builder(*getModule()); + Name origName = func->name; + Name importName = Names::getValidFunctionName( + *getModule(), origName.toString() + "$import"); + + Type boundaryType = getBoundaryType(origType, func->type); + + auto importFunc = std::make_unique(); + importFunc->name = importName; + importFunc->module = func->module; + importFunc->base = func->base; + importFunc->type = boundaryType; + importFunc->hasExplicitName = true; + + func->module = Name(); + func->base = Name(); + func->type = Type(func->type.getHeapType(), NonNullable, Exact); + + // Convert indices passed as params to externrefs. + Signature boundarySig = boundaryType.getHeapType().getSignature(); + std::vector callArgs; + Index i = 0; + for (Type param : boundarySig.params) { + Type localType = func->getParams()[i]; + Expression* get = builder.makeLocalGet(i, localType); + callArgs.push_back(externTable.convertToRef(get, param)); + ++i; + } + + Type extResults = boundarySig.results; + auto* call = builder.makeCall(importName, callArgs, extResults); + + // Convert externrefs received as results to indices. + if (!externTable.hasTableType(extResults)) { + func->body = call; + } else if (extResults.isSingle()) { + Type targetType = func->getResults(); + func->body = externTable.convertToIndex(call, extResults, targetType); + } else { + Index scratch = Builder::addVar(func, extResults); + auto* set = builder.makeLocalSet(scratch, call); + std::vector tupleElems; + Index j = 0; + for (Type t : extResults) { + auto* extract = builder.makeTupleExtract( + builder.makeLocalGet(scratch, extResults), j); + tupleElems.push_back( + externTable.convertToIndex(extract, t, func->getResults()[j])); + ++j; + } + auto* tupleMake = builder.makeTupleMake(tupleElems); + func->body = builder.makeBlock({set, tupleMake}); + } + + getModule()->addFunction(std::move(importFunc)); + } + + Name wrapExport(Export* ex, Type origType) { + // Wrap the exported function with a new function that calls the original + // exported function, converting externref params into indices and index + // results into externrefs. + Builder builder(*getModule()); + auto* internalFunc = getModule()->getFunction(*ex->getInternalName()); + Name origName = internalFunc->name; + Name exportWrapperName = Names::getValidFunctionName( + *getModule(), origName.toString() + "$export"); + + Type boundaryType = getBoundaryType(origType, internalFunc->type); + + auto exportWrapper = std::make_unique(); + exportWrapper->name = exportWrapperName; + exportWrapper->type = Type(boundaryType.getHeapType(), NonNullable, Exact); + exportWrapper->hasExplicitName = true; + + // Forward params, converting externrefs to indices. + Signature boundarySig = boundaryType.getHeapType().getSignature(); + std::vector callArgs; + Index i = 0; + for (Type param : boundarySig.params) { + Type localType = exportWrapper->getParams()[i]; + Expression* get = builder.makeLocalGet(i, localType); + Type targetType = internalFunc->getParams()[i]; + callArgs.push_back(externTable.convertToIndex(get, param, targetType)); + ++i; + } + + Type internalResults = internalFunc->getResults(); + auto* call = builder.makeCall(origName, callArgs, internalResults); + + // Forward results, converting indices to externrefs. + Type extResults = boundarySig.results; + if (!externTable.hasTableType(extResults)) { + exportWrapper->body = call; + } else if (extResults.isSingle()) { + exportWrapper->body = externTable.convertToRef(call, extResults); + } else { + Index scratch = Builder::addVar(exportWrapper.get(), internalResults); + auto* set = builder.makeLocalSet(scratch, call); + std::vector tupleElems; + Index j = 0; + for (Type t : extResults) { + auto* extract = builder.makeTupleExtract( + builder.makeLocalGet(scratch, internalResults), j); + tupleElems.push_back(externTable.convertToRef(extract, t)); + ++j; + } + auto* tupleMake = builder.makeTupleMake(tupleElems); + exportWrapper->body = builder.makeBlock({set, tupleMake}); + } + + getModule()->addFunction(std::move(exportWrapper)); + ex->setInternalName(exportWrapperName); + return exportWrapperName; + } + void visitRefFunc(RefFunc* curr) { Builder builder(*getModule()); replaceCurrent(builder.makeRefI31( @@ -154,7 +531,7 @@ struct MakeSharedObjects auto type = it->second; callRefTypes.erase(it); replaceCurrent( - builder.makeCallIndirect(getTable(), + builder.makeCallIndirect(funcTable.getName(), builder.makeI31Get(curr->target, false), curr->operands, type, @@ -168,7 +545,7 @@ struct MakeSharedObjects Type oldTableType = updatedType(getModule()->getTable(oldTable)->type); auto* index = builder.makeTableGet(oldTable, curr->target, oldTableType); curr->target = builder.makeI31Get(index, false); - curr->table = getTable(); + curr->table = funcTable.getName(); } void visitRefTest(RefTest* curr) { @@ -190,7 +567,7 @@ struct MakeSharedObjects if (curr->ref->type.isNonNullable()) { // (ref.test castType (table.get $t (i31.get_u ref))) curr->ref = builder.makeTableGet( - getTable(), builder.makeI31Get(curr->ref, false), funcref); + funcTable.getName(), builder.makeI31Get(curr->ref, false), funcref); return; } // (if (result i32) @@ -207,7 +584,7 @@ struct MakeSharedObjects auto* ifNull = builder.makeConst(Literal(int32_t(curr->castType.isNullable()))); curr->ref = builder.makeTableGet( - getTable(), + funcTable.getName(), builder.makeI31Get(builder.makeLocalGet(scratch, scratchType), false), funcref); replaceCurrent(builder.makeIf(cond, ifNull, curr, Type::i32)); @@ -260,7 +637,7 @@ struct MakeSharedObjects } auto* getScratch = builder.makeLocalGet(scratch, scratchType); auto* i31get = builder.makeI31Get(getScratch, false); - auto* tableGet = builder.makeTableGet(getTable(), i31get, funcref); + auto* tableGet = builder.makeTableGet(funcTable.getName(), i31get, funcref); auto* refTest = builder.makeRefTest(tableGet, curr->type); Expression* ifPass = builder.makeLocalGet(scratch, scratchType); if (curr->type.isNonNullable()) { @@ -288,6 +665,31 @@ struct MakeSharedObjects WASM_UNREACHABLE("TODO: br_on"); } + void visitRefAs(RefAs* curr) { + if (curr->type == Type::unreachable) { + return; + } + if (curr->op == AnyConvertExtern) { + Type extType = Type(HeapTypes::ext, curr->type.getNullability()); + Expression* ext = externTable.convertToRef(curr->value, extType); + Builder builder(*getModule()); + Type sharedAnyNullable = Type(HeapTypes::any.getBasic(Shared), Nullable); + Expression* call = + builder.makeCall(getExternToAnyName(), {ext}, sharedAnyNullable); + if (curr->type.isNonNullable()) { + call = builder.makeRefAs(RefAsNonNull, call); + } + replaceCurrent(call); + } else if (curr->op == ExternConvertAny) { + Builder builder(*getModule()); + Expression* call = + builder.makeCall(getAnyToExternName(), {curr->value}, externref); + Type extType = Type(HeapTypes::ext, curr->type.getNullability()); + Type targetType = updatedType(curr->type); + replaceCurrent(externTable.convertToIndex(call, extType, targetType)); + } + } + void visitExpression(Expression* curr) { updateType(curr->type); @@ -319,13 +721,60 @@ struct MakeSharedObjects void visitGlobal(Global* curr) { updateType(curr->type); } void doWalkModule(Module* wasm) { + funcTable.wasm = wasm; + externTable.wasm = wasm; wasm->features.setSharedEverything(); WalkerPass::doWalkModule(wasm); } + struct ImportToWrap { + Function* func; + Type origType; + }; + + struct ExportToWrap { + Export* ex; + Type origType; + }; + void visitModule(Module* wasm) { + std::vector importsToWrap; + for (auto& func : wasm->functions) { + if (func->imported() && externTable.funcHasTableType(func.get())) { + importsToWrap.push_back({func.get(), func->type}); + } + } + + std::vector exportsToWrap; + for (auto& ex : wasm->exports) { + if (ex->kind == ExternalKind::Function) { + if (auto* name = ex->getInternalName()) { + auto* func = wasm->getFunction(*name); + if (externTable.funcHasTableType(func)) { + exportsToWrap.push_back(ExportToWrap{ex.get(), func->type}); + } + } + } + } + rewriteTypes(); + + for (auto& info : importsToWrap) { + wrapImport(info.func, info.origType); + } + std::unordered_map wrappedExports; + for (auto& info : exportsToWrap) { + auto* internalFunc = wasm->getFunction(*info.ex->getInternalName()); + auto [it, inserted] = wrappedExports.insert({internalFunc, Name()}); + if (inserted) { + it->second = wrapExport(info.ex, info.origType); + } else { + info.ex->setInternalName(it->second); + } + } + addFunctionTable(); + addExternTable(); ReFinalize().run(getPassRunner(), wasm); } @@ -352,8 +801,9 @@ struct MakeSharedObjects if (auto super = info.types[i].getDeclaredSuperType()) { builder[i].subTypeOf(builder[info.indices.at(*super)]); } + } else { + builder[i].setShared(Shared); } - builder[i].setShared(info.types[i].isSignature() ? Unshared : Shared); } auto built = builder.build(); @@ -373,18 +823,18 @@ struct MakeSharedObjects } void addFunctionTable() { - if (!tableName) { + if (!funcTable.maybeAdd(funcs.size(), funcs.size())) { + return; + } + if (funcs.empty()) { return; } Builder builder(*getModule()); - Table* table = getModule()->addTable(Builder::makeTable(tableName)); - table->type = funcref; - table->initial = table->max = funcs.size(); Name segName = Names::getValidElementSegmentName(*getModule(), "funcs"); auto* offset = builder.makeConst(Literal(int32_t(0))); auto* segment = getModule()->addElementSegment( - Builder::makeElementSegment(segName, tableName, offset)); - segment->type = funcref; + Builder::makeElementSegment(segName, funcTable.getName(), offset)); + segment->type = funcTable.type; segment->data.reserve(funcs.size()); for (auto func : funcs) { @@ -393,6 +843,38 @@ struct MakeSharedObjects } } + void addAnyToExternFunction() { + Type sharedAnyNullable = Type(HeapTypes::any.getBasic(Shared), Nullable); + Type externrefNullable = Type(HeapTypes::ext, Nullable); + auto importFunc = Builder::makeFunction( + anyToExternName, Signature(sharedAnyNullable, externrefNullable), {}); + importFunc->module = "env"; + importFunc->base = "any_to_extern"; + importFunc->hasExplicitName = true; + getModule()->addFunction(std::move(importFunc)); + } + + void addExternToAnyFunction() { + Type externrefNullable = Type(HeapTypes::ext, Nullable); + Type sharedAnyNullable = Type(HeapTypes::any.getBasic(Shared), Nullable); + auto importFunc = Builder::makeFunction( + externToAnyName, Signature(externrefNullable, sharedAnyNullable), {}); + importFunc->module = "env"; + importFunc->base = "extern_to_any"; + importFunc->hasExplicitName = true; + getModule()->addFunction(std::move(importFunc)); + } + + void addExternTable() { + externTable.maybeAdd(0, Table::kUnlimitedSize); + if (anyToExternName) { + addAnyToExternFunction(); + } + if (externToAnyName) { + addExternToAnyFunction(); + } + } + std::unique_ptr create() override { return std::make_unique(); } diff --git a/src/passes/MergeBlocks.cpp b/src/passes/MergeBlocks.cpp index 7010d6d15f7..59f125cc61f 100644 --- a/src/passes/MergeBlocks.cpp +++ b/src/passes/MergeBlocks.cpp @@ -236,7 +236,7 @@ static bool hasDeadCode(Block* block) { } // Given a dropped block, see if we can simplify it by optimizing the drop into -// the block, removing the return value while doin so. Returns whether we +// the block, removing the return value while doing so. Returns whether we // succeeded. static bool optimizeDroppedBlock(Drop* drop, Block* block, diff --git a/src/passes/MultiMemoryLowering.cpp b/src/passes/MultiMemoryLowering.cpp index 7a106410a84..ffc489df7bf 100644 --- a/src/passes/MultiMemoryLowering.cpp +++ b/src/passes/MultiMemoryLowering.cpp @@ -604,7 +604,7 @@ struct MultiMemoryLowering : public Pass { builder.makeReturn(builder.makeConst(-1)))); // If we are not growing the last memory, then we need to copy data, - // shifting it over to accomodate the increase from page_delta + // shifting it over to accommodate the increase from page_delta if (!isLastMemory(memIdx)) { // This offset is the starting pt for copying auto offsetGlobalName = getOffsetGlobal(memIdx + 1); diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp index a6ea8fa2ac2..25852027a0c 100644 --- a/src/passes/OptimizeCasts.cpp +++ b/src/passes/OptimizeCasts.cpp @@ -315,7 +315,7 @@ struct EarlyCastFinder // (ref.cast $B (ref.cast $B (local.get $x))) // // We initially choose to move the inner ref.cast $B. When we consider the - // outer ref.cast $B, we can see that it has the same type as tge existing + // outer ref.cast $B, we can see that it has the same type as the existing // ref.cast $B, so we ignore it. // // Case 4: diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index aa093530543..b862dd9e45c 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -49,6 +49,7 @@ #include "call-utils.h" #include "support/utilities.h" +#include "wasm-type.h" // TODO: Use the new sign-extension opcodes where appropriate. This needs to be // conditionalized on the availability of atomics. @@ -1461,6 +1462,65 @@ struct OptimizeInstructions } } + void visitResume(Resume* curr) { + skipNonNullCast(curr->cont, curr); + if (trapOnNull(curr, curr->cont)) { + return; + } + if (curr->type == Type::unreachable) { + return; + } + + // If this resume operates on a freshly-created continuation of an exact + // function that is known never to suspend, we can turn the resumption into + // a direct call, avoiding the continuation allocation and handler overhead. + + // Continuations are single-shot, so resuming a continuation that has + // already been consumed will trap. If traps are assumed never to happen, we + // can assume this continuation will not be consumed on another path and + // look through tees and conditional branches. Otherwise, avoid looking + // through them to ensure the continuation cannot be consumed elsewhere. + auto behavior = getPassOptions().trapsNeverHappen + ? Properties::FallthroughBehavior::AllowTeeBrIf + : Properties::FallthroughBehavior::NoTeeBrIf; + + auto* contExpr = Properties::getFallthrough( + curr->cont, getPassOptions(), *getModule(), behavior); + auto* contNew = contExpr->dynCast(); + if (!contNew) { + return; + } + if (contNew->func->type == Type::unreachable) { + return; + } + + auto* funcExpr = + Properties::getFallthrough(contNew->func, getPassOptions(), *getModule()); + auto* refFunc = funcExpr->dynCast(); + if (!refFunc) { + return; + } + + auto* target = getModule()->getFunctionOrNull(refFunc->func); + if (!target || target->imported()) { + return; + } + + if (!target->effects || target->effects->suspends) { + return; + } + + auto* block = + ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions()) + .getChildrenReplacement(); + Builder builder(*getModule()); + Type results = target->getResults(); + block->list.push_back( + builder.makeCall(target->name, curr->operands, results)); + block->type = results; + replaceCurrent(block); + } + // Note on removing casts (which the following utilities, skipNonNullCast and // skipCast do): removing a cast is potentially dangerous, as it removes // information from the IR. For example: @@ -1551,6 +1611,17 @@ struct OptimizeInstructions // ref.as_non_null then the struct.set will still trap, of course, but that // will only happen *after* the call, which is wrong. void skipNonNullCast(Expression*& input, Expression* parent) { + // If we must never reorder code, then we cannot remove a non-null cast. + // Such removals are valid because we move the trap later (see the + // struct.set in the example above: we can remove the ref.as_non_null + // because the set will trap anyhow, so we are pushing the trap onward; in + // the example above we have a call we can't move past, and in neverReorder + // mode we need to care about things like branch hints, which do not have + // effects, hence the need for the special neverReorder flag). + if (neverReorder) { + return; + } + // Check the other children for the ordering problem only if we find a // possible optimization, to avoid wasted work. bool checkedSiblings = false; @@ -1717,7 +1788,7 @@ struct OptimizeInstructions if (auto* select = ref->dynCast