From 3fa07a0e7a562861a68234e1d1fdc6bad82ce2b7 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 5 Feb 2024 15:06:55 -0800 Subject: [PATCH] Implement `swift_willThrow` variant for typed throws. `swift_willThrow` is called with an error right before it is thrown. This existing entrypoint requires an already-boxed error existential; with typed errors, we don't have the error existential on hand, so we would need to allocate the box to throw a typed error. That's not okay. Introduce a new `swift_willThrowTypedImpl` entry point into the runtime that will first check for the presence of an error handler and, if one is present, box the error to provide to the error handler. This maintains the no-allocations path for typed errors while still allowing existing error handlers to work. This new entrypoint isn't available on older Swift runtimes, so create a back-deployable shim called by the compiler. On new-enough platforms, this will call through to `swift_willThrowTypedImpl`. On older platforms, we drop the error and don't call the registered will-throw handler at all. This is a compromise that avoids boxing when throwing typed errors, at the cost of a slightly different experience for this new feature on older runtimes. Fixes rdar://119828459. --- include/swift/AST/KnownDecls.def | 2 + include/swift/Runtime/Error.h | 8 +++ lib/SILGen/SILGenStmt.cpp | 63 ++++++++++++++++----- stdlib/public/core/EmbeddedRuntime.swift | 5 ++ stdlib/public/core/ErrorType.swift | 24 ++++++++ stdlib/public/runtime/ErrorObjectCommon.cpp | 27 ++++++++- test/SILGen/typed_throws.swift | 38 ++++++++++++- test/abi/macOS/arm64/stdlib.swift | 1 + test/abi/macOS/x86_64/stdlib.swift | 1 + test/stdlib/Error.swift | 16 ++++++ 10 files changed, 167 insertions(+), 18 deletions(-) diff --git a/include/swift/AST/KnownDecls.def b/include/swift/AST/KnownDecls.def index 146d95877fc..5d6135dabe9 100644 --- a/include/swift/AST/KnownDecls.def +++ b/include/swift/AST/KnownDecls.def @@ -64,6 +64,8 @@ FUNC_DECL(BridgeAnyObjectToAny, FUNC_DECL(ConvertToAnyHashable, "_convertToAnyHashable") +FUNC_DECL(WillThrowTyped, "_willThrowTyped") + FUNC_DECL(DiagnoseUnexpectedError, "_unexpectedError") FUNC_DECL(DiagnoseUnexpectedErrorTyped, "_unexpectedErrorTyped") FUNC_DECL(ErrorInMainTyped, "_errorInMainTyped") diff --git a/include/swift/Runtime/Error.h b/include/swift/Runtime/Error.h index 080b907ae40..185f9dec9f7 100644 --- a/include/swift/Runtime/Error.h +++ b/include/swift/Runtime/Error.h @@ -71,6 +71,14 @@ SWIFT_RUNTIME_STDLIB_API void swift_willThrow(SWIFT_CONTEXT void *unused, SWIFT_ERROR_RESULT SwiftError **object); +/// Called when throwing a typed error. Serves as a breakpoint hook +/// for debuggers. +SWIFT_CC(swift) +SWIFT_RUNTIME_STDLIB_API void +swift_willThrowTypedImpl(OpaqueValue *value, + const Metadata *type, + const WitnessTable *errorConformance); + /// Called when an error is thrown out of the top level of a script. SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_API SWIFT_NORETURN void diff --git a/lib/SILGen/SILGenStmt.cpp b/lib/SILGen/SILGenStmt.cpp index 1a935e59506..245c385a72f 100644 --- a/lib/SILGen/SILGenStmt.cpp +++ b/lib/SILGen/SILGenStmt.cpp @@ -1560,24 +1560,57 @@ void SILGenFunction::emitThrow(SILLocation loc, ManagedValue exnMV, SILValue exn; if (!exnMV.isInContext()) { - // Claim the exception value. If we need to handle throwing - // cleanups, the correct thing to do here is to recreate the - // exception's cleanup when emitting each cleanup we branch through. - // But for now we aren't bothering. - exn = exnMV.forward(*this); - // Whether the thrown exception is already an Error existential box. SILType existentialBoxType = SILType::getExceptionType(getASTContext()); - bool isExistentialBox = exn->getType() == existentialBoxType; + bool isExistentialBox = exnMV.getType() == existentialBoxType; - // FIXME: Right now, we suppress emission of the willThrow builtin if the - // error isn't already the error existential, because swift_willThrow expects - // the existential box. - if (emitWillThrow && isExistentialBox) { - // Generate a call to the 'swift_willThrow' runtime function to allow the - // debugger to catch the throw event. - B.createBuiltin(loc, SGM.getASTContext().getIdentifier("willThrow"), - SGM.Types.getEmptyTupleType(), {}, {exn}); + // If we are supposed to emit a call to swift_willThrow(Typed), do so now. + if (emitWillThrow) { + ASTContext &ctx = SGM.getASTContext(); + if (isExistentialBox) { + // Generate a call to the 'swift_willThrow' runtime function to allow the + // debugger to catch the throw event. + + // Claim the exception value. + exn = exnMV.forward(*this); + + B.createBuiltin(loc, + ctx.getIdentifier("willThrow"), + SGM.Types.getEmptyTupleType(), {}, {exn}); + } else { + // Call the _willThrowTyped entrypoint, which handles + // arbitrary error types. + SILValue tmpBuffer; + SILValue error; + + FuncDecl *entrypoint = ctx.getWillThrowTyped(); + auto genericSig = entrypoint->getGenericSignature(); + SubstitutionMap subMap = SubstitutionMap::get( + genericSig, [&](SubstitutableType *dependentType) { + return exnMV.getType().getASTType(); + }, LookUpConformanceInModule(getModule().getSwiftModule())); + + // Generic errors are passed indirectly. + if (!exnMV.getType().isAddress()) { + // Materialize the error so we can pass the address down to the + // swift_willThrowTyped. + exnMV = exnMV.materialize(*this, loc); + error = exnMV.getValue(); + exn = exnMV.forward(*this); + } else { + // Claim the exception value. + exn = exnMV.forward(*this); + error = exn; + } + + emitApplyOfLibraryIntrinsic( + loc, entrypoint, subMap, + { ManagedValue::forForwardedRValue(*this, error) }, + SGFContext()); + } + } else { + // Claim the exception value. + exn = exnMV.forward(*this); } } diff --git a/stdlib/public/core/EmbeddedRuntime.swift b/stdlib/public/core/EmbeddedRuntime.swift index a0b518be0ca..bdb889b1bfb 100644 --- a/stdlib/public/core/EmbeddedRuntime.swift +++ b/stdlib/public/core/EmbeddedRuntime.swift @@ -302,6 +302,11 @@ public func swift_deletedMethodError() -> Never { public func swift_willThrow() throws { } +/// Called when a typed error will be thrown. +@_silgen_name("swift_willThrowTyped") +public func _willThrowTyped(_ error: E) { +} + @_extern(c, "arc4random_buf") func arc4random_buf(buf: UnsafeMutableRawPointer, nbytes: Int) diff --git a/stdlib/public/core/ErrorType.swift b/stdlib/public/core/ErrorType.swift index 69c135ddd3e..3713475b673 100644 --- a/stdlib/public/core/ErrorType.swift +++ b/stdlib/public/core/ErrorType.swift @@ -177,6 +177,30 @@ internal func _getErrorDefaultUserInfo(_ error: T) -> AnyObject? public func _bridgeErrorToNSError(_ error: __owned Error) -> AnyObject #endif +/// Called to indicate that a typed error will be thrown. +@_silgen_name("swift_willThrowTypedImpl") +@available(SwiftStdlib 5.11, *) +@usableFromInline +func _willThrowTypedImpl(_ error: E) + +#if !$Embedded +/// Called when a typed error will be thrown. +/// +/// On new-enough platforms, this will call through to the runtime to invoke +/// the thrown error handler (if one is set). +/// +/// On older platforms, the error will not be passed into the runtime, because +/// doing so would require memory allocation (to create the 'any Error'). +@inlinable +@_alwaysEmitIntoClient +@_silgen_name("swift_willThrowTyped") +public func _willThrowTyped(_ error: E) { + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *) { + _willThrowTypedImpl(error) + } +} +#endif + /// Invoked by the compiler when the subexpression of a `try!` expression /// throws an error. @_silgen_name("swift_unexpectedError") diff --git a/stdlib/public/runtime/ErrorObjectCommon.cpp b/stdlib/public/runtime/ErrorObjectCommon.cpp index e1bc4be4850..1f146e6b895 100644 --- a/stdlib/public/runtime/ErrorObjectCommon.cpp +++ b/stdlib/public/runtime/ErrorObjectCommon.cpp @@ -30,7 +30,8 @@ void swift::_swift_setWillThrowHandler(void (* handler)(SwiftError *error)) { _swift_willThrow.store(handler, std::memory_order_release); } -/// Breakpoint hook for debuggers, and calls _swift_willThrow if set. +/// Breakpoint hook for debuggers that is called for untyped throws, and +/// calls _swift_willThrow if set. SWIFT_CC(swift) void swift::swift_willThrow(SWIFT_CONTEXT void *unused, SWIFT_ERROR_RESULT SwiftError **error) { @@ -41,3 +42,27 @@ swift::swift_willThrow(SWIFT_CONTEXT void *unused, (* handler)(*error); } } + +/// Breakpoint hook for debuggers that is called for typed throws, and calls +/// _swift_willThrow if set. This implicitly boxes the typed error in an +/// any Error for the call. +SWIFT_CC(swift) void +swift::swift_willThrowTypedImpl(OpaqueValue *value, + const Metadata *type, + const WitnessTable *errorConformance) { + // Cheap check to bail out early, since we expect there to be no callbacks + // the vast majority of the time. + auto handler = _swift_willThrow.load(std::memory_order_acquire); + if (SWIFT_UNLIKELY(handler)) { + // Form an error box containing the error. + BoxPair boxedError = swift_allocError( + type, errorConformance, value, /*isTake=*/false); + + // Hand the boxed error off to the handler. + auto errorBox = reinterpret_cast(boxedError.object); + (* handler)(errorBox); + + // Release the error box. + swift_errorRelease(errorBox); + } +} diff --git a/test/SILGen/typed_throws.swift b/test/SILGen/typed_throws.swift index 7bcc26dce3c..94d68e6de52 100644 --- a/test/SILGen/typed_throws.swift +++ b/test/SILGen/typed_throws.swift @@ -17,11 +17,45 @@ func doesNotThrowConcrete() throws(MyError) { } // CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B8ConcreteyyAA7MyErrorOYKF : $@convention(thin) () -> @error MyError func throwsConcrete() throws(MyError) { // CHECK: [[ERROR:%[0-9]+]] = enum $MyError, #MyError.fail!enumelt - // CHECK-NOT: builtin "willThrow" - // CHECK: throw [[ERROR]] : $MyError + // CHECK: [[ERROR_ALLOC:%.*]] = alloc_stack $MyError + // CHECK: store [[ERROR]] to [trivial] [[ERROR_ALLOC]] : $*MyError + // CHECK: [[FN:%.*]] = function_ref @swift_willThrowTyped : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () + // CHECK: apply [[FN]]([[ERROR_ALLOC]]) : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () + // CHECK: [[ERROR_RELOAD:%.*]] = load [trivial] [[ERROR_ALLOC]] + // CHECK: dealloc_stack [[ERROR_ALLOC]] : $*MyError + // CHECK: throw [[ERROR_RELOAD]] : $MyError throw .fail } +class ClassError: Error { } + +// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B10ClassErroryyAA0cD0CYKF : $@convention(thin) () -> @error ClassError +// CHECK: [[META:%.*]] = metatype $@thick ClassError.Type +// CHECK: [[INIT:%.*]] = function_ref @$s12typed_throws10ClassErrorCACycfC +// CHECK: [[ERROR:%.*]] = apply [[INIT]]([[META]]) : $@convention(method) (@thick ClassError.Type) -> @owned ClassError +// CHECK: [[ERROR_ALLOC:%.*]] = alloc_stack $ClassError +// CHECK: store [[ERROR]] to [init] [[ERROR_ALLOC]] : $*ClassError +// CHECK: [[FN:%.*]] = function_ref @swift_willThrowTyped : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () +// CHECK: apply [[FN]]([[ERROR_ALLOC]]) : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () +// CHECK: [[ERROR_RELOAD:%.*]] = load [take] [[ERROR_ALLOC]] : $*ClassError +// CHECK: dealloc_stack [[ERROR_ALLOC]] : $*ClassError +// CHECK: throw [[ERROR_RELOAD]] : $ClassError +func throwsClassError() throws(ClassError) { + throw ClassError() +} + +// CHECK-LABEL: sil hidden [ossa] @$s12typed_throws0B13IndirectErroryyxxYKs0D0RzlF : $@convention(thin) (@in_guaranteed E) -> @error_indirect E +// CHECK: [[ERROR_ALLOC:%.*]] = alloc_stack $E +// CHECK: copy_addr %1 to [init] [[ERROR_ALLOC]] : $*E +// CHECK: [[FN:%.*]] = function_ref @swift_willThrowTyped : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () +// CHECK: apply [[FN]]([[ERROR_ALLOC]]) : $@convention(thin) <τ_0_0 where τ_0_0 : Error> (@in_guaranteed τ_0_0) -> () +// CHECK: copy_addr [take] [[ERROR_ALLOC]] to [init] %0 : $*E +// CHECK: dealloc_stack [[ERROR_ALLOC]] : $*E +// CHECK-NEXT: throw_addr +func throwsIndirectError(_ error: E) throws(E) { + throw error +} + // CHECK-LABEL: sil hidden [ossa] @$s12typed_throws15rethrowConcreteyyAA7MyErrorOYKF func rethrowConcrete() throws(MyError) { // CHECK: try_apply [[FN:%[0-9]+]]() : $@convention(thin) () -> @error MyError, normal [[NORMALBB:bb[0-9]+]], error [[ERRORBB:bb[0-9]+]] diff --git a/test/abi/macOS/arm64/stdlib.swift b/test/abi/macOS/arm64/stdlib.swift index da6769fb1ab..91e706ca191 100644 --- a/test/abi/macOS/arm64/stdlib.swift +++ b/test/abi/macOS/arm64/stdlib.swift @@ -254,3 +254,4 @@ Added: __swift_pod_destroy Added: __swift_pod_direct_initializeBufferWithCopyOfBuffer Added: __swift_pod_indirect_initializeBufferWithCopyOfBuffer Added: __swift_validatePrespecializedMetadata +Added: _swift_willThrowTypedImpl diff --git a/test/abi/macOS/x86_64/stdlib.swift b/test/abi/macOS/x86_64/stdlib.swift index c28f8d12241..543eb05bacc 100644 --- a/test/abi/macOS/x86_64/stdlib.swift +++ b/test/abi/macOS/x86_64/stdlib.swift @@ -254,3 +254,4 @@ Added: __swift_pod_destroy Added: __swift_pod_direct_initializeBufferWithCopyOfBuffer Added: __swift_pod_indirect_initializeBufferWithCopyOfBuffer Added: __swift_validatePrespecializedMetadata +Added: _swift_willThrowTypedImpl diff --git a/test/stdlib/Error.swift b/test/stdlib/Error.swift index 77e259621d1..7737a19e6b8 100644 --- a/test/stdlib/Error.swift +++ b/test/stdlib/Error.swift @@ -220,6 +220,11 @@ func throwJazzHands() throws { throw SillyError.JazzHands } +@inline(never) +func throwJazzHandsTyped() throws(SillyError) { + throw .JazzHands +} + // Error isn't allowed in a @convention(c) function when ObjC interop is // not available, so pass it through an UnsafeRawPointer. @available(SwiftStdlib 5.8, *) @@ -249,6 +254,17 @@ ErrorTests.test("willThrow") { } catch {} expectEqual(2, errors.count) expectEqual(SillyError.self, type(of: errors.last!)) + + // Typed errors introduced in Swift 5.11 + guard #available(SwiftStdlib 5.11, *) else { + return + } + + do { + try throwJazzHandsTyped() + } catch {} + expectEqual(3, errors.count) + expectEqual(SillyError.self, type(of: errors.last!)) } #endif