Description
When a closure taking AnyHashable is converted to one taking T?, SILGen force-unwraps the optional before erasing it. Optional<T> is itself Hashable, so nil should be erased, not unwrapped.
- macOS and Linux, Swift 5.9 through
main: passing nil traps with Unexpectedly found nil while implicitly unwrapping an Optional value.
- macOS at
-O: for some payloads (for example, [UInt8]?) the compiler crashes instead.
Reproduction
let f: (Int?) -> Bool = { (x: AnyHashable) in x == AnyHashable(1) }
print(f(nil)) // expected false; traps
In practice this is hit through swift-testing. This kills the test run instead of recording a failed expectation:
let bytes: [UInt8]? = nil
#expect(bytes == [1, 0xFF ^ 0xDE])
The arithmetic in the literal matters only because it makes type inference pick AnyHashable. With [1, 0x21], no conversion is emitted.
Compiler crash (macOS, swiftc -O -c Repro.swift)
func check<T, U>(
_ lhs: T,
_ op: (T, @escaping () -> U) -> Bool,
_ rhs: @escaping @autoclosure () -> U
) -> Bool {
op(lhs, rhs)
}
public func run() -> Bool {
let bytes: [UInt8]? = [1, 0x21]
return check(bytes, { $0 == $1() }, [1, 0xFF ^ 0xDE])
}
1. Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)
3. While evaluating request IRGenRequest(IR Generation for file "Repro.swift")
4. While emitting IR SIL function "@$s5Repro3runSbyF".
4 swift-frontend SingleScalarTypeInfo<BridgeObjectTypeInfo, ReferenceTypeInfo>::initialize(...)
6 swift-frontend RecordTypeInfo<LoadableStructTypeInfo, ...>::initialize(...)
9 swift-frontend IRGenSILFunction::visitSILBasicBlock(swift::SILBasicBlock*)
Cause
Transform::transform in lib/SILGen/SILGenPoly.cpp forces any optional input whose output type isn't optional or existential. AnyHashable is a struct, so it isn't exempt. The value is unwrapped, but inputSubstType still says Optional<T>, and the AnyHashable erasure further down uses that stale type. A compiler built with assertions fails in the SIL verifier:
TYPE MISMATCH IN ARGUMENT 0 OF APPLY
argument type: $*Array<UInt8>
parameter type: $*Optional<Array<UInt8>>
Suggested fix
Exempt AnyHashable from the force, as existentials already are, so the erasure consumes the optional directly:
// If the value is an optional, but the desired formal type isn't an
// optional or Any, force it.
if (inputIsOptional && !outputIsOptional &&
- !outputSubstType->isExistentialType()) {
+ !outputSubstType->isExistentialType() &&
+ !outputSubstType->isAnyHashable()) {
Tested against main with assertions: the crash reproducer compiles and nil compares false. The SILGen, IRGen, SILOptimizer, and Interpreter test suites pass.
Description
When a closure taking
AnyHashableis converted to one takingT?, SILGen force-unwraps the optional before erasing it.Optional<T>is itselfHashable, sonilshould be erased, not unwrapped.main: passingniltraps with Unexpectedly found nil while implicitly unwrapping an Optional value.-O: for some payloads (for example,[UInt8]?) the compiler crashes instead.Reproduction
In practice this is hit through swift-testing. This kills the test run instead of recording a failed expectation:
The arithmetic in the literal matters only because it makes type inference pick
AnyHashable. With[1, 0x21], no conversion is emitted.Compiler crash (macOS,
swiftc -O -c Repro.swift)Cause
Transform::transforminlib/SILGen/SILGenPoly.cppforces any optional input whose output type isn't optional or existential.AnyHashableis a struct, so it isn't exempt. The value is unwrapped, butinputSubstTypestill saysOptional<T>, and theAnyHashableerasure further down uses that stale type. A compiler built with assertions fails in the SIL verifier:Suggested fix
Exempt
AnyHashablefrom the force, as existentials already are, so the erasure consumes the optional directly:Tested against
mainwith assertions: the crash reproducer compiles andnilcomparesfalse. The SILGen, IRGen, SILOptimizer, and Interpreter test suites pass.