Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 29 additions & 27 deletions generated/secondlife.d.luau
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,27 @@ export type PrimParamsSetterType = typeof(
)


declare function assert<T>(value: T?, errorMessage: string?): T
-- declare function assert<T>(value: T?, errorMessage: string?): T -- magic type
declare function dangerouslyexecuterequiredmodule(f: (...any) -> ...any): ...any
declare function error<T>(message: T, level: number?): never
declare function gcinfo(): number
declare getfenv: nil
declare function getmetatable<T>(obj: T): getmetatable<T>
-- declare function getmetatable<T>(obj: T): getmetatable<T> -- builtin
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number?, V), {V}, number)
declare loadstring: nil
declare function newproxy(mt: boolean?): any
declare function next<K, V>(t: {[K]: V}, i: K?): (K?, V)
declare function pairs<K, V>(t: {[K]: V}): (({[K]: V}, K?) -> (K?, V), {[K]: V}, K)
-- declare function next<K, V>(t: {[K]: V}, i: K?): (K?, V) -- builtin
-- declare function pairs<K, V>(t: {[K]: V}): (({[K]: V}, K?) -> (K?, V), {[K]: V}, K) -- builtin
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
declare function print<T...>(...: T...): ()
declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(t: {[K]: V}, k: K): V?
declare function rawlen<K, V>(t: {[K]: V} | string): number
declare function rawset<K, V>(t: {[K]: V}, k: K, v: V): {[K]: V}
declare function require(target: any): any
declare function select<A...>(i: string | number, ...: A...): ...any
-- declare function require(target: any): any -- magic type
-- declare function select<A...>(i: string | number, ...: A...): ...any -- magic type
declare setfenv: nil
declare function setmetatable<T, MT>(t: T, mt: MT): setmetatable<T, MT>
-- declare function setmetatable<T, MT>(t: T, mt: MT): setmetatable<T, MT> -- builtin, magic type
declare function tonumber(value: string? | number, base: number?): number?
declare function toquaternion(value: string? | quaternion): quaternion?
declare function torotation(value: string? | quaternion): quaternion?
Expand Down Expand Up @@ -466,31 +466,33 @@ declare quaternion: ((x: number, y: number, z: number, s: number) -> quaternion)
toup: (q: quaternion) -> vector,
}

--[[ commented out to avoid shadowing magic type functions find, format, gmatch, and match

---------------------------
-- Global Table: string
---------------------------

declare string: {
byte: (s: string, i: number?, j: number?) -> ...number,
char: (...number) -> string,
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string),
format: (formatstring: string, ...any) -> string,
gmatch: (s: string, pattern: string) -> () -> ...string,
gsub: (s: string, pattern: string, repl: string | { [string]: string } | (...string) -> string, maxn: number?) -> (string, number),
len: (s: string) -> number,
lower: (s: string) -> string,
match: (s: string, pattern: string, init: number?) -> ...string,
pack: (fmt: string, ...any) -> string,
packsize: (fmt: string) -> number,
rep: (s: string, n: number) -> string,
reverse: (s: string) -> string,
split: (s: string, separator: string?) -> {string},
sub: (s: string, i: number, j: number?) -> string,
unpack: (fmt: string, s: string, init: number?) -> ...any,
upper: (s: string) -> string,
byte: (s: string, i: number?, j: number?) -> ...number, -- builtin
char: (...number) -> string, -- builtin
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string), -- builtin, magic type
format: (formatstring: string, ...any) -> string, -- builtin, magic type
gmatch: (s: string, pattern: string) -> () -> ...string, -- builtin, magic type
gsub: (s: string, pattern: string, repl: string | { [string]: string } | (...string) -> string, maxn: number?) -> (string, number), -- builtin
len: (s: string) -> number, -- builtin
lower: (s: string) -> string, -- builtin
match: (s: string, pattern: string, init: number?) -> ...string, -- builtin, magic type
pack: (fmt: string, ...any) -> string, -- builtin
packsize: (fmt: string) -> number, -- builtin
rep: (s: string, n: number) -> string, -- builtin
reverse: (s: string) -> string, -- builtin
split: (s: string, separator: string?) -> {string}, -- builtin
sub: (s: string, i: number, j: number?) -> string, -- builtin
unpack: (fmt: string, s: string, init: number?) -> ...any, -- builtin
upper: (s: string) -> string, -- builtin
}

--]]

---------------------------
-- Global Table: table
Expand All @@ -508,16 +510,16 @@ declare table: {
extend: <V>(a: {V}, b: {V}) -> {V},
remove: <V>(a: {V}, i: number?) -> V?,
sort: <V>(a: {V}, f: ((a: V, b: V) -> boolean)?) -> (),
pack: <V>(...V) -> { n: number, [number]: V },
pack: <V>(...V) -> { n: number, [number]: V }, -- magic type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--!strict
local _: {string | number} = table.pack("a", 5)

Magic knows to union all the table.pack parameters for the return type:

$ luau-lsp analyze --platform standard magic_table_pack.luau 
[WARN] No definitions file provided by client

Without magic, this is not supported by the type system:

$ luau-lsp analyze --platform standard --defs secondlife.d.luau magic_table_pack.luau 
[INFO] Loading definitions file: @roblox - secondlife.d.luau
magic_table_pack.luau(2,46): TypeError: Expected this to be 'string', but got 'number'

unpack: <V>(a: {V}, i: number?, j: number?) -> ...V,
move: <V>(src: {V}, i: number, j: number, d: number, dest: {V}?) -> {V},
create: <V>(n: number, v: V?) -> {V},
find: <V>(t: {V}, v: V, i: number?) -> number?,
clear: (t: {}) -> (),
shrink: <V>(t: {V}, shrink_sparse: boolean?) -> {V},
freeze: <table>(t: table) -> table,
freeze: <table>(t: table) -> table, -- magic type

@tapple tapple May 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--!strict
type objTable = {x: string}
type hashTable = {[string]: string}

local o: objTable = {x="yo"}
local _o: objTable = table.freeze(o)
_o.x = "error"

local h: hashTable = {x="yo"}
local _h: hashTable = table.freeze(h)
_h.x = "error"

local _n: number = table.freeze(5)

Magic knows that table.freeze only works on tables:

$ luau-lsp analyze --platform standard magic_table_freeze.luau 
[WARN] No definitions file provided by client
magic_table_freeze.luau(13,33): TypeError: Expected this to be '{-  -}', but got 'number'
magic_table_freeze.luau(13,1): TypeError: Expected this to be 'number', but got '{-  -}'

Without magic, "generic table" is an unexpressable type and the file must resort to "generic anything"

$ luau-lsp analyze --platform standard --defs secondlife.d.luau magic_table_freeze.luau 
[INFO] Loading definitions file: @roblox - secondlife.d.luau

Neither one understands the real purpose of freeze is to make a read-only table:

$ luau magic_table_freeze.luau 
./magic_table_freeze.luau:7: attempt to modify a readonly table
stacktrace:
./magic_table_freeze.luau:7

@tapple tapple May 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Luau 0.720 improved the table.freeze magic so that it can warn about read-only access:

--!strict
type objTable = {x: string}
local o: objTable = {x="yo"}
local of = table.freeze(o)
of.x = "error"

Magic now knows the table is read-only (new solver only):

$ luau-lsp analyze --platform standard --flag LuauSolverV2=true magic_table_freeze_bug.luau 
[WARN] No definitions file provided by client
magic_table_freeze_bug.luau(5,1): TypeError: Property x of table 'objTable' is read-only

Without magic, or in the old solver, table writing is not detected as an error:

$ luau-lsp analyze --platform standard --flag LuauSolverV2=true --defs secondlife.d.luau magic_table_freeze_bug.luau 
[INFO] Loading definitions file: @roblox - secondlife.d.luau
$ luau-lsp analyze --platform standard --flag LuauSolverV2=false magic_table_freeze_bug.luau 
[WARN] No definitions file provided by client

isfrozen: (t: {}) -> boolean,
clone: <table>(t: table) -> table,
clone: <table>(t: table) -> table, -- magic type

@tapple tapple May 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--!strict
type objTable = {x: string}
type hashTable = {[string]: string}

local o: objTable = {x="yo"}
local _o: objTable = table.clone(o)

local h: hashTable = {x="yo"}
local _h: hashTable = table.clone(h)

local _n: number = table.clone(5)

Magic knows that table.clone only works on tables:

$ luau-lsp analyze --platform standard magic_table_clone.luau 
[WARN] No definitions file provided by client
magic_table_clone.luau(11,32): TypeError: Expected this to be '{-  -}', but got 'number'
magic_table_clone.luau(11,1): TypeError: Expected this to be 'number', but got '{-  -}'

Without magic, "generic table" is an unexpressable type and the file must resort to "generic anything"

$ luau-lsp analyze --platform standard --defs secondlife.d.luau magic_table_clone.luau 
[INFO] Loading definitions file: @roblox - secondlife.d.luau

clone magic only works on old solver:

$  luau-lsp analyze --platform standard --flag LuauSolverV2=true magic_table_clone.luau 
[WARN] No definitions file provided by client

}


Expand Down
8 changes: 8 additions & 0 deletions lsl_definitions/generators/slua.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ def gen_luau_lsp_defs(definitions: LSLDefinitions, slua_definitions: SLuaDefinit
for func in slua_definitions.functions:
if func.private or func.local_only:
continue
if not func.typechecker_flags.fully_defined:
defs.write("-- ")
defs.write("declare ")
func.write_luau_global_def(defs)
for module in sorted(slua_definitions.modules, key=lambda x: x.name):
if module.name in {"ll", "llcompat"}:
continue
if module.name == "string":
defs.write(
"--[[ commented out to avoid shadowing magic type functions find, format, gmatch, and match\n"
)
module.write_luau_def(defs)
if module.name == "string":
defs.write("--]]\n")
for var in slua_definitions.global_variables:
defs.write("declare ")
defs.write(var.to_luau_def())
Expand Down
38 changes: 37 additions & 1 deletion lsl_definitions/slua.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ def to_luau_def(self, declaration: bool = False) -> str:
return f"{self.name}: {self.type}"


@dataclasses.dataclass
class SLuaTypecheckerFlags:
"""Flags specific to the internals of luau-analyze and luau-lsp."""

builtin: bool = False
"""This function is defined in BuiltinDefinitions.cpp, rather than EmbeddedBuiltinDefinitions.cpp."""
magic: bool = False
"""
The typechecker has custom logic for this function.
For examples of each magic type function, see the comments of
https://github.com/secondlife/lsl-definitions/pull/130
"""

@property
def fully_defined(self) -> bool:
"""True if this function is fully defined and won't cause issues for the typechecker."""
return not self.builtin and not self.magic

@property
def comment_string(self) -> str:
comments = []
if self.builtin:
comments.append("builtin")
if self.magic:
comments.append("magic type")
return " -- " + ", ".join(comments) if comments else ""


@dataclasses.dataclass
class SLuaFunctionBase(abc.ABC):
name: str = ""
Expand Down Expand Up @@ -115,6 +143,10 @@ class SLuaFunction(SLuaFunctionBase):
must_use: bool = False
"""Emit a warning if the return value is not used.
See https://kampfkarren.github.io/selene/usage/std.html#must_use."""
typechecker_flags: SLuaTypecheckerFlags = dataclasses.field(
default_factory=SLuaTypecheckerFlags
)
"""Flags specific to the internals of luau-analyze and luau-lsp."""
overloads: List[SLuaFunctionOverload] = dataclasses.field(default_factory=list)

@property
Expand Down Expand Up @@ -164,7 +196,9 @@ def write_luau_global_def(self, f: TextIO, indent: int = 0) -> None:
f.write(f"function {self.name}")
f.write(self.type_parameters_string)
f.write(self.parameters_string(declaration=True))
f.write(f": {self.return_type}\n")
f.write(f": {self.return_type}")
f.write(self.typechecker_flags.comment_string)
f.write("\n")

def write_luau_table_def(self, f: TextIO, indent: int = 0, suffix=",") -> None:
"""For declaring functions within a table/module"""
Expand All @@ -180,6 +214,7 @@ def write_luau_table_def(self, f: TextIO, indent: int = 0, suffix=",") -> None:
f.write(overload.type_def_string)
f.write(")")
f.write(suffix)
f.write(self.typechecker_flags.comment_string)
f.write("\n")


Expand Down Expand Up @@ -770,6 +805,7 @@ def _validate_function(
local_only=data.get("local-only", False),
slua_removed=data.get("slua-removed", False),
must_use=data.get("must-use", False),
typechecker_flags=SLuaTypecheckerFlags(**data.get("typechecker", {})),
)
self._validate_identifier(func.name)
self._validate_scope(func.name, scope)
Expand Down
17 changes: 17 additions & 0 deletions slua_definitions.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@
"comment": {
"markdownDescription": "A brief description of this function.",
"type": "string"
},
"typechecker": {
"type": "object",
"description": "Flags specific to the internals of luau-analyze and luau-lsp.",
"additionalProperties": false,
"properties": {
"builtin": {
"const": true,
"description": "This function is defined in BuiltinDefinitions.cpp, rather than EmbeddedBuiltinDefinitions.cpp.",
"type": "boolean"
},
"magic": {
"const": true,
"description": "The typechecker has custom logic for this function. Look for attachMagicFunction in the source code.",
"type": "boolean"
}
}
}
},
"required": ["name"],
Expand Down
Loading
Loading