DDC implementation of experimental dynamic modules.

In particular,

* Provides a DDC implementation for dynamic modules, where the download
  and instantiation of the module is delegated to the embedder of the
  program.

* Exposes an embedding API to allow embedders to define the loading logic.

* Adds a flag to compile code as a dynamic module. This includes
  generating an entrypoint trampoline and checks to validate that a
  dynamic module doesn't stump over previously defined libraries.

* Adds test coverage for DDC under `pkg/dynamic_modules/test/`.

Test suite can be run by executing:
```
DART_CONFIGURATION=ReleaseX64 out/ReleaseX64/dart-sdk/bin/dart \
    pkg/dynamic_modules/test/runner/main.dart -t ddc
```

Once we provide integration of test configuration results to that test
runner, we will add it as part of the test matrix.


Change-Id: I626b5fefe9a27546cc6d1630d17e812544a711c6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/379748
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Commit-Queue: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
index 6e9083c..7ecde9a 100644
--- a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
+++ b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
@@ -360,6 +360,14 @@
     // Map from module name to corresponding app to proxy library map.
     let _proxyLibs = new Map();
 
+    /**
+     * Returns an instantiated module given its module name.
+     *
+     * Note: this method is not meant to be used outside DDC generated code,
+     * however it is currently being used in many places becase DDC lacks an
+     * Embedding API. This API will be removed in the future once the Embedding
+     * API is established.
+     */
     function import_(name, appName) {
       // For backward compatibility.
       if (!appName && _lastStartedSubapp) {
@@ -609,8 +617,25 @@
     let _firstStartedAppName;
     let _lastStartedSubapp;
 
-    /// Starts a subapp that is identified with `uuid`, `moduleName`, and
-    /// `libraryName` inside a parent app that is identified by `appName`.
+    /**
+     * Runs a Dart program's main method.
+     *
+     * Intended to be invoked by the bootstrapping code where the application
+     * is being embedded.
+     *
+     * Because multiple programs can be part of a single application on a page
+     * at once, we identify the entrypoint using multiple keys: `appName`
+     * denotes the parent application, this program within that application
+     * (aka. the subapp) is identified by an `uuid`. Finally the entrypoint
+     * method is located from the `moduleName` and `libraryName`.
+     *
+     * Often, when a page contains a single app, the uuid is a fixed trivial
+     * value like '00000000-0000-0000-0000-000000000000'.
+     *
+     * This is one of the current public Embedding APIs exposed by DDC.
+     * Note: this API will be replaced by `dartDevEmbedder.runMain` in the
+     * future.
+     */
     function start(appName, uuid, moduleName, libraryName, isReload) {
       console.info(
         `DDC: Subapp Module [${appName}:${moduleName}:${uuid}] is starting`);
@@ -676,6 +701,75 @@
       library.main([]);
     }
     dart_library.start = start;
+
+
+    /**
+     * Configure the DDC runtime.
+     *
+     * Must be called before invoking [start].
+     *
+     * The configuration parameter is an object that may provide any of the
+     * following keys:
+     *
+     *   - weakNullSafetyErrors: throw errors when types violate sound null
+     *        safety (deprecated).
+     *
+     *   - nonNullAsserts: insert non-null assertions no non-nullable method
+     *        parameters (deprecated, was used to aid in null safety
+     *        migrations).
+     *
+     *   - nativeNonNullAsserts: inject non-null assertions checks to validate
+     *        that browser APIs are sound. This is helpful because browser APIs
+     *        are generated from IDLs and cannot be proven to be correct
+     *        statically.
+     *
+     *   - jsInteropNonNullAsserts: inject non-null assertiosn to check
+     *        nullability of `package:js` JS-interop APIs. The new
+     *        `dart:js_interop` always includes non-null assertions.
+     *
+     *   - dynamicModuleLoader: provide the implementation of dynamic module
+     *        loading. Dynamic modules is an experimental feature.
+     *        The DDC runtime delegates to the embedder how code for dynamic
+     *        modules is downloaded. This entry in the configuration must be a
+     *        function with the signature:
+     *            `function(String, onLoad)`
+     *        It accepts a `String` containing a Uri that denotes the module to
+     *        be loaded. When the load completes, the loader must invoke
+     *        `onLoad` and provide the module name of the dynamic module to it.
+     *
+     *        Note: eventually we may want to make the loader return a promise.
+     *        We avoided that for now because it interfereres with our testing
+     *        in d8.
+     */
+    dart_library.configure = function configure(appName, configuration) {
+      let runtimeLibrary = dart_library.import("dart_sdk", appName).dart;
+      if (!!configuration.weakNullSafetyErrors) {
+        runtimeLibrary.weakNullSafetyErrors(configuration.weakNullSafetyErrors);
+      }
+      if (!!configuration.nonNullAsserts) {
+        runtimeLibrary.nonNullAsserts(configuration.nonNullAsserts);
+      }
+      if (!!configuration.nativeNonNullAsserts) {
+        runtimeLibrary.nativeNonNullAsserts(configuration.nativeNonNullAsserts);
+      }
+      if (!!configuration.jsInteropNonNullAsserts) {
+        runtimeLibrary.jsInteropNonNullAsserts(
+           configuration.jsInteropNonNullAsserts);
+      }
+      if (!!configuration.dynamicModuleLoader) {
+        let loader = configuration.dynamicModuleLoader;
+        runtimeLibrary.setDynamicModuleLoader(loader, (moduleName) => {
+          //  As mentioned in the docs above, loader will not invoke the
+          //  entrypoint, but just return the moduleName to find where to call
+          //  it.  By using the module name, we don't need to expose the
+          //  `import` as part of the embedding API.
+          //  In the future module system, this will change to a library name,
+          //  rather than a module name.
+          return import_(moduleName, appName).__dynamic_module_entrypoint__();
+        });
+      }
+    }
+
   })(dart_library);
 }
 
diff --git a/pkg/dev_compiler/lib/src/compiler/shared_command.dart b/pkg/dev_compiler/lib/src/compiler/shared_command.dart
index f4b21a2..5c2a00b 100644
--- a/pkg/dev_compiler/lib/src/compiler/shared_command.dart
+++ b/pkg/dev_compiler/lib/src/compiler/shared_command.dart
@@ -103,6 +103,9 @@
   /// isolated namespace.
   final bool emitLibraryBundle;
 
+  /// Whether the compiler is generating a dynamic module.
+  final bool dynamicModule;
+
   /// When `true` stars "*" will appear to represent legacy types when printing
   /// runtime types in the compiled application.
   final bool printLegacyStars = false;
@@ -133,6 +136,7 @@
       this.experiments = const {},
       this.soundNullSafety = true,
       this.canaryFeatures = false,
+      this.dynamicModule = false,
       this.precompiledMacros = const [],
       this.macroSerializationMode})
       : emitLibraryBundle = canaryFeatures &&
@@ -160,6 +164,7 @@
                 args['enable-experiment'] as List<String>),
             soundNullSafety: args['sound-null-safety'] as bool,
             canaryFeatures: args['canary'] as bool,
+            dynamicModule: args['dynamic-module'] as bool,
             precompiledMacros: args['precompiled-macro'] as List<String>,
             macroSerializationMode:
                 args['macro-serialization-mode'] as String?);
@@ -231,7 +236,11 @@
       ..addOption('macro-serialization-mode',
           help: 'The serialization mode for communicating with macros.',
           allowed: ['bytedata', 'json'],
-          defaultsTo: 'bytedata');
+          defaultsTo: 'bytedata')
+      ..addFlag('dynamic-module',
+          help: 'Compile to generate a dynamic module',
+          negatable: false,
+          defaultsTo: false);
   }
 
   /// Adds only the arguments used to compile the SDK from a full dill file.
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 5a23f33..fb68e5f 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -388,6 +388,9 @@
   /// classes.
   final _afterClassDefItems = <js_ast.ModuleItem>[];
 
+  /// The entrypoint method of a dynamic module, if any.
+  Procedure? _dynamicEntrypoint;
+
   final Class _jsArrayClass;
   final Class _privateSymbolClass;
   final Class _linkedHashMapImplClass;
@@ -3229,6 +3232,25 @@
 
     _currentUri = savedUri;
     _staticTypeContext.leaveMember(p);
+
+    if (_options.dynamicModule &&
+        p.annotations.any((a) => _isEntrypointPragma(a, _coreTypes))) {
+      if (_dynamicEntrypoint == null) {
+        if (p.function.requiredParameterCount > 0) {
+          // TODO(sigmund): this error should be caught by a kernel checker that
+          // runs prior to DDC.
+          throw StateError('Entrypoint ${p.name.text} must accept being called '
+              'with 0 arguments.');
+        } else {
+          _dynamicEntrypoint = p;
+        }
+      } else {
+        // TODO(sigmund): this error should be caught by a kernel checker that
+        // runs prior to DDC.
+        throw StateError('A module should define a single entrypoint.');
+      }
+    }
+
     return js_ast.Statement.from(body);
   }
 
@@ -8213,6 +8235,20 @@
       // full library uri if we wanted to save space.
       var libraryName = js.escapedString(_jsLibraryDebuggerName(library));
       properties.add(js_ast.Property(libraryName, value));
+
+      // Dynamic modules shouldn't define a library that was previously defined.
+      // We leverage that we track which libraries have been defined via
+      // `trackedLibraries` to query whether a library already exists.
+      if (_options.dynamicModule) {
+        _moduleItems.add(js.statement('''if (# != null) {
+                throw Error(
+                    "Dynamic module provides second definition for " + #);
+            }''', [
+          _runtimeCall('getLibrary(#)', [libraryName]),
+          libraryName
+        ]));
+      }
+
       var partNames = _jsPartDebuggerNames(library);
       if (partNames.isNotEmpty) {
         parts.add(js_ast.Property(libraryName, js.stringArray(partNames)));
@@ -8222,7 +8258,10 @@
     var partMap = js_ast.ObjectInitializer(parts, multiline: true);
 
     // Track the module name for each library in the module.
-    // This data is consumed by the debugger and by the stack trace mapper.
+    // This data is mainly consumed by the debugger and by the stack trace
+    // mapper. It is also used for the experimental dynamic modules feature
+    // to validate that a dynamic module doesn't reintroduce an existing
+    // library.
     //
     // See also the implementation of this API in the SDK.
     _moduleItems.add(_runtimeStatement(
@@ -8284,6 +8323,14 @@
     // Emit all top-level JS symbol containers.
     items.addAll(_symbolContainer.emit());
 
+    if (_dynamicEntrypoint != null) {
+      // Expose the entrypoint of the dynamic module under a reserved name.
+      // TODO(sigmund): this could use a reserved symbol from dartx.
+      var name = _emitTopLevelName(_dynamicEntrypoint!);
+      _moduleItems.add(js_ast.ExportDeclaration(
+          js('var __dynamic_module_entrypoint__ = #', [name])));
+    }
+
     // Add the module's code (produced by visiting compilation units, above)
     _copyAndFlattenBlocks(items, _moduleItems);
     _moduleItems.clear();
@@ -8400,3 +8447,18 @@
 
   _SwitchLabelState(this.label, this.variable);
 }
+
+/// Whether [expression] is a constant of the form
+/// `const pragma('dyn-module:entrypoint')`.
+///
+/// Used to denote the entrypoint method of a dynamic module.
+// TODO(sigmund): move to package:kernel.
+bool _isEntrypointPragma(Expression expression, CoreTypes coreTypes) {
+  if (expression is! ConstantExpression) return false;
+  final value = expression.constant;
+  if (value is! InstanceConstant) return false;
+  if (value.classReference != coreTypes.pragmaClass.reference) return false;
+  final name = value.fieldValues[coreTypes.pragmaName.fieldReference];
+  if (name is! StringConstant) return false;
+  return name.value == 'dyn-module:entrypoint';
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
index 323b6b1..165084c 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
@@ -503,6 +503,9 @@
   /// classes.
   final _afterClassDefItems = <js_ast.ModuleItem>[];
 
+  /// The entrypoint method of a dynamic module, if any.
+  Procedure? _dynamicEntrypoint;
+
   final Class _jsArrayClass;
   final Class _privateSymbolClass;
   final Class _linkedHashMapImplClass;
@@ -3360,6 +3363,25 @@
 
     _currentUri = savedUri;
     _staticTypeContext.leaveMember(p);
+
+    if (_options.dynamicModule &&
+        p.annotations.any((a) => _isEntrypointPragma(a, _coreTypes))) {
+      if (_dynamicEntrypoint == null) {
+        if (p.function.requiredParameterCount > 0) {
+          // TODO(sigmund): this error should be caught by a kernel checker that
+          // runs prior to DDC.
+          throw StateError('Entrypoint ${p.name.text} must accept being called '
+              'with 0 arguments.');
+        } else {
+          _dynamicEntrypoint = p;
+        }
+      } else {
+        // TODO(sigmund): this error should be caught by a kernel checker that
+        // runs prior to DDC.
+        throw StateError('A module should define a single entrypoint.');
+      }
+    }
+
     return js_ast.Statement.from(body);
   }
 
@@ -8235,6 +8257,21 @@
       // full library uri if we wanted to save space.
       var libraryName = js.escapedString(_jsLibraryDebuggerName(library));
       properties.add(js_ast.Property(libraryName, value));
+
+      // Dynamic modules shouldn't define a library that was previously defined.
+      // We leverage that we track which libraries have been defined via
+      // `trackedLibraries` to query whether a library already exists.
+      // TODO(sigmund): enable when `trackLibraries()` is added again.
+      //if (_options.dynamicModule) {
+      //  _moduleItems.add(js.statement('''if (# != null) {
+      //          throw Error(
+      //              "Dynamic module provides second definition for " + #);
+      //      }''', [
+      //    _runtimeCall('getLibrary(#)', [libraryName]),
+      //    libraryName
+      //  ]));
+      //}
+
       var partNames = _jsPartDebuggerNames(library);
       if (partNames.isNotEmpty) {
         parts.add(js_ast.Property(libraryName, js.stringArray(partNames)));
@@ -8246,7 +8283,10 @@
     // var partMap = js_ast.ObjectInitializer(parts, multiline: true);
 
     // Track the module name for each library in the module.
-    // This data is consumed by the debugger and by the stack trace mapper.
+    // This data is mainly consumed by the debugger and by the stack trace
+    // mapper. It is also used for the experimental dynamic modules feature
+    // to validate that a dynamic module doesn't reintroduce an existing
+    // library.
     //
     // See also the implementation of this API in the SDK.
     //   _moduleItems.add(_runtimeStatement(
@@ -8299,6 +8339,14 @@
     // Emit all top-level JS symbol containers.
     items.addAll(_symbolContainer.emit());
 
+    if (_dynamicEntrypoint != null) {
+      // Expose the entrypoint of the dynamic module under a reserved name.
+      // TODO(sigmund): this could use a reserved symbol from dartx.
+      var name = _emitTopLevelName(_dynamicEntrypoint!);
+      _moduleItems.add(js_ast.ExportDeclaration(
+          js('var __dynamic_module_entrypoint__ = #', [name])));
+    }
+
     // Add the module's code (produced by visiting compilation units, above)
     _copyAndFlattenBlocks(items, _moduleItems);
     _moduleItems.clear();
@@ -8415,3 +8463,17 @@
 
   _SwitchLabelState(this.label, this.variable);
 }
+
+/// Whether [expression] is a constant of the form
+/// `const pragma('dyn-module:entrypoint')`.
+///
+/// Used to denote the entrypoint method of a dynamic module.
+bool _isEntrypointPragma(Expression expression, CoreTypes coreTypes) {
+  if (expression is! ConstantExpression) return false;
+  final value = expression.constant;
+  if (value is! InstanceConstant) return false;
+  if (value.classReference != coreTypes.pragmaClass.reference) return false;
+  final name = value.fieldValues[coreTypes.pragmaName.fieldReference];
+  if (name is! StringConstant) return false;
+  return name.value == 'dyn-module:entrypoint';
+}
diff --git a/pkg/dynamic_modules/test/common/testing.dart b/pkg/dynamic_modules/test/common/testing.dart
index 264fd95..0cb7933 100644
--- a/pkg/dynamic_modules/test/common/testing.dart
+++ b/pkg/dynamic_modules/test/common/testing.dart
@@ -6,11 +6,19 @@
 /// provide information to the test harness.
 library;
 
+import 'package:dynamic_modules/dynamic_modules.dart';
+
 /// Load a module and invoke it's entrypoint.
 ///
 /// The module is identified by the name of its entrypoint file within the
 /// `modules/` subfolder.
-Future<Object?> load(String moduleName) => throw "unimplemented";
+Future<Object?> load(String moduleName) {
+  if (const bool.fromEnvironment('dart.library.html')) {
+    // DDC implementation
+    return loadModuleFromUri(Uri(scheme: '', path: moduleName));
+  }
+  throw "load is not implemented for the VM or dart2wasm";
+}
 
 /// Notify the test harness that the test has run to completion.
 void done() {
diff --git a/pkg/dynamic_modules/test/runner/ddc.dart b/pkg/dynamic_modules/test/runner/ddc.dart
new file mode 100644
index 0000000..98e520a
--- /dev/null
+++ b/pkg/dynamic_modules/test/runner/ddc.dart
@@ -0,0 +1,194 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Implementation of the [TargetExecutor] for DDC.
+library;
+
+import 'dart:io';
+import '../common/testing.dart' as helper;
+
+import 'model.dart';
+import 'util.dart';
+import 'target.dart';
+
+/// Logic to build and execute dynamic modules in DDC.
+///
+/// In particular:
+///   * The initial app is built as a regular DDC target, except that
+///     a dynamic interface is used to validate that the public API matches
+///     real declarations.
+///   * For dynamic modules, DDC validates that the module only accesses what's
+///     allowed by the dynamic interface.
+///   * For dynamic modules, DDC also produces a slighly different output
+///     to implement library isolation.
+///   * Tests are executed in d8 using a custom bootstrapping logic. Eventually
+///     this logic needs to be centralized inside the compiler.
+class DdcExecutor implements TargetExecutor {
+  static const rootScheme = 'dev-dart-app';
+  late final bool _shouldCleanup;
+  late final Directory _tmp;
+  final Logger _logger;
+
+  DdcExecutor(this._logger) {
+    /// Allow using an environment variable to run tests on a fixed directory.
+    /// This prevents the directory from getting deleted too.
+    var path = Platform.environment['TMP_DIR'] ?? '';
+    if (path.isEmpty) {
+      _tmp = Directory.systemTemp.createTempSync('_dynamic_module-');
+      _shouldCleanup = true;
+    } else {
+      _tmp = Directory(path);
+      if (!_tmp.existsSync()) _tmp.createSync();
+      _shouldCleanup = false;
+    }
+  }
+
+  @override
+  Future<void> suiteComplete() async {
+    if (!_shouldCleanup) return;
+    try {
+      _tmp.delete(recursive: true);
+    } on FileSystemException {
+      // Windows bots sometimes fail to delete folders, and can make tests
+      // flaky. It is OK in those cases to leak some files in the tmp folder,
+      // these will eventually be cleared when a new bot instance is created.
+      _logger.warning('Error trying to delete $_tmp');
+    }
+  }
+
+  // TODO(sigmund): add support to run also in the ddc-canary mode.
+  Future _compile(
+      String testName, String source, Uri sourceDir, bool isMain) async {
+    var testDir = _tmp.uri.resolve(testName).toFilePath();
+    var args = [
+      '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+      ddcSnapshot.toFilePath(),
+      '--modules=ddc',
+      '--no-summarize',
+      '--no-source-map',
+      '--multi-root',
+      sourceDir.resolve('../../').toFilePath(),
+      '--multi-root-scheme',
+      rootScheme,
+      '$rootScheme:/data/$testName/$source',
+      '--dart-sdk-summary',
+      ddcSdkOutline.toFilePath(),
+      // Note: this needs to change if we ever intend to support packages within
+      // the dynamic loading tests themselves
+      '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+      if (!isMain) ...[
+        // TODO(sigmund): consider specifying the module name directly
+        '--dynamic-module',
+        '--summary=main.dart.dill=main.dart',
+      ],
+      '-o',
+      '$source.js',
+    ];
+    await runProcess(Platform.resolvedExecutable, args, testDir, _logger,
+        'compile $testName/$source');
+  }
+
+  Future _buildKernelOutline(
+      String testName, String source, Uri sourceDir) async {
+    assert(source == 'main.dart');
+    var testDir = _tmp.uri.resolve(testName).toFilePath();
+    var args = [
+      '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+      kernelWOrkerSnapshot.toFilePath(),
+      '--summary-only',
+      '--target',
+      'ddc',
+      '--multi-root',
+      sourceDir.resolve('../../').toFilePath(),
+      '--multi-root-scheme',
+      rootScheme,
+      '--packages-file=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+      '--dart-sdk-summary',
+      ddcSdkOutline.toFilePath(),
+      '--source',
+      '$rootScheme:/data/$testName/$source',
+      '--output',
+      '$source.dill',
+    ];
+
+    await runProcess(Platform.resolvedExecutable, args, testDir, _logger,
+        'sumarize $testName/$source');
+  }
+
+  @override
+  Future compileApplication(DynamicModuleTest test) async {
+    _ensureDirectory(test.name);
+    _logger.info('Compile ${test.name} app');
+    await _buildKernelOutline(test.name, test.main, test.folder);
+    await _compile(test.name, test.main, test.folder, true);
+  }
+
+  @override
+  Future compileDynamicModule(DynamicModuleTest test, String name) async {
+    _logger.info('Compile module ${test.name}.$name');
+    _ensureDirectory(test.name);
+    await _compile(test.name, test.dynamicModules[name]!, test.folder, false);
+  }
+
+  @override
+  Future executeApplication(DynamicModuleTest test) async {
+    _logger.info('Execute ${test.name}');
+    _ensureDirectory(test.name);
+
+    // We generate a self contained script that loads necessary preambles,
+    // ddc module loader, the necessary modules (the SDK and the main module),
+    // and finally launches the app.
+    var testDir = _tmp.uri.resolve('${test.name}/');
+    var bootstrapUri = testDir.resolve('bootstrap.js');
+    // TODO(sigmund): remove hardwired entrypoint name
+    File.fromUri(bootstrapUri).writeAsStringSync('''
+      load('${ddcPreamblesJs.toFilePath()}');        // preambles/d8.js
+      load('${ddcSealNativeObjectJs.toFilePath()}'); // seal_native_object.js
+      load('${ddcModuleLoaderJs.toFilePath()}');     // ddc_module_loader.js
+      load('${ddcSdkJs.toFilePath()}');              // dart_sdk.js
+      load('main.dart.js');                          // compiled test module
+
+      self.dartMainRunner(function () {
+        dart_library.configure(
+          "_dynamic_module_test",                 // app name
+          {
+            dynamicModuleLoader: (uri, onLoad) => {
+              let name = uri;                     // our test framework simply
+                                                  // provides the module name as
+                                                  // the uri.
+              load(`modules/\${name}.js`);
+              onLoad(name);
+          },
+        });
+        dart_library.start(
+          "_dynamic_module_test",                 // app name
+          '00000000-0000-0000-0000-000000000000', // uuid
+          "main.dart",                            // module
+          "data__${test.name}__main",             // library containing main
+          false
+        );
+      });
+    ''');
+    var result = await runProcess(
+        d8Uri.toFilePath(),
+        [bootstrapUri.toFilePath()],
+        testDir.toFilePath(),
+        _logger,
+        'd8 ${test.name}/bootstrap.js');
+    var stdout = result.stdout as String;
+    if (!stdout.contains(helper.successToken)) {
+      _logger.error('Error: test didn\'t complete as expected.\n'
+          'Make sure the test finishes and calls `helper.done()`.\n'
+          'Test output:\n$stdout');
+      throw Exception('missing helper.done');
+    }
+  }
+
+  void _ensureDirectory(String name) {
+    var dir = Directory.fromUri(_tmp.uri.resolve(name));
+    if (!dir.existsSync()) {
+      dir.createSync();
+    }
+  }
+}
diff --git a/pkg/dynamic_modules/test/runner/main.dart b/pkg/dynamic_modules/test/runner/main.dart
index 4fc9e15..065f402 100644
--- a/pkg/dynamic_modules/test/runner/main.dart
+++ b/pkg/dynamic_modules/test/runner/main.dart
@@ -7,6 +7,7 @@
 
 import 'package:args/args.dart';
 
+import 'ddc.dart';
 import 'load.dart';
 import 'model.dart';
 import 'target.dart';
@@ -47,7 +48,7 @@
   late TargetExecutor executor;
   try {
     executor = switch (target) {
-      Target.ddc => UnimplementedExecutor(logger),
+      Target.ddc => DdcExecutor(logger),
       Target.aot => UnimplementedExecutor(logger),
       Target.dart2wasm => UnimplementedExecutor(logger),
     };
diff --git a/pkg/dynamic_modules/test/runner/util.dart b/pkg/dynamic_modules/test/runner/util.dart
index 2b1a6d5..21c47af 100644
--- a/pkg/dynamic_modules/test/runner/util.dart
+++ b/pkg/dynamic_modules/test/runner/util.dart
@@ -7,6 +7,7 @@
 
 import 'dart:convert';
 import 'dart:io';
+import 'dart:ffi' show Abi;
 
 /// Locate the root of the SDK repository.
 ///
@@ -23,6 +24,39 @@
   return script.resolve("../" * (segments.length - index - 1));
 })();
 
+String _outFolder = Platform.isMacOS ? 'xcodebuild' : 'out';
+String configuration =
+    Platform.environment['DART_CONFIGURATION'] ?? 'ReleaseX64';
+String buildFolder = '$_outFolder/$configuration/';
+String arch = Abi.current().toString().split('_')[1];
+String _d8Path = (() {
+  if (Platform.isWindows) {
+    return 'third_party/d8/windows/$arch/d8.exe';
+  } else if (Platform.isLinux) {
+    return 'third_party/d8/linux/$arch/d8';
+  } else if (Platform.isMacOS) {
+    return 'third_party/d8/macos/$arch/d8';
+  } else {
+    throw UnsupportedError('Unsupported platform for running d8: '
+        '${Platform.operatingSystem}');
+  }
+})();
+
+Uri d8Uri = repoRoot.resolve(_d8Path);
+Uri _dartBin = Uri.file(Platform.resolvedExecutable);
+Uri ddcSnapshot = _dartBin.resolve('snapshots/dartdevc.dart.snapshot');
+Uri kernelWOrkerSnapshot =
+    _dartBin.resolve('snapshots/kernel_worker.dart.snapshot');
+Uri buildRootUri = repoRoot.resolve(buildFolder);
+Uri ddcSdkOutline = buildRootUri.resolve('ddc_outline.dill');
+Uri ddcSdkJs = buildRootUri.resolve('gen/utils/ddc/stable/sdk/ddc/dart_sdk.js');
+Uri ddcPreamblesJs = repoRoot
+    .resolve('sdk/lib/_internal/js_dev_runtime/private/preambles/d8.js');
+Uri ddcSealNativeObjectJs = repoRoot.resolve(
+    'sdk/lib/_internal/js_runtime/lib/preambles/seal_native_object.js');
+Uri ddcModuleLoaderJs =
+    repoRoot.resolve('pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js');
+
 // Encodes test results in the format expected by Dart's CI infrastructure.
 class TestResultOutcome {
   // This encoder must generate each output element on its own line.
diff --git a/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
index 1aacab0..0941d9f 100644
--- a/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
+++ b/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
@@ -4,6 +4,7 @@
 
 import 'dart:core' hide Symbol;
 import 'dart:core' as core show Symbol;
+import 'dart:async' show Completer;
 import 'dart:_js_primitives' show printString;
 import 'dart:_internal' show patch;
 import 'dart:_interceptors' show JSArray;
@@ -67,5 +68,32 @@
 T unsafeCast<T>(dynamic v) => v;
 
 @patch
-Future<Object?> loadDynamicModule({Uri? uri, Uint8List? bytes}) =>
-    throw 'Unsupported operation';
+Future<Object?> loadDynamicModule({Uri? uri, Uint8List? bytes}) {
+  if (bytes != null) {
+    throw ArgumentError('DDC implementation of dynamic modules doesn\'t'
+        ' accept bytes as input');
+  }
+  if (uri == null) {
+    throw ArgumentError('DDC implementation of dynamic modules expects a'
+        'non-null Uri input.');
+  }
+  if (dart.dynamicModuleLoader == null) {
+    throw StateError('Dynamic module loader has not be configured.');
+  }
+  var completer = Completer<Object?>();
+  void _callback(String moduleName) {
+    try {
+      var result = JS('!', '#(#)', dart.dynamicEntrypointHelper, moduleName);
+      completer.complete(result);
+    } catch (e, st) {
+      completer.completeError(e, st);
+    }
+  }
+
+  try {
+    JS('!', '#(#, #)', dart.dynamicModuleLoader, uri.toString(), _callback);
+  } catch (e, st) {
+    completer.completeError(e, st);
+  }
+  return completer.future;
+}
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
index 8b029c1..50e9ec6 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
@@ -1037,6 +1037,17 @@
   }
 }
 
+/// Provides the experimental functionality for dynamic modules.
+Object? dynamicModuleLoader;
+Object? dynamicEntrypointHelper;
+void setDynamicModuleLoader(Object loaderFunction, Object entrypointHelper) {
+  if (dynamicModuleLoader != null) {
+    throw StateError('Dynamic module loader already configured.');
+  }
+  dynamicModuleLoader = loaderFunction;
+  dynamicEntrypointHelper = entrypointHelper;
+}
+
 /// Defines lazy statics.
 ///
 /// TODO: Remove useOldSemantics when non-null-safe late static field behavior is
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
index db56066..9d67d12 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
@@ -203,9 +203,12 @@
 /// * To convert JS stack traces to Dart: the
 ///   stack trace mapper companion program will request source maps via
 ///   [getSourceMap].
+/// * To validate a library is only defined by one dynamic module: the compiler
+///   generates calls to [getLibrary] when compiling the experimental dynamic
+///   modules feature.
 ///
 /// Note: calls to [getLibrary], [getLibraryMetadata], [getSourceMap], among
-/// others don't originate from code in the SDK repo. For example, the
+/// others don't always originate from code in the SDK repo. For example, the
 /// debugger calls are initiated by DWDS, whereas the [getSourceMap] call is
 /// done from bootstapping scripts that set up the stack trace mapper.
 // TODO(39630): move these public facing APIs to a dedicated public interface.