mirror of
https://github.com/luau-lang/rfcs.git
synced 2025-05-04 10:43:48 +01:00
some revisions + typelib->type
This commit is contained in:
parent
21c55072c3
commit
f701e882d7
1 changed files with 54 additions and 56 deletions
|
@ -25,7 +25,7 @@ end
|
|||
For instance, the `rawget` type function can be written as:
|
||||
```luau
|
||||
type function rawget(tbl, prop)
|
||||
if not typelib.type(tbl) == "table" then
|
||||
if not type.type(tbl) == "table" then
|
||||
error("First argument is not a table!") -- fails to reduce
|
||||
end
|
||||
|
||||
|
@ -42,72 +42,70 @@ type ty = rawget<Person, "name"> -- ty = string
|
|||
|
||||
Type functions operate on two stages: type analysis and runtime. When calling type functions at the type level (e.g. annotating a variable as a type function), angle brackets must be used, but when calling them at the runtime level (e.g. calling other type functions within type functions), parenthesis must be used. Declarations of type functions use parenthesis because it defines the runtime operations on the runtime representation of types.
|
||||
|
||||
For the first iteration, type functions will support a subset of the Luau language, mainly the constructs that do not allow type functions to run infinitely. Infinitely running type functions are problematic because they can halt the analysis from making further progress. For example, reducing this type function will halt analysis until a stack overflow occurs in the VM:
|
||||
For the first iteration, the body of a type function will be sandboxed, and its scope will be limited, meaning it will be unable to refer statements defined in the outer scope, including other type functions. Additionally, type functions will be limited on what globals/libraries they could call. The list of available globals/libraries are:
|
||||
|
||||
- TODO add to this list
|
||||
|
||||
There is also a problem of infinitely running type functions whom can halt the analysis from making further progress. For example, reducing this type function will halt analysis until the VM stack overflows:
|
||||
```luau
|
||||
type function neverending(t)
|
||||
return neverending(t) -- note: parentheses are used here because the runtime value of types is being passed in, rather than the static annotation of types
|
||||
end
|
||||
```
|
||||
As a result, the current plan is to maintain a tightly scoped implementation, focusing on the simplest version of type functions to minimize the risk of developers accidentally starving themselves of features provided by the analysis, and gradually enhance capabilities through successive iterations.
|
||||
|
||||
We have considered implementing a user-configurable execution limit based on time or instruction count for reducing type functions where if a type function exceeds the limit, it fails to reduce. However, these methods are not consistently reliable for terminating type functions that take too long to execute. Time-based timeouts are dependent on CPU performance; programs that type check on fast CPUs may not type check on slower CPUs. Similarly, instruction-based timeouts are dependent on the compiler optimizations; programs that type check on one compiler version may not type check on versions without the same optimizations. We will be experimenting with various forms of limiting the execution of type functions and reserve the right to change how termination of type functions is managed.
|
||||
The simplest approach (and the approach we plan on taking for the first iteration) and one that is currently supported by the Luau API is to enforce a time limit to the type function VM. We are aware that this methods are not consistently reliable. Time-based timeouts are dependent on CPU performance; programs that type check on fast CPUs may not type check on slower CPUs. We will be experimenting with various forms of limiting the execution of type functions and reserve the right to change how termination of type functions is managed.
|
||||
|
||||
<details><summary>List of illegal constructs in type functions</summary>
|
||||
|
||||
* `while` / `repeat` loops
|
||||
* invoking other type functions / regular functions / lambdas
|
||||
* we will not (and probably never) allow type functions to call regular functions for the sake of maintaining discrete stages between runtime and analysis
|
||||
* referring to locals / globals in the outer scope
|
||||
* global functions: `getfenv`, `setfenv`, `pcall`, `xpcall`, `require`
|
||||
* libraries: `coroutine`, `debug`, `string.gsub`
|
||||
|
||||
Note: we are aware that for loops can cause infinite runtime. For the time being, we will not be handling this case. In the event that a developer accidentally creates an infinitely long type function, autocomplete will timeout in their editor environments and running luau-analyze will not complete. They will need to fix the type function and restart their environment / analysis.
|
||||
|
||||
</details>
|
||||
|
||||
To fail reductions, developers can use `error()` with custom error messages. If nothing is returned by the type function, it will fail to reduce with the default message: "Failed to reduce \<Name\> type function with no return value". If the return value of the type function is not an instance of typelib, it will fail to reduce with the message: "Failed to reduce \<Name\> type function with non-typelib return value".
|
||||
To fail reductions, developers can use `error()` with custom error messages. Type functions always expect to have one return value of `type` instance.
|
||||
|
||||
To allow Luau developers to modify the runtime values of types in type functions, this RFC proposes introducing a new userdata called `typelib`. A `typelib` object is a runtime representation of all types within the program and provides a basic set of library methods that can be used to modify types. As such, under the hood, the `typelib` library will closely mimic the implementation of static types in Luau Analysis. Most importantly, they are *only accessible within type functions* and are *not a runtime type for other use cases than type functions*.
|
||||
To allow Luau developers to modify the runtime values of types in type functions, this RFC proposes introducing a new userdata called `type` (for the purpose of clarity, `type` refers to the userdata and type refers to their actual word). A `type` object is a runtime representation of all types within the program and provides a basic set of library methods that can be used to modify types. They are *only accessible within type functions* and are *not a runtime value/userdata/library anywhere else*.
|
||||
|
||||
<details><summary>typelib library (dropdown)</summary>
|
||||
Because the name clashes with the global function `type()`, this new userdata's `__call` metamethod will be set to the original `type()` function.
|
||||
|
||||
<details><summary>`type` library (dropdown)</summary>
|
||||
|
||||
Methods under a different type heading (ex: `Singleton`) imply that the methods are only available for those types. At the implementation level, there is a check to make sure that the type-specific methods are being called on the correct types.
|
||||
|
||||
#### typelib
|
||||
All attributes of newly created typelib are initialized with empty tables / arrays and `nil`. For instance, `typelib.newtable()` initializes its properties with an empty table and index / index result type as `nil`. Additionally, all arguments are passed by references.
|
||||
#### `type`
|
||||
All attributes of newly created `type` are initialized with empty tables / arrays and `nil`. For instance, `type.newtable()` initializes its properties with an empty table and index / index result type as `nil`. Additionally, all arguments are passed by references.
|
||||
|
||||
| Instance Attributes | Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `niltype` | `typelib` | an immutable runtime representation of the built-in type `nil` |
|
||||
| `unknown` | `typelib` | an immutable runtime representation of the built-in type `unknown` |
|
||||
| `never` | `typelib` | an immutable runtime representation of the built-in type `never` |
|
||||
| `any` | `typelib` | an immutable runtime representation of the built-in type `any` |
|
||||
| `boolean` | `typelib` | returns an immutable runtime representation of the built-in type `boolean` |
|
||||
| `number` | `typelib` | returns an immutable runtime representation of the built-in type `number` |
|
||||
| `string` | `typelib` | returns an immutable runtime representation of the built-in type `string` |
|
||||
| `niltype` | `type` | an immutable runtime representation of the built-in type `nil` |
|
||||
| `unknown` | `type` | an immutable runtime representation of the built-in type `unknown` |
|
||||
| `never` | `type` | an immutable runtime representation of the built-in type `never` |
|
||||
| `any` | `type` | an immutable runtime representation of the built-in type `any` |
|
||||
| `boolean` | `type` | returns an immutable runtime representation of the built-in type `boolean` |
|
||||
| `number` | `type` | returns an immutable runtime representation of the built-in type `number` |
|
||||
| `string` | `type` | returns an immutable runtime representation of the built-in type `string` |
|
||||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `issubtypeof(arg: typelib)` | `boolean` | returns true if self is syntactically a subtype or equal to arg in the type hierarchy |
|
||||
| `__eq(arg: typelib)` | `boolean` | overrides the == operator to return true if self is syntactically equal to arg in the type hierarchy |
|
||||
| `__eq(arg: type)` | `boolean` | overrides the == operator to return true if self is syntactically equal to arg |
|
||||
|
||||
| Static Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `getnegation(arg: typelib)` | `typelib` | returns an immutable runtime representation of the negation of the argument; the argument cannot be an instance of a table or a function. |
|
||||
| `getstringsingleton(arg: string)` | `typelib` | returns an immutable runtime representation of a string singleton type of the argument |
|
||||
| `getbooleansingleton(arg: boolean)` | `typelib` | returns an immutable runtime representation of a boolean singleton type of the argument |
|
||||
| `getunion(arg: {typelib})` | `typelib` | returns an immutable runtime representation of union type of its argument |
|
||||
| `getintersection(arg: {typelib})` | `typelib` | returns an immutable runtime representation of intersection type of its argument |
|
||||
| `newtable(props: {[typelib]: typelib}?, indexer: {key: typelib, value: typelib}?, metatable: typelib?)` | `typelib` | returns a mutable runtime representation of a `table` type. If provided the metatable parameter, this table becomes a metatable. |
|
||||
| `newfunction(parameters: {typelib} \| typelib?, returns: {typelib} \| typelib?)` | `typelib` | returns a mutable runtime representation of a `function` type. Calling `newfunction(X)` will by default set `parameters` to `X` |
|
||||
| `type(arg: typelib)` | `string` | returns the tag of the argument ("nil", "unknown", "never", "any", "boolean", "number", "string", "boolean singleton", "string singleton", "negation", "union", "intersection", "table", "function", "class") |
|
||||
| `copy(arg: typelib)` | `typelib` | returns a deep copy of the argument |
|
||||
| `getnegation(arg: type)` | `type` | returns an immutable runtime representation of the negation of the argument; the argument cannot be an instance of a table or a function. |
|
||||
| `getstringsingleton(arg: string)` | `type` | returns an immutable runtime representation of a string singleton type of the argument |
|
||||
| `getbooleansingleton(arg: boolean)` | `type` | returns an immutable runtime representation of a boolean singleton type of the argument |
|
||||
| `getunion(arg: {type})` | `type` | returns an immutable runtime representation of union type of its argument |
|
||||
| `getintersection(arg: {type})` | `type` | returns an immutable runtime representation of intersection type of its argument |
|
||||
| `newtable(props: {[type]: type}?, indexer: {key: type, value: type}?, metatable: type?)` | `type` | returns a mutable runtime representation of a `table` type. If provided the metatable parameter, this table becomes a metatable. |
|
||||
| `newfunction(parameters: {type} \| type?, returns: {type} \| type?)` | `type` | returns a mutable runtime representation of a `function` type. Calling `newfunction(X)` will by default set `parameters` to `X` |
|
||||
| `type(arg: type)` | `string` | returns the tag of the argument ("nil", "unknown", "never", "any", "boolean", "number", "string", "boolean singleton", "string singleton", "negation", "union", "intersection", "table", "function", "class") |
|
||||
| `copy(arg: type)` | `type` | returns a deep copy of the argument |
|
||||
|
||||
#### Negation
|
||||
|
||||
| Instance Methods | Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `gettype()` | `typelib` | returns the runtime representation of the self's type being negated |
|
||||
| `gettype()` | `type` | returns the runtime representation of the self's type being negated |
|
||||
|
||||
#### StringSingleton
|
||||
|
||||
|
@ -125,43 +123,43 @@ All attributes of newly created typelib are initialized with empty tables / arra
|
|||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `setprop(key: typelib, value: typelib?)` | `nil` | adds / overrides (if same key exists) a key, value pair to self's table properties; if value is nil, removes the key, value pair from self's table properties; if the key does not exist and the value is nil, nothing happens |
|
||||
| `getprop(key: typelib)` | `typelib?` | returns the value associated with the key from self's table properties if the key exists, else nil |
|
||||
| `getprops()` | `{[typelib]: typelib}` | returns a table of self's table properties (e.g. `{["age"] = 20}` will return `{typelib.getstringsingleton("age") = typelib.getnumber()}`) |
|
||||
| `setindexer(key: typelib, value: typelib)` | `nil` | sets self's indexer key type to the first argument and indexer value type to the second |
|
||||
| `getindexer()` | `{key: typelib, value: typelib}?` | returns a table containing self's indexer key type and value type if they exist, else nil |
|
||||
| `setmetatable(arg: typelib)` | `nil` | sets self's metatable to the argument |
|
||||
| `getmetatable()` | `typelib?` | returns self's runtime representation of metatable if it exists, else nil |
|
||||
| `setprop(key: type, value: type?)` | `nil` | adds / overrides (if same key exists) a key, value pair to self's table properties; if value is nil, removes the key, value pair from self's table properties; if the key does not exist and the value is nil, nothing happens |
|
||||
| `getprop(key: type)` | `type?` | returns the value associated with the key from self's table properties if the key exists, else nil |
|
||||
| `getprops()` | `{[type]: type}` | returns a table of self's table properties (e.g. `{["age"] = 20}` will return `{type.getstringsingleton("age") = type.getnumber()}`) |
|
||||
| `setindexer(key: type, value: type)` | `nil` | sets self's indexer key type to the first argument and indexer value type to the second |
|
||||
| `getindexer()` | `{key: type, value: type}?` | returns a table containing self's indexer key type and value type if they exist, else nil |
|
||||
| `setmetatable(arg: type)` | `nil` | sets self's metatable to the argument |
|
||||
| `getmetatable()` | `type?` | returns self's runtime representation of metatable if it exists, else nil |
|
||||
|
||||
#### Function
|
||||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `setparameters(arg: {typelib} \| typelib?)` | `nil` | sets self's parameter types to the argument, where an array implies a TypePack and the latter implies a Variadic |
|
||||
| `getparameters()` | `{typelib} \| typelib?` | returns the runtime representation of self's parameter type if it exists, else nil. Return an array implies a TypePack and a single value implies a Variadic |
|
||||
| `setreturns(arg: {typelib} \| typelib?)` | `nil` | sets self's return types to the argument, where an array implies a TypePack and the latter implies a Variadic |
|
||||
| `getreturns()` | `{typelib} \| typelib?` | returns the runtime representation of self's return type if it exists, else nil. Return an array implies a TypePack and a single value implies a Variadic |
|
||||
| `setparameters(arg: {type} \| type?)` | `nil` | sets self's parameter types to the argument, where an array implies a TypePack and the latter implies a Variadic |
|
||||
| `getparameters()` | `{type} \| type?` | returns the runtime representation of self's parameter type if it exists, else nil. Return an array implies a TypePack and a single value implies a Variadic |
|
||||
| `setreturns(arg: {type} \| type?)` | `nil` | sets self's return types to the argument, where an array implies a TypePack and the latter implies a Variadic |
|
||||
| `getreturns()` | `{type} \| type?` | returns the runtime representation of self's return type if it exists, else nil. Return an array implies a TypePack and a single value implies a Variadic |
|
||||
|
||||
#### Union
|
||||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `getcomponents()` | `{typelib}` | returns an array of types that the self's union can represent. For instance, `string \| number` returns `{typelib.string, typelib.number}` |
|
||||
| `getcomponents()` | `{type}` | returns an array of types that the self's union can represent. For instance, `string \| number` returns `{type.string, type.number}` |
|
||||
|
||||
#### Intersection
|
||||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `getcomponents()` | `{typelib}` | returns an array of types represented by self's intersection. For instance, `string & number` returns `{typelib.string, typelib.number}` |
|
||||
| `getcomponents()` | `{type}` | returns an array of types represented by self's intersection. For instance, `string & number` returns `{type.string, type.number}` |
|
||||
|
||||
#### Class
|
||||
|
||||
| Instance Methods | Return Type | Description |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `getprops()` | `{[typelib]: typelib}` | returns the runtime representation self's properties |
|
||||
| `getparent()` | `typelib?` | returns the runtime representation of self's parent class if it exists, else nil |
|
||||
| `getmetatable()` | `typelib?` | returns the runtime representation of self's metatable if it exists, else nil |
|
||||
| `getindexer()` | `{key: typelib, value: typelib}?` | returns a table containing self's indexer key type and value type |
|
||||
| `getprops()` | `{[type]: type}` | returns the runtime representation self's properties |
|
||||
| `getparent()` | `type?` | returns the runtime representation of self's parent class if it exists, else nil |
|
||||
| `getmetatable()` | `type?` | returns the runtime representation of self's metatable if it exists, else nil |
|
||||
| `getindexer()` | `{key: type, value: type}?` | returns a table containing self's indexer key type and value type |
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -169,11 +167,11 @@ The reason for going with userdata instead using another representation or addin
|
|||
|
||||
### Implementation
|
||||
|
||||
A `typelib` library will be implemented using the Luau API to interface between C++ and Luau and support the library methods, including type serialization for arguments and deserialization for return values. To implement type functions, a new AST node called `AstStatTypeFunction` will be introduced and created when parsing a type alias followed by the keyword "function." In the constraint generator, visiting this new AST node will generate a `TypeAliasExpansionConstraint` and in the constraint solver, reducing `TypeAliasExpansionConstraint` will generate a `ReduceConstraint`. To reduce `ReduceConstraints`, user-defined type functions will be integrated as built-in type functions in Luau where when being invoked, their arguments will be serialized into an instance of `typelib`. These functions will interact with the Luau VM to execute the function body in an established environment with only the specified libraries and constructs available. The return value of the type function will be deserialized and be the value that the `ReduceConstraint` reduces to.
|
||||
A `type` library will be implemented using the Luau API to interface between C++ and Luau and support the library methods, including type serialization for arguments and deserialization for return values. To implement type functions, a new AST node called `AstStatTypeFunction` will be introduced and created when parsing a type alias followed by the keyword "function." In the constraint generator, visiting this new AST node will generate a `TypeAliasExpansionConstraint` and in the constraint solver, reducing `TypeAliasExpansionConstraint` will generate a `ReduceConstraint`. To reduce `ReduceConstraints`, user-defined type functions will be integrated as built-in type functions in Luau where when being invoked, their arguments will be serialized into an instance of `type`. These functions will interact with the Luau VM to execute the function body in an established environment with only the specified libraries and constructs available. The return value of the type function will be deserialized and be the value that the `ReduceConstraint` reduces to.
|
||||
|
||||
## Drawback
|
||||
|
||||
Type functions are handled at the analysis time, while `typelib` is an implementation in the runtime. As a result, the proposed design causes the analysis time to be dependent on Luau's runtime to reduce user-defined type functions. This is generally discouraged as it is best to isolate the compile time, analysis time, and runtime from each other for the purpose of maintaining a clean separation of concerns, which helps minimize side effects and dependencies across different phases of the program execution and improves the modularity of the compiler. Overlaps between the analysis time and runtime can lead to code that is more complex and harder to manage, potentially increasing the risk of bugs and making the outcomes less predictable.
|
||||
Type functions are handled at the analysis time, while `type` is an implementation in the runtime. As a result, the proposed design causes the analysis time to be dependent on Luau's runtime to reduce user-defined type functions. This is generally discouraged as it is best to isolate the compile time, analysis time, and runtime from each other for the purpose of maintaining a clean separation of concerns, which helps minimize side effects and dependencies across different phases of the program execution and improves the modularity of the compiler. Overlaps between the analysis time and runtime can lead to code that is more complex and harder to manage, potentially increasing the risk of bugs and making the outcomes less predictable.
|
||||
|
||||
The build / analysis times will also be negatively impacted as reducing type functions takes variable amount of time based on the program. Developers will be able to write non-performant code that impacts their (and any of their depedent code's) analysis time. The larger the type function, the longer it will take to reduce it in the constraint solver.
|
||||
|
||||
|
@ -181,10 +179,10 @@ The build / analysis times will also be negatively impacted as reducing type fun
|
|||
|
||||
### `table` Runtime Representation
|
||||
|
||||
Currently, the runtime representation of types is a userdata called `typelib`. Another representation is to use the already-existing type in Luau `table`; instead of serializing types into `typelib`, we can serialize them into tables with predefined properties. For instance, the representation for a string singleton `"abc"` could be `{type = "stringSingleton", value = "abc"}`. So instead of writing:
|
||||
Currently, the runtime representation of types is a userdata called `type`. Another representation is to use the already-existing type in Luau `table`; instead of serializing types into `type`, we can serialize them into tables with predefined properties. For instance, the representation for a string singleton `"abc"` could be `{type = "stringSingleton", value = "abc"}`. So instead of writing:
|
||||
```luau
|
||||
type function issingleton(t)
|
||||
if typelib.type(t) == "string singleton" then
|
||||
if type.type(t) == "string singleton" then
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
@ -198,7 +196,7 @@ type function issingleton(t)
|
|||
end
|
||||
```
|
||||
|
||||
In some sense, this design could be considered "cleaner" than introducing an entirely new userdata, but it requires developers to have a deeper understanding of the type runtime representation. For example, under the proposed design, a new table type can be created using `typelib.newtable()`, while under the alternative design, tables must be declared with attributes: `{type = "table", props = {}, indexer = {}}`. This adds complexity to the developer experience and increases the chance of making syntax errors in their program.
|
||||
In some sense, this design could be considered "cleaner" than introducing an entirely new userdata, but it requires developers to have a deeper understanding of the type runtime representation. For example, under the proposed design, a new table type can be created using `type.newtable()`, while under the alternative design, tables must be declared with attributes: `{type = "table", props = {}, indexer = {}}`. This adds complexity to the developer experience and increases the chance of making syntax errors in their program.
|
||||
|
||||
### More Builtin Type Functions
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue