Commit Graph

87 Commits

Author SHA1 Message Date
Martin Kustermann 2d78883f27 [dart2wasm] Simplify handling of JS interop callbacks
Right now a JS interop callback works like this:

* Each wasm module that gets instantiated will be given it's module
  instance (JS calls Dart to set it) via `setThisModule`

* When Dart code calls JS and gives it a callback to invoke, it gave it
  this module instance. It will also make the callback wasm function
  weakly exported.

* The JS trampoline code, when invoked, would then call the weakly
  exported wasm function from the module instance.

We simplify this now by making the Dart code simply give the wasm
function reference to JS, then JS can later on invoke it. No need to
weakly export a function and call back via
`module.exports.<weaklyExportedCallback>`

To ensure binaryen is aware that the wasm function may be called from
JS, we annotate it via the `(@binaryen.js.called)` annotation.

Change-Id: I828dd0cf8d3b36db338792c4e277a4bb94c76faf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511080
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-06-11 12:24:16 -07:00
Martin Kustermann 892e6a8b53 [dart2wasm] Pass on @pragma('wasm:prefer-inline') to wasm-opt
Now that we have a way to tell binaryen to inline functions (see [0],
which added support for `(@binaryen.inline <0...127>)` annotations) we
can tell it to inline functions that were annotated with
`@pragma('wasm:prefer-inline')`

Since binaryen sometimes can devirtualize call sites that TFA cannot, it
can then inline those devirtualized calls.

[0] https://github.com/WebAssembly/binaryen/issues/7972

Change-Id: I139bd43976a1ddb83afe756d4fbac09419f7199e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/510822
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-06-10 15:59:53 -07:00
Martin Kustermann 5cd42e6b21 [dart2wasm] Start emitting binaryen.inline custom section
Binaryen introduced `binaryen.inline` which allows us to tell it
inlining hints now, including "never inline" hint (see [0])

This allows us to remove the ugly mangling of wasm function names with
`<noInline>` postfix.

We also now pass `--strip-toolchain-annotations`: The annotations
occupy size in the wasm binary and wasm runtimes ignore them (they are
for `wams-opt` only).

Except for IR tests: Here we want to see the annotations, so we keep
them there.

We also rename the package:wasm_builder classes to clearly indicate
those are binaryen specific sections.

We also make the ir_test.dart put it's options first, allowing the IR
tests to override options if needed.

[0] https://github.com/WebAssembly/binaryen/commit/3c25487214600a9

Change-Id: I96688bfaeba403a39cd5e7376f8d2889bcbae030
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/510000
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-06-10 15:51:47 -07:00
Martin Kustermann 9edafc75f7 [dart2wasm] Disallow inlining of static field initializers
Static field initializers are only run once and as such are cold code.
They should never be inlined into hot code that accesses the static
field.

Though right now `wasm-opt` will inline the static field initializers
e.g. if there's only one use of a static field (as it makes size a bit
smaller if we avoid the extra wasm function). That has the problem that
it may inline cold code into hot code. That hot code in return then gets
big and may not be allowed to be inlined into it's caller.

One can instruct binaryen to only inline functions with one caller up to
some size by passing `--one-caller-inline-max-function-size` (see [0]).
Though that can regress size.

So instead we specifically mark the function we know are cold to not be
inlined, namely static field initializer functions above a certain size.

This changes performance of a benchmark from [1] from
  cascadeUnhoisted: 390000 us
to
  cascadeUnhoisted: 138000 us

NOTE: This will cause some size regressions for e.g. e main module by
0.4-0.5% - but seems to be the right thing to do.

[0] https://github.com/WebAssembly/binaryen/issues/8619
[1] https://github.com/dart-lang/sdk/issues/63484

Change-Id: I626b8cd879a804aef638901ede190f74276d8e8b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/509960
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-06-08 12:21:42 -07:00
Johnni Winther 80c9d57250 [kernel][Contexts] Rename VariableDeclaration to Variable
This is a step towards split variables from their declarations as part of encoding variables and their usage more precisely.

TEST=existing

Change-Id: I4a0eeb2947bdebce3667afda4e6cfbfdf5d7de18
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/504201
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
2026-05-21 01:51:10 -07:00
Martin Kustermann 147ed24717 [dart2wasm] Explicitly model functions that do not return
Similar to [0] which recognizes functions that return always nulls, we
recognize functions that never return and

* make the wasm function have no outputs
* make call sites emit `unreachable()` after the call (to inform
  binaryen & wasm runtime that this is unreachable)

Before we had an artificial construct where we made such functions
have a `w.RefType(HeapType.none, nullable: false)` return type (i.e.
bottom, i.e. no values) and encoded that way it's unreachable.

We also change some exported wasm functions to explicitly use wasm
types in the signature (namely `WasmVoid` instead of Dart `void`).

[0] https://dart-review.googlesource.com/c/sdk/+/497620

Change-Id: I3724e777cda23c0cf2c8a7dd2e473f3fef0a4f54
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499240
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-05-13 00:23:45 -07:00
Martin Kustermann d0e0290702 [dart2wasm] Remove synthesizing values from nothing
When there's no expression on the stack but we expect something on the
stack, then the code should be unreachable.

Though the current code would just synthesize a value that matches
the expected type (`convertType(voidMarker, <some type>)`). This
is problematic: If we ever used that synthesized value we may
have incorrect program behavior.

Now there were some valid uses where we synthesize values

* A function that has `void` return type but no explicit return
  => Here we should synthesize `null`
* Synthesize `null` in cases where we know it's not going to be used
  => e.g. for CFE desugaring of `a[i] = b` is roughly
     `let tmp = b in (let ignored = a.[]=(tmp) in b)`
     where we synthesize `null` as `a.[]=(tmp)` result,
     `ignored` isn't used.
* ...

With this CL we no longer allow synthesizing a value of a type
out of thin air, instead all the places where this occurs have
to do that explicitly.

There's some impurities around how setters and index setters
are handled today (and even after this CL). Those impurities
start all the way at CFE, which treats setters and index
setters very differently. See the CFE issue [0].

For those we have two choices:

* special case all call sites that require synthesizing
  null values
* special case all call sites that require dropping an
  auto synthesized null value

This CL now marks instance setter/index-setter methods as
requiring auto-synthesizeing null values on usage sites and
make code that doesn't need them explicitly drop them.

Somewhat related to this change is how we deal with `void`
on the Dart <-> Wasm Import / Wasm Export boundary: When we
call an imported wasm function that has `void` as return
type (meaning no return values) we have to synthesize a `null`
(as the caller may "use"/"observe" the `void`).

=> We now are more strict and instead use `WasmVoid` as type
   instead of allowing `void` as type on the import/export
   functions.

[0] https://github.com/dart-lang/sdk/issues/63360

Change-Id: Ie30df3bd68553724437607bab3163c98f5467efe
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501061
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-05-13 00:22:52 -07:00
Martin Kustermann 96d8359e32 [dart2wasm] Cleanup closure related things
Move the creation of

* name of a lambda to `translator.functions.getLambdaFunctionName`
* type of a lambda to `translator.functions.getLambdaFunctionType`

Removes dependency on `w.FunctionBuilder` in the lambda code generators
- as they don't need access to the wasm function. This may allow
inlining closure calls in the future.

Avoid creating `w.FunctionBuilder` eagerly when analyzing closures and
instead create it only when there's a call to it (or the closure object
gets instantiated).

Use `CallTarget` abstraction when invoking lambdas.

Avoid carrying around
`(lambda, enclosingMember, enclosingMemberClosures)` throughout
the codebase and instead store this information on `Lambda`.

=> All these changes make the codebase more uniform between
   lambdas and normal functions and also is net code removal.

Change-Id: Ib69566ea9580827be0ed52c0c179884e84599d88
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501983
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-05-11 01:48:14 -07:00
Martin Kustermann b8b0cad67a [dart2wasm] Make most void functions have no return values
This gives around 0.2% improvement in compressed e main module.

In Dart a function with `void` return type can actually return values
that callers can observe. But most of the time this doesn't happen, most
times those functions return `null` values and callers don't observe
them.

Let's use inferred return value information to see if a function is
guaranteed to only return `null`. If so we make the wasm function
signature not return any values. Callers will then synthesize a `null`
which may immediatly be dropped or (in rare cases) actually be used.

This leads to less less instructions in the callee (as a callee doesn't
need to push the null onto the stack) and the caller (as the caller
doesn't have to drop it from the stack).

Change-Id: I3ed1be7592798ad0c697c5bc3ab2c4b64c156f03
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/497620
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-04-28 00:20:02 -07:00
Nate Biggs d43d9df1a7 [dart2wasm] Remove dynamic modules support from the dart2wasm compiler.
Change-Id: If92f55296dfe83b64165a2bd07eaefb7d137198c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/497341
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2026-04-24 12:04:23 -07:00
Nate Biggs fdd833a689 Update import/export naming to take into account "internal" names and allow those names to be minified.
This reduces the size of both the mjs file (which contains the JS implementations) and the wasm file
(which includes these names as import/export names).

The savings here is relatively small for most apps.

A main impetus for this is the useful side-effect that it makes the names more stable. By assigning
names after TFA has run, the indices don't change when an unused JS method is added/removed. This
helps make the ir_tests more stable.

Change-Id: If47e3460d060a979bd41ef430c13c6828d98b549
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/494720
Commit-Queue: Nate Biggs <natebiggs@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
2026-04-22 08:31:35 -07:00
Martin Kustermann 12174830ec [dart2wasm] Optimize the way we implement constructors
This reduces essentials main module around -0.4% and possibly
opens up for changes in the inlining (specifically to possibly
not force-inline all initializers anymore)

This shrinks the amount of information
* initializer result values
* the body parameters
* the allocator needs to forward less from initializer to body

We do that by analyzing constructor parameters to see
which parameters are needed for the constructor

Change-Id: I967fa4102ea6e9d498ff07aedabc368b038e1085
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/496341
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-04-21 00:13:42 -07:00
Martin Kustermann 4ee6a66270 [dart2wasm] Format pkg/dart2wasm after language version was increased
The change in [0] increased the language version of pkg/dart2wasm. That
in return changes how the package is formatted by the autoformatter.

This CL runs now the formatter to re-format the code. Unfortunately this
makes blame lists worse. But not doing it will make us have to disable
auto-formatting before saving files which is very annoying.

[0] https://dart-review.googlesource.com/c/sdk/+/487944

Change-Id: I6953fe0d6a824b2b79a26bbadb0bb977cec70b7a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/490821
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-03-26 04:32:19 -07:00
Martin Kustermann b1df65ded8 [dart2wasm] Use table based dispatch for dynamic calls
This reduces e mail module by -4% in compressed form. It does have
a increase of 2.5% in uncompressed form. This is due to repeated
entries in the element section of the dynamic dispatch table and
will be addressed in future CLs.

Currently if the main module of an app has a dynamic call, it will have
a caller-shape specific dynamic dispatcher that checks (via class ids)
all possible targets in the app and issues calls to them.

That means the code size is O(targets) - even if most targets reside in
deferred modules.

We change this now to use a row displacement table based dispatch,
just as our normal typed dispatch table. Though there's a few
differences

* In a typed call we know the target exists, in a dynamic call we don't
  know whether the target exists (it may be a NSM case). To accomodate
  for this we make 2 wasm tables of the same layout: First we load a
  table of i31refs and check if the value is in agreement with the
  receiver class id. If so we have a match and can load the actual
  destination from the funcref table.

* In typed calls we use one row in the table for each selector. In the
  dynamic invocation case we'll use one row per dynamic selector + call
  shape.

* The wasm module that contains the actual instance method will also
  hold all it's dynamic forwarder functions (one per dynamic caller
  shape). The elements section of that module will cause initialization
  of the two slots in the two tables mentioned above.

Issue https://github.com/dart-lang/sdk/issues/62639

Change-Id: I4e631f98fe9c58e2110bac34c3f5ff6d11bef909
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/487660
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-03-18 03:17:42 -07:00
Martin Kustermann b5cc7e17f2 [dart2wasm] Refactoring of dynamic call implementation
The CL does a few things:

* Rename "forwarder" to "dispatcher" to disambiguate them from actual
  dynamic forwarders:

    - "dynamic dispatcher": A function that takes all arguments of a
      dynamic call & dispatches them to the right target (which is a
      dynamic forwarder).

    - "dynamic forwarder": A function that takes arguments, type checks
      them, unboxes them (if applicable), calls the real function, boxes
      the result (if applicable).

* Make dynamic setter dispatchers & forwarders have `void` return type.
  Setters in Dart don't produce values, the call sites turn `a.b = c`
  into something like this: `let tmp = c in (let a.b=(tmp) in tmp)`

* Make `CallShape` remember not just the argument shape but also whether
  it was a getter, setter or method.

Issue https://github.com/dart-lang/sdk/issues/62639

Change-Id: Ia616f504a3ed3e9fa86a93d274af794b9437e5ed
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/488081
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-03-17 03:40:03 -07:00
Martin Kustermann 2e2f8ae7b3 [dart2wasm] Make normal dynamic calls avoid wasm array creations
Currently a dynamic call will call a helper which will create
3 array objects (for type, positional and named arguments).

It will then pass those wasm arrays to type checker methods.
Those will then get values out of the array, type check and call
the target.

We change this now such that in the normal case (**) we avoid
the array creations. Instead we make a dynamic forwarder function
per target and call shape.

We also outline the array creation when creating `Invocation`
objects. So for simple caller shapes - such as `clone()` the
creation of an `Invocation` object is a call without arguments
instead of various array creations.

Issue https://github.com/dart-lang/sdk/issues/62640

(**) If the dynamic call could be call-via-field we still create
those arrays, as we use them for closure type checking.

Change-Id: Ia8f3f3cd95f650bd8706a15ed68a43db4de80a6e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/487020
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-03-12 02:17:07 -07:00
Martin Kustermann 95120a02b3 [dart2wasm] Allow deferred loading partitioning to work on getters/setters separately
The CL changes a few things

  * do not use instance field references in partitioning (they do
    not have a storage location - compared to static fields
    which need to be placed into a specific module)
  * allow static getters and setters to possibly reside in different
    modules
  * separate concept of static field initializer function vs static
    field storage (storage needed for getter&setter, init function
    only for getter)
  * fix `reference.isSetter` to also check for `isTypeCheckerReference`
  * do not create class->method dependencies on abstract instance
    methods
  * run partitioning tests with compiler assertions

Change-Id: I61231b912c3f73f67a4d476f35113859694a80c7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/485441
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
2026-03-05 11:26:31 -08:00
Martin Kustermann 4b2d61e9ca [dart2wasm] Make operator[]= methods have 0 return values
This avoids extra `ref.null none` in the `[]=` method bodies and
avoids `drop` instructions on each call site.

This is analogous to how we treat setters.

Change-Id: Idb66c258c64856e0adcbd1c8e902851dfa640d15
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/485221
Reviewed-by: Nate Biggs <natebiggs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-03-03 12:57:00 -08:00
Martin Kustermann e70f5ebc37 [dart2wasm] Mark String.+ as a pure function
This removes 60 kb from essentials app main module (~0.8%)

We have code patterns such as

  ```
    main() => print(Foo('prefix-'));

    class Foo {
      final String foo;
      final String bar;

      Foo(String prefix)
        : foo = prefix + 'foo',
          bar = prefix + 'bar';
    }
  ```

If we can tree shake `foo` and `bar` fields, we still emit code for
evaluating the field initializers (as they could have side-effects).

Though for String.+ we know there's no side-effects, so we can mark the
method as `@pragma('wasm:pure-function')`. That in return will allow
binaryen to remove unused field initializer evaluation code like the
above.

Change-Id: I8a99bc35bfa252b3043bbc35405b68d03bc1184f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/484980
Reviewed-by: Nate Biggs <natebiggs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-03-03 10:51:02 -08:00
Martin Kustermann f99f5b1a2b [dart2wasm] Allow marking functions as pure functions
If a function doesn't have an effect we can now mark it via
`@pragma('wasm:pure-function')`. We'll then emit this as metadata
in the `binaryen.remove.if.unused` custom section.

This allows `wasm-opt` to remove calls to such functions if the result
of the call isn't used.

For now we mark a few string functions as pure.

Closes https://github.com/dart-lang/sdk/issues/62665

Change-Id: I8d38fb5894fd98248dc4d648d99c8cdcddc271a5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/481802
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
2026-02-23 01:39:37 -08:00
Simon Binder 7d8cd032dc [dart2wasm] Interfaces for accessing memories
This adds the `Memory` class to `dart:_wasm`, allowing Dart code to
load and store numeric types in linear memory.
Since `dart2wasm` doesn't generate a memory instance by default, there
is no singleton instance of `Memory`. Instead, memories are defined as
`external` top-level getters annotated with a pragma like
`@pragma('wasm:memory-tyype', MemoryType(limits: Limits(1, 10)))` to
declare their type.

Interop happens in a static way: Methods on `Memory` cannot be torn-off
and, since the target memory is encoded directly in the store/load
instruction, there's also no polymorphism for memories in Dart.
Attempting to call methods on a memory instance that isn't a direct
reference to its definition is a compile-time error.

Memories can also be imported and exported through the existing
`wasm:import` and `wasm:export` pragmas.

TEST=tests/web/wasm/memory_test.dart

Change-Id: I726f33ac2ec04afab55c5a2b6bc09079d0193e02
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/470020
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
2026-01-15 03:45:21 -08:00
Ömer Ağacan ff2ae7bff4 [dart2wasm] Make ExportNamer private
Make `ExportNamer` a state of `Exporter` and make it private.

`Translator` has too many fields already and it maintains too much
state. Making `ExportNamer` a private field of `Exporter` makes the
translator API simpler as it needs to initialize one less field, and
exporter API becomes simpler with one less type. `ExportNamer` is also
not aliased in multiple classes now, which makes it easier to understand
how it's used.

Change-Id: Id32c25ca518ca211de8202647fd0974968402665
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/472220
Commit-Queue: Ömer Ağacan <omersa@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
2026-01-13 03:03:39 -08:00
Martin Kustermann d4531a418d [dart2wasm] Allow cross module function inlining
This reduces main wasm module a bit (<1%) as it avoids the main module
from having to define and export simple accessor (e.g. field accessor)
functions.

There's two scenarios to consider:

Deferred module calls function from main module
=> No issues

Main module calls function from deferred module
=> This call must have been preceded by a CheckLibraryIsLoaded
=> The wasm modules of the deferred import must have been loaded
=> No issues.

We also change the inlining to be a little more conservative on static
field getters, as a static field may need to do a lazy initialized check
and call initializer function.

We also give the initializer function a better name.

Change-Id: I5d9b138c9fcb41f8580602fd1ba9ec5aa4af5341
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/467820
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
2025-12-16 03:09:19 -08:00
Martin Kustermann b0e18fa40b [dart2wasm] Fix bug in @pragma('wasm:static-dispatch') & remove corresponding dispatch table entries
There was a bug in `CallTarget.signature` which triggers if there's only
one entry that's statically dispatched against, which causes us to
inline the polymorphic dispatcher, which relies on this (previously
incorrect) signature.

The CL also changes the dispatch table building logic to not allocate
table entries for the statically dispatched regions (as they would
never be used).

Change-Id: Ic2d0c387e8863e89ef811e1892642fe81df9189a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/468000
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2025-12-16 01:42:00 -08:00
Nate Biggs 5feffef90d [dart2wasm] Handle members inherited into the interface of dynamic submodule subclasses.
If a member is not considered "callable" from submodules, user code
should not be able to invoke it. However, dynamic module subclasses may
"inherit" the member. Virtual calls in the main module may then try to
invoke the inherited member.

We need to support updating the selector row in the dispatch table with
the class ID of the new subclass. This is similar to an overridable
function except that the slot will be filled with an imported member
from the main module (not a newly defined member).

See https://dart-review.googlesource.com/c/sdk/+/457441 for more
context.

Bug: https://github.com/dart-lang/sdk/issues/61813
Change-Id: I4162931747149a7356909ec62084375fc766b0cc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/458160
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2025-10-29 08:08:35 -07:00
Martin Kustermann 6c228bff4a [dart2wasm] Add simple way to test IR changes
In order to test generated code for a function one can

  * place a dart file in `pkg/dart2wasm/tets/ir_tests`
  * annotate functions that shouldn't be inlined
  * describe which functions we want to dump in the expectation file
  * generate an expectation file.

This will allow generating renatively small expectation files for only
functions we care about and types/globals/... those functions need.

Change-Id: Ic7b6b6dece16ab453202aa2c4f9412de2fc251ae
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/454840
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2025-10-15 14:25:20 -07:00
Mayank Patke 58f700c438 [dart2wasm] Don't use IDs as keys for export name mapping.
We have means of serializing References and Constants directly, so it is
unnecessary to generate IDs as indices. This does cause the export name
mapping to get split up a little (just as callable reference IDs were
stored separately from constant export IDs).

This is because the kernel metadata repositories are serialized by the
kernel's BinarySink, which correctly handles Constants, but cannot
serialize References from the TFA'd component to the metadata of the
un-TFA'd component. Meanwhile, the dart2wasm DataSerializer can
serialize these References, but does not handle Constants.

Therefore, the callable reference mapping is stored on the main module
metadata (just as callable reference IDs were), while the constant
mapping is stored in a metadata repository (just as constant IDs were).

The exporter must then be aware of both of these to be used as a
centralized interface.

Change-Id: I1c0fc3eed5a96b3c3e755826749ebd29c3fa2b4b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/446990
Reviewed-by: Nate Biggs <natebiggs@google.com>
2025-08-27 14:33:55 -07:00
Mayank Patke 6cbc6df06a [dart2wasm] Add export name minification to dynamic modules.
Change-Id: Id316930bc9d65566d16a12b494caacc846f899ff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441989
Reviewed-by: Nate Biggs <natebiggs@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
2025-08-27 14:33:55 -07:00
Nate Biggs 7d199f2171 [dart2wasm] Enable compiler asserts for dynamic module tests.
Fixes several asserts that were throwing when running with dynamic modules enabled.

Bug: https://github.com/dart-lang/sdk/issues/60743
Change-Id: I6bcd3723ecdee784b3d1603e1c87a47e3703a7f3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423261
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2025-05-22 16:48:22 -07:00
Mayank Patke c56a8be4e5 [dart2wasm] Replace "dynamic module" with "submodule" where appropriate.
Change-Id: I4e51e10928ccc26c18f2a63f6cb6b23cd618d853
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426003
Commit-Queue: Mayank Patke <fishythefish@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
2025-05-02 10:15:40 -07:00
Ömer Ağacan 8ab606ba91 [dart2wasm] Simplify FunctionCollector.getFunctionName
Change-Id: I8518a0adc20889ff36535932328baffe203be012
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/417462
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
2025-03-24 06:37:37 -07:00
Nate Biggs bd7f26f54d [dart2wasm] Allow dynamic modules to run without re-compiling or running TFA on main module.
Accomplishes this by serializing more metadata in the main module metadata:
1) Most of this new metadata is to have access to the main module's dispatch table from the dynamic modules, including ProcedureMetadataAttributes and the DispatchTable itself.
2) Indices to create correct calling names from the dynamic module into the main module (or into the global updateable functions "table").
3) Basic tree-shaking information about classes (e.g. did TFA fully delete a member or just delete its body).

Some metadata from TFA is still expected throughout the compiler. For dynamic modules, we create pessimistic versions of this information and attach it to the new Component.

All the same dynamic module tests that were passing (or failing) before are still in the same state. This significantly speeds up compilation of dynamic modules though as only necessary code is compiled and TFA is not run.

Change-Id: I109f53cf5dcbe6579c0f78e71ce7779d593455e9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415500
Reviewed-by: Martin Kustermann <kustermann@google.com>
2025-03-21 20:53:53 -07:00
Martin Kustermann d66befd347 [dart2wasm] Add support for @pragma('wasm:weak-export', '<name>')
We explicitly & unconditionally export functions to JS via the
`@pragma('wasm:export', '<name>')` annotation.

This is mainly useful for external APIs that the JavaScript side can
invoke - for example `$invokeMain()`.

Though we currently use the same mechanism also in other places where a
Dart function `A` (if used) calls to JS which calls back into Dart via
calling exported Dart function `A*`.

The issue is that this mechanism doesn't work very well with tree
shaking: If the function `A` is not used it will be tree shaken. We will
then also not emit the JS code, but we still compile the exported
function `A*` as it's a root due to `@pragma('wasm:export', '<name')`. We then also have to compile everything reachable from `A*`.

This is the case for a few functions in the core libraries but even more
pronounced in code that the modular JS interop transformer generates for
callbacks: It generates `|_<num>` functions that call out to JS which
call back into a dart-exported `_<num>` function. The former may be
unused & tree shaken (as well as their JS code) but the ladder are
force-exported and therefore treated as entrypoints.

This CL solves problem by

  * Mark function `A*` as weakly exported via
    `@pragma('wasm:weak-export', '<name>')`

    => TFA will not consider such functions as entrypoints
    => TFA will only retain such functions if they are referenced by
       other functions that aren't tree shaken.
    => The backend will export such functions as `<name>` if they are
       referenced by any other code that's compiled.

  * Making the code that calls function `A` also reference (but
    not use) `A*`.

    => This will make TFA retain function `A*` if it retains `A`.
    => In core libraries we manually reference `A*` in code that uses
    `A` and mark `A*` as weakly exported
    => In JS interop transformer we emit similar code for callbacks
    => We refer `A*` by using `exportWasmFunction()` which is an opaque
       external function that prevents TFA and backend from optimizing
       it away - so the function will be generated & exported.

Overall this CL ensures we only keep the exported functions if we
actually need them (i.e. we call to JS and JS calls those exported
functions).

This shrinks stripped dart2wasm hello world file in -O4 from
28 KB to 12 KB.

It also enables tree shaking of callback using JS interop code.

Change-Id: Ie81eac49cbcb574d569ea95a90538e8f417e2a12
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415220
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2025-03-14 01:31:10 -07:00
Nate Biggs 7b28c5a94f [dart2wasm] Update serialization strategy for dynamic modules.
Introduces some more machinery to simplify the serialization pipeline. Now the logic to index kernel nodes is hidden away in the serializer. Now throughout the compiler if entities (i.e. classes, members and references) need to be serialized they can be passed to the serializer directly.

Also adds support for tracking brand type assignments from the main module to the dynamic module. This is necessary to decouple the execution order of both compilations which is a WIP.

Change-Id: I38dd0ed11811ea12a2230959588b261719976592
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412461
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2025-03-07 16:45:35 -08:00
Nate Biggs a4a4ca8a41 [dart2wasm] Dynamic modules
Missing from this implementation:
- Closure/dynamic calls with differing signatures
- Overrides with extra optional parameters
- Records with same shape defined in different dynamic modules
- Avoiding running TFA on dynamic module.
- Recompilation of only updateable functions from main module.
- Persist wasm def types from main module.

Testing is currently done locally via the dynamic_modules package test suite:
dart pkg/dynamic_modules/test/runner/main.dart --runtime=dart2wasm

Immediately after this lands we can introduce a new step to one of the wasm test matrix configurations that runs the above test suite (the VM has a similar configuration).

Change-Id: I3386d84be11b773842d45f4268a62a54c47e352b
Tested: Tested via new tests in dynamic_modules package. Tests run locally but will add to existing config.
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/397721
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2025-02-11 13:59:46 -08:00
Martin Kustermann c833c1227c [dart2wasm] Add support for dispatch table calls to unchecked entries
Now that we have support for generating checked & unchecked entrypoints,
we can make dispatch tables also target unchecked entry points.

This is beneficial especially in cases where there's dispatches on
`this` that require covariant checks but we don't know the target method (i.e. we cannot devirtualize it because the method we
dispatch to may beoverriden).

We keep the existing selectors that we have, but a selector will now
have

  * one row if none of the implementations of the selector need to
    perform type checks
    => `SelectorInfo` has a `SelectorTargets _normal`

  * two rows if any of the implementations of the selector need t
    perform
    a type check
    => `SelectorInfo` has a `SelectorTargets _checked`
    => `SelectorInfo` has a `SelectorTargets _unchecked`

Once an unchecked entrypoint is also used in the dispatch table (only
if there's any unchecked calls to that selector) then binaryen can no
longer optimize the signature of the function. It means we may have
perform e.g. downcasts / boxing in the unchecked entry where we
wouldn't do before (because we only had static calls to unchecked entry
before this PR).

So we're going to force-inline calls to unchecked entrypoints. This
avoids sometimes down casts and boxing. It also seems to actually
shrink the binary size.

Change-Id: I3ba4980c42886cc883fb610533f5fac9cce39b65
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/407740
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2025-02-05 04:57:00 -08:00
Martin Kustermann f05ae1c3af [dart2wasm] Introduce unchecked entrypoints to instance members
If an instance member is a normal method or a setter, it has arguments
that may need to be type-checked. In some situations we know that we
don't have to perform them at all (e.g. if we know there's only
dispatches on `this`). In other situations we have guarantees on
individual call sites that we can skip the checks.

Up until now we have only taken advantage of the call site guarantees
when we decided to inline the target (then we avoided doing the type
checks).

In this CL we will make this also work if the target isn't inlined, but
called: Whenever a member has any parameters that we need to type check,
then we will generate checked & unchecked entrypoints. Both of them
do the optional parameter handling, but only the checked entrypoint will
perform the type checks, the unchecked entrypoint skips them. Both
unchecked & checked will call to a body function that has the body of
the member.

=> We will skip the type checks whether we inline the target or not.

The dart2wasm compiler currently represents targets it can call via
via `Reference`s: A member may be used in different ways: as a tear-off,
as a setter, type checker, etc.

=> We introduce now 3 more `Reference` kinds, namely checked, unchecked
and body.
=> The rest of the compiler is adjusted to also handle those new
`Reference` types.

All calls in the code generator that may target members that could have
checked and unchecked entrypoints now use
```
  Reference getFunctionEntry(Reference target, {required bool uncheckedEntry})
```

We maintain an invariant throughout the code base that a function

* **either** has only one "normal" entry if no arguments need type checks
* **or** has "unchecked" and "checked" entries (which both call a "body")

The dispatch table currently has only "normal" or "checked" entries in
it. So the "unchecked" entries are (if used) always called directly.

=> We only generate "unchecked" if there's direct unchecked calls.
=> We only generate "checked" if there's direct checked calls or
   dispatch table calls.
=> If only one entrypoint ends up being generated, binaryen can inline
   the body into the entrypoint function.
=> If both entrypoints are present, binaryen may often inline the
   unchecked one into call sites that then directly call the body.

In a future CL we may allow calling unchecked entries also via the
dispatch table.

Overall this approach leads to minimal changes to code size changes, but
brings -O2 performance closer to -O4.

Change-Id: Ic3082cc397335b969fd413f72652cc5af753adf7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406980
Reviewed-by: Ömer Ağacan <omersa@google.com>
2025-02-03 00:08:26 -08:00
Martin Kustermann 74aa17dd3e [dart2wasm] Emit unreachable instructions after calls that never return
This allows wasm optimizers as well as wasm runtimes to optimize code
better as they know calls to slow paths that throw will never return.


Change-Id: Iace1827062dbe00ce24c737b6369bf588e748ee9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404582
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2025-01-21 06:02:22 -08:00
Nate Biggs 96c4e4c81f [dart2wasm] Use field type instead of global type for static field type.
The calls to 'getGlobalForStaticField' eagerly generate the initializer constant for the associated field. In both these cases we don't need that just to get the type of the field. Instead we can use 'translateTypeOfField' directly (same as 'getGlobalForStaticField').

This also removes unnecessary nullness from the type when the field is lazily instantiated. The global type may be nullable even if the field type isn't.

Change-Id: Id369e07335fc5350524a1b8b6c04f19dd94f8b8a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/401280
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2024-12-17 15:59:28 -08:00
Ömer Sinan Ağacan dbee089237 [dart2wasm] Devirtualize closure calls based on TFA direct-call metadata
Use the TFA direct-call metadata to directly call a closure in function
invocations.

Closes #55231.

Tested: existing tests cover the new code paths, but I also added a new
test.
Change-Id: Ib5f26b10efd77570e256196b4bfb07e6bef800c0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/397260
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
2024-12-11 10:46:20 +00:00
Ömer Sinan Ağacan 3140b09545 [dart2wasm] Name globals and functions for static fields
Adds new names:

- Globals for static field initialization flags are named as
  "$memberName initialized".

- Globals for static field values are named as "$memberName".

- Static field initialization functions are named as "$memberName
  initializer".

Also documents fields of `Globals` and removes unused field
`_globalInitializers`.

Change-Id: Id54c6e679b2d028917e86184e2913bb51e6b5a97
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/396240
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
2024-11-19 11:13:22 +00:00
Ömer Sinan Ağacan d746c5f941 [dart2wasm] Some refactoring around context and capture generation
This mainly refactors code that use the `Closures` type to make it a bit
more clear what it does and how it does it, and make it more difficult
to misuse.

Changes:

- Make `CaptureFinder`, `ContextCollector`, `Closures.findCapures`,
  `Closures.collectContexts`, `Closures.buildContexts` private.

  It doesn't make sense to use these types outside, and the `Closures`
  members need to be called together and in the right order. Make them
  private and call them in the constructor.

  Reduces API surface of `closures.dart` and makes it easier to use.

- Document all public members of `Closures`.

- Remove unused `ClosureRepresentation.exportSuffix`.

- In `TearOffCodeGenerator`, inline the single-use function
  `generateTearOffGetter`. Makes it clear that the code is not reused
  elsewhere.

- Make the type of `Types.nonNullableTypeType` more precise. Use it in
  `closures.dart` instead of having a separate copy of the same thing.

- In a few places where we had function body entirely guarded with an
  `if`, add an early return.

  For example:

  ```
  void f() {
      if (x) { ... lots of code ... }
  }
  ```

  Becomes:

  ```
  void f() {
      if (!x) return;
      ... lots of code ...
  }
  ```

  Similarly do it in loop bodies.

Change-Id: Ia2a74b89ae311b8f32b9f1a4e72c51d4ea3861e8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/392942
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
2024-11-01 13:30:40 +00:00
Nate Biggs 56536825ed [dart2wasm] Add deferred loading support to dart2wasm (10/X).
Remove all references to a global `ModuleBuilder` (code like `ModuleBuilder get m => ...`). References to module builders should be more specific now, whether that be to the `mainModule` on Translator or some other module.

Technically this could remain and always refer to the mainModule but making the name more specific makes it clear there is no single ModuleBuilder anymore and code that needs to access a ModuleBuilder will need to consider which one it needs.

Change-Id: I140c4e80a131a1786fa66c93be07a622dd0756c3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381443
Reviewed-by: Martin Kustermann <kustermann@google.com>
2024-09-04 21:58:12 +00:00
Nate Biggs 7d0742c9e1 [dart2wasm] Add deferred loading support to dart2wasm (9/X).
Update intrinsic field table allocation to support importing tables across modules. The tables will always be defined in the main module but can be imported into subsequent modules.

Change-Id: I09c8f316f528fa28c02fddcf356a33583a88ef56
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381442
Reviewed-by: Martin Kustermann <kustermann@google.com>
2024-09-04 21:58:12 +00:00
Nate Biggs c5bd4d05db [dart2wasm] Add deferred loading support to dart2wasm (6/X).
Wasm globals serve a few purposes in the compiler such as storing static fields and closure vtables. Sometimes the access of these globals will be from a different module than the ones they're defined in. We need some indirection to be able to access them in these cross-module situations.

This change adds getter and setter functions that can be called via the StaticTable when a global needs to be accessed from a different module.

We use References to track the owning module for each global to determine if we can access it directly or not.

Change-Id: I93191c83dee1b7a47171c5808e64b071479cdeea
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381324
Reviewed-by: Martin Kustermann <kustermann@google.com>
2024-09-04 21:58:12 +00:00
Nate Biggs b5b335fb75 [dart2wasm] Add deferred loading support to dart2wasm (4/X).
Adds `Translator.callReference` which will be the main indirection point for calls between modules.

Any call that might need to be made across modules should go through `callReference` and this will handle checking if the call is local to the same module. If it is then it will use a normal "call" instruction, otherwise it will re-route the call through a table and "call_indirect".
`Reference` is the primary module assignment mechanism, we will generate synthetic References for anything that doesn't have one from the Kernel.

For now just maintains the current behavior of generating a `call` instruction.

Change-Id: I9a300d100bc7c27ec2aba42af367e91201dcadc3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381322
Reviewed-by: Martin Kustermann <kustermann@google.com>
2024-09-04 21:58:12 +00:00
Nate Biggs 5ca6b36bf1 [dart2wasm] Add deferred loading support to dart2wasm (0/X).
Remove code similar to `m.types` from the compiler. With upcoming changes there will not be a global `ModuleBuilder` to get the `TypesBuilder` from. There will be multiple modules being built at once.

Change-Id: I42c6616f2babb28e14ad418ffd9e8e9b3f3e946e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381342
Reviewed-by: Martin Kustermann <kustermann@google.com>
2024-09-04 21:58:12 +00:00
Ömer Sinan Ağacan 3a2a800f8f [dart2wasm] Minor refactoring in FunctionCollector:
- The comments around `collectImportsAndExports` call were wrong. Since
  d9b5eec `wasm_builder` can only generate minimal recursive groups, and
  `translateExternalType` only generates `ref null any` for references
  to structs for Dart types. We don't need to treat imports and exports
  specially to make sure they are in their own minimal groups.

- With the special case for imports and exports gone, we move
  `collectImportsAndExports` to `FunctionCollector` and simply the
  `FunctionCollector` API by making it private.

- Also remove `FunctionCollector.addExport` which has one call site, and
  rename `getExport` as `getExportName` for clarity and consistency with
  the rest of the code where we refer export names as `exportName`.

Change-Id: I399aa0553370d2b7983ae8cd0f89e1ef84b0f2ef
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/383323
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
2024-09-03 17:57:30 +00:00
Martin Kustermann e5704eacfa [dart2wasm] Fix missing type check in implicit field setters if field is covariant
For a class like this:

   class A<T> {
     T? value;
   }

the implicit setter function (`A.value=`) has to check the argument
type.

TEST=language/covariant_field_implicit_setter_test

Change-Id: Ie990b79f631275fb0a7c88ec7d7dd3a82a784148
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381000
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2024-08-16 11:49:19 +00:00
Martin Kustermann ca60a2fb77 [dart2wasm] Introduce [CallTarget] abstraction
Right now dart2wasm inlining abilities is restricted

- Only [CodeGenerator]s can inline things
- The [CondeGenerator]s can only inling AST targets (it takes
  [Reference]).

This CL extends our `MacroAssembler` (which is just a static extension
on the `InstructionsBuilder`) to have an `invoke(CallTarget target)` method.

The new [CallTarget] abstraction allows any [CodeGenerator] (not just
the AST specific ones) to be inlined.

=> Allow all asm building code to inline things
=> Allow any [CodeGenerator] that is inline'able to be inlined

Change-Id: I0480282ee07adb8dbe5c37609c96de16535c49ab
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378920
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2024-08-08 10:40:14 +00:00