- Introduced a new endpoint `/api/syncback` for one-click full sync from Roblox Studio to the filesystem, eliminating the need for an active WebSocket connection. - Implemented `SyncbackRequest` and `ServiceChunk` structures to handle service-level properties and child instances during syncback. - Enhanced `encodeService` to include ObjectValue carriers for accurate reference handling and prevent data loss. - Updated the syncback process to utilize Luau-style relative paths for reference attributes, improving path management and consistency. - Integrated session caching in the matching algorithm to optimize performance during syncback operations. These changes significantly improve the live syncback experience, ensuring reliable data synchronization and enhanced performance during development.
10 KiB
New non-strict mode
Summary
Currently, strict mode and non-strict mode infer different types for the same program. With this feature, strict and non-strict modes will share the local type inference engine, and the only difference between the modes will be in which errors are reported.
Motivation
Having two different type inference engines is unnecessarily confusing, and can result in unexpected behaviors such as changing the mode of a module can cause errors in the users of that module.
The current non-strict mode infers very coarse types (e.g. all local
variables have type any) and so is not appropriate for type-driven
tooling, which results in expensively and inconsistently using
different modes for different tools.
Design
Code defects
The main goal of non-strict mode is to minimize false positives, that is if non-strict mode reports an error, then we have high confidence that there is a code defect. Example defects are:
- Run-time errors
- Dead code
- Using an expression whose only possible value is
nil - Writing to a table property that is never read
Run-time errors: this is an obvious defect. Examples include:
- Built-in operators (
"hi" + 5) - Luau APIs (
math.abs("hi")) - Function calls from embeddings (
CFrame.new("hi")) - Missing properties from embeddings (
CFrame.new().nope)
Detecting run-time errors is undecidable, for example
if cond() then
math.abs(“hi”)
end
It is undecidable whether this code produces a run-time error, but we
do know that if math.abs("hi") is executed, it will produce a
run-time error, and so report a type error in this case.
Expressions guaranteed to be nil: Luau tables do not error when a
missing property is accessed (though embeddings may). So something
like
local t = { Foo = 5 }
local x = t.Fop
won’t produce a run-time error, but is more likely than not a
programmer error. In this case, if the programmer intent was to
initialize x as nil, they could have written
local t = { Foo = 5 }
local x = nil
For this reason, we consider it a code defect to use a value that the
type system guarantees is of type nil.
Writing properties that are never read: There is a matching problem with misspelling properties when writing. For example
function f()
local t = {}
t.Foo = 5
t.Fop = 7
print(t.Foo)
end
won’t produce a run-time error, but is more likely than not a
programmer error, since t.Fop is written but never read. We can use
read-only and write-only table properties for this, and make it an
error to create a write-only property.
We have to be careful about this though, because if f ended with
return t, then it would be a perfectly sensible function with type
() -> { Foo: number, Fop: number }. The only way to detect that Fop
was never read would be whole-program analysis, which is prohibitively
expensive.
Implicit coercions: Luau supports various implicit coercions, such
as allowing math.abs("-12"). These should be reported as defects.
New Non-strict error reporting
The difficult part of non-strict mode error-reporting is detecting
guaranteed run-time errors. We can do this using an error-reporting
pass that generates a type context such that if any of the x : T in
the type context are satisfied, then the program is guaranteed to
produce a type error.
For example in the program
function h(x, y)
math.abs(x)
string.lower(y)
end
an error is reported when x isn’t a number, or y isn’t a string, so the generated context is
x : ~number
y : ~string
In the function:
function f(x)
math.abs(x)
string.lower(x)
end
an error is reported when x isn’t a number or isn’t a string, so the constraint set is
x : ~number | ~string
Since ~number | ~string is equivalent to unknown, non-strict mode
can report a warning, since calling the function is guaranteed to
throw a run-time error. In contrast:
function g(x)
if cond() then
math.abs(x)
else
string.lower(x)
end
end
generates context
x : ~number & ~string
Since ~number & ~string is not equivalent to unknown, non-strict mode reports no warning.
- The disjunction of contexts
C1andC2containsx : T1|T2, wherex : T1is inC1andx : T2is inC2. - The conjunction of contexts
C1andC2containsx : T1&T2, wherex : T1is inC1andx : T2is inC2.
The context generated by a block is:
x = Egenerates the context ofE : never.B1; B2generates the disjunction of the context ofB1and the context ofB2.if C then B1 else B2end generates the conjunction of the context ofB1and the context ofB2.local x; Bgenerates the context ofB, removing the constraintx : T. If the type inferred forxis a subtype ofT, then issue a warning.function f(x1,...,xN) B endgenerates the context forB, removingx1 : T1, ..., xN : TN. If any of theTiare equivalent tounknown, then issue a warning.
The constraint set generated by a typed expression is:
- The context generated by
x : Tisx : T. - The context generated by
s : T(wheresis a scalar) is trivial. Issue a warning ifshas typeT. - The context generated by
F(M1, ..., MN) : Tis the disjunction of the contexts generated byF : ~function, and byM1 : T1, ...,MN : TNwhere for eachi,Fhas an overload(unknown^(i-1),Ti,unknown^(N-i)) -> error. (PickTito beneverif no such overload exists). Issue a warning ifFhas an overload(unknown^N) -> SwhereS <: (T | error). - The context generated by
M.pis the context generated byM : ~table. - The context generated by
M[N]is the disjunction of the contexts generated byM : ~tableandN : never.
For example:
- The context generated by
math.abs("hi") : neveris- the context generated by
"hi" : ~number, sincemath.abshas an overload(~number) -> error, which is trivial. - A warning is issued since
"hi"has type~number.
- the context generated by
- The context generated by
function f(x) math.abs(x); string.lower(x) endis- the context generated by
math.abs(x); string.lower(x)which is the disjunction of- the context generated by
math.abs(x), which is- the context
x : ~number, sincemath.abshas an overload(~number)->error
- the context
- the context generated by
string.lower(x), which is- the context
x : ~string, sincestring.lowerhas an overload(~string)->error
- the context
- the context generated by
- remove the binding
x : (~number | ~string) - A warning is issued since
(~number | ~string)is equivalent tounknown.
- the context generated by
- The context generated by
math.abs(string.lower(x))is- the context generated by
string.lower(x) : ~number, sincemath.abshas an overload(~number)->error, which is- the text
x : ~string, sincestring.lowerhas an overload(~string)->error. - An warning is issued, since
string.lowerhas an overload(unknown) -> (string | error)and(string | error) <: (~number | error).
- the text
- the context generated by
Ergonomics
Error reporting. A straightforward implementation of this design issues warnings at the point that data flows into a place guaranteed to later produce a run-time error, which may not be perfect ergonomics. For example, in the program:
local x
if cond() then
x = 5
else
x = nil
end
string.lower(x)
- the type inferred for
xisnumber?and the context generated is `x - ~string
. Sincenumber? <: ~string, a warning is issued at the declarationlocal x. For ergonomics, we might want to identify eitherstring.lower(x)orx = 5` (or both!) in the error report.
Stringifying checked functions. This design depends on functions
having overloads with error return type. This integrates with
type error suppression, but would not be
a perfect way to present types to users. A common case is that the
checked type is the negation of the function type, for example the
type of math.abs:
(number) -> number & (~number) -> error
This might be better presented as an annotation on the argument type, something like:
@checked (number) -> number
The type
@checked (S1,...,SN) -> T
is equivalent to
(S1,...,SN) -> T
& (~S1, unknown^N-1) -> error
& (unknown, ~S2, unknown^N-2) -> error
...
& (unknown^N-1, SN) -> error
As a further extension, we might allow users to explicitly provide @checked type annotations.
Checked functions are known as strong functions in Elixir.
Drawbacks
This is a breaking change, since it results in more errors being issued.
Strict mode infers more precise (and hence more complex) types than current non-strict mode, which are presented by type error messages and tools such as type hover.
Alternatives
Success typing (used in Erlang Dialyzer) is the nearest existing
solution. It is similar to this design, except that it only works in
(the equivalent of) non-strict mode. The success typing function type
(S)->T is the equivalent of our
(~S)->error & (unknown)->(T|error).
We could put the @checked annotation on individual function argments
rather than the function type.
We could use this design to infer checked functions. In function
f(x1, ..., xN) B end, we could generate the context
(x1 : T1, ..., xN : TN) for B, and add an overload
(unknown^(i-1),Ti,unknown^(N-i))->error to the inferred type of f. For
example, for the function
function h(x, y)
math.abs(x)
string.lower(y)
end
We would infer type
(number, string) -> ()
& (~number, unknown) -> error
& (unknown, ~string) -> error
which is the same as
@checked (number, string) -> ()
The problem with doing this is what to do about recursive functions.
References
Lily Brown, Andy Friesen and Alan Jeffrey Position Paper: Goals of the Luau Type System, in HATRA '21: Human Aspects of Types and Reasoning Assistants, 2021. https://doi.org/10.48550/arXiv.2109.11397
Giuseppe Castagna, Guillaume Duboc, José Valim The Design Principles of the Elixir Type System, 2023. https://doi.org/10.48550/arXiv.2306.06391
Tobias Lindahl and Konstantinos Sagonas, Practical Type Inference Based on Success Typings, in PPDP '06: Principles and Practice of Declarative Programming, pp. 167–178, 2006. https://doi.org/10.1145/1140335.1140356