[dart2wasm] Introduce unresolved accesses & link phase

This reduces ACX Gallery's main module from 8.4 MB to 2.75 MB.

This CL adds new infrastructure to our compiler: We allow codegen to
emit unresolved instructions which will get patched up later on in a
final link phase.

We use this for constants: Generating the body of a function (or
initializer of a global) may need to access a constant. Though in
deferred loading mode we may not have decided (yet) into which module
to place the constant.

* We emit an unresolved constant access (kind of dummy instructions
  which still maintain stack machine) as a patchable region in the
  instruction stream and remember that the constant (and any constant it
  refers to, directly or indirectly) was used by the corresponding module.

* After code generation we have collected all constant uses (and the
  modules they are used in) and have therefore all knowledge to decide
  where to place constants. (See below on placement logic)

* In a final link phase that will walk over any instructions with
  unresolved constant accesses (patchable regions) and patch them up
  with the actual instructions to access the constants.

Part 1) Determination of global order

So far the creation order of globals determined the order in the global
section. But now we emit unresolved global uses and later on have to
define (or import) the globals in modules.

=> To allow this we now determine the order of globals when we build the
   globals section instead.

=> This would also allow other things: Choose ordering of globals based
   on usage count, etc.

Part 2) Separation of concerns in `constants.ensureConstant()`

So far the `constants.ensureConstant()` has done several things:

  * performed constant lowering
  * analyzing whether the constant should be lazy or eager
  * determine the type of the global of the constant
  * actual creation of global & initializer function (if needed)
  * doing the above for all transitive constants

=> The result was the `ConstantInfo` object.

We now separate these things:

The first part will lower constants, determine lazy or not, determine
type. This will recursively walk the constant DAGs and create
`Constantinfo` as needed for all of them.

=> Each `ConstantInfo` (representing information about a `Constant`)
   will now also remember the child constants (in the form of
   `List<ConstantInfo> children`) it will use when defining the constant.

=> When code generation uses a constant we will remember that that
   module-use of the constant and all it's child constants.

=> Representing this as `constantInfo.children` avoids recursive AST
   visiting, avoids re-lowering the constants and ensures we don't have
   to keep two AST visitors in sync.

Part 3) Tracking constant uses

When the code generation uses a constant, we remember it being used in
the module being currently compiled. We use this usage information in
the final stage to determine where to place constants.

Special situation: If we have constant uses across modules where
deferred loading is involved. For example here:
    ```
    import 'foo.d.dart' deferred as foo;

    main() {
      ...
      print(foo.topLevelConstant);
    }
    ```
which gets lowered to something like this
    ```
      StaticInvocation(target=print, args=[
        let
          _ = StaticInvocation(target=checkLibraryIsLoaded, args=[StringLiteral('foo')])
        in
          ConstantExpression(topLevelConstant)
      )
    ```

Even though the main module is using the `topLevelConstant` it does so
under what I call a deferred loading "load guard": The code accessing
the constant will never be executed unless the `foo` deferred library
was successfully loaded.

=> We make our usage tracking consider such uses not a usage of the main
   module but rather the module containing deferred library of the
   "load guard".
=> The `CodeGenerator` will track the active "load guard" when it goes
   down the tree.
=> This allows pushing constants to deferred modules even if they are
   used in main module code.

Part 4) Defining of constants

During code generation we (generally speaking) emit a patchable region &
record the constant use of the constant DAG (see above).

During the linking phase we then have global knowldge of constant uses
and start defining them.

Theoretically we want to define a constant in a wasm module in the
loading graph where all using modules have it as direct or indirect
dependency but the dependency being the closest one to the uses.

=> As simplification for now: If two different modules use a constant we
   place it in the main module. We can later on make this more precise if
   complexity is warrented.

To avoid emitting many patchable regions that we later on have to fix
up we add an optimization during code generation:

=> As soon as a use is in the main module, we define the constant DAG
   in the main module.
=> As soon as there's 2 uses in different modules, we define the
   constant DAG in the main module.

Misc

* We separate constant definition from importing / exporting them.

* The new architecture changes constant visiting slightly so the names
  of constants in expectation files change as a side-effect of this.

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

Change-Id: Ib44dee4c2514fb4af871e7078f5bfe43077922fd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/458240
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>

https://dart.googlesource.com/sdk/+/6184c447abe2b876de0d883fd675651c77d43821
2 files changed
tree: fadce9e638b19214c1cd4bc97d2b7c5d843eff2d
  1. engine/
  2. tools/
  3. .gitignore
  4. commits.json
  5. DEPS
  6. OWNERS
  7. README.md
README.md

Monorepo

A gclient solution for checking out Dart and Flutter source trees

Monorepo is:

  • Optimized for Tip-of-Tree testing: The Monorepo DEPS used to check out Dart and Flutter dependencies comes from the Flutter engine DEPS with updated dependencies from Dart.

Checking out Monorepo

With depot_tools installed and on your path, create a directory for your monorepo checkout and run these commands to create a gclient solution in that directory:

mkdir monorepo
cd monorepo
gclient config --unmanaged https://dart.googlesource.com/monorepo
gclient sync -D

This gives you a checkout in the monorepo directory that contains:

monorepo/
  DEPS - the DEPS used for this gclient checkout
  commits.json - the pinned commits for Dart, flutter/engine,
                 and flutter/flutter
  tools/ - scripts used to create monorepo DEPS
engine/src/ - the flutter/buildroot repo
    flutter/ - the flutter/engine repo
    out/ - the build directory, where Flutter engine builds are created
    third_party/ - Flutter dependencies checked out by DEPS
      dart/ - the Dart SDK checkout.
        third_party - Dart dependencies, also used by Flutter
flutter/ - the flutter/flutter repo

Building Flutter engine

Flutter's instructions for building the engine are at Compiling the engine

They can be followed closely, with a few changes:

  • Googlers working on Dart do not need to switch to Fuchsia's Goma RBE, except for Windows. The GOMA_DIR enviroment variable can just point to the .cipd_bin directory in a depot_tools installation, and just goma_ctl ensure_start is sufficient.
  • The --no-prebuilt-dart-sdk option has to be added to every gn command, so that the build is set up to build and use a local Dart SDK.
  • The --full-dart-sdk option must be added to gn for the host build target if you will be building web or desktop apps.

Example build commands that work on linux:

MONOREPO_PATH=$PWD
if [[ ! $PATH =~ (^|:)$MONOREPO_PATH/flutter/bin(:|$) ]]; then
  PATH=$MONOREPO_PATH/flutter/bin:$PATH
fi

export GOMA_DIR=$(dirname $(command -v gclient))/.cipd_bin
goma_ctl ensure_start

pushd engine/src
flutter/tools/gn --goma --no-prebuilt-dart-sdk --unoptimized --full-dart-sdk
autoninja -C out/host_debug_unopt
popd

Building Flutter apps

The Flutter commands used to build and run apps will use the locally built Flutter engine and Dart SDK, instead of the one downloaded by the Flutter tool, if the --local-engine option is provided.

For example, to build and run the Flutter spinning square sample on the web platform,

MONOREPO_PATH=$PWD
cd flutter/examples/layers
flutter --local-engine=host_debug_unopt \
  -d chrome run widgets/spinning_square.dart
cd $MONOREPO_PATH

To build for desktop, specify the desktop platform device in flutter run as -d macos or -d linux or -d windows. You may also need to run the command

flutter create --platforms=windows,macos,linux

on existing apps, such as sample apps. New apps created with flutter create already include these support files. Details of desktop support are at Desktop Support for Flutter

Testing

Tests in the Flutter source tree can be run with the flutter test command, run in the directory of a package containing tests. For example:

MONOREPO_PATH=$PWD
cd flutter/packages/flutter
flutter test --local-engine=host_debug_unopt
cd $MONOREPO_PATH

Troubleshooting

Please file an issue or email the dart-engprod team with any problems with or questions about using monorepo.

We will update this documentation to address them.

  • flutter commands may download the engine and Dart SDK files for the configured channel, even though they will be using the local engine and its SDK.

Windows

  • On Windows, gclient sync needs to be run in an administrator session, because some installed dependencies create symlinks.