Merge branch 'master' into merge

This commit is contained in:
Arseny Kapoulkine 2022-01-06 14:11:50 -08:00
commit bdf81c0ed1
9 changed files with 558 additions and 93 deletions

View File

@ -66,22 +66,24 @@ jobs:
coverage:
runs-on: ubuntu-latest
env:
NODE_COVERALLS_DEBUG: 1
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: install
run: |
sudo apt install llvm
- name: make coverage
run: |
CXX=clang++-10 make -j2 config=coverage coverage
- name: debug coverage
run: |
git status
git log -5
echo SHA: $GITHUB_SHA
- name: upload coverage
uses: coverallsapp/github-action@master
with:
path-to-lcov: ./coverage.info
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: false
- uses: actions/upload-artifact@v2
with:
name: coverage

View File

@ -1,50 +1,100 @@
<form>
<div>
<label>Script:</label>
<label class="header-center"><b>Input</b></label>
<br>
<textarea rows="10" cols="70" id="script">print("Hello World!")</textarea>
<br><br>
<button onclick="clearInput(); return false;">
Clear Input
</button>
<button onclick="executeScript(); return false;">
Run
</button>
<textarea rows="10" cols="70" id="script"></textarea>
<div class="button-group">
<button onclick="executeScript(); return false;" class="demo-button">Run</button>
<input type="checkbox" checked="true" class="demo-button" id="output-clear" />
<label for="output-clear">Clear Output</label>
</div>
</div>
<br><br>
<div>
<label>Output:</label>
<label class="header-center"><b>Output</b></label>
<br>
<textarea readonly rows="10" cols="70" id="output"></textarea>
<br><br>
<button onclick="clearOutput(); return false;">
Clear Output
</button>
</div>
</form>
<!-- Styles for editor -->
<style>
.header-center {
text-align: center;
}
.demo-button {
padding: 7px 7px;
vertical-align: middle;
}
.line-error {
background: #e65f55;
}
</style>
<!-- CodeMirror -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.0/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.0/addon/edit/matchbrackets.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.0/codemirror.min.css" />
<!-- Luau Parser for CodeMirror -->
<script src="assets/js/luau_mode.js"></script>
<!-- CodeMirror Luau Editor (MUST BE LOADED AFTER CODEMIRROR!) -->
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("script"), {
theme: "default",
mode: "luau",
matchBrackets: true,
lineNumbers: true,
smartIndent: true,
indentWithTabs: true,
indentUnit: 4,
});
editor.setValue("print(\"Hello World!\")\n");
editor.addKeyMap({
"Ctrl-Enter": function(cm) {
executeScript();
},
"Shift-Tab": function (cm) {
// dedent functionality
cm.execCommand("indentLess");
}
});
var lastError = undefined;
function output(text) {
document.getElementById("output").value += "[" + new Date().toLocaleTimeString() + "] " + text.replace('stdin:', '') + "\n";
var output_box = document.getElementById("output");
output_box.value += text.replace('stdin:', '') + "\n";
// scroll to bottom
output_box.scrollTop = output_box.scrollHeight;
}
var Module = {
'print': function (msg) { output(msg) }
};
function clearInput() {
document.getElementById("script").value = "";
}
function clearOutput() {
document.getElementById("output").value = "";
}
function executeScript() {
var err = Module.ccall('executeScript', 'string', ['string'], [document.getElementById("script").value]);
if (lastError) {
editor.removeLineClass(lastError, "background", "line-error");
lastError = undefined;
}
var output_clear = document.getElementById("output-clear");
if (output_clear.checked) {
var output_box = document.getElementById("output");
output_box.value = '';
}
var err = Module.ccall('executeScript', 'string', ['string'], [editor.getValue()]);
if (err) {
output('Error:' + err.replace('stdin:', ''));
var err_text = err.replace('stdin:', '');
output('Error:' + err_text);
var err_line = parseInt(err_text);
if (err_line) {
lastError = editor.addLineClass(err_line-1, "background", "line-error");
}
}
}
</script>
<!-- Luau WASM (async fetch; should be the last line) -->
<script async src="https://github.com/Roblox/luau/releases/latest/download/Luau.Web.js"></script>

View File

@ -1,6 +1,7 @@
---
permalink: /demo
title: Demo
classes: wide
---
{% include repl.html %}

View File

@ -1,7 +1,7 @@
---
permalink: /grammar
title: Grammar
toc: true
classes: wide
---
This is the complete syntax grammar for Luau in EBNF. More information about the terminal nodes String and Number
@ -10,73 +10,73 @@ is available in the [syntax section](syntax).
> Note: this grammar is currently missing type pack syntax for generic arguments
```ebnf
chunk ::= {stat [`;']} [laststat [`;']]
block ::= chunk
stat ::= varlist `=' explist |
chunk = block
block = {stat [';']} [laststat [';']]
stat = varlist '=' explist |
var compoundop exp |
functioncall |
do block end |
while exp do block end |
repeat block until exp |
if exp then block {elseif exp then block} [else block] end |
for binding `=' exp `,' exp [`,' exp] do block end |
for bindinglist in explist do block end |
function funcname funcbody |
local function NAME funcbody |
local bindinglist [`=' explist] |
[export] type NAME [`<' GenericTypeList `>'] `=' Type
'do' block 'end' |
'while' exp 'do' block 'end' |
'repeat' block 'until' exp |
'if' exp 'then' block {'elseif' exp 'then' block} ['else' block] 'end' |
'for' binding '=' exp ',' exp [',' exp] 'do' block 'end' |
'for' bindinglist 'in' explist 'do' block 'end' |
'function' funcname funcbody |
'local' 'function' NAME funcbody |
'local' bindinglist ['=' explist] |
['export'] type NAME ['<' GenericTypeList '>'] '=' Type
laststat ::= return [explist] | break | continue
laststat = 'return' [explist] | 'break' | 'continue'
funcname ::= NAME {`.' NAME} [`:' NAME]
funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end
parlist ::= bindinglist [`,' `...'] | `...'
funcname = NAME {'.' NAME} [':' NAME]
funcbody = '(' [parlist] ')' [':' ReturnType] block 'end'
parlist = bindinglist [',' '...'] | '...'
explist ::= {exp `,'} exp
namelist ::= NAME {`,' NAME}
explist = {exp ','} exp
namelist = NAME {',' NAME}
binding ::= NAME [`:' TypeAnnotation]
bindinglist ::= binding [`,' bindinglist] (* equivalent of Lua 5.1 `namelist`, except with optional type annotations *)
binding = NAME [':' TypeAnnotation]
bindinglist = binding [',' bindinglist] (* equivalent of Lua 5.1 'namelist', except with optional type annotations *)
var ::= NAME | prefixexp `[' exp `]' | prefixexp `.' Name
varlist ::= var {`,' var}
prefixexp ::= var | functioncall | `(' exp `)'
functioncall ::= prefixexp funcargs | prefixexp `:' NAME funcargs
var = NAME | prefixexp '[' exp ']' | prefixexp '.' Name
varlist = var {',' var}
prefixexp = var | functioncall | '(' exp ')'
functioncall = prefixexp funcargs | prefixexp ':' NAME funcargs
exp ::= (asexp | unop exp) { binop exp }
ifelseexp ::= if exp then exp {elseif exp then exp} else exp
asexp ::= simpleexp [`::' Type]
simpleexp ::= NUMBER | STRING | nil | true | false | `...' | tableconstructor | function body | prefixexp | ifelseexp
funcargs ::= `(' [explist] `)' | tableconstructor | STRING
exp = (asexp | unop exp) { binop exp }
ifelseexp = 'if' exp 'then' exp {'elseif' exp 'then' exp} 'else' exp
asexp = simpleexp ['::' Type]
simpleexp = NUMBER | STRING | 'nil' | 'true' | 'false' | '...' | tableconstructor | 'function' body | prefixexp | ifelseexp
funcargs = '(' [explist] ')' | tableconstructor | STRING
tableconstructor ::= `{' [fieldlist] `}'
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= `[' exp `]' `=' exp | NAME `=' exp | exp
fieldsep ::= `,' | `;'
tableconstructor = '{' [fieldlist] '}'
fieldlist = field {fieldsep field} [fieldsep]
field = '[' exp ']' '=' exp | NAME '=' exp | exp
fieldsep = ',' | ';'
compoundop :: `+=' | `-=' | `*=' | `/=' | `%=' | `^=' | `..='
binop ::= `+' | `-' | `*' | `/' | `^' | `%' | `..' | `<' | `<=' | `>' | `>=' | `==' | `~=' | and | or
unop ::= `-' | not | `#'
compoundop :: '+=' | '-=' | '*=' | '/=' | '%=' | '^=' | '..='
binop = '+' | '-' | '*' | '/' | '^' | '%' | '..' | '<' | '<=' | '>' | '>=' | '==' | '~=' | 'and' | 'or'
unop = '-' | 'not' | '#'
SimpleType ::=
nil |
NAME[`.' NAME] [ `<' TypeList `>' ] |
`typeof' `(' exp `)' |
SimpleType =
'nil' |
NAME ['.' NAME] [ '<' TypeList '>' ] |
'typeof' '(' exp ')' |
TableType |
FunctionType
Type ::=
SimpleType [`?`] |
SimpleType [`|` Type] |
SimpleType [`&` Type]
Type =
SimpleType ['?'] |
SimpleType ['|' Type] |
SimpleType ['&' Type]
GenericTypeList ::= NAME [`...'] {`,' NAME [`...']}
TypeList ::= Type [`,' TypeList] | ...Type
ReturnType ::= Type | `(' TypeList `)'
TableIndexer ::= `[' Type `]' `:' Type
TableProp ::= NAME `:' Type
TablePropOrIndexer ::= TableProp | TableIndexer
PropList ::= TablePropOrIndexer {fieldsep TablePropOrIndexer} [fieldsep]
TableType ::= `{' PropList `}'
FunctionType ::= [`<' GenericTypeList `>'] `(' [TypeList] `)' `->` ReturnType
GenericTypeList = NAME ['...'] {',' NAME ['...']}
TypeList = Type [',' TypeList] | '...' Type
ReturnType = Type | '(' TypeList ')'
TableIndexer = '[' Type ']' ':' Type
TableProp = NAME ':' Type
TablePropOrIndexer = TableProp | TableIndexer
PropList = TablePropOrIndexer {fieldsep TablePropOrIndexer} [fieldsep]
TableType = '{' PropList '}'
FunctionType = ['<' GenericTypeList '>'] '(' [TypeList] ')' '->' ReturnType
```

View File

@ -19,10 +19,10 @@ function assert<T>(value: T, message: string?): T
```
`assert` checks if the value is truthy; if it's not (which means it's `false` or `nil`), it raises an error. The error message can be customized with an optional parameter.
Upon success the function returns the `condition` argument.
Upon success the function returns the `value` argument.
```
function error(object: any, level: number?)
function error(obj: any, level: number?)
```
`error` raises an error with the specified object. Note that errors don't have to be strings, although they often are by convention; various error handling mechanisms like `pcall`
@ -110,7 +110,7 @@ Changes metatable for the given table. Note that unlike `getmetatable`, this fun
function tonumber(s: string, base: number?): number?
```
Converts the input string to the number in base `base` (default 10) and returns the resulting number. If the conversion fails, returns `nil` instead.
Converts the input string to the number in base `base` (default 10) and returns the resulting number. If the conversion fails (that is, if the input string doesn't represent a valid number in the specified base), returns `nil` instead.
```
function tostring(obj: any): string
@ -161,7 +161,7 @@ Note that `f` can yield, which results in the entire coroutine yielding as well.
function unpack<V>(a: {V}, f: number?, t: number?): ...V
```
Returns all values of `a` with indices in `[f..t]` range. `f` defaults to 1 and `t` defaults to `#a`.
Returns all values of `a` with indices in `[f..t]` range. `f` defaults to 1 and `t` defaults to `#a`. Note that this is equivalent to `table.unpack`.
## math library
@ -346,13 +346,14 @@ Returns 3D Perlin noise value for the point `(x, y, z)` (`y` and `z` default to
function math.clamp(n: number, min: number, max: number): number
```
Returns `n` if the number is in `[min, max]` range; otherwise, returns `min` when `n < min`, and `max` otherwise. The function errors if `min > max`.
Returns `n` if the number is in `[min, max]` range; otherwise, returns `min` when `n < min`, and `max` otherwise. If `n` is NaN, may or may not return NaN.
The function errors if `min > max`.
```
function math.sign(n: number): number
```
Returns `-1` if `n` is negative, `1` if `n` is positive, and `0` if `n` is zero.
Returns `-1` if `n` is negative, `1` if `n` is positive, and `0` if `n` is zero or NaN.
```
function math.round(n: number): number
@ -384,7 +385,7 @@ Iterates over numeric keys of the table in `[1..#t]` range in order; for each ke
function table.getn<V>(t: {V}): number
```
Returns the length of table `t` (aka `#t`).
Returns the length of table `t` (equivalent to `#t`).
```
function table.maxn<V>(t: {V}): number
@ -518,7 +519,7 @@ When `f` is a string, the substitution uses the string as a replacement. When `f
function string.len(s: string): number
```
Returns the number of bytes in the string.
Returns the number of bytes in the string (equivalent to `#s`).
```
function string.lower(s: string): string

167
docs/assets/js/luau_mode.js Normal file
View File

@ -0,0 +1,167 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
// Luau mode. Based on Lua mode from CodeMirror and Franciszek Wawrzak (https://codemirror.net/mode/lua/lua.js)
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("luau", function(_, parserConfig) {
var indentUnit = 4;
function prefixRE(words) {
return new RegExp("^(?:" + words.join("|") + ")", "i");
}
function wordRE(words) {
return new RegExp("^(?:" + words.join("|") + ")$", "i");
}
var specials = wordRE(parserConfig.specials || ["type"]);
// long list of standard functions from lua manual
var builtins = wordRE([
"_G","_VERSION","assert","error","getfenv","getmetatable","ipairs","load", "loadstring","next","pairs","pcall",
"print","rawequal","rawget","rawset","require","select","setfenv","setmetatable","tonumber","tostring","type",
"unpack","xpcall",
"coroutine.create","coroutine.resume","coroutine.running","coroutine.status","coroutine.wrap","coroutine.yield",
"debug.debug","debug.getfenv","debug.gethook","debug.getinfo","debug.getlocal","debug.getmetatable",
"debug.getregistry","debug.getupvalue","debug.setfenv","debug.sethook","debug.setlocal","debug.setmetatable",
"debug.setupvalue","debug.traceback",
"math.abs","math.acos","math.asin","math.atan","math.atan2","math.ceil","math.cos","math.cosh","math.deg",
"math.exp","math.floor","math.fmod","math.frexp","math.huge","math.ldexp","math.log","math.log10","math.max",
"math.min","math.modf","math.pi","math.pow","math.rad","math.random","math.randomseed","math.sin","math.sinh",
"math.sqrt","math.tan","math.tanh",
"os.clock","os.date","os.difftime","os.execute","os.exit","os.getenv","os.remove","os.rename","os.setlocale",
"os.time","os.tmpname",
"string.byte","string.char","string.dump","string.find","string.format","string.gmatch","string.gsub",
"string.len","string.lower","string.match","string.rep","string.reverse","string.sub","string.upper",
"table.concat","table.insert","table.maxn","table.remove","table.sort"
]);
var keywords = wordRE(["and","break","elseif","false","nil","not","or","return",
"true","function", "end", "if", "then", "else", "do",
"while", "repeat", "until", "for", "in", "local", "continue" ]);
var indentTokens = wordRE(["function", "if","repeat","do", "\\(", "{"]);
var dedentTokens = wordRE(["end", "until", "\\)", "}"]);
var dedentPartial = prefixRE(["end", "until", "\\)", "}", "else", "elseif"]);
function readBracket(stream) {
var level = 0;
while (stream.eat("=")) ++level;
stream.eat("[");
return level;
}
function normal(stream, state) {
var ch = stream.next();
if (ch == "-" && stream.eat("-")) {
if (stream.eat("[") && stream.eat("["))
return (state.cur = bracketed(readBracket(stream), "comment"))(stream, state);
stream.skipToEnd();
return "comment";
}
if (ch == "\"" || ch == "'")
return (state.cur = string(ch))(stream, state);
if (ch == "[" && /[\[=]/.test(stream.peek()))
return (state.cur = bracketed(readBracket(stream), "string"))(stream, state);
if (/\d/.test(ch)) {
stream.eatWhile(/[\w.%]/);
return "number";
}
if (/[\w_]/.test(ch)) {
stream.eatWhile(/[\w\\\-_.]/);
return "variable";
}
return null;
}
function bracketed(level, style) {
return function(stream, state) {
var curlev = null, ch;
while ((ch = stream.next()) != null) {
if (curlev == null) {
if (ch == "]") curlev = 0;
} else if (ch == "=") {
++curlev;
} else if (ch == "]" && curlev == level) {
state.cur = normal;
break;
} else {
curlev = null;
}
}
return style;
};
}
function string(quote) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
if (ch == quote && !escaped) {
break;
}
escaped = !escaped && ch == "\\";
}
if (!escaped) {
state.cur = normal;
}
return "string";
};
}
return {
startState: function(basecol) {
return {basecol: basecol || 0, indentDepth: 0, cur: normal};
},
token: function(stream, state) {
if (stream.eatSpace()) {
return null;
}
var style = state.cur(stream, state);
var word = stream.current();
if (style == "variable") {
if (keywords.test(word)) {
style = "keyword";
} else if (builtins.test(word)) {
style = "builtin";
} else if (specials.test(word)) {
style = "variable-2";
}
}
if ((style != "comment") && (style != "string")) {
if (indentTokens.test(word)) {
++state.indentDepth;
} else if (dedentTokens.test(word)) {
--state.indentDepth;
}
}
return style;
},
indent: function(state, textAfter) {
var closing = dedentPartial.test(textAfter);
return state.basecol + indentUnit * (state.indentDepth - (closing ? 1 : 0));
},
electricInput: /^\s*(?:end|until|else|\)|\})$/,
lineComment: "--",
blockCommentStart: "--[[",
blockCommentEnd: "]]"
}});
CodeMirror.defineMIME("text/x-luau", "luau");
});

View File

@ -0,0 +1,102 @@
# Safe navigation postfix operator (?)
## Summary
Introduce syntax to navigate through `nil` values, or short-circuit with `nil` if it was encountered.
## Motivation
nil values are very common in Lua, and take care to prevent runtime errors.
Currently, attempting to index `dog.name` while caring for `dog` being nil requires some form of the following:
```lua
local dogName = nil
if dog ~= nil then
dogName = dog.name
end
```
...or the unusual to read...
```lua
local dogName = dog and dog.name
```
...which will return `false` if `dog` is `false`, instead of throwing an error because of the index of `false.name`.
Luau provides the if...else expression making this turn into:
```lua
local dogName = if dog == nil then nil else dog.name
```
...but this is fairly clunky for such a common expression.
## Design
The safe navigation operator will make all of these smooth, by supporting `x?.y` to safely index nil values. `dog?.name` would resolve to `nil` if `dog` was nil, or the name otherwise.
The previous example turns into `local dogName = dog?.name` (or just using `dog?.name` elsewhere).
Failing the nil-safety check early would make the entire expression nil, for instance `dog?.body.legs` would resolve to `nil` if `dog` is nil, rather than resolve `dog?.body` into nil, then turning into `nil.legs`.
```lua
dog?.name --[[ is the same as ]] if dog == nil then nil else dog.name
```
The short-circuiting is limited within the expression.
```lua
dog?.owner.name -- This will return nil if `dog` is nil
(dog?.owner).name -- `(dog?.owner)` resolves to nil, of which `name` is then indexed. This will error at runtime if `dog` is nil.
dog?.legs + 3 -- `dog?.legs` is resolved on its own, meaning this will error at runtime if it is nil (`nil + 3`)
```
The operator must be used in the context of either a call or an index, and so:
```lua
local value = x?
```
...would be invalid syntax.
This syntax would be based on expressions, and not identifiers, meaning that `(x or y)?.call()` would be valid syntax.
### Type
If the expression is typed as an optional, then the resulting type would be the final expression, also optional. Otherwise, it'll just be the resulting type if `?` wasn't used.
```lua
local optionalObject: { name: string }?
local optionalObjectName = optionalObject?.name -- resolves to `string?`
local nonOptionalObject: { name: string }
local nonOptionalObjectName = nonOptionalObject?.name -- resolves to `string`
```
### Calling
This RFC only specifies `x?.y` as an index method. `x?:y()` is currently unspecified, and `x?.y(args)` as a syntax will be reserved (will error if you try to use it).
While being able to support `dog?.getName()` is useful, it provides [some logistical issues for the language](https://github.com/Roblox/luau/pull/142#issuecomment-990563536).
`x?.y(args)` will be reserved both so that this can potentially be resolved later down the line if something comes up, but also because it would be a guaranteed runtime error under this RFC: `dog?.getName()` will first index `dog?.getName`, which will return nil, then will attempt to call it.
### Assignment
`x?.y = z` is not supported, and will be reported as a syntax error.
## Drawbacks
As with all syntax additions, this adds complexity to the parsing of expressions, and the execution of cancelling the rest of the expression could prove challenging.
Furthermore, with the proposed syntax, it might lock off other uses of `?` within code (and not types) for the future as being ambiguous.
## Alternatives
Doing nothing is an option, as current standard if-checks already work, as well as the `and` trick in other use cases, but as shown before this can create some hard to read code, and nil values are common enough that the safe navigation operator is welcome.
Supporting optional calls/indexes, such as `x?[1]` and `x?()`, while not out of scope, are likely too fringe to support, while adding on a significant amount of parsing difficulty, especially in the case of shorthand function calls, such as `x?{}` and `x?""`.
It is possible to make `x?.y = z` resolve to only setting `x.y` if `x` is nil, but assignments silently failing can be seen as surprising.

View File

@ -0,0 +1,76 @@
# Unsealed table literals
## Summary
Currently the only way to create an unsealed table is as an empty table literal `{}`.
This RFC proposes making all table literals unsealed.
## Motivation
Table types can be *sealed* or *unsealed*. These are different in that:
* Unsealed table types are *precise*: if a table has unsealed type `{ p: number, q: string }`
then it is guaranteed to have only properties `p` and `q`.
* Sealed tables support *width subtyping*: if a table has sealed type `{ p: number }`
then it is guaranteed to have at least property `p`, so we allow `{ p: number, q: string }`
to be treated as a subtype of `{ p: number }`
* Unsealed tables can have properties added to them: if `t` has unsealed type
`{ p: number }` then after the assignment `t.q = "hi"`, `t`'s type is updated to be
`{ p: number, q: string }`.
* Unsealed tables are subtypes of sealed tables.
Currently the only way to create an unsealed table is using an empty table literal, so
```lua
local t = {}
t.p = 5
t.q = "hi"
```
typechecks, but
```lua
local t = { p = 5 }
t.q = "hi"
```
does not.
This causes problems in examples, in particular developers
may initialize properties but not methods:
```lua
local t = { p = 5 }
function t.f() return t.p end
```
## Design
The proposed change is straightforward: make all table literals unsealed.
## Drawbacks
Making all table literals unsealed is a conservative change, it only removes type errors.
It does encourage developers to add new properties to tables during initialization, which
may be considered poor style.
It does mean that some spelling mistakes will not be caught, for example
```lua
local t = {x = 1, y = 2}
if foo then
t.z = 3 -- is z a typo or intentional 2-vs-3 choice?
end
```
In particular, we no longer warn about adding properties to array-like tables.
```lua
local a = {1,2,3}
a.p = 5
```
## Alternatives
We could introduce a new table state for unsealed-but-precise
tables. The trade-off is that that would be more precise, at the cost
of adding user-visible complexity to the type system.
We could continue to treat array-like tables as sealed.

View File

@ -0,0 +1,66 @@
# Only strip optional properties from unsealed tables during subtyping
## Summary
Currently subtyping allows optional properties to be stripped from table types during subtyping.
This RFC proposes only allowing that when the subtype is unsealed and the supertype is sealed.
## Motivation
Table types can be *sealed* or *unsealed*. These are different in that:
* Unsealed table types are *precise*: if a table has unsealed type `{ p: number, q: string }`
then it is guaranteed to have only properties `p` and `q`.
* Sealed tables support *width subtyping*: if a table has sealed type `{ p: number }`
then it is guaranteed to have at least property `p`, so we allow `{ p: number, q: string }`
to be treated as a subtype of `{ p: number }`
* Unsealed tables can have properties added to them: if `t` has unsealed type
`{ p: number }` then after the assignment `t.q = "hi"`, `t`'s type is updated to be
`{ p: number, q: string }`.
* Unsealed tables are subtypes of sealed tables.
Currently we allow subtyping to strip away optional fields
as long as the supertype is sealed.
This is necessary for examples, for instance:
```lua
local t : { p: number, q: string? } = { p = 5, q = "hi" }
t = { p = 7 }
```
typechecks because `{ p : number }` is a subtype of
`{ p : number, q : string? }`. Unfortunately this is not sound,
since sealed tables support width subtyping:
```lua
local t : { p: number, q: string? } = { p = 5, q = "hi" }
local u : { p: number } = { p = 5, q = false }
t = u
```
## Design
The fix for this source of unsoundness is twofold:
1. make all table literals unsealed, and
2. only allow stripping optional properties from when the
supertype is sealed and the subtype is unsealed.
This RFC is for (2). There is a [separate RFC](unsealed-table-literals.md) for (1).
## Drawbacks
This introduces new type errors (it has to, since it is fixing a source of
unsoundness). This means that there are now false positives such as:
```lua
local t : { p: number, q: string? } = { p = 5, q = "hi" }
local u : { p: number } = { p = 5, q = "lo" }
t = u
```
These false positives are so similar to sources of unsoundness
that it is difficult to see how to allow them soundly.
## Alternatives
We could just live with unsoundness.