Establish an RFC process (#29)

This is going to apply to language and core library changes and will supplant the internal Roblox API proposal process for Luau only.
This commit is contained in:
Arseny Kapoulkine 2021-05-03 18:52:43 -07:00 committed by GitHub
parent b8627707c4
commit e2176e35e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 698 additions and 0 deletions

53
rfcs/README.md Normal file
View File

@ -0,0 +1,53 @@
Background
===
Whenever Luau language changes its syntax or semantics (including behavior of builtin libraries), we need to consider many implications of the changes.
Whenever new syntax is introduced, we need to ask:
- Is it backwards compatible?
- Is it easy for machines and humans to parse?
- Does it create grammar ambiguities for current and future syntax?
- Is it stylistically coherent with the rest of the language?
- Does it present challenges with editor integration like autocomplete?
For changes in semantics, we should be asking:
- Is behavior easy to understand and non-surprising?
- Can it be implemented performantly today?
- Can it be sandboxed assuming malicious usage?
- Is it compatible with type checking and other forms of static analysis?
In addition to these questions, we also need to consider that every addition carries a cost, and too many features will result in a language that is harder to learn, harder to implement and ensure consistent implementation quality throughout, slower, etc. In addition, any language is greater than the sum of its parts and features often have non-intuitive interactions with each other.
Since reversing these decisions is incredibly costly and can be impossible due to backwards compatibility implications, all user facing changes to Luau language and core libraries must go through an RFC process.
Process
===
To open an RFC, a Pull Request must be opened which creates a new Markdown file in `rfcs/` folder. The RFCs should follow the template `rfcs/TEMPLATE.md`, and should have a file name that is a short human readable description of the feature (using lowercase alphanumeric characters and dashes only). Try using the general area of the RFC as a prefix, e.g. `syntax-generic-functions.md` or `function-debug-info.md`. The PR needs to use `rfc` label.
> Note: we currently don't accept community contributions for RFCs, although this will likely change in the future.
Every open RFC will be open for at least two calendar weeks. This is to make sure that there is sufficient time to review the proposal and raise concerns or suggest improvements. The discussion points should be reflected on the PR comments; when discussion happens outside of the comment stream, the points salient to the RFC should be summarized as a followup.
When the initial comment period expires, the RFC can be merged if there's consensus that the change is important and that the details of the syntax/semantics presented are workable. The decision to merge the RFC is made by the Luau team.
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 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.
When there's no consensus that the feature is broadly beneficial and can be implemented, an RFC will be closed. The decision to close the RFC is made by the Luau team.
Note that in some cases an RFC may be closed because we don't have sufficient data or believe that at this point in time, the stars do not line up sufficiently for this change to be worthwhile, but this doesn't mean that it may never be considered again; an RFC PR may be reopened if new data is available since the original discussion, or if the PR has changed substantially to address the core problems raised in the prior round.
Implementation
===
When an RFC gets merged, the feature *can* be implemented; however, there's no set timeline for that implementation. In some cases implementation may land in a matter of days after an RFC is merged, in some it may take months.
To avoid having permanently stale RFCs, in rare cases Luau team can *remove* a previously merged RFC when the landscape is believed to change enough for a feature like this to warrant further discussion.
To track progress of implemented features, it's advised to follow documentation instead; while RFCs aren't meant to be user-facing documentation, recaps in particular can reference RFCs for additional context behind the changes.

21
rfcs/TEMPLATE.md Normal file
View File

@ -0,0 +1,21 @@
# FEATURE_NAME
## Summary
One paragraph explanation of the feature.
## Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
## Design
This is the bulk of the proposal. Explain the design in enough detail for somebody familiar with the language to understand, and include examples of how the feature is used.
## Drawbacks
Why should we *not* do this?
## Alternatives
What other designs have been considered? What is the impact of not doing this?

View File

@ -0,0 +1,57 @@
# Always call `__eq` when comparing for equality
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
`__eq` metamethod will always be called during `==`/`~=` comparison, even for objects that are rawequal.
## Motivation
Lua 5.x has the following algorithm it uses for comparing userdatas and tables:
- If two objects are not of the same type (userdata vs number), they aren't equal
- If two objects are referentially equal, they are equal (!)
- If no object has a metatable with `__eq` metamethod, they are equal iff they are referentially equal
- Otherwise, pick one of the `__eq` metamethods, call it with both objects as arguments and return the result.
In mid-2019, we've released Luau which implements a fast path for userdata comparison. This fast path accidentally omitted step 2 for userdatas with C `__eq` implementations (!), and thus comparing a userdata object vs itself would actually run `__eq` metamethod. This is significant as it allowed users to use `v == v` as a NaN check for vectors, coordinate frames, and other objects that have floating point contents.
Since this was a bug, we're in a rather inconsistent state:
- `==` and `~=` in the code always call `__eq` for userdata with C `__eq`
- `==` and `~=` don't call `__eq` for tables and custom newproxy-like userdatas with Lua `__eq` when objects are ref. equal
- `table.find` *doesn't* call `__eq` when objects are ref. equal
## Design
Since developers started relying on `==` behavior for NaN checks in the last two years since Luau release, the bug has become a feature. Additionally, it's sort of a good feature since it allows to implement NaN semantics for custom types - userdatas, tables, etc.
Thus the proposal suggests changing the rules so that when `__eq` metamethod is present, `__eq` is always called even when comparing the object to itself.
This would effectively make the current ruleset for userdata objects official, and change the behavior for `table.find` (which is probably not significant) and, more significantly, start calling user-provided `__eq` even when the object is the same. It's expected that any reasonable `__eq` implementation can handle comparing the object to itself so this is not expected to result in breakage.
## Drawbacks
This represents a difference in a rather core behavior from all upstream versions of Lua.
## Alternatives
We could instead equalize (ha!) the behavior between Luau and Lua. In fact, this is what we tried to do initially as the userdata behavior was considered a bug, but encountered the issue with games already depending on the new behavior.
We could work with developers to change their games to stop relying on this. However, this is more complicated to deploy and - upon reflection - makes `==` less intuitive than the main proposal when comparing objects with NaN, since e.g. it means that these two functions have a different behavior:
```
function compare1(a: Vector3, b: Vector3)
return a == b
end
function compare2(a: Vector3, b: Vector3)
return a.X == b.X and a.Y == b.Y and a.Z == b.Z
end
```
## References
https://devforum.roblox.com/t/call-eq-even-when-tables-are-rawequal/1088886
https://devforum.roblox.com/t/nan-vector3-comparison-broken-cframe-too/1130778

View File

@ -0,0 +1,19 @@
# Change \_VERSION global to "Luau"
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Change \_VERSION global to "Luau" to differentiate Luau from Lua
## Motivation
Provide an official way to distinguish Luau from Lua implementation.
## Design
We inherit the global string \_VERSION from Lua (this is distinct from Roblox `version()` function that returns a full version number such as 0.432.43589).
The string is set to "Lua 5.1" for us (and "Lua 5.2" etc for newer versions of Lua.
Since our implementation is sufficiently divergent from upstream, this proposal suggests setting \_VERSION to "Luau".

107
rfcs/function-debug-info.md Normal file
View File

@ -0,0 +1,107 @@
# debug.info
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add `debug.info` as programmatic debug info access API, similarly to Lua's `debug.getinfo`
## Motivation
Today Luau provides only one method to get the callstack, `debug.traceback`. This method traverses the entire stack and returns a string containing the call stack details - with no guarantees about the format of the call stack. As a result, the string doesn't present a formal API and can't be parsed programmatically.
There are a few cases where this can be inconvenient:
- Sometimes it is useful to pass the resulting call stack to some system expecting a structured input, e.g. for crash aggreggation
- Sometimes it is useful to use the information about the caller for logging or filtering purposes; in these cases using just the script name can be useful, and getting script name out of the traceback is slow and imprecise
Additionally, in some cases instead of getting the information (such as script or function name) from the callstack, it can be useful to get it from a function object for diagnostic purposes. For example, maybe you want to call a callback and if it doesn't return expected results, display a user-friendly error message that contains the function name & script location - these aren't possible today at all.
## Design
The proposal is to expose a function from Lua standard library, `debug.getinfo`, to fix this problem - but change the function's signature for efficiency:
> debug.info([thread], [function | level], options) -> any...
(note that the function has been renamed to make it more obvious that the behavior differs from that of Lua)
The parameters of the function match that of Lua's variant - the first argument is either a function object or a stack level (which is a number starting from 1, where 1 means "my caller"), or a thread (followed by the stack level), followed by a string that contains a list of things the result needs to contain:
* s - function source identifier, in Roblox environment this is equal to the full name of the script the function is defined in
* l - line number that the function is defined on (when examining a function) or line number of the stack frame (when examining a stack frame)
* n - function name if present; this can be absent for anonymous functions or some C functions that don't have an assigned debug name
* a - function arity information, which refers to the parameter count and whether the function is variadic or not
* f - function object
Unlike Lua version, which would use the options given to fill a resulting table (e.g. "l" would map to a "currentline" and "linedefined" fields of the output table), our version will return the requested information in the order that it was requested in in the string - all letters specified above map to one extra returned value, "a" maps to a pair of a parameter number and a boolean indicating variadic status.
For example, here's how you implement a stack trace function:
```
for i=1,100 do -- limit at 100 entries for very deep stacks
local source, name, line = debug.info(i, "snl")
if not source then break end
if line >= 0 then
print(string.format("%s(%d): %s", source, line, name or "anonymous"))
else
print(string.format("%s: %s", source, name or "anonymous"))
end
end
```
output:
```
cs.lua(3): stacktrace
cs.lua(17): bar
cs.lua(13): foo
[C]: pcall
cs.lua(20): anonymous
```
When the first argument is a number and the input level is out of bounds, the function returns no values.
### Why the difference from Lua?
Lua's variant of this function has the same string as an input and the same thread/function/level combo as arguments before that, but returns a table with the requested data - or nil, when stack is exhausted.
The problem with this solution is performance. It results in generating excessive garbage by wrapping results in a table, which slows down the function call itself and generates extra garbage that needs to be collected later. This is not a problem for error handling scenarios, but can be an issue when logging is required; for example, `debug.info` with options containing a single result, "s" (mapping to source identifier aka script name), runs 3-4x slower when using a table variant with the current implementation of both functions in our VM.
While the difference in behavior is unfortunate, note that Lua has a long-standing precedent of using characters in strings to define the set of inputs or outputs for functions; of particular note is string.unpack which closely tracks this proposal where input string characters tell the implementation what data to return.
### Why not hardcode the options?
One possibility is that we could return all data associated with the function or a stack frame as a tuple.
This would work but has issues:
1. Because of the tuple-like API, the code becomes more error prone and less self-descriptive.
2. Some data is more expensive to access than other data - by forcing all callers to process all possible data we regress in performance; this is also why the original Lua API has an options string
To make sure we appropriately address 1, unlike Lua API in our API options string is mandatory to specify.
### Sandboxing risk?
Compared to information that you can already parse from traceback, the only extra data we expose is the function object. This is valuable when collecting stacks because retrieving the function object is faster than retrieving the associated source/name data - for example a very performant stack tracing implementation could collect data using "fl" (function and line number), and later when it comes the time to display the results, use `debug.info` again with "sn" to get script & name data from the object.
This technically wasn't possible to get before - this means in particular that if your function is ever called by another function, a malicious script could grab that function object again and call it with different arguments. However given that it's already possible to mutate global environment of any function on the callstack using getfenv/setfenv, the extra risk presented here seems minimal.
### Options delta from Lua
Lua presents the following options in getinfo:
* `n´ selects fields name and namewhat
* `f´ selects field func
* `S´ selects fields source, short_src, what, and linedefined
* `l´ selects field currentline
* `u´ selects field nup
We chose to omit `namewhat` as it's not meaningful in our implementation, omit `what` as it's redundant wrt source/short_src for C functions, replace source/short_src with only a single option (`s`) to avoid leaking script source via callstack API, remove `u` because there are no use cases for knowing the number of upvalues without debug.getupvalue API, and add `a` which has been requested by Roact team before for complex backwards compatibility workarounds wrt passed callbacks.
## Drawbacks
Having a different way to query debug information from Lua requires language-specific dispatch for code that wants to work on Lua and Luau.
## Alternatives
We could expose `debug.getinfo` from Lua as is; the problem is that in addition to performance issues highlighted above, Luau implementation doesn't track the same data and as such can't provide a fully compatible implementation short of implementing a shim for the sake of compatibility - an option this proposal keeps open.

View File

@ -0,0 +1,69 @@
# string.pack/unpack/packsize from Lua 5.3
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add string pack/unpack from Lua 5.3 for binary interop, with small tweaks to format specification to make format strings portable.
## Motivation
While the dominant usecase for Luau is a game programming language, for backend work it's sometimes the case that developers need to work with formats defined outside of Roblox. When these are structured as JSON, it's easy, but if they are binary, it's not. Additionally for the game programming, often developers end up optimizing their data transmission using custom binary codecs where they know the range of the data (e.g. it's much more efficient to send a number using 1 byte if you know the number is between 0 and 1 and 8 bits is enough, but RemoteEvent/etc won't do it for you because it guarantees lossless roundtrip). For both working with external data and optimizing data transfer, it would be nice to have a way to work with binary data.
This is doable in Luau using `string.byte`/`string.char`/`bit32` library/etc. but tends to be a bit cumbersome. Lua 5.3 provides functions `string.pack`/`string.unpack`/`string.packsize` that, while not solving 100% of the problems, often make working with binary much easier and much faster. This proposal suggests adding them to Luau - this will both further our goal to be reasonably compatible with latest Lua versions, and make it easier for developers to write some types of code.
## Design
Concretely, this proposal suggests adding the following functions:
```
string.pack (fmt, v1, v2, ···)
```
Returns a binary string containing the values v1, v2, etc. packed (that is, serialized in binary form) according to the format string fmt.
```
string.packsize (fmt)
```
Returns the size of a string resulting from string.pack with the given format. The format string cannot have the variable-length options 's' or 'z'.
```
string.unpack (fmt, s [, pos])
```
Returns the values packed in string s (see string.pack) according to the format string fmt. An optional pos marks where to start reading in s (default is 1). After the read values, this function also returns the index of the first unread byte in s.
The format string is a sequence of characters that define the data layout that is described here in full: https://www.lua.org/manual/5.3/manual.html#6.4.2. We will adopt this wholesale, but we will guarantee that the resulting code is cross-platform by:
a) Ensuring native endian is little endian (de-facto true for all our platforms)
b) Fixing sizes of native formats to 2b short, 4b int, 8b long
c) Treating `size_t` in context of `T` and `s` formats as a 32-bit integer
Of course, the functions are memory-safe; if the input string is too short to provide all relevant data they will fail with "data string is too short" error.
This may seem slightly unconventional but it's very powerful and expressive, in much the same way format strings and regular expressions are :) Here's a basic example of how you might transmit a 3-component vector with this:
```
-- returns a 24-byte string with 64-bit double encoded three times, similar to how we'd replicate 3 raw numbers
string.pack("ddd", x, y, z)
-- returns a 12-byte string with 32-bit float encoded three times, similar to how we'd replicate Vector3
string.pack("fff", x, y, z)
-- returns a 3-byte string with each value stored in 8 bits
-- assumes -1..1 range; this code doesn't round the right way because I'm too lazy
string.pack("bbb", x * 127, y * 127, z * 127)
```
The unpacking of the data is symmetrical - using the same format string and `string.unpack` you get the encoded data back.
## Drawbacks
The format specification is somewhat arbitrary and is likely to be unfamiliar to people who come with prior experience in other languages (having said that, this feature closely follows equivalent functionality from Ruby).
The implementation of string pack/unpack requires yet another format string matcher, which increases complexity of the builtin libraries and static analysis (since we need to provide linting for another format string syntax).
## Alternatives
We could force developers to rely on existing functionality for string packing; it is possible to replicate this proposal in a library, although at a much reduced performance.

View File

@ -0,0 +1,19 @@
# table.clear
> Note: this RFC was adapted from an internal proposal that predates RFC process and as such doesn't follow the template precisely
## Summary
Add `table.clear` function that removes all elements from the table but keeps internal capacity allocated.
## Design
`table.clear` adds a fast way to clear a Lua table. This is effectively a sister function to `table.create()`, only for reclaiming an existing table's memory rather than pre-allocating a new one. Use cases:
* Often you want to recalculate a set or map data structure based on a table. Currently there is no good way to do this, the fastest way is simply to throw away the old table and construct a new empty one to work with. This is wasteful since often the new structure will take a similar amount of memory to the old one.
* Sometimes you have a shared table which multiple scripts access. In order to clear this kind of table, you have no other option than to use a slow for loop setting each index to nil.
These use cases can technically be accomplished via `table.move` moving from an empty table to the table which is to be edited, but I feel that they are frequent enough to warrant a clearer more understandable method which has an opportunity to be more efficient.
Like `table.move`, does not invoke any metamethods. Not that it would anyways, given that assigning nil to an index never invokes a metamethod.

View File

@ -0,0 +1,26 @@
# table.create and table.find
> Note: this RFC was adapted from an internal proposal that predates RFC process and as such doesn't follow the template precisely
## Design
This proposal suggests adding two new builtin table functions:
`table.create(count, value)`: Creates an array with count values, initialized to value. This can be useful to preallocate large tables - repeatedly appending an element to the table repeatedly reallocates it. count is converted to an integer using standard conversion/coercion rules (strings are converted to doubles, doubles are converted to integers using truncation). Negative counts result in the function failing. Positive counts that are too large and would cause a heap allocation error also result in function failing. When value is nil or omitted, table is preallocated without storing anything in it - this is roughly equivalent to creating a large table literal filled with `nil`, or preallocating a table by assigning a sufficiently large numeric index to a value and then erasing it by reassigning it to nil.
`table.find(table, value [, init])`: Looks for value in the array part of the table; returns index of first occurrence or nil if value is not found. Comparison is performed using standard equality (non-raw) to make sure that objects like Vector3 etc. can be found. The first nil value in the array part of the table terminates the traversal. init is an optional numeric index where the search starts and it defaults to 1; this can be useful to go through repeat occurrences.
`table.create` can not be replicated efficiently in Lua at all; `table.find` is provided as a faster and more convenient option compared to the code above.
`table.find` is roughly equivalent to the following code modulo semantical oddities with #t and performance:
```
function find(table, value, init)
for i=init or 1, #table do
if rawget(table, i) == value then
return i
end
end
return nil
end
```

View File

@ -0,0 +1,63 @@
# Array-like table types
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add special syntax for array-like table types, `{ T }`
## Motivation
Luau supports annotating table types. Tables are quite complex beasts, acting as essentially an associative container mapping any value to any other value, and to make it possible to reason about them at type level we have a more constrained definition of what a table is:
- A table can contain a set of string keys with a specific type for each key
- A table can additionally have an "indexer" for a given key/value type, meaning that it acts as an associative container mapping keys of type K to values of type V
The syntax for this right now looks like this:
```
{ key1: Type1, key2: Type2, [KeyType]: ValueType }
```
This is an example of a hybrid table that has both an indexer and a list of specific key/value pairs.
While Luau technically doesn't support arrays, canonically tables with integer keys are called arrays, or, more precisely, array-like tables. Luau way to specify these is to use an indexer with a number key:
```
{ [number]: ValueType }
```
(note that this permits use of non-integer keys, so it's technically richer than an array).
As the use of arrays is very common - for example, many library functions such as `table.insert`, `table.find`, `ipairs`, work on array-like tables - Luau users who want to type-annotate their code have to use array-like table annotations a lot.
`{ [number]: Type }` is verbose, and the only alternative is to provide a slightly shorter generic syntax:
```
type Array<T> = { [number]: T }
```
... but this is necessary to specify in every single script, as we don't support preludes.
## Design
This proposal suggests adding syntactic sugar to make this less cumbersome:
```
{T}
```
This will be exactly equivalent to `{ [number]: T }`. `T` must be a type definition immediately followed by `}` (ignoring whitespace characters of course)
Conveniently, `{T}` syntax matches the syntax for arrays in Typed Lua (a research project from 2014) and Teal (a recent initiative for a TypeScript-like Lua extension language from 2020).
## Drawbacks
This introduces a potential ambiguity wrt a tuple-like table syntax; to represent a table with two values, number and a string, it's natural to use syntax `{ number, string }`; however, how would you represent a table with just one value of type number? This may seem concerning but can be resolved by requiring a trailing comma for one-tuple table type in the future, so `{ number, }` would mean "a table with one number", vs `{ number }` which means "an array-like table of numbers".
## Alternatives
A different syntax along the lines of `[T]` or `T[]` was considered and rejected in favor of the current syntax:
a) This allows us to, in the future - if we find a good workaround for b - introduce "real" arrays with a distinct runtime representation, maybe even starting at 0! (whether we do this or not is uncertain and outside of scope of this proposal)
b) Square brackets don't nest nicely due to Lua lexing rules, where [[foo]] is a string literal "foo", so with either syntax with square brackets array-of-arrays is not easy to specify

View File

@ -0,0 +1,47 @@
# Compound assignment using `op=` syntax
> Note: this RFC was adapted from an internal proposal that predates RFC process and as such doesn't follow the template precisely
## Design
A feature present in many many programming languages is assignment operators that perform operations on the left hand side, for example
```
a += b
```
Lua doesn't provide this right now, so it requires code that's more verbose, for example
```
data[index].cost = data[index].cost + 1
```
This proposal suggests adding `+=`, `-=`, `*=`, `/=`, `%=`, `^=` and `..=` operators to remedy this. This improves the ergonomics of writing code, and occasionally results in code that is easier to read to also be faster to execute.
The semantics of the operators is going to be as follows:
- Only one value can be on the left and right hand side
- The left hand side is evaluated once as an l-value, similarly to the left hand side of an assignment operator
- The right hand side is evaluated as an r-value (which results in a single Lua value)
- The assignment-modification is performed, which can involve table access if the left hand side is a table dereference
- Unlike C++, these are *assignment statements*, not expressions - code like this `a = (b += 1)` is invalid.
Crucially, this proposal does *not* introduce new metamethods, and instead uses the existing metamethods and table access semantics, for example
```
data[index].cost += 1
```
translates to
```
local table = data[index]
local key = "cost"
table[key] = table[key] + 1
```
Which can invoke `__index` and `__newindex` on table as necessary, as well as `__add` on the element. In this specific example, this is *faster* than `data[index].cost = data[index].cost + 1` because `data[index]` is only evaluated once, but in general the compound assignment is expected to have the same performance and the goal of this proposal is to make code easier and more pleasant to write.
The proposed new operators are currently invalid in Lua source, and as such this is a backwards compatible change.
From the implementation perspective, this requires adding new code/structure to AST but doesn't involve adding new opcodes, metatables, or any extra cost at runtime.

View File

@ -0,0 +1,96 @@
# continue statement
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add `continue` statement to `for`, `while` and `repeat` loops using a context-sensitive keyword to preserve compatibility.
## Motivation
`continue` statement is a feature present in basically all modern programming languages. It's great for ergonomics - often you want the loop to only process items of a specific kind, so you can say `if item.kind ~= "blah" then continue end` in the beginning of the loop.
`continue` never makes code that was previously impossible to write possible, but it makes some code easier to write.
We'd like to add this to Luau but we need to keep backwards compatibility - all existing scripts that parse correctly must parse as they do now. The rest of the proposal outlines the exact syntax and semantics that makes it possible.
## Design
`continue` statement shall be the statement that *starts* with "continue" identifier (*NOT* keyword - effectively it will be a context-sensitive keyword), and such that the *next* token is none of (`.`, `[`, `:`, `{`, `(`, `=`, string literal or ',').
These rules effectively say that continue statement is the statement that *does not* parse as a function call or the beginning of an assignment statement.
This is a continue statement:
```
do
continue
end
```
This is not a continue statement:
```
do
continue = 5
end
```
This is not a continue statement:
```
do
continue(5)
end
```
This is not a continue statement either, why do you ask?
```
do
continue, foo = table.unpack(...)
end
```
These rules are simple to implement. In any Lua parser there is already a point where you have to disambiguate an identifier that starts an assignment statement (`foo = 5`) from an identifier that starts a function call (`foo(5)`). It's one of the few, if not the only, place in the Lua grammar where single token lookahead is not sufficient to parse Lua, because you could have `foo.bar(5)` or `foo.bar=5` or `foo.bar(5)[6] = 7`.
Because of this, we need to parse the entire left hand side of an assignment statement (primaryexpr in Lua's BNF) and then check if it was a function call; if not, we'd expect it to be an assignment statement.
Alternatively in this specific case we could parse "continue", parse the next token, and if it's one of the exclusion list above, roll the parser state back and re-parse the non-continue statement. Our lexer currently doesn't support rollbacks but it's also an easy strategy that other implementations might employ for `continue` specifically.
The rules make it so that the only time we interpret `continue` as a continuation statement is when in the old Lua the program would not have compiled correctly - because this is not valid Lua 5.x:
```
do
continue
end
```
There is one case where this can create new confusion in the newly written code - code like this:
```
do
continue
(foo())(5)
end
```
could be interpreted both as a function call to `continue` (which it is!) and as a continuation statement followed by a function call (which it is not!). Programmers writing this code might expect the second treatment which is wrong.
We have an existing linter rule to prevent this, however *for now* we will solve this in a stronger way:
Once we parse `continue`, we will treat this as a block terminator - similarly to `break`/`return`, we will expect the block to end and the next statement will have to be `end`. This will make sure there's no ambiguity. We may relax this later and rely on the linter to tell people when the code is wrong.
Semantically, continue will work as you would expect - it would skip the rest of the loop body, evaluate the condition for loop continuation (e.g. check the counter value for numeric loops, call the loop iterator for generic loops, evaluate while/repeat condition for while/repeat loops) and proceed accordingly. Locals declared in the loop body would be closed as well.
One special case is the `until` expression: since it has access to the entire scope of `repeat` statement, using `continue` is invalid when it would result in `until` expression accessing local variables that are declared after `continue`.
## Drawbacks
Adding `continue` requires a context-sensitive keyword; this makes editor integration such as syntax highlighting more challenging, as you can't simply assume any occurrence of the word `continue` is referring to the statement - this is different from `break`.
Implementing `continue` requires special care for `until` statement as highlighted in the design, which may make compiler slower and more complicated.
## Alternatives
In later versions of Lua, instead of `continue` you can use `goto`. However, that changes control flow to be unstructured and requires more complex implementation and syntactic changes.

View File

@ -0,0 +1,12 @@
# Extended numeric literal syntax
> Note: this RFC was adapted from an internal proposal that predates RFC process and as such doesn't follow the template precisely
## Design
This proposal suggests extending Lua number syntax with:
1. Binary literals: `0b10101010101`. The prefix is either '0b' or '0B' (to match Lua's '0x' and '0X'). Followed by at least one 1 or 0.
2. Number literal separators: `1_034_123`. We will allow an arbitrary number and arrangement of underscores in all numeric literals, including hexadecimal and binary. This helps with readability of long numbers.
Both of these features are standard in all modern languages, and can help write readable code.

View File

@ -0,0 +1,66 @@
# Type ascriptions
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Implement syntax for type ascriptions using `::`
## Motivation
Luau would like to provide a mechanism for requiring a value to be of a specific type:
```
-- Asserts that the result of a + b is a number.
-- Emits a type error if it isn't.
local foo = (a + b) as number
```
This syntax was proposed in the original Luau syntax proposal. Unfortunately, we discovered that there is a syntactical ambiguity with `as`:
```
-- Two function calls or a type assertion?
foo() as (bar)
```
## Design
To provide this functionality without introducing syntactical confusion, we want to change this syntax to use the `::` symbol instead of `as`:
```
local foo = (a + b) :: number
```
This syntax is borrowed from Haskell, where it performs the same function.
The `::` operator will bind very tightly, like `as`:
```
-- type assertion applies to c, not (b + c).
local a = b + c :: number
```
Note that `::` can only cast a *single* value to a type - not a type pack (multiple values). This means that in the following context, `::` changes runtime behavior:
```
foo(1, bar()) -- passes all values returned by bar() to foo()
foo(1, bar() :: any) -- passes just the first value returned by bar() to foo()
```
## Drawbacks
It's somewhat unusual for Lua to use symbols as operators, with the exception of arithmetics (and `..`). Also a lot of Luau users may be familiar with TypeScript, where the equivalent concept uses `as`.
`::` may make it more difficult for us to use Turbofish (`::<>`) in the future.
## Alternatives
We considered requiring `as` to be wrapped in parentheses, and then relaxing this restriction where there's no chance of syntactical ambiguity:
```
local foo: SomeType = (fn() as SomeType)
-- Parentheses not needed: unambiguous!
bar(foo as number)
```
We decided to not go with this due to concerns about the complexity of the grammar - it requires users to internalize knowledge of our parser to know when they need to surround an `as` expression with parentheses. The rules for when you can leave the parentheses out are somewhat nonintuitive.

View File

@ -0,0 +1,43 @@
# Typed variadics
> Note: this RFC was adapted from an internal proposal that predates RFC process
## Summary
Add syntax for ascribing a type to variadic pack (`...`).
## Motivation
Luau's type checker internally can represent a typed variadic: any number of values of the same type. Developers should be able to describe this construct in their own code, for cases where they have a function that accepts an arbitrary number of `string`s, for example.
## Design
We think that the postfix `...: T` syntax is the best balance of readability and simplicity. In function type annotations, we will use `...T`:
```
function math.max(...: number): number
end
type fn = (...string) -> string
type fn2 = () -> ...string
```
This doesn't introduce syntactical ambiguity and should cover all cases where we need to represent this construct. Like `...` itself, this syntax is only legal as the last parameter to a function.
Like all type annotations, the `...: T` syntax has no effect on runtime behavior versus an unannotated `...`.
There are currently no plans to introduce named variadics, but this proposal leaves room to adopt them with the form `...name: Type` in function declarations in the future.
## Drawbacks
The mismatch between the type of `...` in function declaration (`number`) and type declaration (`...number`) is a bit awkward. This also gets more complicated when we introduce generic variadic packs.
## Alternatives
We considered several other syntaxes for this construct:
* `...T`: leaves no room to introduce named variadics
* `...: T...`: redundant `...`
* `... : ...T`: feels redundant, same as above
* `...: T*`: potentially confusing for users with C knowledge, where `T*` is a pointer type