Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 language mode compatibility #286

Merged
merged 19 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28f3471
Concurrency: Use `LazyThreadLocal` without @PW syntax
kateinoigakukun Mar 4, 2025
917ab57
Concurrency: Annotate `jsObject` property of `JSError` as `nonisolate…
kateinoigakukun Mar 4, 2025
30f78ff
Concurrency: Update Package.swift tools version to 6.0
kateinoigakukun Mar 4, 2025
2642df9
Concurrency: Replace `swjs_thread_local_closures` with `LazyThreadLocal`
kateinoigakukun Mar 4, 2025
daa8209
Concurrency: Remove `Error` conformance from `JSError`
kateinoigakukun Mar 5, 2025
9f0197d
Concurrency: Isolate global executor installation by MainActor
kateinoigakukun Mar 5, 2025
fa77908
Concurrency: Remove `@Sendable` requirement from scheduling primitives
kateinoigakukun Mar 5, 2025
97aad00
Concurrency: Fix sendability errors around `JSClosure.async`
kateinoigakukun Mar 5, 2025
75b8cd3
Concurrency: Introduce `JSException` and remove `Error` conformance f…
kateinoigakukun Mar 5, 2025
d1781a8
CI: Remove Xcode 15.2 (Swift 5.9) from the matrix
kateinoigakukun Mar 5, 2025
39c207b
Fix `JAVASCRIPTKIT_WITHOUT_WEAKREFS` build
kateinoigakukun Mar 5, 2025
0fc7f41
Concurrency: Use `JSPromise.Result` instead of `Swift.Result` for `JS…
kateinoigakukun Mar 5, 2025
899fa63
Add `Equatable` conformances to new types
kateinoigakukun Mar 5, 2025
042e26e
Concurency: Remove `@MainActor` requirement from `JSEL.installGlobalE…
kateinoigakukun Mar 5, 2025
2257233
Concurrency: Adjust test cases for new exception handling
kateinoigakukun Mar 5, 2025
0c43cbf
CI: Update Swift toolchain to 2025-02-26-a
kateinoigakukun Mar 5, 2025
7a7acb4
Concurrency: Remove unnecessary `sending` keyword
kateinoigakukun Mar 5, 2025
18ad4e3
Swift 6.1 and later uses .xctest for XCTest bundle
kateinoigakukun Mar 5, 2025
3f3b494
Concurrency: Explicitly mark `Sendable` conformance as unavailable fo…
kateinoigakukun Mar 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node

runs-on: ${{ matrix.entry.os }}
Expand Down Expand Up @@ -52,8 +52,6 @@ jobs:
strategy:
matrix:
include:
- os: macos-14
xcode: Xcode_15.2
- os: macos-15
xcode: Xcode_16
runs-on: ${{ matrix.os }}
Expand All @@ -71,7 +69,7 @@ jobs:
entry:
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/install-swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func entrypoint() async throws {
resolve(.failure(.number(3)))
})
let error = try await expectAsyncThrow(await p.value)
let jsValue = try expectCast(error, to: JSValue.self)
let jsValue = try expectCast(error, to: JSException.self).thrownValue
try expectEqual(jsValue, 3)
try await expectEqual(p.result, .failure(.number(3)))
}
Expand Down Expand Up @@ -157,7 +157,7 @@ func entrypoint() async throws {
)
}
let promise2 = promise.then { _ in
throw JSError(message: "should not succeed")
throw MessageError("Should not be called", file: #file, line: #line, column: #column)
} failure: { err in
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func expectThrow<T>(_ body: @autoclosure () throws -> T, file: StaticString = #f
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}

func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error {
func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue {
JSObject.global.callThrowingClosure.function!(JSClosure { _ in
body()
return .undefined
Expand Down
37 changes: 17 additions & 20 deletions IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ try test("Closure Lifetime") {
let c1Line = #line + 1
let c1 = JSClosure { $0[0] }
c1.release()
let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue
try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)"))
let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException
try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)"))
}
#endif

Expand All @@ -275,8 +275,8 @@ try test("Closure Lifetime") {

do {
let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") }
let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue
try expectEqual(error.description, "RuntimeError: unreachable")
let error = try expectThrow(try evalClosure.throws(c1)) as! JSException
try expectEqual(error.thrownValue.description, "RuntimeError: unreachable")
}
}

Expand Down Expand Up @@ -770,32 +770,32 @@ try test("Exception") {

// MARK: Throwing method calls
let error1 = try expectThrow(try prop_9.object!.throwing.func1!())
try expectEqual(error1 is JSValue, true)
let errorObject = JSError(from: error1 as! JSValue)
try expectEqual(error1 is JSException, true)
let errorObject = JSError(from: (error1 as! JSException).thrownValue)
try expectNotNil(errorObject)

let error2 = try expectThrow(try prop_9.object!.throwing.func2!())
try expectEqual(error2 is JSValue, true)
let errorString = try expectString(error2 as! JSValue)
try expectEqual(error2 is JSException, true)
let errorString = try expectString((error2 as! JSException).thrownValue)
try expectEqual(errorString, "String Error")

let error3 = try expectThrow(try prop_9.object!.throwing.func3!())
try expectEqual(error3 is JSValue, true)
let errorNumber = try expectNumber(error3 as! JSValue)
try expectEqual(error3 is JSException, true)
let errorNumber = try expectNumber((error3 as! JSException).thrownValue)
try expectEqual(errorNumber, 3.0)

// MARK: Simple function calls
let error4 = try expectThrow(try prop_9.func1.function!.throws())
try expectEqual(error4 is JSValue, true)
let errorObject2 = JSError(from: error4 as! JSValue)
try expectEqual(error4 is JSException, true)
let errorObject2 = JSError(from: (error4 as! JSException).thrownValue)
try expectNotNil(errorObject2)

// MARK: Throwing constructor call
let Animal = JSObject.global.Animal.function!
_ = try Animal.throws.new("Tama", 3, true)
let ageError = try expectThrow(try Animal.throws.new("Tama", -3, true))
try expectEqual(ageError is JSValue, true)
let errorObject3 = JSError(from: ageError as! JSValue)
try expectEqual(ageError is JSException, true)
let errorObject3 = JSError(from: (ageError as! JSException).thrownValue)
try expectNotNil(errorObject3)
}

Expand Down Expand Up @@ -824,18 +824,15 @@ try test("Unhandled Exception") {

// MARK: Throwing method calls
let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() }
try expectEqual(error1 is JSValue, true)
let errorObject = JSError(from: error1 as! JSValue)
let errorObject = JSError(from: error1)
try expectNotNil(errorObject)

let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() }
try expectEqual(error2 is JSValue, true)
let errorString = try expectString(error2 as! JSValue)
let errorString = try expectString(error2)
try expectEqual(errorString, "String Error")

let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() }
try expectEqual(error3 is JSValue, true)
let errorNumber = try expectNumber(error3 as! JSValue)
let errorNumber = try expectNumber(error3)
try expectEqual(errorNumber, 3.0)
}

Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ test:
CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \
CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test

TEST_RUNNER := node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs
.PHONY: unittest
unittest:
@echo Running unit tests
swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS)
node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm
# Swift 6.1 and later uses .xctest for XCTest bundle but earliers used .wasm
# See https://github.com/swiftlang/swift-package-manager/pull/8254
if [ -f .build/debug/JavaScriptKitPackageTests.xctest ]; then \
$(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.xctest; \
else \
$(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.wasm; \
fi

.PHONY: benchmark_setup
benchmark_setup:
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.8
// swift-tools-version:6.0

import PackageDescription

Expand Down
20 changes: 13 additions & 7 deletions Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {

/// A function that queues a given closure as a microtask into JavaScript event loop.
/// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
public var queueMicrotask: @Sendable (@escaping () -> Void) -> Void
public var queueMicrotask: (@escaping () -> Void) -> Void
/// A function that invokes a given closure after a specified number of milliseconds.
public var setTimeout: @Sendable (Double, @escaping () -> Void) -> Void
public var setTimeout: (Double, @escaping () -> Void) -> Void

/// A mutable state to manage internal job queue
/// Note that this should be guarded atomically when supporting multi-threaded environment.
var queueState = QueueState()

private init(
queueTask: @Sendable @escaping (@escaping () -> Void) -> Void,
setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void
queueTask: @escaping (@escaping () -> Void) -> Void,
setTimeout: @escaping (Double, @escaping () -> Void) -> Void
) {
self.queueMicrotask = queueTask
self.setTimeout = setTimeout
Expand Down Expand Up @@ -102,14 +102,20 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
return eventLoop
}

private static var didInstallGlobalExecutor = false
@MainActor private static var didInstallGlobalExecutor = false

/// Set JavaScript event loop based executor to be the global executor
/// Note that this should be called before any of the jobs are created.
/// This installation step will be unnecessary after custom executor are
/// introduced officially. See also [a draft proposal for custom
/// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor)
public static func installGlobalExecutor() {
MainActor.assumeIsolated {
Self.installGlobalExecutorIsolated()
}
}

@MainActor private static func installGlobalExecutorIsolated() {
guard !didInstallGlobalExecutor else { return }

#if compiler(>=5.9)
Expand Down Expand Up @@ -218,7 +224,7 @@ public extension JSPromise {
return JSValue.undefined
},
failure: {
continuation.resume(throwing: $0)
continuation.resume(throwing: JSException($0))
return JSValue.undefined
}
)
Expand All @@ -227,7 +233,7 @@ public extension JSPromise {
}

/// Wait for the promise to complete, returning its result or exception as a Result.
var result: Result<JSValue, JSValue> {
var result: JSPromise.Result {
get async {
await withUnsafeContinuation { [self] continuation in
self.then(
Expand Down
9 changes: 5 additions & 4 deletions Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -426,14 +426,15 @@ public final class WebWorkerTaskExecutor: TaskExecutor {

// MARK: Global Executor hack

private static var _mainThread: pthread_t?
private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer?
private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer?
private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer?
@MainActor private static var _mainThread: pthread_t?
@MainActor private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer?
@MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer?
@MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer?

/// Install a global executor that forwards jobs from Web Worker threads to the main thread.
///
/// This function must be called once before using the Web Worker task executor.
@MainActor
public static func installGlobalExecutor() {
#if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
// Ensure this function is called only once.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import JavaScriptEventLoop
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
@_cdecl("swift_javascriptkit_activate_js_executor_impl")
func swift_javascriptkit_activate_js_executor_impl() {
JavaScriptEventLoop.installGlobalExecutor()
MainActor.assumeIsolated {
JavaScriptEventLoop.installGlobalExecutor()
}
}

#endif
5 changes: 2 additions & 3 deletions Sources/JavaScriptKit/BasicObjects/JSArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
/// that exposes its properties in a type-safe and Swifty way.
public class JSArray: JSBridgedClass {
public static var constructor: JSFunction? { _constructor }
@LazyThreadLocal(initialize: { JSObject.global.Array.function })
private static var _constructor: JSFunction?
public static var constructor: JSFunction? { _constructor.wrappedValue }
private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Array.function })

static func isArray(_ object: JSObject) -> Bool {
constructor!.isArray!(object).boolean!
Expand Down
5 changes: 2 additions & 3 deletions Sources/JavaScriptKit/BasicObjects/JSDate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
*/
public final class JSDate: JSBridgedClass {
/// The constructor function used to create new `Date` objects.
public static var constructor: JSFunction? { _constructor }
@LazyThreadLocal(initialize: { JSObject.global.Date.function })
private static var _constructor: JSFunction?
public static var constructor: JSFunction? { _constructor.wrappedValue }
private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Date.function })

/// The underlying JavaScript `Date` object.
public let jsObject: JSObject
Expand Down
7 changes: 3 additions & 4 deletions Sources/JavaScriptKit/BasicObjects/JSError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
exposes its properties in a type-safe way.
*/
public final class JSError: Error, JSBridgedClass {
public final class JSError: JSBridgedClass {
/// The constructor function used to create new JavaScript `Error` objects.
public static var constructor: JSFunction? { _constructor }
@LazyThreadLocal(initialize: { JSObject.global.Error.function })
private static var _constructor: JSFunction?
public static var constructor: JSFunction? { _constructor.wrappedValue }
private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function })

/// The underlying JavaScript `Error` object.
public let jsObject: JSObject
Expand Down
24 changes: 16 additions & 8 deletions Sources/JavaScriptKit/BasicObjects/JSPromise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ public final class JSPromise: JSBridgedClass {
return Self(jsObject)
}

/// The result of a promise.
public enum Result: Equatable {
/// The promise resolved with a value.
case success(JSValue)
/// The promise rejected with a value.
case failure(JSValue)
}

/// Creates a new `JSPromise` instance from a given `resolver` closure.
/// The closure is passed a completion handler. Passing a successful
/// `Result` to the completion handler will cause the promise to resolve
/// with the corresponding value; passing a failure `Result` will cause the
/// promise to reject with the corresponding value.
/// Calling the completion handler more than once will have no effect
/// (per the JavaScript specification).
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> Void) -> Void) {
public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) {
let closure = JSOneshotClosure { arguments in
// The arguments are always coming from the `Promise` constructor, so we should be
// safe to assume their type here
Expand Down Expand Up @@ -90,7 +98,7 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure.async {
try await success($0[0]).jsValue
}
Expand All @@ -101,8 +109,8 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@discardableResult
public func then(
success: @escaping (JSValue) -> ConvertibleToJSValue,
failure: @escaping (JSValue) -> ConvertibleToJSValue
success: @escaping (sending JSValue) -> ConvertibleToJSValue,
failure: @escaping (sending JSValue) -> ConvertibleToJSValue
) -> JSPromise {
let successClosure = JSOneshotClosure {
success($0[0]).jsValue
Expand All @@ -117,8 +125,8 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue,
failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue,
failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
{
let successClosure = JSOneshotClosure.async {
try await success($0[0]).jsValue
Expand All @@ -132,7 +140,7 @@ public final class JSPromise: JSBridgedClass {

/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@discardableResult
public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure {
failure($0[0]).jsValue
}
Expand All @@ -143,7 +151,7 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func `catch`(failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure.async {
try await failure($0[0]).jsValue
}
Expand Down
Loading