Playing around with parameter packs I was trying to compile the following but failed. Now this feels like it's either a bug in the compiler or I'm doing something wrong using the parameter pack feature :) Would love to understand if I'm missing something:
The following code is a reproducer I made from the larger swift-parsing package codebase.
This fails to compile with the following error: Value pack expansion can only appear inside a function argument list, tuple element, or as the expression of a for-in loop
public protocol Parser<Input, Output> {
associatedtype Input
associatedtype Output
func parse(_ input: inout Input) -> Output
}
public protocol ParserPrinter<Input, Output>: Parser {
func print(_ output: Output, into input: inout Input)
}
@available(macOS 14.0.0, *)
public struct Take<P0: Parser, P1: Parser, each Outputs>: Parser
where P0.Input == P1.Input, P0.Output == (repeat each Outputs) {
let p0: P0
let p1: P1
init(_ p0: P0, _ p1: P1) {
self.p0 = p0
self.p1 = p1
}
public func parse(_ input: inout P0.Input) -> (repeat each Outputs, P1.Output) {
let outputs = self.p0.parse(&input)
return (repeat each outputs, self.p1.parse(&input))
}
}
@available(macOS 14.0.0, *)
extension Take: ParserPrinter where P0: ParserPrinter, P1: ParserPrinter {
public func print(_ output: (repeat each Outputs, P1.Output), into input: inout P0.Input) {
self.p1.print(output.1, into: &input)
self.p0.print(output.0, into: &input) // Value pack expansion can only appear inside a function argument list, tuple element, or as the expression of a for-in loop
}
}
when I put repeat each output.0 inside the print(:) function call I get a second error as well: Cannot pass value pack expansion to non-pack parameter of type 'Self.Output'
and the original: Value pack expansion can only appear inside a function argument list, tuple element, or as the expression of a for-in loop
Which feels strange since that call shouldn't expect Self.Output but P0.Output which is constrained to equal to the parameter pack each Outputs
Thanks for the insight! You mention that it crashes, is that due to a compiler bug? Or does the compiler simply not support it yet? Or was it an oversight at the original design of the syntax?
I'm asking to understand what would be needed to get this kind of functionality :) a bug fix, an evolution proposal, etc etc.
My example crashes due to a bug that just needs to be fixed. It would be nice if there was a cleaner way to express it, with pattern matching for example. I suspect pattern matching with packs and enums might need a proposal to spell out the details.
We could also allow .0/.1 on tuples that contain pack expansions (it should be diagnosed today because you saw it’s not fully implemented) but the semantics would be a bit odd, I think.
What do you mean with pattern matching with enums in this case? I don't fully understand how they're related.
I'd love to but feel I'm not experienced enough with the parameter pack apis and have a clear vision on what problems the pattern matching should cover to put together a full proposal.
Would love to help though if you think it's something that needs to be addressed! Let me know
I was able to get around the compiler crash by bit-casting the flat parameter pack to a tuple-ized one
let combined: (repeat each A, repeat each B)
let tuple = unsafeBitCast(
combined,
to: ((repeat each A), (repeat each B)).self
)
// Access 'tuple.0' or 'tuple.1'...
Is this legit to do? I imagine the memory layout should always be the same here?
I think this can be incorrect because of alignment:
(Int32, Int8) has size 5, align 4
(Int8, Int32) has size 8, align 4 (with three bytes of interior padding)
((Int32, Int8), (Int8, Int32)) has size 16, align 4
(Int32, Int8, Int8, Int32) has size 12, align 4 (with two bytes of interior padding)
This isn’t the only possible layout algorithm, but it’s the one that shipped in Swift 5’s stable ABI on Apple platforms, so we’re at least a bit stuck with it.
I believe this crash is closely related to this one:
func flatten<each T>(_ packs: (Int, repeat each T)) {
func homo<each U>(_ p: (repeat each U)) {
for element in repeat each p {
print(element)
}
}
homo(packs)
}
For the above code, the compiler complains "can't extract elements from tuples containing pack expansions right now".
I think this is the same reason why Swift fails for Slava's code snippet, it cannot pass uncons2 to untuple as a (repeat each T) -> U
The problem is, you can't currently call entuplePack on an argument. The following will crash the compiler, so you have to copy/paste/macro? the function body to wherever you need it, adjusting tuple and Prefix to be the local equivalents.
func f<each Prefix>(
_ tuple: (repeat each Prefix, Int)
) -> ((repeat each Prefix), Int) {
entuplePack(tuple)
}
E.g.
public func print(_ output: (repeat each Outputs, P1.Output), into input: inout P0.Input) {
let output: ((repeat each Outputs), P1.Output) = {
var iterator = Mirror(reflecting: output).children.map(\.value).makeIterator()
func next<Cast>() -> Cast { iterator.next() as! Cast }
return (
(repeat { _ in next() } ((each Outputs).self)),
next()
)
} ()
p1.print(output.1, into: &input)
p0.print(output.0, into: &input)
}
I was hoping it would be possible to use collection operations, but it also fails to compile.
func dropLast<each T, U>(_ t: (repeat each T, U)) -> (repeat each T) {
Array(t).dropLast().tuple()
}
extension [Any] {
init<each T>(_ tuple: (repeat each T)) {
var array: [Any] = []
for element in repeat each tuple {
array.append(element)
}
self = array
}
}
extension Sequence {
func tuple<each T>(type: (repeat (each T).Type) = (repeat (each T).self)) -> (repeat each T) /*where repeat each T == Element*/ { // same-type requirement will only be supported in Swift 6.1+
var result: Any = ()
var iterator = makeIterator()
for _ in repeat each type {
let value = iterator.next()!
result = join(result, value)
}
return result as! (repeat each T)
}
}
func join<each T, Suffix>(_ value: (repeat each T), _ suffix: Suffix) -> (repeat each T, Suffix) {
(repeat each value, suffix)
}
The original parameter pack proposal explicitly mentions pack destructuring operations in the future directions section, which is currently not pitched or implemented.
struct List<each Element> {
let element: repeat each Element
}
extension List {
func firstRemoved<First, each Rest>() -> List<repeat each Rest> where (repeat each Element) == (First, repeat each Rest) {
let (first, rest) = (repeat each element)
return List(repeat each rest)
}
}
let list = List(1, "Hello", true)
let firstRemoved = list.firstRemoved() // 'List("Hello", true)'
Thanks for that! Mirrors are quite slow but accessing the runtime metadata directly can be quite a bit faster, so I've found the following adaptation to be a decent workaround for now (bundled in your helper for clarity):
func entuplePack<each Prefix, Last>(
_ tuple: (repeat each Prefix, Last)
) -> ((repeat each Prefix), Last) {
withUnsafeBytes(of: tuple) { ptr in
let metadata = TupleMetadata((repeat each Prefix, Last).self)
var iterator = (0..<metadata.count).makeIterator()
func next<Cast>() -> Cast {
let element = metadata[iterator.next()!]
return ptr.load(fromByteOffset: element.offset, as: Cast)
}
return (
(repeat { _ in next() } ((each Prefix).self)),
next()
)
}
}
private struct TupleMetadata {
let pointer: UnsafeRawPointer
init(_ type: Any.Type) {
pointer = unsafeBitCast(type, to: UnsafeRawPointer.self)
}
var count: Int {
pointer
.advanced(by: pointerSize)
.load(as: Int.self)
}
subscript(position: Int) -> Element {
Element(
pointer:
pointer
.advanced(by: pointerSize)
.advanced(by: pointerSize)
.advanced(by: pointerSize)
.advanced(by: position * 2 * pointerSize)
)
}
struct Element {
let pointer: UnsafeRawPointer
var offset: Int { pointer.load(fromByteOffset: pointerSize, as: Int.self) }
}
}
private let pointerSize = MemoryLayout<UnsafeRawPointer>.size