Skip to content

Initial Semantics for Variadic Generics #40587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 20, 2021
Merged

Conversation

CodaFi
Copy link
Contributor

@CodaFi CodaFi commented Dec 16, 2021

Implements a variation of the semantics described in the initial pitch.

This patchset implements

  • Variadic type argument binding with structural saturation rules
  • AST for parameter packs, pack expansions, and their corresponding types
  • Type inference rules for variadic generic applies
  • Substitution rules for polyvariadic types with eager pack expansions
  • A pack-expansion-to-tuple conversion

Notably missing:

  • Syntax for null-bound variadic generic types e.g. Foo<> for Foo<T...>
  • Semantics for null-bound variadic generic functions func foo<T...>(_ xs: T...); foo()
  • Pack arity propagation along type equality constraints

The heart of this patchset is an inference scheme for variadic generic functions and associated pack expansions.

A traditional variadic function looks like

func foo<T>(_ xs: T...) {}

Along with the corresponding function type

<T>(T [variadic]) -> Void

which the constraint system only has to resolve one type for. Hence it opens <T> as a type variable and uses each argument to the function to try to solve for <T>. This approach cannot work for variadic generics as each argument would try to bind to the same <T> and the constraint system would lose coherency in the one case we need it: when the arguments all have different types.

Instead, notice when we encounter expansion types:

func print<T...>(_ xs: T...)
print("Macs say Hello in", 42, "different languages")

We open this type as

print : ($t0...) -> ($t0...)

Now for the brand new stuff: We need to create and bind a pack to $t0, which will trigger the expansion we need for CSApply to see a coherent view of the world. This means we need to examine the argument list and construct the pack type <$t1, $t2, $t3, ...> - one type variable per argument - and bind it to $t0. There's also the catch that each argument that references the opened type $t0 needs to have the same parallel structure, including its arity. The algorithm is thus:

For input type F : (... ($t0...), ..., ($tn..) ...) -> R and an apply site F(a0, ..., an) we walk the parameters of F and record an entry in a mapping for each opened variadic generic parameter. Now, for each argument ai in (a0, ..., an), we create a fresh type variable corresponding to the argument ai, and record an additional entry in the parameter pack type elements corresponding to that argument.

Concretely, suppose we have

func print2<T..., U...>(first: T..., second: U...) {}
print2(first: "", 42, (), second: [42])

We open print2 as

print2 : ($t0..., $t1...) -> Void

And construct a mapping

  $t0 => <$t2, $t3, $t4> // eventually <String, Int, Void>
  $t1 => <$t5> // eventually [Int]

We then yield the entries of this map back to the solver, which constructs bind constraints where each => occurs in the diagram above. The pack type thus is immediately substituted into the corresponding pack expansion type which produces a fully-expanded pack type of the correct arity that the solver can actually use to make forward progress.

Applying the solution is as simple as pulling out the pack type and coercing arguments element-wise into a pack expression.

You would think this would appear in the literature somewhere, but lo and behold I'm pretty sure nobody's thought to do this yet.

A pack type looks a lot like a tuple in the surface language, except there
is no way for the user to spell a pack. Pack types are created by the solver
when it encounters an apply of a variadic generic function, as in

```
func print<T...>(_ xs: T...) {}
// Creates a pack type <String, Int, String>
print("Macs say Hello in", 42, " different languages")
```

Pack types substituted into the variadic generic arguments of a
PackExpansionType "trip" the pack expansion and cause it to produce a
new pack type with the pack expansion pattern applied.

```
typealias Foo<T...> = (T?...)
Foo<Int, String, Int> // Forces expansion to (Int?, String?, Int?)
```
A PackExpansionType is the interface type of the explicit expansion of a
corresponding set of variadic generic parameters.
Pack expansions are spelled as single-element tuples with a single variadic
component in most contexts except functions where they are allowed to appear without parentheses to match normal variadic declaration syntax.

```
func expand<T...>(_ xs: T...) -> (T...)
                        ~~~~     ~~~~~~
```

A pack expansion type comes equipped with a pattern type spelled before
the ellipses - `T` in the examples above. This pattern type is the subject
of the expansion of the pack that is tripped when its variadic generic
parameter is substituted for a `PackType`.
Pack expressions take a series of argument values and bundle them together as a pack - much like how a tuple expression bundles argument expressions into a tuple.

Pack reification represents the operation that converts packs to tuples/scalar types in the AST. This is important since we want pack types in return positions to resolve to tuples contextually.
Behaves much like isTypeParameter but specifically checks for the type sequence bits. Also, add the type sequence bits as a recursive type property.
Also begin resolving (T...) as a pack expansion.
The algorithm is detailed below:

Suppose we encounter a type T<..., U, V..., W, ...> for V... the _sole_ variadic generic parameter.

When we encounter an argument list <A, B, C, D, E, F, ...> to T, we must work in three steps

- Bind all generic parameters up to U in parallel with the argument list until we encounter V...
- Measure the tail of parameters after V... and call it `t`. Assuming `m` type variables hav been bound already, we must bind `n - t - m` type arguments to V...
- Finally, bind the remaining `t` arguments.

This procedure is often called "Saturation" in the literature. Variadic generics have a special saturation property where unlike normal generic parameters it is possible to bind zero arguments to them. In such a case, the result is an empty pack type, which is semantically well-formed.
The heart of this patchset: An inference scheme for variadic generic functions and associated pack expansions.

A traditional variadic function looks like

func foo<T>(_ xs: T...) {}

Along with the corresponding function type

<T>(T [variadic]) -> Void

which the constraint system only has to resolve one two for. Hence it opens <T> as a type variable and uses each argument to the function to try to solve for <T>. This approach cannot work for variadic generics as each argument would try to bind to the same <T> and the constraint system would lose coherency in the one case we need it: when the arguments all have different types.

Instead, notice when we encounter expansion types:

func print<T...>(_ xs: T...)
print("Macs say Hello in", 42, "different languages")

We open this type as

print : ($t0...) -> ($t0...)

Now for the brand new stuff: We need to create and bind a pack to $t0, which will trigger the expansion we need for CSApply to see a coherent view of the world. This means we need to examine the argument list and construct the pack type <$t1, $t2, $t3, ...> - one type variable per argument - and bind it to $t0. There's also the catch that each argument that references the opened type $t0 needs to have the same parallel structure, including its arity. The algorithm is thus:

For input type F<... ($t0...), ..., ($tn..) ...> and an apply site F(a0, ..., an) we walk the type `F` and record an entry in a mapping for each opened variadic generic parameter. Now, for each argument ai in (a0, ..., an), we create a fresh type variable corresponding to the argument ai, and record an additional entry in the parameter pack type elements corresponding to that argument.

Concretely, suppose we have

func print2<T..., U...>(first: T..., second: U...) {}
print2(first: "", 42, (), second: [42])

We open print2 as

print2 : ($t0..., $t1...) -> Void

And construct a mapping

  $t0 => <$t2, $t3, $t4> // eventually <String, Int, Void>
  $t1 => <$t5> // eventually [Int]

We then yield the entries of this map back to the solver, which constructs bind constraints where each => exists in the diagram above. The pack type thus is immediately substituted into the corresponding pack expansion type which produces a fully-expanded pack type of the correct arity that the solver can actually use to make forward progress.

Applying the solution is as simple as pulling out the pack type and coercing arguments element-wise into a pack expression.
@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 16, 2021

@swift-ci test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 72d5678ac041e4437b3deff727042e7b4e2f2917

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 72d5678ac041e4437b3deff727042e7b4e2f2917

Insert an implicit conversion from pack types to tuples with equivalent parallel structure. That means

1) The tuple must have the same arity
2) The tuple may not have any argument labels
3) The tuple may not have any variadic or inout components
4) The tuple must have the same element types as the pack
@CodaFi CodaFi force-pushed the substitute-teacher branch from 72d5678 to d44d8ec Compare December 16, 2021 16:52
@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 16, 2021

@swift-ci test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - d44d8ec

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 16, 2021

@swift-ci test macOS platform

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - d44d8ec

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 17, 2021

@swift-ci smoke test macOS

@CodaFi CodaFi merged commit e5bfda7 into swiftlang:main Dec 20, 2021
@CodaFi CodaFi deleted the substitute-teacher branch December 20, 2021 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants