Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 96 additions & 10 deletions 106 Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct ClosureCodegen {
let closureParams = signature.parameters.map { "\(sendingPrefix)\($0.closureSwiftType)" }.joined(
separator: ", "
)
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws(JSException)" : "")
let swiftReturnType = signature.returnType.closureSwiftType
return "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
}
Expand Down Expand Up @@ -73,7 +73,17 @@ public struct ClosureCodegen {
helperEnumDeclPrinter.indent {
helperEnumDeclPrinter.write("let callback = JSObject.bridgeJSLiftParameter(callbackId)")
let parameters: String
if signature.parameters.isEmpty {
if signature.isThrows || signature.isAsync {
let sendingPrefix = signature.sendingParameters ? "sending " : ""
let typedParams =
signature.parameters.enumerated().map { index, paramType in
"param\(index): \(sendingPrefix)\(paramType.closureSwiftType)"
}.joined(separator: ", ")
let returnType = signature.returnType.closureSwiftType
let effects =
(signature.isAsync ? " async" : "") + (signature.isThrows ? " throws(JSException)" : "")
parameters = " (\(typedParams))\(effects) -> \(returnType)"
} else if signature.parameters.isEmpty {
parameters = ""
} else if signature.parameters.count == 1 {
parameters = " param0"
Expand Down Expand Up @@ -146,22 +156,25 @@ public struct ClosureCodegen {
liftedParams.append("\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))")
}

let closureCallExpr = ExprSyntax("closure(\(raw: liftedParams.joined(separator: ", ")))")
let tryPrefix = signature.isThrows ? "try " : ""
let closureCallExpr = ExprSyntax("\(raw: tryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))")
let asyncTryPrefix = (signature.isThrows ? "try " : "") + "await "
let asyncClosureCallExpr = ExprSyntax(
"\(raw: asyncTryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))"
)

let abiReturnWasmType = try signature.returnType.loweringReturnInfo().returnType
let abiReturnWasmType =
signature.isAsync
? try BridgeType.jsObject(nil).loweringReturnInfo().returnType
: try signature.returnType.loweringReturnInfo().returnType

// Build signature using SwiftSignatureBuilder
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
abiParameters: abiParams,
returnType: abiReturnWasmType
)

// Build function declaration using helper
let funcDecl = SwiftCodePattern.buildExposedFunctionDecl(
abiName: abiName,
signature: funcSignature
) { printer in
printer.write("let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure")
let emitCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
if signature.returnType == .void {
printer.write(closureCallExpr.description)
} else {
Expand Down Expand Up @@ -189,6 +202,79 @@ public struct ClosureCodegen {
}
}

let emitAsyncCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
printer.write("let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure")
let resolveType = signature.returnType
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
let rejectName = "Promise_reject"
let closureHead: String
if signature.isThrows {
let returnSpelling = resolveType == .void ? "" : " -> \(resolveType.closureSwiftType)"
closureHead = " () async throws(JSException)\(returnSpelling) in"
} else {
closureHead = ""
}
printer.write("return _bjs_makePromise(resolve: \(resolveName), reject: \(rejectName)) {\(closureHead)")
printer.indent {
if resolveType == .void {
printer.write(asyncClosureCallExpr.description)
} else {
printer.write("return \(asyncClosureCallExpr)")
}
}
printer.write("}")
}

let catchPlaceholderStmt = abiReturnWasmType?.swiftReturnPlaceholderStmt

// Build function declaration using helper
let funcDecl = SwiftCodePattern.buildExposedFunctionDecl(
abiName: abiName,
signature: funcSignature
) { printer in
if signature.isAsync {
emitAsyncCallAndLower(printer)
} else if signature.isThrows {
printer.write(
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
)
printer.write("do {")
printer.indent {
emitCallAndLower(printer)
}
printer.write("} catch let error {")
printer.indent {
printer.write("if let error = error.thrownValue.object {")
printer.indent {
printer.write("withExtendedLifetime(error) {")
printer.indent {
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
}
printer.write("}")
}
printer.write("} else {")
printer.indent {
printer.write("let jsError = JSError(message: error.description)")
printer.write("withExtendedLifetime(jsError.jsObject) {")
printer.indent {
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
}
printer.write("}")
}
printer.write("}")
if let catchPlaceholderStmt {
printer.write(catchPlaceholderStmt)
}
}
printer.write("}")
} else {
printer.write(
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
)
emitCallAndLower(printer)
}
}

return DeclSyntax(funcDecl)
}

Expand Down
9 changes: 4 additions & 5 deletions 9 Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,7 @@ public struct ImportTS {
}
}

// Add exception check for ImportTS context (skipped for async, where
// errors are funneled through the JS-side reject path)
if !effects.isAsync && context == .importTS {
if !effects.isAsync && (context == .importTS || effects.isThrows) {
body.write("if let error = _swift_js_take_exception() { throw error }")
}
}
Expand Down Expand Up @@ -323,18 +321,19 @@ public struct ImportTS {
let innerBody = body
body = CodeFragmentPrinter()

let tryKeyword = effects.isThrows ? "try" : "try!"
let rejectFactory = "makeRejectClosure: { JSTypedClosure<(sending JSValue) -> Void>($0) }"
if returnType == .void {
let resolveFactory = "makeResolveClosure: { JSTypedClosure<() -> Void>($0) }"
body.write(
"try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
"\(tryKeyword) await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
)
} else {
let resolveSwiftType = returnType.closureSwiftType
let resolveFactory =
"makeResolveClosure: { JSTypedClosure<(sending \(resolveSwiftType)) -> Void>($0) }"
body.write(
"let resolved = try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
"let resolved = \(tryKeyword) await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
)
}
body.indent {
Expand Down
20 changes: 15 additions & 5 deletions 20 Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,21 @@ import SwiftSyntax
import class Foundation.ProcessInfo

public struct DiagnosticError: Error {
public enum Severity: String, Sendable {
case error
case warning
}

public let node: Syntax
public let message: String
public let hint: String?
public let severity: Severity

public init(node: some SyntaxProtocol, message: String, hint: String? = nil) {
public init(node: some SyntaxProtocol, message: String, hint: String? = nil, severity: Severity = .error) {
self.node = Syntax(node)
self.message = message
self.hint = hint
self.severity = severity
}

/// Formats the diagnostic error as a string.
Expand All @@ -166,12 +173,14 @@ public struct DiagnosticError: Error {

let lineNumberWidth = max(3, String(lines.count).count)

let severityLabel = severity.rawValue
let severityColor = severity == .warning ? ANSI.boldYellow : ANSI.boldRed
let header: String = {
guard colorize else {
return "\(displayFileName):\(startLocation.line):\(startLocation.column): error: \(message)"
return "\(displayFileName):\(startLocation.line):\(startLocation.column): \(severityLabel): \(message)"
}
return
"\(displayFileName):\(startLocation.line):\(startLocation.column): \(ANSI.boldRed)error: \(ANSI.boldDefault)\(message)\(ANSI.reset)"
"\(displayFileName):\(startLocation.line):\(startLocation.column): \(severityColor)\(severityLabel): \(ANSI.boldDefault)\(message)\(ANSI.reset)"
}()

let highlightStartColumn = min(max(1, startLocation.column), mainLine.utf8.count + 1)
Expand Down Expand Up @@ -227,8 +236,8 @@ public struct DiagnosticError: Error {
let pointerSpacing = max(0, highlightStartColumn - 1)
let pointerMessage: String = {
let pointer = String(repeating: " ", count: pointerSpacing) + "`- "
guard colorize else { return pointer + "error: \(message)" }
return pointer + "\(ANSI.boldRed)error: \(ANSI.boldDefault)\(message)\(ANSI.reset)"
guard colorize else { return pointer + "\(severityLabel): \(message)" }
return pointer + "\(severityColor)\(severityLabel): \(ANSI.boldDefault)\(message)\(ANSI.reset)"
}()
descriptionParts.append(
Self.formatSourceLine(
Expand Down Expand Up @@ -304,6 +313,7 @@ public struct BridgeJSCoreDiagnosticError: Swift.Error, CustomStringConvertible
private enum ANSI {
static let reset = "\u{001B}[0;0m"
static let boldRed = "\u{001B}[1;31m"
static let boldYellow = "\u{001B}[1;33m"
static let boldDefault = "\u{001B}[1;39m"
static let cyan = "\u{001B}[0;36m"
static let underline = "\u{001B}[4;39m"
Expand Down
65 changes: 45 additions & 20 deletions 65 Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public final class SwiftToSkeleton {
private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = []
private var usedExternalModules = Set<String>()

/// Non-fatal diagnostics collected during `finalize()`. These do not fail the build.
public private(set) var warnings: [(file: String, diagnostic: DiagnosticError)] = []

public init(
progress: ProgressReporting,
moduleName: String,
Expand Down Expand Up @@ -87,10 +90,15 @@ public final class SwiftToSkeleton {
)
importCollector.walk(sourceFile)

let importErrorsFatal = importCollector.errors.filter { !$0.message.contains("Unsupported type '") }
if !exportCollector.errors.isEmpty || !importErrorsFatal.isEmpty {
let exportErrors = exportCollector.errors.filter { $0.severity == .error }
let importErrorsFatal = importCollector.errors.filter {
$0.severity == .error && !$0.message.contains("Unsupported type '")
}
let fileWarnings = (exportCollector.errors + importCollector.errors).filter { $0.severity == .warning }
warnings.append(contentsOf: fileWarnings.map { (file: inputFilePath, diagnostic: $0) })
if !exportErrors.isEmpty || !importErrorsFatal.isEmpty {
perSourceErrors.append(
(inputFilePath: inputFilePath, errors: exportCollector.errors + importErrorsFatal)
(inputFilePath: inputFilePath, errors: exportErrors + importErrorsFatal)
)
}

Expand Down Expand Up @@ -191,7 +199,40 @@ public final class SwiftToSkeleton {
}

let isAsync = functionType.effectSpecifiers?.asyncSpecifier != nil
let isThrows = functionType.effectSpecifiers?.throwsClause != nil

if isAsync, !returnType.isAsyncResolvable {
errors.append(
DiagnosticError(
node: functionType,
message:
"Returning '\(returnType.swiftType)' from an async closure is not yet supported",
hint:
"Return a type lowerable through the async resolve ABI "
+ "(String/Int/Bool/Double/Float/raw-value or case-only enum/@JS struct/JSObject/Optional/Array/Dictionary), "
+ "or make the closure non-async."
)
)
return nil
}

var isThrows = false
if let throwsClause = functionType.effectSpecifiers?.throwsClause {
guard let thrownType = throwsClause.type,
thrownType.trimmedDescription == "JSException"
else {
errors.append(
DiagnosticError(
node: throwsClause,
message:
"Only JSException is supported for thrown type of Swift closures, "
+ "got \(throwsClause.type?.trimmedDescription ?? "unspecified")",
hint: "Annotate the closure as `throws(JSException)`"
)
)
return nil
}
isThrows = true
}

return .closure(
ClosureSignature(
Expand Down Expand Up @@ -1028,22 +1069,6 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
guard let type = resolvedType else {
continue // Skip unsupported types
}
if case .closure(let signature, _) = type {
if signature.isAsync {
diagnose(
node: param.type,
message: "Async is not supported for Swift closures yet."
)
continue
}
if signature.isThrows {
diagnose(
node: param.type,
message: "Throws is not supported for Swift closures yet."
)
continue
}
}
if case .nullable(let wrappedType, _) = type, wrappedType.isOptional {
diagnoseNestedOptional(node: param.type, type: param.type.trimmedDescription)
continue
Expand Down
6 changes: 4 additions & 2 deletions 6 Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ public struct BridgeJSLink {
) throws -> [String] {
let printer = CodeFragmentPrinter()
let builder = ExportedThunkBuilder(
effects: Effects(isAsync: false, isThrows: true),
effects: Effects(isAsync: signature.isAsync, isThrows: signature.isAsync ? signature.isThrows : true),
hasDirectAccessToSwiftClass: false,
intrinsicRegistry: intrinsicRegistry
)
Expand Down Expand Up @@ -3743,7 +3743,9 @@ extension BridgeType {
let paramTypes = signature.parameters.enumerated().map { index, param in
"arg\(index): \(param.tsType)"
}.joined(separator: ", ")
return "(\(paramTypes)) => \(signature.returnType.tsType)"
let returnTS =
signature.isAsync ? "Promise<\(signature.returnType.tsType)>" : signature.returnType.tsType
return "(\(paramTypes)) => \(returnTS)"
case .array(let elementType):
let inner = elementType.tsType
if inner.contains("|") || inner.contains("=>") {
Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.