Skip to content

Commit

Permalink
Experimental global executor cooperating with JS event loop
Browse files Browse the repository at this point in the history
  • Loading branch information
kateinoigakukun committed Oct 3, 2021
1 parent aa521a1 commit d1814e1
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 0 deletions.
2 changes: 2 additions & 0 deletions IntegrationTests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ benchmark: benchmark_setup run_benchmark
.PHONY: test
test: build_rt dist/PrimaryTests.wasm
node bin/primary-tests.js
concurrency_test: build_rt dist/ConcurrencyTests.wasm
node bin/concurrency-tests.js
15 changes: 15 additions & 0 deletions IntegrationTests/TestSuites/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@ import PackageDescription

let package = Package(
name: "TestSuites",
platforms: [
// This package doesn't work on macOS host, but should be able to be built for it
// for developing on Xcode. This minimum version requirement is to prevent availability
// errors for Concurrency API, whose runtime support is shipped from macOS 12.0
.macOS("12.0")
],
products: [
.executable(
name: "PrimaryTests", targets: ["PrimaryTests"]
),
.executable(
name: "ConcurrencyTests", targets: ["ConcurrencyTests"]
),
.executable(
name: "BenchmarkTests", targets: ["BenchmarkTests"]
),
],
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [
.target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]),
.target(
name: "ConcurrencyTests",
dependencies: [
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
]
),
.target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import JavaScriptKit

var printTestNames = false
// Uncomment the next line to print the name of each test suite before running it.
// This will make it easier to debug any errors that occur on the JS side.
//printTestNames = true

func test(_ name: String, testBlock: () throws -> Void) throws {
if printTestNames { print(name) }
do {
try testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void {
if printTestNames { print(name) }
do {
try await testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

struct MessageError: Error {
let message: String
let file: StaticString
let line: UInt
let column: UInt
init(_ message: String, file: StaticString, line: UInt, column: UInt) {
self.message = message
self.file = file
self.line = line
self.column = column
}
}

func expectEqual<T: Equatable>(
_ lhs: T, _ rhs: T,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws {
if lhs != rhs {
throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column)
}
}

func expectCast<T, U>(
_ value: T, to type: U.Type = U.self,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws -> U {
guard let value = value as? U else {
throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column)
}
return value
}

func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject {
switch value {
case let .object(ref): return ref
default:
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
}

func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray {
guard let array = value.array else {
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
return array
}

func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction {
switch value {
case let .function(ref): return ref
default:
throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column)
}
}

func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool {
switch value {
case let .boolean(bool): return bool
default:
throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column)
}
}

func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double {
switch value {
case let .number(number): return number
default:
throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column)
}
}

func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
switch value {
case let .string(string): return String(string)
default:
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
}
}

func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error {
do {
_ = try await body()
} catch {
return error
}
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}

func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
switch value {
case .some: return
case .none:
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
}
}
94 changes: 94 additions & 0 deletions IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import JavaScriptEventLoop
import JavaScriptKit


func entrypoint() async throws {
struct E: Error, Equatable {
let value: Int
}

try await asyncTest("Task.init value") {
let handle = Task { 1 }
try expectEqual(await handle.value, 1)
}

try await asyncTest("Task.init throws") {
let handle = Task {
throw E(value: 2)
}
let error = try await expectAsyncThrow(await handle.value)
let e = try expectCast(error, to: E.self)
try expectEqual(e, E(value: 2))
}

try await asyncTest("await resolved Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.success(1))
})
try await expectEqual(p.get(), 1)
}

try await asyncTest("await rejected Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.failure(.number(3)))
})
let error = try await expectAsyncThrow(await p.get())
let jsValue = try expectCast(error, to: JSValue.self)
try expectEqual(jsValue, 3)
}

try await asyncTest("Continuation") {
let value = await withUnsafeContinuation { cont in
cont.resume(returning: 1)
}
try expectEqual(value, 1)
}

try await asyncTest("Task.sleep(_:)") {
await Task.sleep(1_000_000_000)
}

// FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst
// at the end of thunk.
// This issue is not only on JS host environment, but also on standalone coop executor.
// try await asyncTest("Task.sleep(nanoseconds:)") {
// try await Task.sleep(nanoseconds: 1_000_000_000)
// }
}


// Note: Please define `USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5` if the swift-tools-version is newer
// than 5.5 to avoid the linking issue.
#if USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5
// Workaround: The latest SwiftPM rename main entry point name of executable target
// to avoid conflicting "main" with test target since `swift-tools-version >= 5.5`.
// The main symbol is renamed to "{{module_name}}_main" and it's renamed again to be
// "main" when linking the executable target. The former renaming is done by Swift compiler,
// and the latter is done by linker, so SwiftPM passes some special linker flags for each platform.
// But SwiftPM assumes that wasm-ld supports it by returning an empty array instead of nil even though
// wasm-ld doesn't support it yet.
// ref: https://github.com/apple/swift-package-manager/blob/1be68e811d0d814ba7abbb8effee45f1e8e6ec0d/Sources/Build/BuildPlan.swift#L117-L126
// So define an explicit "main" by @_cdecl
@_cdecl("main")
func main(argc: Int32, argv: Int32) -> Int32 {
JavaScriptEventLoop.installGlobalExecutor()
Task {
do {
try await entrypoint()
} catch {
print(error)
}
}
return 0
}
#else
JavaScriptEventLoop.installGlobalExecutor()
Task {
do {
try await entrypoint()
} catch {
print(error)
}
}

#endif
6 changes: 6 additions & 0 deletions IntegrationTests/bin/concurrency-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { startWasiTask } = require("../lib");

startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
console.log(err);
process.exit(1);
});
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ import PackageDescription

let package = Package(
name: "JavaScriptKit",
platforms: [
// This package doesn't work on macOS host, but should be able to be built for it
// for developing on Xcode. This minimum version requirement is to prevent availability
// errors for Concurrency API, whose runtime support is shipped from macOS 12.0
.macOS("12.0")
],
products: [
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
],
targets: [
.target(
name: "JavaScriptKit",
dependencies: ["_CJavaScriptKit"]
),
.target(name: "_CJavaScriptKit"),
.target(
name: "JavaScriptEventLoop",
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"]
),
.target(name: "_CJavaScriptEventLoop"),
]
)
Loading

0 comments on commit d1814e1

Please sign in to comment.