`var`-backed parameters passed in with `@lValue`, causing inefficient parameter packing

I go into this extensively in Issue #85924, but TL;DR...

These two statements type-check differently:

func merge<each A, B>(_ a: (repeat each A), _ b: B) -> (repeat each A, B) {
    return (repeat each a, b)
}

// The `var` tuple in `a` is not flattened and remains intact in the output tuple:
let mergedVar/*: ((String, Int), String) */  = {
    var a = ("a", 1) // @lvalue (String, Int)
    let b = "c" // String
    let c = merge(a, b) /*: ((String, Int), String) <- */
    return c 
}()

// The `let` value of `a` is flattened into a single-level tuple:
let mergedLet /*: (String, Int, String) */ = { 
    let a = ("a", 1) // (String, Int)
    let b = "c" // String
    let c = merge(a, b) /*: (String, Int, String) <- */
    return c
}()

It seems like the culprit is passingvar a into merge causes parameter a to be of type @lvalue (String, Int), resulting in a nested packing. let a is @lValue-free and packs as expected (or as I'd expect?).

I found this randomly investigating #85837 where ResultBuilders were not optimally packing parameters, with a fix option here.

Questions

What parameter pack shapes are valid

  • What are the actual intended shape options for parameter packs?

  • Given that my first experience with parameter packs is also coming across this bug, I'm not sure.

Based on the proposal I can't figure out if these are equally valid:

let flat: (String, Int, String) = merge(("A", 2), "C")
let nested: ((String, Int), String) = merge(("A", 2), "C")

Mainly is the var a case's outcome a vaild result to begin with, or is it a bug that it results/packs like this?

What does lValue do here?

What purpose does @lValue serve in the type system here. Is the intent to signal that the var value could be mutable memory in some way, even if it's not something you could mutate within a function call?

If it's being considered within parameter packing, is it being considered correctly in this case?

Why @lValue?

What is the reasoning behind having the @lValue designation on these parameters?

Does this have something to do with supporting inout parameters? As in, if var a is passed in as inout, the compiler would avoid changing the type/shape of a?

If it is about inout and not generically about var parameters, is this a general bug that just hasn't been an issue in the past?

edit: On searching again I found this, which seems to indicate this might be indended.

Bonus - ABI/Reverse Compatability

If a fix is made for #85837 or #85924 , are there ABI or runtime implications?

As in are callees of a generic function using parameter packs impacted if substantive changes to the result of calls to func merge<each A, B> occur, or is this a safe type of fix to make overall?

2 Likes

(Sorry if this is wordy I might edit it down later)

No, this behavior is definitely not intended. Changing a let to a var should not change type inference behavior if the binding is not mutated or passed inout. @lvalue types are an implementation detail in the type checker, and it looks like a mistake in the parameter pack implementation prevents them from being unwrapped correctly here.

2 Likes

Oh yeah I just meant the @lValue-ness being present at all when the value is passed in, not specifically the way parameter packs react to that.

It’s good to hear I’m not crazy here though :slight_smile:

To clarify, while you'll see @lvalue show up in -dump-ast and -debug-constraints output, it should never surface in diagnostics, or in terms of user-visible behavior differences when a type is wrapped in an lvalue or not.

1 Like

Nice, got it. I guess out of curiosity where is a good place to look into this? The type system is kind of dense and idk if GenPack or CSApply or just ConstraintSystem is where to look.

Also thanks for getting back to me so fast! I stumbled upon this issue/fix yesterday afternoon and was kinda psyched about it.

GenPack.cpp is in lib/IRGen/ which runs after type checking. The problem is in lib/Sema/. I would start by looking at -debug-constraints output with the good and bad example and seeing what the differences are.

I think both are valid, the difference will be whether you get a one-element pack whose single element is a tuple type, or if the tuple is exploded into a pack via swift-evolution/proposals/0399-tuple-of-value-pack-expansion.md at main Β· swiftlang/swift-evolution Β· GitHub. Perhaps the problem is specifically in the implementation of the latter feature.

Yeah so it seems like the big point of divergence is this simplification step:

You can see the (_: @lvalue (String, Int)) does not get exploded into Pack{String, Int}, where (String, Int) does.

var Version:

          (considering: (_: @lvalue (String, Int)) arg conv (_: $T9) @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0]
            (simplification result:
              (added constraint: Pack{@lvalue (String, Int)} same-shape $T7 @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0 β†’ tuple type '(_: @lvalue (String, Int))' β†’ tuple type '(/* shape: $T7 */ repeat $T7)' β†’ tuple element #0 β†’ pack shape])
              (added constraint: @lvalue (String, Int) conv $T13 @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0 β†’ tuple type '(_: @lvalue (String, Int))' β†’ tuple type '(/* shape: $T7 */ repeat $T7)' β†’ tuple element #0 β†’ pack type 'Pack{@lvalue (String, Int)}' β†’ pack type 'Pack{$T13}' β†’ pack element #0])
              (removed constraint: (_: @lvalue (String, Int)) arg conv (_: $T9) @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0])
            )

let Version:

          (considering: (String, Int) arg conv (_: $T9) @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0]
            (simplification result:
              (added constraint: Pack{String, Int} same-shape $T7 @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0 β†’ tuple type '(String, Int)' β†’ tuple type '(/* shape: $T7 */ repeat $T7)' β†’ tuple element #0 β†’ pack shape])
              (added constraint: String conv $T13 @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0 β†’ tuple type '(String, Int)' β†’ tuple type '(/* shape: $T7 */ repeat $T7)' β†’ tuple element #0 β†’ pack type 'Pack{String, Int}' β†’ pack type 'Pack{$T13, $T14}' β†’ pack element #0])
              (added constraint: Int conv $T14 @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0 β†’ tuple type '(String, Int)' β†’ tuple type '(/* shape: $T7 */ repeat $T7)' β†’ tuple element #0 β†’ pack type 'Pack{String, Int}' β†’ pack type 'Pack{$T13, $T14}' β†’ pack element #1])
              (removed constraint: (String, Int) arg conv (_: $T9) @ [Call@VarBug_Repro.swift:10:13 β†’ apply argument β†’ comparing call argument #0 to parameter #0])
            )

I'll proably put this down for a bit and get back to it later, but thanks!

Sorry if this is noise, but could this be related to the issue I’m trying to fix here? Fix issue when creating a `some` type from a type with a parameter pack by GeorgeLyon Β· Pull Request #85894 Β· swiftlang/swift Β· GitHub

Seems like parameter packs are being expanded improperly causing some types to register as wrongly shaped.

Yeah I'm not sure these would be related, but maybe they're in similar parts of the code. Applying your fix didn't resolve my issue at least...

Ookkkk I figured it out :sweat_smile:

(_: @lvalue (String, Int)) is very, very subtly actually ((String, Int)) if you don't notice/clock the (_: part as being especially significant.

It turns out there's paramater packing logic that breaks on lValues and adds additional wrapping: