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.
This commit is contained in:
Doug Gregor
2024-02-05 15:06:55 -08:00
parent e7be8f32f1
commit 3fa07a0e7a
10 changed files with 167 additions and 18 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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<E: Error>(_ error: E) {
}
@_extern(c, "arc4random_buf")
func arc4random_buf(buf: UnsafeMutableRawPointer, nbytes: Int)

View File

@@ -177,6 +177,30 @@ internal func _getErrorDefaultUserInfo<T: Error>(_ 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<E: Error>(_ 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<E: Error>(_ 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")

View File

@@ -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<SwiftError *>(boxedError.object);
(* handler)(errorBox);
// Release the error box.
swift_errorRelease(errorBox);
}
}

View File

@@ -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]]<MyError>([[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]]<ClassError>([[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) <E where E : Error> (@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]]<E>([[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<E: Error>(_ 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]+]]

View File

@@ -254,3 +254,4 @@ Added: __swift_pod_destroy
Added: __swift_pod_direct_initializeBufferWithCopyOfBuffer
Added: __swift_pod_indirect_initializeBufferWithCopyOfBuffer
Added: __swift_validatePrespecializedMetadata
Added: _swift_willThrowTypedImpl

View File

@@ -254,3 +254,4 @@ Added: __swift_pod_destroy
Added: __swift_pod_direct_initializeBufferWithCopyOfBuffer
Added: __swift_pod_indirect_initializeBufferWithCopyOfBuffer
Added: __swift_validatePrespecializedMetadata
Added: _swift_willThrowTypedImpl

View File

@@ -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