From 130d22ca10cc77b410afdeeebf1a17ea45f1a254 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Wed, 6 Oct 2021 21:58:18 +0300 Subject: [PATCH 1/4] RFC: Type alias type packs --- rfcs/syntax-type-alias-type-packs.md | 162 +++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 rfcs/syntax-type-alias-type-packs.md diff --git a/rfcs/syntax-type-alias-type-packs.md b/rfcs/syntax-type-alias-type-packs.md new file mode 100644 index 000000000..b9b046386 --- /dev/null +++ b/rfcs/syntax-type-alias-type-packs.md @@ -0,0 +1,162 @@ +# Type alias type packs + +## Summary + +Provide semantics for referencing type packs inside the body of a type alias declaration + +## Motivation + +We now have an ability to declare a placeholder for a type pack in type alias declaration, but there is no support to reference this pack inside the body of the alias: +```lua +type X = () -> A... -- cannot reference A... as the return value pack + +type Y = X -- invalid number of arguments +``` + +Additionally, while a simple introduction of these generic type packs into the scope will provide an ability to reference them in function declarations, we want to be able to use them to instantiate other type aliases as well. + +Declaration syntax also supports multiple type packs, but we don't have defined semantics on instantiation of such alias. + +## Design + +We currently support type packs at these locations: +```lua +-- for variadic function parameter when type pack is generic +local function f(...: a...) + +-- for multiple return values +local function f(): a... + +-- as the tail item of function return value pack +local function f(): (number, a...) +``` + +We want to be able to use type packs for type alias instantiation: +```lua +type X = -- + +type A = X -- T... = (S...) +``` + +Similar to function calls, we want to be able to assign multiple regular types to a single type pack: +```lua +type A = X -- T... = (). Note: check 'Alternatives' +type B = X -- T... = (number) +type C = X -- T... = (number, string) +``` + +Variadic types can also be assigned to type alias type pack: +```lua +type D = X<...number> -- T... = (...number) +type E = X -- T... = (number, ...string) +``` + +Multiple regular types can be assigned together with a type pack argument in a tail position: +```lua +type F = X -- T... = (number, S...) +type G = X -- error, type arguments can't follow type pack arguments +``` + +### Multiple type pack parameters + +We have to keep in mind that it is also possible to declare a type alias that takes multiple type pack parameters. + +And since we only allow type pack arguments after regular type arguments, trailing regular types with a type pack that follows are combined into a single type pack argument: +```lua +type Y = -- + +type A = Y -- T... = (S...), U... = (S...) +type B = Y -- T... = (number, ...string), U... = S... +type C = Y -- error, T... = (number, string, S...), but U... = undefined +type D = Y<...number> -- error, T = (...number), but U... = undefined, not (...number) even though one infinite set is enough to fill two, we may have '...number' inside a type pack argument and we'll be unable to see its content + +type Z = -- + +type E = Z -- T = number, U... = (S...) +type F = Z -- T = number, U... = (string, S...) +type G = Z -- error, not enough regular type arguments, can't split the front of S... into T + +type W = -- + +type H = W -- U... = S..., V... = R... +type I = W -- U... = (string, S...), V... = R... +``` + +## Drawbacks + +### Type pack element extraction + +Because our type alias instantiations are not lazy, it's impossible to split of a single type from a type pack: +```lua +type Car = T + +type X = Car -- number +type Y = Car -- error, not enough regular type arguments +type Z = Y -- error, Y doesn't have a valid definition +``` + +Splitting off a single type is is a common pattern with variadic templates in C++, but we don't allow type alias overloads, so use cases are more limited. + +### Type alias can't result in a type pack + +We don't propose type aliases to generate type packs, which could have looked as: +```lua +type Car = T +type Cdr = U... +type Cons = (T, U...) + +--[[ + using type functions to operate on type packs as a list of types +]] +``` + +We wouldn't be able to differentiate if an instantiation results in a type or a type pack and our type system only allows variadic types as the type pack tail element. + +Support for variadic types in the middle of a type pack can be found in TypeScript's tuples. + +### Explicit type pack syntax + +To enable additional control for the content of a type pack, we would have liked to introduce an explicit type pack syntax. + +Similar to variadic types `...a` and generic type packs `T...`, explicit type packs can only be used at type pack positions: +```lua +type Y = (T...) -> (U...) + +type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean) +type F2 = Y<(), ()> -- T... = (), U... = () +type F3 = Y -- T... = (string, S...), U... = (number, S...) +``` + +This explicit type pack syntax could've been used in other syntax positions where type packs are allowed: +```lua +local function f(...: (number, ...string)) end +local function g(...: (number, (string, number)) end +local function h(): (number, (string, ...boolean)) return 1, 's', true, false end +``` + +Unfortunately, our syntax supports placing parenthesis around the type: +```lua +local a: number +local b: (number) +``` + +This means that the syntax around type packs is ambiguous: +```lua +type Y = (T...) -> (U...) + +type A = Y<(number), (number)> -- will resolve as Y + +local function f(...: (number)) end -- '...' is a '...number', not a type pack with a single element +``` + +## Alternatives + +The syntax we use right now to instantiate type alias with a single type pack parameter to an empty type pack is to not specify the argument list at all: +```lua +type X = -- + +type A = X -- ok +type B = X<> -- error +``` + +This is the current parser behavior and while we could introduce `X<>` as an alternative, without a warning/deprecation/epoch system we might not be able to disallow the `X` syntax. From 7237cd6e984b207326471be3658edd44b63d3374 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Wed, 13 Oct 2021 16:54:50 +0300 Subject: [PATCH 2/4] Add support for explicit type pack syntax in type parameter list Also, `type A = X` is no longer allowed when X expects a type pack. Instead, support for `X<>` syntax is proposed. Alternatives section is reworked to talk about explicit type pack syntax and no longer mentions the `type A = X<>` question. --- rfcs/syntax-type-alias-type-packs.md | 96 ++++++++++++++++------------ 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/rfcs/syntax-type-alias-type-packs.md b/rfcs/syntax-type-alias-type-packs.md index b9b046386..563d2925a 100644 --- a/rfcs/syntax-type-alias-type-packs.md +++ b/rfcs/syntax-type-alias-type-packs.md @@ -15,7 +15,7 @@ type Y = X -- invalid number of arguments Additionally, while a simple introduction of these generic type packs into the scope will provide an ability to reference them in function declarations, we want to be able to use them to instantiate other type aliases as well. -Declaration syntax also supports multiple type packs, but we don't have defined semantics on instantiation of such alias. +Declaration syntax also supports multiple type packs, but we don't have defined semantics on instantiation of such type alias. ## Design @@ -38,13 +38,15 @@ type X = -- type A = X -- T... = (S...) ``` -Similar to function calls, we want to be able to assign multiple regular types to a single type pack: +Similar to function calls, we want to be able to assign zero or more regular types to a single type pack: ```lua -type A = X -- T... = (). Note: check 'Alternatives' +type A = X<> -- T... = () type B = X -- T... = (number) type C = X -- T... = (number, string) ``` +Definition of `A` doesn't parse right now, we would like to make it legal going forward. + Variadic types can also be assigned to type alias type pack: ```lua type D = X<...number> -- T... = (...number) @@ -54,14 +56,20 @@ type E = X -- T... = (number, ...string) Multiple regular types can be assigned together with a type pack argument in a tail position: ```lua type F = X -- T... = (number, S...) -type G = X -- error, type arguments can't follow type pack arguments +type G = X -- T... = (number, string, S...) +``` + +Regular type parameters cannot follow type pack parameters: +```lua +type H = X -- error, type parameters can't follow type pack parameters ``` ### Multiple type pack parameters We have to keep in mind that it is also possible to declare a type alias that takes multiple type pack parameters. -And since we only allow type pack arguments after regular type arguments, trailing regular types with a type pack that follows are combined into a single type pack argument: +Similar to the previous examples, type parameters that haven't been matched with type arguments are combined together with the next type pack (if present) into the first type pack. +Type pack parameters after the first one have to be type packs: ```lua type Y = -- @@ -82,6 +90,32 @@ type H = W -- U... = S..., V... = R... type I = W -- U... = (string, S...), V... = R... ``` +### Explicit type pack syntax + +To enable additional control for the content of a type pack, especially in cases where multiple type pack parameters are expected, we introduce an explicit type pack syntax for use in type alias instantiation. + +Similar to variadic types `...a` and generic type packs `T...`, explicit type packs can only be used at type pack positions: +```lua +type Y = (T...) -> (U...) + +type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean) +type F2 = Y<(), ()> -- T... = (), U... = () +type F3 = Y -- T... = (string, S...), U... = (number, S...) +``` + +In type parameter list, types inside the parentheses always produce a type pack. +This is in contrast to function return type pack annotation, where `() -> number` is the same as `() -> (number)`. + +This is a breaking change. + +Users can already have type alias instantiations like these: +```lua +type X = T? +type A = X<(number)> -- valid right now, typechecking error after this RFC +``` + +Explicit type pack syntax is not available in other type pack annotation contexts. + ## Drawbacks ### Type pack element extraction @@ -114,49 +148,31 @@ We wouldn't be able to differentiate if an instantiation results in a type or a Support for variadic types in the middle of a type pack can be found in TypeScript's tuples. -### Explicit type pack syntax - -To enable additional control for the content of a type pack, we would have liked to introduce an explicit type pack syntax. - -Similar to variadic types `...a` and generic type packs `T...`, explicit type packs can only be used at type pack positions: -```lua -type Y = (T...) -> (U...) - -type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean) -type F2 = Y<(), ()> -- T... = (), U... = () -type F3 = Y -- T... = (string, S...), U... = (number, S...) -``` +## Alternatives -This explicit type pack syntax could've been used in other syntax positions where type packs are allowed: -```lua -local function f(...: (number, ...string)) end -local function g(...: (number, (string, number)) end -local function h(): (number, (string, ...boolean)) return 1, 's', true, false end -``` +### Backwards compatibility for single type in parentheses -Unfortunately, our syntax supports placing parenthesis around the type: +It is possible to allow single element type pack parameter assignment to a type argument: ```lua -local a: number -local b: (number) +type X = T? +type A = X<(number)> ``` -This means that the syntax around type packs is ambiguous: -```lua -type Y = (T...) -> (U...) +This is not proposed to keep separation between type and type packs more clear. +If we supported warning generation, we could create a deprecation period, but since our typechecking errors don't block compilation, it is not that critical. -type A = Y<(number), (number)> -- will resolve as Y +### Function return type syntax for explicit type packs -local function f(...: (number)) end -- '...' is a '...number', not a type pack with a single element -``` +Another option that was considered is to parse `(T)` as `T`, like we do for return type annotation. -## Alternatives - -The syntax we use right now to instantiate type alias with a single type pack parameter to an empty type pack is to not specify the argument list at all: +This option complicates the match ruleset since the typechecker will never know if the user has written `T` or `(T)` so each regular type could be a single element type pack and vice versa. ```lua -type X = -- +type X +type C = X -- T... = (number, number) +type D = X<(number), (number)> -- T... = (number, number) -type A = X -- ok -type B = X<> -- error -``` +type Y -This is the current parser behavior and while we could introduce `X<>` as an alternative, without a warning/deprecation/epoch system we might not be able to disallow the `X` syntax. +--- two items that were enough to satisfy only a single T... in X are enough to satisfy two T..., U... in Y +type E = Y -- T... = (number), U... = (number) +``` From ed70fd019a1593054f31b938327ba1ba7dc40037 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Thu, 14 Oct 2021 18:12:23 +0300 Subject: [PATCH 3/4] Apply the alternative that allows single element type pack parameter assignment to a type argument Added an additional note on type pack element extraction. --- rfcs/syntax-type-alias-type-packs.md | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/rfcs/syntax-type-alias-type-packs.md b/rfcs/syntax-type-alias-type-packs.md index 563d2925a..188d97497 100644 --- a/rfcs/syntax-type-alias-type-packs.md +++ b/rfcs/syntax-type-alias-type-packs.md @@ -106,12 +106,13 @@ type F3 = Y -- T... = (string, S...), U... = In type parameter list, types inside the parentheses always produce a type pack. This is in contrast to function return type pack annotation, where `() -> number` is the same as `() -> (number)`. -This is a breaking change. - -Users can already have type alias instantiations like these: +However, to preserve backwards-compatibility with optional parenthesis around regular types, type alias instantiation is allowed to assign a non-variadic type pack parameter with a single element to a type argument: ```lua -type X = T? -type A = X<(number)> -- valid right now, typechecking error after this RFC +type X = (T) -> U? +type A = X<(number), (string)> -- T = number, U = string + +type Y = (T...) -> () +type B = Y<(number), (string)> -- error: too many type pack parameters ``` Explicit type pack syntax is not available in other type pack annotation contexts. @@ -129,6 +130,8 @@ type Y = Car -- error, not enough regular type argument type Z = Y -- error, Y doesn't have a valid definition ``` +With our immediate instantiation, at the point of `Car`, we only know that `S...` is a type pack, but contents are not known. + Splitting off a single type is is a common pattern with variadic templates in C++, but we don't allow type alias overloads, so use cases are more limited. ### Type alias can't result in a type pack @@ -150,17 +153,6 @@ Support for variadic types in the middle of a type pack can be found in TypeScri ## Alternatives -### Backwards compatibility for single type in parentheses - -It is possible to allow single element type pack parameter assignment to a type argument: -```lua -type X = T? -type A = X<(number)> -``` - -This is not proposed to keep separation between type and type packs more clear. -If we supported warning generation, we could create a deprecation period, but since our typechecking errors don't block compilation, it is not that critical. - ### Function return type syntax for explicit type packs Another option that was considered is to parse `(T)` as `T`, like we do for return type annotation. @@ -176,3 +168,17 @@ type Y --- two items that were enough to satisfy only a single T... in X are enough to satisfy two T..., U... in Y type E = Y -- T... = (number), U... = (number) ``` + +### Special mark for single type type packs + +In the Rust language, there is a special disambiguation syntax for single element tuples and single element type packs using a trailing comma: +```rust +(Type,) +``` + +In Python, the same idea is used for single element tuple values: +```python +value = (1, ) +``` + +Since our current ruleset no longer has a problem with single element type tuples, I don't think we need syntax-directed disambiguation option like this one. From 802503879f5486b9004065e1b640ee735185c457 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Thu, 21 Oct 2021 19:46:24 +0300 Subject: [PATCH 4/4] Do not combine a type pack together with regular types into the first type pack argument --- rfcs/syntax-type-alias-type-packs.md | 80 +++++++++++++++++++--------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/rfcs/syntax-type-alias-type-packs.md b/rfcs/syntax-type-alias-type-packs.md index 188d97497..aa0882990 100644 --- a/rfcs/syntax-type-alias-type-packs.md +++ b/rfcs/syntax-type-alias-type-packs.md @@ -49,45 +49,34 @@ Definition of `A` doesn't parse right now, we would like to make it legal going Variadic types can also be assigned to type alias type pack: ```lua -type D = X<...number> -- T... = (...number) -type E = X -- T... = (number, ...string) -``` - -Multiple regular types can be assigned together with a type pack argument in a tail position: -```lua -type F = X -- T... = (number, S...) -type G = X -- T... = (number, string, S...) -``` - -Regular type parameters cannot follow type pack parameters: -```lua -type H = X -- error, type parameters can't follow type pack parameters +type D = X<...number> -- T... = (...number) ``` ### Multiple type pack parameters We have to keep in mind that it is also possible to declare a type alias that takes multiple type pack parameters. -Similar to the previous examples, type parameters that haven't been matched with type arguments are combined together with the next type pack (if present) into the first type pack. +Again, type parameters that haven't been matched with type arguments are combined together into the first type pack. +After the first type pack parameter was assigned, following type parameters are not allowed. Type pack parameters after the first one have to be type packs: ```lua type Y = -- -type A = Y -- T... = (S...), U... = (S...) -type B = Y -- T... = (number, ...string), U... = S... -type C = Y -- error, T... = (number, string, S...), but U... = undefined +type A = Y -- T... = S..., U... = S... +type B = Y<...string, S...> -- T... = (...string), U... = S... +type C = Y -- T... = (number, string), U... = S... type D = Y<...number> -- error, T = (...number), but U... = undefined, not (...number) even though one infinite set is enough to fill two, we may have '...number' inside a type pack argument and we'll be unable to see its content +type E = Y -- error, type parameters are not allowed after a type pack type Z = -- -type E = Z -- T = number, U... = (S...) -type F = Z -- T = number, U... = (string, S...) +type F = Z -- T = number, U... = S... type G = Z -- error, not enough regular type arguments, can't split the front of S... into T type W = -- -type H = W -- U... = S..., V... = R... -type I = W -- U... = (string, S...), V... = R... +type H = W -- U... = S..., V... = R... +type I = W -- U... = (string), V... = S... ``` ### Explicit type pack syntax @@ -98,9 +87,9 @@ Similar to variadic types `...a` and generic type packs `T...`, explicit type pa ```lua type Y = (T...) -> (U...) -type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean) -type F2 = Y<(), ()> -- T... = (), U... = () -type F3 = Y -- T... = (string, S...), U... = (number, S...) +type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean) +type F2 = Y<(), ()> -- T... = (), U... = () +type F3 = Y -- T... = (string, number), U... = (number, S...) ``` In type parameter list, types inside the parentheses always produce a type pack. @@ -110,6 +99,7 @@ However, to preserve backwards-compatibility with optional parenthesis around re ```lua type X = (T) -> U? type A = X<(number), (string)> -- T = number, U = string +type A = X<(number), string> -- same type Y = (T...) -> () type B = Y<(number), (string)> -- error: too many type pack parameters @@ -182,3 +172,45 @@ value = (1, ) ``` Since our current ruleset no longer has a problem with single element type tuples, I don't think we need syntax-directed disambiguation option like this one. + +### Only type pack arguments for type pack parameters + +One option that we have is to remove implicit pack assignment from a set of types and always require new explicit type pack syntax: + +```lua +type X = -- + +type B = X<> -- invalid +type C = X -- invalid +type D = X -- invalid + +type B = X<()> -- T... = () +type C = X<(number)> -- T... = (number) +type D = X<(number, string)> -- T... = (number, string) +``` + +But this doesn't allow users to define type aliases where they only care about a few types and use the rest as a 'tail': + +```lua +type X = (T, U, Rest...) -> Rest... + +type A = X -- forced to use a type pack when there are no tail elements +``` + +It also makes it harder to change the type parameter count without fixing up the instantiations. + +### Combining types together with the following type pack into a single argument + +Earlier version of the proposal allowed types to be combined together with a type pack as a tail: +```lua +type X = -- + +type A = X --- T... = (number, S...) +``` + +But this syntax resulted in some confusing behavior when multiple type pack arguments are expected: +```lua +type Y = -- + +type B = Y -- not enough type pack parameters +```