From 332a2c13ce6908c7feb4778f06451a61f9a9c45f Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Thu, 5 Dec 2024 17:59:19 +0200 Subject: [PATCH 1/3] RFC: Support for Generic Types and Packs in User-Defined Type Functions --- ...on-types-in-user-defined-type-functions.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/support-for-generic-function-types-in-user-defined-type-functions.md diff --git a/docs/support-for-generic-function-types-in-user-defined-type-functions.md b/docs/support-for-generic-function-types-in-user-defined-type-functions.md new file mode 100644 index 00000000..172611ed --- /dev/null +++ b/docs/support-for-generic-function-types-in-user-defined-type-functions.md @@ -0,0 +1,118 @@ +# Support for Generic Types and Packs in User-Defined Type Functions + +## Summary + +Add support for generic function types in user-defined type function to allow more types to be accepted by them, including tables and classes which might contain generic function types. + +## Motivation + +Generic functions are very common in Luau, especially when inferred by the typechecking engine. They are also often present in classes. + +Omitting generic types from appearing in user-defined type functions limits their use. +Even if there is no direct manipulation of generics, an serialization error appears immediately. + +## Design + +We will introduce a single 'generic' kind for both generic types and generic packs in runtime. + +While this doesn't match up the typechecking engine representation directly, we avoid having to deal with two different userdata types. Being opaque about the contents, assigning separate tags is also not that different. + +We already have variadic types that get represented as a regular runtime type in the 'tail' table member/arguments. +We also have a different representation of a table with a metatable. + +Generics are created by specifying a name and whether or not it is a pack. +Generics that are coming from type inference might not have a display name, but still hold an auto-generated name internally. + +Names are the way generic identity is specified. +Generic types are immutable and two generics with the same name are equal, even if they are defined in different function type scopes. + +For example, consider a type `(T, U, (T) -> U) -> ()` + +From the typechecking perspective, inner and outer function 'T' types are different, but they are still represented by the same runtime generic named 'T'. + +The ambiguity is resolved purely on the scoping of the generic in question. +When runtime type is returned back to the typechecking engine, only those generics that are in current set of function type generic lists are allowed to be mentioned. +When inner generic function list places name 'T' again, it represents a different typechecking type and hides outer 'T'. + +So we can say generics are defined by the lexical scope and that actually matches how they are written out in code. + +Just like in the syntax of the language, reusing the same generic name, even between types and packs in a single list in a duplicate generic definition error. + +### Examples + +Creating a function type with generic types and packs: + +```lua +type function pass() + local T = types.generic("T") + local Us, Vs = types.generic("U", true), types.generic("V", true) + + local f = types.newfunction() + f:setparameters({T}, Us); + f:setreturns({T}, Vs); + f:setgenerics({T, Us, Vs}); + return f +end + +type X = pass<> -- (T, U...) -> (T, V...) +``` + +Creating a function type with a nested function type that reuses a generic name: + +```lua +type function pass() + local T, U = types.generic("T"), types.generic("U") + + -- (T) -> () + local func = types.newfunction({ head = {T} }, {}, {T}); + + -- { x: (T) -> (), y: U } + local tbl = types.newtable({ [types.singleton("x")] = func, [types.singleton("y")] = U }) + + -- (T, { x: (T) -> (), y: U }) -> () + return types.newfunction({ head = {T, tbl} }, {}, {T, U}) +end +``` + +## Updates to the library and type userdata + +### `types` Library + +| New/Update | Library Functions | Return Type | Description | +| ------------- | ------------- | ------------- | ------------- | +| New | `generic(name: string, ispack: boolean?)` | `type` | returns an immutable instance of a generic type or a pack; name cannot be empty | +| Update | `newfunction(parameters: { head: {type}?, tail: type? }, returns: { head: {type}?, tail: type? }, generics: {type}?)` | `type` | New `generic` argument is added to specify generic types and packs in a single list | + +### `type` Instance + +| New/Update | Instance Properties | Type | Description | +| ------------- | ------------- | ------------- | ------------- | +| Update | `tag` | `"nil" \| "unknown" \| "never" \| "any" \| "boolean" \| "number" \| "string" \| "singleton" \| "negation" \| "union" \| "intersection" \| "table" \| "function" \| "class" \| "thread" \| "buffer" \| "generic"` | Added `generic` as a possible tag | + +#### Generic `type` instance + +| New/Update | Instance Methods | Type | Description | +| ------------- | ------------- | ------------- | ------------- | +| New | `name()` | `string?` | name of the generic; `nil` for inferred generics | +| New | `ispack()` | `boolean` | `true` if the generic represents a pack, `false` otherwise | + +#### Function `type` instance + +| New/Update | Instance Methods | Return Type | Description | +| ------------- | ------------- | ------------- | ------------- | +| New | `setgenerics({type}?)` | `()` | sets the function's generic types and packs list; types cannot follow a pack | +| New | `generics()` | `{type}` | returns the function's generic types and packs (in that order in the array) | + +## Alternatives + +We can assign a different `tag` to a generic pack instead of the `ispack` property. + +It is also possible to split generic type and packs list into two. +This was done in an initial prototype implementation as was inconvenient to specify. +One benefit was that you didn't have to split them manually when reading them. + +## Drawbacks + +Generics do bring additional complexity to user-defined type functions, especially with the way we can now hold a generic type pack as a separate item (while `type T = U...` impossible to define in the syntax of the language). + +The way they bring in lexical scoping consideration is also new for user-defined type functions. From 264d542d05bb1e610a82a43ade252952ae159f2d Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Thu, 5 Dec 2024 18:03:19 +0200 Subject: [PATCH 2/3] Language --- ...-function-types-in-user-defined-type-functions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/support-for-generic-function-types-in-user-defined-type-functions.md b/docs/support-for-generic-function-types-in-user-defined-type-functions.md index 172611ed..1417f420 100644 --- a/docs/support-for-generic-function-types-in-user-defined-type-functions.md +++ b/docs/support-for-generic-function-types-in-user-defined-type-functions.md @@ -2,14 +2,14 @@ ## Summary -Add support for generic function types in user-defined type function to allow more types to be accepted by them, including tables and classes which might contain generic function types. +Add support for generic function types in user-defined type functions to allow more types to be accepted by them, including tables and classes which might contain generic function types. ## Motivation Generic functions are very common in Luau, especially when inferred by the typechecking engine. They are also often present in classes. Omitting generic types from appearing in user-defined type functions limits their use. -Even if there is no direct manipulation of generics, an serialization error appears immediately. +Even if there is no direct manipulation of generics, a serialization error appears immediately. ## Design @@ -31,12 +31,12 @@ For example, consider a type `(T, U, (T) -> U) -> ()` From the typechecking perspective, inner and outer function 'T' types are different, but they are still represented by the same runtime generic named 'T'. The ambiguity is resolved purely on the scoping of the generic in question. -When runtime type is returned back to the typechecking engine, only those generics that are in current set of function type generic lists are allowed to be mentioned. -When inner generic function list places name 'T' again, it represents a different typechecking type and hides outer 'T'. +When the runtime type is returned back to the typechecking engine, only those generics that are in the current set of function type generic lists are allowed to be mentioned. +When the inner generic function list places the name 'T' again, it represents a different typechecking type and hides the outer 'T'. So we can say generics are defined by the lexical scope and that actually matches how they are written out in code. -Just like in the syntax of the language, reusing the same generic name, even between types and packs in a single list in a duplicate generic definition error. +Just like in the syntax of the language, reusing the same generic name, even between types and packs in a single list results in a duplicate generic definition error. ### Examples @@ -107,7 +107,7 @@ end We can assign a different `tag` to a generic pack instead of the `ispack` property. -It is also possible to split generic type and packs list into two. +It is also possible to split a single list containing both generic types and packs into two. This was done in an initial prototype implementation as was inconvenient to specify. One benefit was that you didn't have to split them manually when reading them. From ac5cc80f9b1fc2c03216af90c4ae853b87a1ec0b Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Thu, 5 Dec 2024 20:19:22 +0200 Subject: [PATCH 3/3] Wording improvements --- ...on-types-in-user-defined-type-functions.md | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/support-for-generic-function-types-in-user-defined-type-functions.md b/docs/support-for-generic-function-types-in-user-defined-type-functions.md index 1417f420..4c4e854f 100644 --- a/docs/support-for-generic-function-types-in-user-defined-type-functions.md +++ b/docs/support-for-generic-function-types-in-user-defined-type-functions.md @@ -2,25 +2,27 @@ ## Summary -Add support for generic function types in user-defined type functions to allow more types to be accepted by them, including tables and classes which might contain generic function types. +Add support for generic function types in type functions to allow more types to be accepted by them, including tables and classes which might contain generic function types. ## Motivation -Generic functions are very common in Luau, especially when inferred by the typechecking engine. They are also often present in classes. +Generic functions are very common in Luau, especially when inferred by the typechecking engine. They are also often present in classes defined by the environment. -Omitting generic types from appearing in user-defined type functions limits their use. -Even if there is no direct manipulation of generics, a serialization error appears immediately. +Omitting generic types from appearing in type functions limits their use. +Today, even if there is no direct manipulation of generics, a serialization error appears immediately. +Ideally, we want use-defined type functions to support any possible type, aside from temporary implementation-detail types of the type solver. ## Design -We will introduce a single 'generic' kind for both generic types and generic packs in runtime. +We will introduce a single 'generic' kind of runtime type for both generic types and generic packs. -While this doesn't match up the typechecking engine representation directly, we avoid having to deal with two different userdata types. Being opaque about the contents, assigning separate tags is also not that different. +While this doesn't match up the typechecking engine representation directly, we avoid having to deal with two different userdata types. +Being content of a generic type or pack is not known upfront, assigning separate tags is not that different from having an extra property on it. -We already have variadic types that get represented as a regular runtime type in the 'tail' table member/arguments. -We also have a different representation of a table with a metatable. +We already have variadic types (`...number`) that get represented as a regular runtime type and only gain that special meaning when placed in a 'tail' spot. +We also have a different representation of a table with a metatable between type functions and typechecking. -Generics are created by specifying a name and whether or not it is a pack. +Generics are created by specifying a name and a boolean marking it as a type or a pack. Generics that are coming from type inference might not have a display name, but still hold an auto-generated name internally. Names are the way generic identity is specified. @@ -30,11 +32,11 @@ For example, consider a type `(T, U, (T) -> U) -> ()` From the typechecking perspective, inner and outer function 'T' types are different, but they are still represented by the same runtime generic named 'T'. -The ambiguity is resolved purely on the scoping of the generic in question. +The ambiguity is resolved based on the scoping of the generic in question. When the runtime type is returned back to the typechecking engine, only those generics that are in the current set of function type generic lists are allowed to be mentioned. -When the inner generic function list places the name 'T' again, it represents a different typechecking type and hides the outer 'T'. +When the inner generic function list places the name 'T' again, it represents a different typechecking type and hides the outer 'T' until the scope of the inner type ends. -So we can say generics are defined by the lexical scope and that actually matches how they are written out in code. +So we can say generics are defined by the lexical scope, which matches how they are written out in source code. Just like in the syntax of the language, reusing the same generic name, even between types and packs in a single list results in a duplicate generic definition error. @@ -108,11 +110,11 @@ end We can assign a different `tag` to a generic pack instead of the `ispack` property. It is also possible to split a single list containing both generic types and packs into two. -This was done in an initial prototype implementation as was inconvenient to specify. -One benefit was that you didn't have to split them manually when reading them. +This was done in an initial prototype implementation and was inconvenient to specify. +One benefit was that list didn't have to be split manually when reading types or packs individually. ## Drawbacks -Generics do bring additional complexity to user-defined type functions, especially with the way we can now hold a generic type pack as a separate item (while `type T = U...` impossible to define in the syntax of the language). +Generics do bring additional complexity to type functions, especially with the way we can now hold a generic type pack as a separate item (while `type T = U...` impossible to define in the syntax of the language). -The way they bring in lexical scoping consideration is also new for user-defined type functions. +The way they bring in lexical scoping consideration is also new for type function runtime.