Skip to content

SILGen force-unwraps Optional when converting to AnyHashable, so nil traps (and -O can crash) #92469

Description

@timsneath

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    crashBug: A crash, i.e., an abnormal termination of softwaretriage neededThis issue needs more specific labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions