Skip to content

Merge upstream main into oscar/expose-new-exceptions - #1

Merged
ospencer merged 53 commits into
ospencer:oscar/expose-new-exceptionsfrom
chharvey:oscar/expose-new-exceptions
Sep 21, 2026
Merged

ospencer merged 53 commits into
ospencer:oscar/expose-new-exceptionsfrom
chharvey:oscar/expose-new-exceptions

Conversation

@chharvey

Copy link
Copy Markdown

Changes in upstream/main (namely from WebAssembly#9098) caused breakages in the JS API. This PR syncs them up with your branch.

tlively and others added 30 commits August 28, 2026 12:01
- In printWrap, avoid printing trailing spaces before newlines and on
empty lines.
- Remove whitespace indentation before closing parentheses on raw string
literal tool descriptions in wasm-opt and wasm-reduce.
- In printStackIR, skip Pop pseudo-instructions before emitting
indentation so that they do not produce empty lines with indentation.

The version of filecheck we currently use handles whitespace-only lines
fine, but LLVM FileCheck and newer versions of Python filecheck require
explicit `{{^ +$}}` regex matchers for such lines. It's nicer to just
not emit whitespace-only lines in the first place if we want to upgrade
our version of filecheck.
- Update update_lit_checks.py and update_help_checks.py to emit
  CHECK-EMPTY: for blank lines instead of empty CHECK: / CHECK-NEXT:.
- Update check_line_re in update_lit_checks.py to recognize all
  standard FileCheck suffixes.
- Regenerate help tests and manual tests with CHECK-EMPTY:.
Update requirements-dev.txt to use the modern Python filecheck package
(1.0.3+).
…ro (WebAssembly#9056)

This is a standard part of `fast-math` in clang and gcc:

https://clang.llvm.org/docs/UsersManual.html#cmdoption-ffast-math

We may have already been optimizing it, I'm not sure, but a later
PR will do so, so it seems worth documenting explicitly.
Remove the PYTHONUTF8 variable from the Windows CI because the new
filecheck version handles UTF-8 correcty by default.
…ly#9061)

`update_lit_checks.py` doesn't always work well with wasm-split. For
example, for some tests, the test CHECK lines are mixed like this after
running it:
```wast
;; PRIMARY: ...
;; SECONDARY: ....
;; PRIMARY: ...
;; SECONDARY: ...
```

A follow-up PR will change these two test files' CHECK lines to be mixed
when using `update_lit_checks.py`, and making them not use in that PR
will make it hard to see what actually change in that PR. To make the
next PR's diff tidy, this makes a few tests not use the auto-updating
script.

This also adds `-all` to `transtiive-globals-multi.wast` to be
consistent with other `transitive-globals*.wast` tests.
…y#9058)

We didn't look through the tee properly, and also did wasted work, since
we can just stop at the first tee we see and use the value there.
A follow-up PR will move immutable globals to secondary modules when
possible, and it will make the test expectations of
`transitive-globals-multi.wast` different for mutable and immutable
globals. Creating a new test in that PR will make it different to see
what changes in that PR. So this PR duplicates the test to make mutable
and immutable versions. Note that `$f` can't be converted to mutable
because it is used in a `global.get`.

This also adds mutable/immutable versions of globals to
`split-module-items.wast`.
…embly#9065)

`10 * MaxBinaryActions` is far too low, as CFGs can be complex enough to
hit that. Use something far, far higher, unlikely to ever be seen in
practice, but enough to assert instead of hanging, in case we have a bug.
This fixes typos in the codebase using codespell.
This handles cases where the term is equal but not constant (we already
handled constants before). E.g. this proves `x < y => x <= y`, which is
true even though `y` is unknown.
This script runs and monitors fuzz_opt.py for up to a given number of
iterations, redirecting its output to a rotating log file and printing
its progress to stdout once a minute. When it detects that the fuzzer
has found a bug, it prints the iteration number and the seed so the bug
can be reproduced.

This script is nicer to run in agent harnesses than raw fuzz_opt.py
because agents can easily run the fuzzer for X iterations and can show
the progress without overly polluting the context.
This will be necessary for a future optimization that turns resumes of
continuations that never suspend into calls. Update the effect analysis
of suspends to set the new effect and clobber global state because the
suspend handler might do anything before returning. Test that the
effects are analyzed as intended and that they work with global effect
analysis.
…y#9076)

Reduces compression time from ~67s to ~6.5s with a ~18MB / 9.5% increase
in bundle size.
As proposed in
WebAssembly/shared-everything-threads#119. Add
parsing, printing, and validation. The optimization passes will not yet
preserve the semantics of publish instructions in general; a future
change will have to augment EffectAnalyzer to model its effects
correctly.
GCData previously represented all allocations using a vector of
Literals. Storing numeric array elements this way introduces unnecessary
memory overhead and prevents efficient byte-level operations.

Represent primitive numeric GC arrays using a raw byte buffer in GCData
while preserving Literals storage for reference arrays and structs.
`llvm.sh` from the LLVM project is a standard way to get LLVM, but
it downloads lots of stuff we don't need.

We can just use clang from the runner in most cases. Do that,
and use a newer ubuntu in the one place we need newer clang
(for clang-format-21).
…#9083)

- Add shared.run_parallel_tests helper with thread-safe output capture
and interrupt handling
- Refactor spec tests to use shared.run_parallel_tests

The eventual goal here is to have all the suites (that don't parallelize
themselves) using a ThreadPool to parallelize more.
Run the wasm-opt passes and print tests in parallel to speed up check.py
runs. Isolate per-test temporary files and pass the debug environment
explicitly to avoid overwriting other environments.

This reduces runtime from 30s to 4s on a 128-core machine.
`getImmediateFallthroughPtr` needs to make sure that the fallthrough
value is not modified or otherwise rendered unusable by the side effects
of expressions that are evaluated after the fallthrough expression but
before the top-level expression. Previously the only such expression was
the br_if condition, and `getImmediateFallthroughPtr` only looked
through the br_if if the condition did not have side effects that would
interfere with the rest of the fallthrough expression.

Make this effect analysis slightly more precise by using `orderedBefore`
instead of `canReorder`. Also add similar effects analysis for the
`desc` operands of `RefCast` and `BrOn` expressions, if it exists. These
operands are also evaluated after the fallthrough expression.

Finally, update `areConsecutiveAndEqual` in `OptimizeInstructions` to
look through `RefCast` and `BrOn` expressions despite any effects that
may occur in their `desc` operands; it will do its own more precise
effects analysis afterward.
Add a `visitResume` in OptimizeInstructions that does the normal
optimizations on null continuations and then tries to turn resumes into
calls. We can do this when we are resuming a freshly allocated
continuation created with a reference to a known function that
GlobalEffects tells us will not suspend.
Handles this common code pattern:

```wat
(if
  (i32.and (A) (B))
  (then ..
```

We can apply both A and B in the first arm, and `!(A && B)` in the
second.

Also pattern-match `eqz(eqz(..))` as that is the only way to represent
`!= 0` in a nested position (otherwise, just `if (local.get)` works, which we
already matched).
When GTO removes fields or makes them immutable, it may introduce an
immutable externref first field on the descriptor of a JS-exposed type
where there was none before. That means that the described type could
now have a JS-observable prototype where it did not before optimization,
which makes this a misoptimization.

Fix the problem by inserting an i8 placeholder first field wherever we
would otherwise start exposing a prototype where there was none before.
This is expected to be exceptionally rare in practice, so the extra
memory use is not expected to be a real problem.

Instead of adding a placeholder field, we could have inhibited
optimization of the existing first field, but that would be more likely
than an unaccessed placeholder field to have adverse effects in later
passes.

Fixes WebAssembly#9026.
One some ABIs, if a base class ends with padding, derived classes right
after it can reuse that padding, placing their data there. This PR optimizes the
key Expression class that way.

Size changes on linux64 (common classes):

| class     | old size | new size |
| --------  | -------- | -------- |
| LocalGet  |    24    |    16    |
| LocalSet  |    32    |    24    |
| Load      |    64    |    48    |
| Store     |    80    |    64    |
| Unary     |    32    |    24    |
| Binary    |    40    |    32    |
| StructGet |    40    |    24    |
| StructSet |    48    |    32    |

On 5 real-world binaries I tested, this reduced peak RAM usage by
3%, 4%, 6%, 9%, 9%.
brendandahl and others added 23 commits September 10, 2026 15:42
Run the wasm2js test suite in parallel to speed up check.py runs.
Isolate per-test temporary files and pass the debug environment
explicitly for thread safety.

This reduces runtime from 60s to 8s on a 128-core machine.
- Run native C/C++ builds, links, and runs in parallel using
shared.run_parallel_tests
- Remove use of `subprocess.check_call` to avoid interleaved stdout

This reduces runtime from 26s to 3s on a 64-core machine.
Optimize out publishes of allocations and publishes of publishes where
it is clearly not possible for there to be a write to the published
object between the allocation or former publish and the outer publish.
Also optimize out publishes of unshared and immutable reference types.
- Run the 6 fixed wasm-reduce tests in parallel using
shared.run_parallel_tests
- Isolate per-test temporary wasm and wat filenames
- Pipe wasm-reduce stderr to prevent interleaved output

This reduces runtime from 1m21s to 46s.
wasm-ctor-eval had a bug where return calls with non-serializable
arguments would incorrectly overwrite global state before failing to
serialize, leading to later crashes. Fix the problem by waiting to
update global state until we are sure that the operation will be
committed.
WebAssembly#9093)

## Summary

Fix a parser bug where `wasm-opt` rejected valid modules containing
declarative element segments with GC reftypes, failing with
`parse exception: invalid tag index`.

## Problem

Binaryen's binary reader (`WasmBinaryReader::readElementSegments` in
`src/wasm/wasm-binary.cpp`) misread the type field of **declarative**
element segments (flag `0x07`).

In the binary format, a declarative segment is encoded as:
```text
0x07 reftype vec(expr)
```

where `reftype` can be a single byte (e.g. `nullref`) or a prefix byte
plus a heap-type LEB (e.g. `0x63 0x02` for `(ref null 2)`).

The reader only consumed a single `getU32LEB()` for the type. When the
reftype was `(ref null 2)` — i.e. a prefixed heap type — the reader
would consume the prefix byte as if it were the whole type, then
misinterpret the following bytes as the vector length. This misaligned
the stream, and downstream parsing eventually tried to resolve a value
against the tag table, producing:
parse exception: 
```invalid tag index (at 0:51)```

Minimal repro (from the issue):

```wat
(module
  (rec
    (type (array i16))
    (type (sub (struct)))
    (type (array (mut nullexternref)))
    (type (sub (func (param i64))))
    (type (array (mut f32)))
    (type (sub (array (mut v128))))
  )
  (elem declare (ref null 2))
  (elem declare (ref null 3) (ref.null 3))
  (elem declare nullref)
  (func (type 3) (param i64))
  (func (type 3) (param i64))
)
```
```wasm-tools``` validate accepts this module; Binaryen rejects it.

## Fix
Apply the same logic that is already used for passive and table-indexed
segments to the declarative case:

If usesExpressions is set (flag `0x07`), read a full reftype using
`getType()`.

Otherwise (flag `0x03`), read the single-byte elemkind and validate
it is 0 (funcref).

Previously, the declarative branch unconditionally called
`getU32LEB()` for the type, which works only for flag 0x03.

```cpp
if (isDeclarative) {
  // Declared segments are needed in wasm text and binary, but not in
  // Binaryen IR; skip over the segment.
  if (usesExpressions) {
    [[maybe_unused]] auto type = getType();
  } else {
    auto elemKind = getU32LEB();
    if (elemKind != 0x0) {
      throwError("Invalid kind (!= funcref(0)) since !usesExpressions.");
    }
  }
  auto num = getU32LEB();
  for (Index i = 0; i < num; i++) {
    if (usesExpressions) {
      readExpression();
    } else {
      getU32LEB();
    }
  }
  continue;
}
```

Why this approach: The fix is minimal and localized to the
declarative branch. It reuses the existing, correct reftype-reading
path rather than introducing a new helper. Declared segments are
intentionally dropped from Binaryen IR (they are not needed there), so
no IR representation changes are required — only the binary reader
needs to consume the correct number of bytes.

## Testing

Manual verification:

Before the fix:

```text
$ ./bin/wasm-opt repro.wasm --all-features -o /dev/null
[parse exception: invalid tag index (at 0:51)]
Fatal: error parsing wasm
```
After the fix:

```text
$ ./bin/wasm-opt repro.wasm --all-features -o /dev/null
warning: no passes specified, not doing any work
```
Cross-checked against the reference toolchain:

```text
$ wasm-tools validate repro.wasm
$ wasm-tools print repro.wasm
(module
  (rec
    (type (;0;) (array i16))
    ...
  )
  (elem (;0;) declare (ref null 2))
  (elem (;1;) declare (ref null 3) (ref.null 3))
  (elem (;2;) declare nullref)
  ...
)
```

```wasm-tools``` accepts the module, confirming the input is spec-compliant.

New regression test: ```test/lit/binary/gc-elem-declare.wast```
exercises ```--roundtrip``` on the ```reproducer```, which forces Binaryen to
write and re-read the module through the binary format. Before the
fix, --roundtrip would hit the same invalid tag index error.

```text
$ ./bin/binaryen-lit test/lit/binary/gc-elem-declare.wast -v
PASS: Binaryen lit tests :: binary/gc-elem-declare.wast
```

**Full test suite:**

```text
$ python3 check.py lit
...
PASS: Binaryen lit tests :: binary/gc-elem-declare.wast (94 of 994)
...
```
All lit tests pass. 

**Related Issues**
Fixes WebAssembly#8540.

Follow-up Work
None. Declared segments remain intentionally dropped from Binaryen IR,
so no additional handling is needed beyond correct parsing.
…erReorder is set (WebAssembly#9096)

In the neverReorder testing mode, no code can be reordered, as reordering it
past branch hints leads to fuzzer errors.
Add a --jobs (-j) argument to monitor_fuzz.py to control how many
instances of fuzz_opt.py are run in parallel. This can increase
throughput because a single instance of fuzz_opt.py does not always
saturate all cores. The instances are run in separate directories to
avoid interfering with each other.

To support running simultaneously in separate directories, make a few
tweaks to fuzz_opt.py to make it more location-independent. For example,
pass `-C` to git operations to run them explicitly in the binaryen root
instead of the current directory.
Port changes from
WebAssembly/acquire-release-atomics#21 and
WebAssembly/acquire-release-atomics#23. After
this PR the test matches the test from the proposal exactly. Once the
acquire/release atomics proposal is in
https://github.com/WebAssembly/testsuite, we won't have to copy this
manually anymore.

* Add missing `(register ...)` statements within `thread` blocks
* Use unique names for threads and memories
…bly#9108)

PR WebAssembly#9043 ("Avoid newly exposing prototypes in GTO") introduced
prepending
an i8 placeholder field at index 0 of exposed no-proto descriptors when
their first field becomes prototype-exposing. When all fields of such a
descriptor are kept, the new index of the last field equals the old
struct's `fields.size()`.

Because `updateInstructions` runs before `updateTypes` (so expression
heap types still refer to the pre-optimization `Struct` definitions),
and
`FieldRemover` is a `PostWalker`, child `struct.get`, `struct.rmw`, and
`struct.cmpxchg` instructions had their `index` mutated in-place to the
new index before an enclosing `struct.new` or `struct.set` ran
`ChildLocalizer` or `getResultOfFirst`. When `EffectAnalyzer`
subsequently
inspected those child instructions, `readsStruct` accessed
`type.getStruct().fields[index]` using the old `HeapType` and the new
`index`, causing an out-of-bounds assertion failure (`__n <
this->size()`)
or reading the wrong field's mutability.

Fix this by splitting instruction updating into two passes executed in a
single nested `PassRunner` invocation (`FieldRemover` followed by
`IndexUpdater`), along with sequential `runOnModuleCode` runs.
`FieldRemover` removes/reorders `struct.new` operands and replaces
removed `struct.set`s while all struct instructions still have their old
field indices matching their old `HeapType`s, and `IndexUpdater` then
updates the field indices on all remaining struct instructions.
Add a pass that converts call, call_indirect, and call_ref instructions
in tail position into return calls. Whether expressions are in tail
position is propagated down from parent expressions to children, so we
need to do a pre-order traversal instead of our normal post-order
traversal. Add a bespoke PreWalker class that additionally passes
`isTail` to the various expression visitors. Keep track of the exception
handling depth to avoid incorrectly turning calls inside exception
handlers into return calls.
Parsing tees allows us to handle more code, as it is common to see
a tee at the start of a constraint, e.g.

```wat
(if
  (i32.eq ..
    (local.tee $x ..)
    (i32.const 42)
  )
```

This is also a bugfix, as we were not handling unparsed tees before:
we need to make sure that no tee tramples a get that we parse into
a constraint. E.g.

```wat
(i32.and
  (local.get $x)
  (local.tee $x ..)
)
```
We cannot parse that into `$x && ..` because at the AND, we have already
trampled `$x`. This code looks for any such conflict.
Switch handlers on `resume` and `resume_throw` instructions do not have
associated labels. GUFA previously assumed that all handlers would have
labels, and would crash after trying to look up the target of an empty
name. Fix the bug by explicitly checking for the presence of a label on
the handlers.
Late in the development of the TailCall pass, we removed a run of DCE in
a nested pass runner. It turns out that had been load bearing, because
without it TailCall could optimize unreachable calls whose callee return
types did not match the caller return type, producing invalid IR. Fix
the bug by not optimizing unreachable calls, leaving them for a separate
DCE pass to clean up instead.

Also fix the lowering of call.without.effects when it is used as a tail
call. Previously intrinsic lowering would turn such a tail call into a
normal call, again producing invalid IR because the normal call is not
unreachable and is not valid in all the places a tail call would be.

---------

Co-authored-by: Alon Zakai <azakai@google.com>
When a new BINARYEN_FUZZ_STATS environment variable is set, collect
statistics about fuzzer patterns and events. Save these statistics to a
file, where they can be accumulated and updated across several fuzzer
runs. The API is designed to make it easy to add new patterns and events
for one-off local experiments. Include sample patterns collecting
frequencies of various cast instructions as an example.
Two such instructions, one after the other, can return different things
despite appearances, like a `call` or a `struct.new`.
If the replacement value is not unreachable, we still need to fix it up.
…ebAssembly#9114)

Because arbitrary unshared externrefs cannot be made shared directly,
lower unshared externrefs to shared i31ref table indices using a
LazyTable abstraction that manages the table and runtime conversion
helpers on demand.

Unlike function references, externrefs are still passed across the
module boundary, so wrap imported and exported functions that accept or
return externrefs to convert between externrefs and table indices. Also
lower any.convert_extern and extern.convert_any using imported
conversion helpers, and check that table.grow succeeds (>= 0) when
converting references to indices, trapping otherwise.
Alternative to WebAssembly#9085, which renamed `binaryen.none` to `binaryen.void`
and added a new `binaryen.none` referring to the heap type.
There were concerns that `binaryen.void` mapping to
the C API `BinaryenTypeNone()` might be confusing.

This PR moves all types from the top-level `binaryen.*` namespace
into respective enums called `Type`, `HeapType`, and `PackedType`.

This change plays nicely with AssemblyScript’s established
[TypeScript
typings](https://github.com/AssemblyScript/binaryen.js/blob/main/index.d.ts#L3)
because now types are scoped in an enum space.
This is not a breaking change for the TS typings file —
e.g., a function that accepts a `Type` (previously just `number`)
now accepts a member of the new `Type` enum.

This change also follows well-established patterns of existing enums
such as `ExpressionIds` and `Features`.

`binaryen.Type.none` now uses `BinaryenTypeNone()` and
`binaryen.HeapType.none` uses `BinaryenHeapTypeNone()`,
so there’s no conflict across enums.

This *is* a breaking change for JS/TS users:
they will now have to reference types from the enum space
instead of at the top level.
E.g. `binaryen.i32` becomes `binaryen.Type.i32`,
`binaryen.any` becomes `binaryen.HeapType.any`,
`binaryen.i8` becomes `binaryen.PackedType.i8`, etc.
Return 1 instead of 0 when a bug is found to distinguish it from the
case when --max-iters is reached. Return 2 in the case of other errors.
This change lets this helpful bash one-liner work correctly:

```bash
while (git pull origin main && ninja && ./scripts/monitor_fuzz.py -j --max-iters=1000000); do :; done
```
@ospencer
ospencer merged commit 410077f into ospencer:oscar/expose-new-exceptions Sep 21, 2026
@chharvey
chharvey deleted the oscar/expose-new-exceptions branch September 21, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants