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.