Parameter packs: Creating an `Equatable` box

Hello,

I have experienced a bug in a compiler (crash, see below). I would like to ask how did you solve this issue.

Essentially, I need to pass parameters from my variadic API into an API, that accepts an Equatable concrete type. I have tried to pass tuple, but tuples don't have synthesized conformance. Therefore I have created my own variadic equatable box.

The code looks like it should work, but it does not. Is there some workaround for this?


The crash

Code Example

#!/usr/bin/swift

struct EquatableWitness<each Value: Equatable>: Equatable {
    private let value: (repeat each Value)
    init(_ value: (repeat each Value)) { self.value = value }

    static func == (lhs: EquatableWitness<repeat each Value>, rhs: EquatableWitness<repeat each Value>) -> Bool {
        var results = [Bool]()
        repeat (results.append(each lhs.value == each rhs.value))
        return results.allSatisfy { $0 }
    }
}

func foo<E: Equatable>(_ lhs: E, _ rhs: E) {}

func onChange<
    each SourceValue: Equatable
>(
    of sourceValue: repeat each SourceValue
) {
    let xxx = EquatableWitness( (repeat each sourceValue) )
    let yyy = EquatableWitness( (repeat each sourceValue) )
    foo(xxx, yyy)
}

onChange(of: 1, 1, 1)

Crash

Summary
mikolasstuchlik@android-cf98bf54c82f6da scripts % swift --version
swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
Target: arm64-apple-macosx14.0
mikolasstuchlik@android-cf98bf54c82f6da scripts % swiftc varia.swift 
error: compile command failed due to signal 11 (use -v to see invocation)
Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.
Stack dump:
0.      Program arguments: /Applications/Xcode-15.0.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c -primary-file varia.swift -target arm64-apple-macosx14.0 -Xllvm -aarch64-use-tbi -enable-objc-interop -stack-check -sdk /Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk -color-diagnostics -new-driver-path /Applications/Xcode-15.0.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-driver -empty-abi-descriptor -resource-dir /Applications/Xcode-15.0.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift -module-name varia -disable-clang-spi -target-sdk-version 14.0 -target-sdk-name macosx14.0 -external-plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/lib/swift/host/plugins#/Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/bin/swift-plugin-server -external-plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/local/lib/swift/host/plugins#/Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/bin/swift-plugin-server -external-plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins#/Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -external-plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/local/lib/swift/host/plugins#/Applications/Xcode-15.0.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/plugins -plugin-path /Applications/Xcode-15.0.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/local/lib/swift/host/plugins -o /var/folders/rw/7crj4v7j30n5359y24wyp6nr0000gn/T/TemporaryDirectory.YKlIap/varia-1.o
1.      Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
2.      Compiling with the current language version
3.      While evaluating request ASTLoweringRequest(Lowering AST to SIL for file "varia.swift")
4.      While silgen emitFunction SIL function "@$s5varia8onChange2ofyxxQp_tRvzSQRzlF".
 for 'onChange(of:)' (at varia.swift:16:1)
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  swift-frontend           0x0000000107a8f14c llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
1  swift-frontend           0x000000010a60df2c llvm::sys::RunSignalHandlers() + 112
2  swift-frontend           0x000000010634f10c SignalHandler(int) + 352
3  libsystem_platform.dylib 0x000000018d2e9a24 _sigtramp + 56
4  swift-frontend           0x0000000106b7b564 swift::Lowering::AbstractionPattern::forEachExpandedTupleElement(swift::CanType, llvm::function_ref<void (swift::Lowering::AbstractionPattern, swift::CanType, swift::TupleTypeElt const&)>) const + 788
5  swift-frontend           0x0000000106e86304 swift::CanTypeVisitor<(anonymous namespace)::TypeClassifier, swift::Lowering::TypeLowering::RecursiveProperties, swift::Lowering::AbstractionPattern, swift::Lowering::IsTypeExpansionSensitive_t>::visit(swift::CanType, swift::Lowering::AbstractionPattern, swift::Lowering::IsTypeExpansionSensitive_t) (.llvm.11273825785063854797) + 596
6  swift-frontend           0x0000000108c890ac (anonymous namespace)::TypeClassifierBase<(anonymous namespace)::TypeClassifier, swift::Lowering::TypeLowering::RecursiveProperties>::visit(swift::CanType, swift::Lowering::AbstractionPattern, swift::Lowering::IsTypeExpansionSensitive_t) (.llvm.11273825785063854797) + 148
7  swift-frontend           0x0000000106e9cd18 (anonymous namespace)::LowerType::visitAnyStructType(swift::CanType, swift::Lowering::AbstractionPattern, swift::StructDecl*, swift::Lowering::IsTypeExpansionSensitive_t) + 992
8  swift-frontend           0x0000000106e7ff74 (anonymous namespace)::TypeClassifierBase<(anonymous namespace)::LowerType, swift::Lowering::TypeLowering*>::visit(swift::CanType, swift::Lowering::AbstractionPattern, swift::Lowering::IsTypeExpansionSensitive_t) + 388
9  swift-frontend           0x0000000108c7ede8 swift::Lowering::TypeConverter::getTypeLowering(swift::Lowering::AbstractionPattern, swift::Type, swift::TypeExpansionContext) + 756
10 swift-frontend           0x0000000108c5dfdc swift::CanTypeVisitor<(anonymous namespace)::SILTypeSubstituter, swift::CanType>::visitGenericTypeParamType(swift::CanTypeWrapper<swift::GenericTypeParamType>) + 128
11 swift-frontend           0x0000000106e6cb58 (anonymous namespace)::SILTypeSubstituter::substSILFunctionType(swift::CanTypeWrapper<swift::SILFunctionType>, bool) + 1140
12 swift-frontend           0x0000000108c531c4 swift::SILFunctionType::substGenericArgs(swift::SILModule&, swift::SubstitutionMap, swift::TypeExpansionContext) + 444
13 swift-frontend           0x0000000107ebb7f4 (anonymous namespace)::Callee::createCalleeTypeInfo(swift::Lowering::SILGenFunction&, llvm::Optional<swift::SILDeclRef>, swift::SILType) const & + 344
14 swift-frontend           0x0000000107df99e8 (anonymous namespace)::Callee::getTypeInfo(swift::Lowering::SILGenFunction&) const & + 1672
15 swift-frontend           0x0000000105476750 (anonymous namespace)::CallEmission::apply(swift::Lowering::SGFContext) + 976
16 swift-frontend           0x0000000107df174c swift::Lowering::SILGenFunction::emitApplyExpr(swift::ApplyExpr*, swift::Lowering::SGFContext) + 3072
17 swift-frontend           0x0000000108054468 swift::Lowering::SILGenFunction::emitIgnoredExpr(swift::Expr*) + 892
18 swift-frontend           0x0000000105c2b570 swift::ASTVisitor<(anonymous namespace)::StmtEmitter, void, void, void, void, void, void>::visit(swift::Stmt*) (.llvm.5384354941888427540) + 5512
19 swift-frontend           0x0000000108181864 swift::Lowering::SILGenFunction::emitFunction(swift::FuncDecl*) + 632
20 swift-frontend           0x0000000105464a4c swift::Lowering::SILGenModule::emitFunctionDefinition(swift::SILDeclRef, swift::SILFunction*) + 8004
21 swift-frontend           0x0000000107d7163c emitOrDelayFunction(swift::Lowering::SILGenModule&, swift::SILDeclRef) (.llvm.12369091251732681984) + 168
22 swift-frontend           0x0000000107d58be8 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl*) + 292
23 swift-frontend           0x0000000107d80608 swift::ASTLoweringRequest::evaluate(swift::Evaluator&, swift::ASTLoweringDescriptor) const + 2908
24 swift-frontend           0x00000001069a84fc swift::SimpleRequest<swift::ASTLoweringRequest, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>> (swift::ASTLoweringDescriptor), (swift::RequestFlags)9>::evaluateRequest(swift::ASTLoweringRequest const&, swift::Evaluator&) + 200
25 swift-frontend           0x0000000107d9e948 llvm::Expected<swift::ASTLoweringRequest::OutputType> swift::Evaluator::getResultUncached<swift::ASTLoweringRequest>(swift::ASTLoweringRequest const&) + 584
26 swift-frontend           0x000000010a5c58d8 performCompile(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 1680
27 swift-frontend           0x000000010a5c9474 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 4568
28 swift-frontend           0x0000000109f0f4e0 swift::mainEntry(int, char const**) + 4116
29 dyld                     0x000000018cf41058 start + 2224
error: fatalError

It might be a bit late, but the following works (and it is more efficient).

struct EquatableBox<each V: Equatable>: Equatable {
  private let value: (repeat each V)

  init(_ value: repeat each V) {
    self.value = (repeat each value)
  }

  static func == (lhs: Self, rhs: Self) -> Bool {
    func throwIfNotEqual<T: Equatable>(_ lhs: T, _ rhs: T) throws {
      guard lhs == rhs else { throw CancellationError() }
    }

    do {
      repeat try throwIfNotEqual(each lhs.value, each rhs.value)
    } catch {
      return false
    }
    return true
  }
}

Three things to notice:

  1. The initializer doesn't take a tuple, but a pack.
  2. The equatable function uses Self to reduce boilerplate.
  3. The equatable function uses the shortcircuit mechanics described in the pack iteration proposal.
    Once Swift 5.10 is released, you can use a simple for-in loop.

Your testing onChange(_:) function then looks like:

func onChange<each S: Equatable>(of sourceValue: repeat each S) {
  let xxx = EquatableBox(repeat each sourceValue)
  let yyy = EquatableBox(repeat each sourceValue)
  foo(xxx, yyy)
}
  var isEqualBuildsWithWorkaround: Bool {
    (repeat each self.lhsElements) == (repeat each self.rhsElements)
  }
  
  var isEqualFailsToBuild: Bool {
    self.lhsElements == self.rhsElements  //  Assertion failed: (!tupleTy.containsPackExpansionType() && "can't extract elements from tuples containing pack expansions " "right now"), function extractElements, file RValue.cpp, line 713.
  }

I saw a similar crash from passing packs to test for equality… there seems to be a workaround in here that worked for me.

@dehesa can you show us how please? I wasn't able to make it :sweat_smile:

The pack iteration feature (i.e. the for-in loop for variadic generics) got delayed to Swift 6 (Xcode 16). If you are running the Xcode betas you could create an "EquatableBox" as @stuchlej wanted as follows:

struct EquatableBox<each Element: Equatable>: Equatable {
  private let elements: (repeat each Element)

  init(_ elements: repeat each Element) {
    self.elements = (repeat each elements)
  }

  static func == (lhs: Self, rhs: Self) -> Bool {
    for (left, right) in repeat (each lhs.elements, each rhs.elements) {
      guard left == right else { return false }
    }
    return true
  }
}

This will not crash and fulfill the sample code @stuchlej put:

func onChange<each Value: Equatable>(of values: repeat each Value) {
    let xxx = EquatableWitness(repeat each values)
    let yyy = EquatableWitness(repeat each values)
    foo(xxx, yyy)
}

onChange(of: 1, 1, 1)

However, I don't find it useful. Because that will only work for cases where the types are the same. The following will compile and run

let boxA = EquatableBox(1, 2.0, "A")
let boxB = EquatableBox(3, 4.0, "B")
print(boxA == boxB) // This returns false

The following sample code won't compile, because EquatableBox<String, Int, Double> is not of the same type as EquatableBox<Int, Double, String> (which is very true) :smile:

let boxA = EquatableBox(1, 2.0, "A")
let boxB = EquatableBox("C", 3, 4.0)
print(boxA == boxB)
3 Likes

Thank you, it worked.

My use case is a little different, what I want is to refresh .task(id: EquatableBox()) if any of the elements got changed to re-perform the task.