[dart2wasm] Add support for testing dart2wasm with JSC

JSC only supports `print()` but not `console.log()`.

=> The changes to `printToConsole()` are therefore extended
to check for `console.log()` as well as `print()`.
=> This is extending it to a broader subset of dart2js's print

Change-Id: I7efa697477aa60e473d01716b104fc1526035c67
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/347283
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js
index cf6096f..f6886bb 100644
--- a/pkg/dart2wasm/bin/run_wasm.js
+++ b/pkg/dart2wasm/bin/run_wasm.js
@@ -11,6 +11,14 @@
 //       -- /abs/path/to/<dart_module>.mjs <dart_module>.wasm [<ffi_module>.wasm] \
 //       [-- Dart commandline arguments...]
 //
+// Run as follows on JSC:
+//
+// $> export JSC_useWebAssemblyTypedFunctionReferences=1
+// $> export JSC_useWebAssemblyExtendedConstantExpressions=1
+// $> export JSC_useWebAssemblyGC=1
+// $> jsc run_wasm.js -- <dart_module>.ms <dart_module>.wasm  \
+//       [-- Dart commandline arguments...]
+//
 // Run as follows on JSShell:
 //
 // $> js run_wasm.js \
@@ -36,6 +44,19 @@
 const wasmArg = 1;
 const ffiArg = 2;
 
+// This script is intended to be used by D8, JSShell or JSC. We distinguish
+// them by the functions they offer to read files:
+//
+// Engine         | Shell    | FileRead             |  Arguments
+// --------------------------------------------------------------
+// V8             | D8       | readbuffer           |  arguments (arg0 arg1)
+// JavaScriptCore | JSC      | readFile             |  arguments (arg0 arg1)
+// SpiderMonkey   | JSShell  | readRelativeToScript |  scriptArgs (-- arg0 arg1)
+//
+const isD8 = (typeof readbuffer === "function");
+const isJSC = (typeof readFile === "function");
+const isJSShell = (typeof readRelativeToScript === "function");
+
 // d8's `setTimeout` doesn't work as expected (it doesn't wait before calling
 // the callback), and d8 also doesn't have `setInterval` and `queueMicrotask`.
 // So we define our own event loop with these functions.
@@ -282,10 +303,16 @@
   }
 
   async function eventLoop(action) {
+    if (isJSC) asyncTestStart(1);
     while (action) {
       try {
         await action();
       } catch (e) {
+        // JSC doesn't report/print uncaught async exceptions for some reason.
+        if (isJSC) {
+          print('Error: ' + e);
+          print('Stack: ' + e.stack);
+        }
         if (typeof onerror == "function") {
           onerror(e, null, -1);
         } else {
@@ -294,6 +321,7 @@
       }
       action = nextEvent();
     }
+    if (isJSC) asyncTestPassed();
   }
 
   // Global properties. "self" refers to the global object, so adding a
@@ -315,17 +343,11 @@
   self.dartUseDateNowForTicks = true;
 })(this, []);
 
-
-// This script is intended to be used by either D8 or JSShell. We distinguish
-// the two by seeing whether the global `arguments` exists (D8 uses `arguments`
-// and JsShell uses `scriptArgs`).
-var isD8 = (typeof arguments != "undefined");
-
 // We would like this itself to be a ES module rather than a script, but
 // unfortunately d8 does not return a failed error code if an unhandled
 // exception occurs asynchronously in an ES module.
 const main = async () => {
-    var args =  isD8 ? arguments : scriptArgs;
+    var args =  (isD8 || isJSC) ? arguments : scriptArgs;
     var dartArgs = [];
     const argsSplit = args.indexOf("--");
     if (argsSplit != -1) {
@@ -336,7 +358,14 @@
     const dart2wasm = await import(args[jsRuntimeArg]);
     function compile(filename) {
         // Create a Wasm module from the binary wasm file.
-        var bytes = isD8 ? readbuffer(filename) : readRelativeToScript(filename, "binary") ;
+        var bytes;
+        if (isJSC) {
+          bytes = readFile(filename, "binary");
+        } else if (isD8) {
+          bytes = readbuffer(filename);
+        } else {
+          bytes = readRelativeToScript(filename, "binary");
+        }
         return new WebAssembly.Module(bytes);
     }
 
diff --git a/pkg/dart2wasm/lib/js/runtime_blob.dart b/pkg/dart2wasm/lib/js/runtime_blob.dart
index dc59d21..a050b61 100644
--- a/pkg/dart2wasm/lib/js/runtime_blob.dart
+++ b/pkg/dart2wasm/lib/js/runtime_blob.dart
@@ -67,6 +67,20 @@
 ''';
 
 const jsRuntimeBlobPart3 = r'''
+    // Prints to the console
+    function printToConsole(value) {
+      if (typeof console == "object" && typeof console.log != "undefined") {
+        console.log(value);
+        return;
+      }
+      if (typeof print == "function") {
+        print(value);
+        return;
+      }
+
+      throw "Unable to print message: " + js;
+    }
+
     // Converts a Dart List to a JS array. Any Dart objects will be converted, but
     // this will be cheap for JSValues.
     function arrayFromDartList(constructor, list) {
@@ -96,7 +110,7 @@
     }
 
     if (WebAssembly.String === undefined) {
-        console.log("WebAssembly.String is undefined, adding polyfill");
+        printToConsole("WebAssembly.String is undefined, adding polyfill");
         WebAssembly.String = {
             "charCodeAt": (s, i) => s.charCodeAt(i),
             "compare": (s1, s2) => {
diff --git a/pkg/smith/lib/configuration.dart b/pkg/smith/lib/configuration.dart
index 1df9816..346539f 100644
--- a/pkg/smith/lib/configuration.dart
+++ b/pkg/smith/lib/configuration.dart
@@ -868,6 +868,7 @@
       case Compiler.dart2wasm:
         return const [
           Runtime.none,
+          Runtime.jsc,
           Runtime.jsshell,
           Runtime.d8,
           Runtime.chrome,
@@ -980,6 +981,7 @@
   static const flutter = Runtime._('flutter');
   static const dartPrecompiled = Runtime._('dart_precompiled');
   static const d8 = Runtime._('d8');
+  static const jsc = Runtime._('jsc');
   static const jsshell = Runtime._('jsshell');
   static const firefox = Runtime._('firefox');
   static const chrome = Runtime._('chrome');
@@ -998,6 +1000,7 @@
     flutter,
     dartPrecompiled,
     d8,
+    jsc,
     jsshell,
     firefox,
     chrome,
@@ -1035,7 +1038,7 @@
   bool get isSafari => name.startsWith("safari");
 
   /// Whether this runtime is a command-line JavaScript environment.
-  bool get isJSCommandLine => const [d8, jsshell].contains(this);
+  bool get isJSCommandLine => const [d8, jsc, jsshell].contains(this);
 
   /// If the runtime doesn't support `Window.open`, we use iframes instead.
   bool get requiresIFrame => !const [ie11, ie10].contains(this);
@@ -1063,6 +1066,9 @@
       case chromeOnAndroid:
         return Compiler.dart2js;
 
+      case jsc:
+        return Compiler.dart2wasm;
+
       case none:
         // If we aren't running it, we probably just want to analyze it.
         return Compiler.dart2analyzer;
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index 0e7e0f2..d07b1f1 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -610,11 +610,12 @@
     final filename = artifact!.filename;
     final args = testFile.dartOptions;
     final isD8 = runtimeConfiguration is D8RuntimeConfiguration;
+    final isJSC = runtimeConfiguration is JSCRuntimeConfiguration;
     return [
       if (isD8) '--turboshaft-wasm',
       if (isD8) '--experimental-wasm-imported-strings',
       'pkg/dart2wasm/bin/run_wasm.js',
-      if (isD8) '--',
+      if (isD8 || isJSC) '--',
       '${filename.substring(0, filename.lastIndexOf('.'))}.mjs',
       filename,
       ...testFile.sharedObjects
diff --git a/pkg/test_runner/lib/src/options.dart b/pkg/test_runner/lib/src/options.dart
index 1cbe554..c118203 100644
--- a/pkg/test_runner/lib/src/options.dart
+++ b/pkg/test_runner/lib/src/options.dart
@@ -87,8 +87,9 @@
         help: '''Where the tests should be run.
 vm:               Run Dart code on the standalone Dart VM.
 dart_precompiled: Run a precompiled snapshot on the VM without a JIT.
-d8:               Run JavaScript from the command line using v8.
-jsshell:          Run JavaScript from the command line using Firefox js-shell.
+d8:               Run JavaScript from the command line using Chrome's v8.
+jsc:              Run JavaScript from the command line using Safari/WebKit's jsc.
+jsshell:          Run JavaScript from the command line using Firefox's js-shell.
 
 firefox:
 chrome:
diff --git a/pkg/test_runner/lib/src/runtime_configuration.dart b/pkg/test_runner/lib/src/runtime_configuration.dart
index 0b42637..643b75f 100644
--- a/pkg/test_runner/lib/src/runtime_configuration.dart
+++ b/pkg/test_runner/lib/src/runtime_configuration.dart
@@ -31,6 +31,9 @@
         // TODO(ahe): Replace this with one or more browser runtimes.
         return DummyRuntimeConfiguration();
 
+      case Runtime.jsc:
+        return JSCRuntimeConfiguration(configuration.compiler);
+
       case Runtime.jsshell:
         return JsshellRuntimeConfiguration(configuration.compiler);
 
@@ -144,6 +147,15 @@
     return d8;
   }
 
+  String get jscFileName {
+    final d8Dir = Repository.dir.append('third_party/jsc');
+    final d8Path = d8Dir.append(
+        '${Platform.operatingSystem}/${Architecture.host}/jsc$executableExtension');
+    final d8 = d8Path.toNativePath();
+    TestUtils.ensureExists(d8, _configuration);
+    return d8;
+  }
+
   String get jsShellFileName {
     var executable = 'jsshell$executableExtension';
     var jsshellDir = Repository.uri.resolve("tools/testing/bin").path;
@@ -218,6 +230,35 @@
   }
 }
 
+/// Safari/WebKit/JavaScriptCore-based development shell (jsc).
+class JSCRuntimeConfiguration extends CommandLineJavaScriptRuntime {
+  final Compiler compiler;
+
+  JSCRuntimeConfiguration(this.compiler) : super('jsc');
+
+  @override
+  List<Command> computeRuntimeCommands(
+      CommandArtifact? artifact,
+      List<String> arguments,
+      Map<String, String> environmentOverrides,
+      List<String> extraLibs,
+      bool isCrashExpected) {
+    checkArtifact(artifact!);
+    if (compiler != Compiler.dart2wasm) {
+      throw 'No test runner setup for jsc + dart2js yet';
+    }
+    final environment = {
+      ...environmentOverrides,
+      'JSC_useWebAssemblyTypedFunctionReferences': '1',
+      'JSC_useWebAssemblyExtendedConstantExpression': '1',
+      'JSC_useWebAssemblyGC': '1',
+    };
+    return [
+      Dart2WasmCommandLineCommand(moniker, jscFileName, arguments, environment)
+    ];
+  }
+}
+
 /// Firefox/SpiderMonkey-based development shell (jsshell).
 class JsshellRuntimeConfiguration extends CommandLineJavaScriptRuntime {
   final Compiler compiler;
diff --git a/sdk/lib/_internal/wasm/lib/print_patch.dart b/sdk/lib/_internal/wasm/lib/print_patch.dart
index f7ee492..3dca9f4 100644
--- a/sdk/lib/_internal/wasm/lib/print_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/print_patch.dart
@@ -6,4 +6,4 @@
 
 @patch
 void printToConsole(String line) =>
-    JS<void>('s => console.log(stringFromDartString(s))');
+    JS<void>('s => printToConsole(stringFromDartString(s))');
diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json
index be531ec..9fca581 100644
--- a/tools/bots/test_matrix.json
+++ b/tools/bots/test_matrix.json
@@ -451,7 +451,7 @@
         "timeout": 240
       }
     },
-    "dart2wasm-(linux|mac|win)-(d8|jsshell|chrome|firefox)": {
+    "dart2wasm-(linux|mac|win)-(d8|jsshell|jsc|chrome|firefox)": {
       "options": {
         "host-checked": true,
         "dart2wasm-options": [
@@ -460,7 +460,7 @@
         "timeout": 240
       }
     },
-    "dart2wasm-(linux|mac|win)-optimized-(d8|jsshell|chrome|firefox)": {
+    "dart2wasm-(linux|mac|win)-optimized-(d8|jsshell|jsc|chrome|firefox)": {
       "options": {
         "dart2wasm-options": [
           "--optimize"
@@ -469,7 +469,7 @@
         "timeout": 240
       }
     },
-    "dart2wasm-(linux|mac|win)-jscm-(d8|jsshell|chrome|firefox)": {
+    "dart2wasm-(linux|mac|win)-jscm-(d8|jsshell|jsc|chrome|firefox)": {
       "options": {
         "dart2wasm-options": [
           "--no-optimize",