Add more accepted RFCs. (#32)

This finishes the set of fully baked/accepted RFCs; 3 more proposals have been accepted but not implemented yet and will be submitted separately as actual RFCs.
This commit is contained in:
Arseny Kapoulkine 2021-05-04 00:13:57 -07:00 committed by GitHub
parent e2176e35e1
commit ba2138ce10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 1 deletions

View File

@ -35,7 +35,7 @@ When the initial comment period expires, the RFC can be merged if there's consen
When revisions on the RFC text that affect syntax/semantics are suggested, they need to be incorporated before a RFC is merged; a merged RFC represents a maximally accurate version of the language change that is going to be implemented.
In some cases RFCs may contain conditional compatibility clauses. E.g. there are cases where a change is potentially not backwards compatible, but is believed to be substantially beneficial that it can be implemented if, in practice, the backwards compatibility implications are minimal. As a strawman example, if we wanted to introduce a non-context-specific keyword `globally_coherent_buffer`, we would be able to do so if our analysis of Luau code (based on the Roblox platform at the moment) informs us that no script in existence uses this keyword. In cases like this an RFC may need to be revised after the initial implementation attempt based on the data that we gather.
In some cases RFCs may contain conditional compatibility clauses. E.g. there are cases where a change is potentially not backwards compatible, but is believed to be substantially beneficial that it can be implemented if, in practice, the backwards compatibility implications are minimal. As a strawman example, if we wanted to introduce a non-context-specific keyword `globallycoherent`, we would be able to do so if our analysis of Luau code (based on the Roblox platform at the moment) informs us that no script in existence uses this keyword. In cases like this an RFC may need to be revised after the initial implementation attempt based on the data that we gather.
In general, RFCs can also be updated after merging to make the language of the RFC more clear, but should not change their meaning. When a new feature is built on top of an existing feature that has an RFC, a new RFC should be created instead of editing an existing RFC.

View File

@ -0,0 +1,61 @@
# Change `assert` to return one value
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Change `assert` to only return the first argument on success
## Motivation
Today `assert()` function has confusing semantics:
`assert(x)` fails if `x` is falsy and returns `x` otherwise (this is okay!)
`assert(x, y)` fails with `y` as the error message if `x` is falsy, and returns `x, y` otherwise (why?)
`assert(x, y, z)` fails with `y` as the error message if `x` is falsy, and returns `x, y, z` otherwise (why???)
It's not clear what purpose is there behind returning more than one argument from `assert`, as it doesn't seem like it can be useful.
Specifically, when a two-argument form is used, the second argument must be an error message (otherwise if the first argument is falsy, the error behavior will be confusing); if it *is* in fact an error message, it's not clear why it's useful to return that error message when the first argument was truthful.
When a three-argument form is used, the third argument is just ignored and passed through, again for no apparent benefit.
## Design
This proposal argues that long term it's cleaner for us to fix the semantics here to just return the first argument. Any extra arguments will be ignored at runtime as usual, but our type checker can start treating these as invalid.
To be more precise, we'd be formally switching from
```
declare function assert<T...>(...: T...): T...
```
to
```
declare function assert<T>(value: T, error: string?): T
```
This allows us to align the runtime behavior of `assert` to the type definition and reduces the chance of an accidental error - since all return values are forwarded when the call is used as the last function argument, this code transformation is safe:
```
- foo(bar)
+ foo(assert(bar))
```
But this code transformation may be unsafe if `foo` has optional parameters:
```
- foo(assert(bar))
+ foo(assert(bar, "bar should not be nil"))
```
After this proposal the transformation becomes safe.
## Drawbacks
This may break user code. Unfortunately there is only one way for us to find out if it does, which is to roll this change out. We will use release notes to communicate the change and, if concerns are raised by the community ahead of time, we will investigate them.
## Alternatives
Alternatively we could keep `assert` behavior exactly as it is; this would match mainline Lua but we want to make the language better and if we were designing the standard library again we'd never make this choice, hence the proposal.

View File

@ -0,0 +1,53 @@
# table.freeze
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add `table.freeze` which allows to make a table read-only in a shallow way.
## Motivation
Lua tables by default are freely modifiable in every possible way: you can add new fields, change values for existing fields, or set or unset the metatable.
Today it is possible to customize the behavior for *adding* new fields by setting a metatable that overrides `__newindex` (including setting `__newindex` to a function that always errors to prohibit additions of new fields).
Today it is also possible to customize the behavior of setmetatable by "locking" the metatable - this can be achieved by setting a meta-index `__metatable` to something, which would block setmetatable from functioning and force metatable to return the provided value. With this it's possible to prohibit customizations of a table's behavior, but existing fields can still be assigned to.
To make an existing table read-only, one needs to combine these mechanisms, by creating a new table with a locked metatable, which has an `__index` function pointing to the old table. However, this results in iteration and length operator not working on the resulting table, and carries a performance cost - both for creating the table, and for repeated property access.
## Design
This proposal proposes formalizing the notion of "read-only" tables by providing a new table functions:
`table.freeze(t)`: given a non-frozen table t, freezes it; fails when t is not a table or is already frozen. Returns t.
`table.isfrozen(t)`: given a table t, returns a boolean indicating the frozen status; fails when t is not a table.
When a table is frozen, the following is true:
- Attempts to modify the existing keys of the table fail (regardless of how they are performed - via table assignments, rawset, or any other methods like table.sort)
- Attempts to add new keys to the table fail, unless `__newindex` is defined on the metatable (in which case the assignment is routed through `__newindex` as usual)
- Attempts to change the metatable of the table fail
- Reading the table fields or iterating through the table proceeds as usual
This feature is useful for two reasons:
a) It allows an easier way to expose sandboxed objects that aren't possible to monkey-patch for security reasons. We actually already have support for freezing and use it internally on various builtin tables like math, we just don't expose it to Lua.
b) It allows an easier way to expose immutable objects for consistency/correctness reasons. For example, Cryo library provides an implementation of immutable data structures; with this functionality, it's possible to implement a lighter-weight library by, for example, extending a table with methods to return mutated versions of the table, but retaining the usual table interface
To limit the use of `table.freeze` to cases when table contents can be freely manipulated, `table.freeze` shall fail when the table has a locked metatable (but will succeed if the metatable isn't locked).
## Drawbacks
Exposing the internal "readonly" feature may have an impact on interoperability between scripts - for example, it becomes possible to freeze some tables that scripts may be expecting to have write access to from other scripts. Since we don't provide a way to unfreeze tables and freezing a table with a locked metatable fails, in theory the impact should not be any worse than allowing to change a metatable, but the full extents are unclear.
There may be existing code in the VM that allows changing frozen tables in ways that are benign to the current sanboxing code, but expose a "gap" in the implementation that becomes significant with this feature; thus we would need to audit all table writes when implementing this.
## Alternatives
We've considered exposing a recursive freeze. The correct generic implementation is challenging since it requires supporting infinitely nested tables when working on the C stack (or a stackless implementation that requires heap allocation); also, to handle self-recursive tables requires a separate temporary tracking table since stopping the traversal at frozen sub-tables is insufficient as their children may not have been frozen. As such, we leave recursive implementation to user code.
We've considered exposing thawing. The problem with this is that freezing is required for sandboxing, and as such we'd need to support "permafrozen" status that is separate from "frozen". This complicates implementation and we didn't find compelling use cases for thawing - if it becomes necessary we can always expose it separately.
We've considered calling this "locking", but the term has connotations coming from multithreading that aren't applicable here, and in absence of unlocking, "locking" makes a bit less sense.

View File

@ -0,0 +1,88 @@
# if-then-else expression
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Introduce a form of ternary conditional using `if cond then value else alternative` syntax.
## Motivation
Luau does not have a first-class ternary operator; when a ternary operator is needed, it is usually emulated with `and/or` expression, such as `cond and value or alternative`.
This expression evaluates to `value` if `cond` and `value` are truthful, and `alternative` otherwise. In particular it means that when `value` is `false` or `nil`, the result of the entire expression is `alternative` even when `cond` is truthful - which doesn't match the expected ternary logic and is a frequent source of subtle errors.
Instead of `and/or`, `if/else` statement can be used but since that requires a separate mutable variable, this option isn't ergonomic. An immediately invoked function expression is also unergonomic and results in performance issues at runtime.
## Design
To solve these problems, we propose introducing a first-class ternary conditional. Instead of `? :` common in C-like languages, we propose an `if-then-else` expression form that is syntactically similar to `if-then-else` statement, but lacks terminating `end`.
Concretely, the `if-then-else` expression must match `if <expr> then <expr> else <expr>`; it can also contain an arbitrary number of `elseif` clauses, like `if <expr> then <expr> elseif <expr> then <expr> else <expr>`. Note that in either case, `else` is mandatory.
The result of the expression is the then-expression when condition is truthful (not `nil` or `false`) and else-expression otherwise.
Example:
```lua
local x = if FFlagFoo then A else B
MyComponent.validateProps = t.strictInterface({
layoutOrder = t.optional(t.number),
newThing = if FFlagUseNewThing then t.whatever() else nil,
})
```
## Drawbacks
Studio's script editor autocomplete currently adds an indented block followed by `end` whenever a line ends that includes a `then` token. This can make use of the if expression unpleasant as developers have to keep fixing the code by removing auto-inserted `end`. We can work around this on the editor side by (short-term) differentiating between whether `if` token is the first on its line, and (long-term) by refactoring completion engine to use infallible parser for the block completer.
Parser recovery can also be more fragile due to leading `if` keyword - when `if` was encountered previously, it always meant an unfinished expression, but now it may start an `if-expr` that, when confused with `if-end` statement can lead to a substantially incorrect parse that is difficult to recover from. However, similar issues occur frequently due to function call statements and as such it's not clear that this makes the recovery materially worse.
## Alternatives
We've evaluated many alternatives for the proposed syntax.
### Python syntax
```
b if a else c
```
Undesirable because expression evaluation order is not left-to-right which is a departure from all other Lua expressions. Additionally, since `b` may be ending a statement (followed by `if` statement), resolving this ambiguity requires parsing `a` as expression and backtracking if `else` is not found, which is expensive and likely to introduce further ambiguities.
### C-style ternary operator
```
a ? b : c
```
Problematic because `:` is used for method calls. In Julia `? :` and `:` are both operators which are disambiguated by _requiring_ spaces in the first case and _prohibiting_ them in the second case; this breaks backwards compatibility and doesn't match the rest of the language where whitespace in the syntax is not significant.
### Function syntax
```
iff(a, b, c)
```
If implemented as a regular function call, this would break short-circuit behavior. If implemented as a special builtin, it would look like a regular function call but have magical behavior -- something likely to confuse developers.
### Perl 6 syntax
```
a ?? b !! c
```
Syntax deemed too unconventional to use in Luau.
### Smaller variations
```
(if a then b else c)
```
Ada uses this syntax (with parentheses required for clarity). Similar solutions were discussed for `as` previously and rejected to make it easier for humans and machines to understand the language syntax.
```
a then b else c
```
This is ambiguous in some cases (like within if condition) so not feasible from a grammar perspective.
```
if a then b else c end
```
The `end` here is unnecessary since `c` is not a block of statements -- it is simply an expression. Thus, use of `end` here would be inconsistent with its other uses in the language. It also makes the syntax more cumbersome to use and could lead to developers sticking with the error-prone `a and b or c` alternative.
### `elseif` support
We discussed a simpler version of this proposal without `elseif` support. Unlike if statements, here `elseif` is purely syntactic sugar as it's fully equivalent to `else if`. However, supporting `elseif` makes if expression more consistent with if statement - it is likely that developers familiar with Luau are going to try using `elseif` out of habit. Since supporting `elseif` here is trivial we decided to keep it for consistency.