Skip to content

Mark the functions that have a magic type#130

Merged
HaroldCindy merged 7 commits into
secondlife:mainfrom
tapple:magic-type
May 31, 2026
Merged

Mark the functions that have a magic type#130
HaroldCindy merged 7 commits into
secondlife:mainfrom
tapple:magic-type

Conversation

@tapple

@tapple tapple commented May 13, 2026

Copy link
Copy Markdown
Contributor

Mark 2 classes of functions that need special handling in the generators of secondlife.d.luau and EmbeddedBuiltinDefinitions.cpp

  • builtin: functions not defined in EmbeddedBuiltinDefinitions.cpp must be left out of both files
  • magic type functions: Custom logic defined in BuiltinDefinitions.cpp must be left out of only secondlife.d.luau. These all use attachMagicFunction(), hence the name "magic"

I also wanted to document what all the magic type functions do, and this PR seems like the most appropriate place to host that documentation for now

@tapple tapple marked this pull request as draft May 13, 2026 01:29
@tapple tapple changed the title mark functions with a magic type Mark the functions that have a magic type May 13, 2026
Comment thread generated/secondlife.d.luau Outdated
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string),
format: (formatstring: string, ...any) -> string,
gmatch: (s: string, pattern: string) -> () -> ...string,
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string), -- magic type

@tapple tapple May 13, 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
local _start, _finish, _tag, _text, _extra = string.find(
    'monospace:: <code>print("hello regex")</code>. hooray',
    "<(%w-)>(.-)</%1>")

Magic detects the extra string.find variable assignment:

$ luau-lsp analyze --platform standard magic_string_find.luau 
[WARN] No definitions file provided by client
magic_string_find.luau(2,1): TypeError: Function only returns 4 values, but 5 are required here

Without magic, the extra parameter passes silently:

$ luau-lsp analyze --platform standard --defs secondlife.d.luau magic_string_find.luau 
[INFO] Loading definitions file: @roblox - sl-vscode-plugin/slua_fixes.d.luau

Comment thread generated/secondlife.d.luau Outdated
format: (formatstring: string, ...any) -> string,
gmatch: (s: string, pattern: string) -> () -> ...string,
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string), -- magic type
format: (formatstring: string, ...any) -> string, -- magic type

@tapple tapple May 13, 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
print(string.format("I am %s. I am %d years old.", "Pippin", "six"))

Magic knows that the 3rd string.format parameter should be a number:

$ luau-lsp analyze --platform standard magic_string_format.luau 
[WARN] No definitions file provided by client
magic_string_format.luau(2,62): TypeError: Expected this to be 'number', but got 'string'

Without magic, it passes silently:

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

Comment thread generated/secondlife.d.luau Outdated
gmatch: (s: string, pattern: string) -> () -> ...string,
find: (s: string, pattern: string, init: number?, plain: boolean?) -> (number?, number?, ...string), -- magic type
format: (formatstring: string, ...any) -> string, -- magic type
gmatch: (s: string, pattern: string) -> () -> ...string, -- magic type

@tapple tapple May 13, 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
for tag, text, extra in string.gmatch(
    'monospace:: <code>print("hello regex")</code>. hooray',
    "<(%w-)>(.-)</%1>"
) do
    print(tag, text, extra)
end

Magic detects the extra string.gmatch variable assignment:

$ luau-lsp analyze --platform standard magic_string_gmatch.luau 
[WARN] No definitions file provided by client
magic_string_gmatch.luau(2,1): TypeError: Argument count mismatch. Function expects 3 arguments, but only 2 are specified

Without magic, the extra parameter passes silently:

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

Comment thread generated/secondlife.d.luau Outdated
lower: (s: string) -> string,
match: (s: string, pattern: string, init: number?) -> ...string,
pack: (fmt: string, ...any) -> string,
match: (s: string, pattern: string, init: number?) -> ...string, -- magic type

@tapple tapple May 13, 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
local _tag, _text, _extra = string.match(
    'monospace:: <code>print("hello regex")</code>. hooray',
    "<(%w-)>(.-)</%1>")

Magic detects the extra string.match variable assignment:

$ luau-lsp analyze --platform standard magic_string_match.luau 
[WARN] No definitions file provided by client
magic_string_match.luau(2,1): TypeError: Function only returns 2 values, but 3 are required here

Without magic, the extra parameter passes silently:

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

freeze: <table>(t: table) -> table, -- magic type
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

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

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'

Comment thread generated/secondlife.d.luau Outdated
declare function select(i: string | number, ...any): any -- magic type
declare setfenv: nil
declare function setmetatable(t: { [any]: any }, mt: { [any]: any }?): ()
declare function setmetatable(t: { [any]: any }, mt: { [any]: any }?): () -- 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.

In the new solver, setmetatable does not need a magic type function to work, as it instead can be fully defined based on the builtin type function `setmetatable' (fixed in #135):

declare function setmetatable<T, MT>(t: T, mt: MT): setmetatable<T, MT>

In the old solver, setmetatable has both

  1. an unexpressable builtin type
  2. a magic type function

It's not possible to untangle those and demonstrate the builtin type without the magic type function (not without editing slua and recompiling), so I didn't make example code here to demonstrate it

@HaroldCindy

HaroldCindy commented May 15, 2026

Copy link
Copy Markdown
Collaborator

What's "magic"? I'm assuming it's an internal luau-analyze thing that does dynamic dispatch on how specifically to check the type based on the arguments passed. Is there a better term for it (i.e. whose magic is it anyway)?

Comment thread slua_definitions.schema.json Outdated
@tapple tapple force-pushed the magic-type branch 2 times, most recently from 2ba48a9 to 48898eb Compare May 26, 2026 22:37
Comment thread generated/secondlife.d.luau Outdated


declare function assert<T>(value: T?, message: string?): T
-- declare function assert<T>(value: T?, message: string?): T -- magic type

@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.

--!strict
local _a: string? = "hello"
assert(_a ~= nil)
local _b: string = _a

Magic lets assert do type narrowing:

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

Without magic, no type narrowing happens:

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

Comment thread generated/secondlife.d.luau Outdated
declare function rawset<K, V>(t: {[K]: V}, k: K, v: V): {[K]: V}
declare function require(target: string): any
declare function select(i: string | number, ...any): any
-- declare function require(target: string): any -- magic type

@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.

magic_require.luau

--!strict
local mod = require("magic_require_module")
local _nope = require("non_existent_module")
local _x: number = mod.x
local _hello: string = mod.world

magic_require_module.luau

--!strict
return { x = 1, y = 2 }

Magic lets require copy types between files:

$ luau-lsp analyze --platform standard magic_require.luau 
[WARN] No definitions file provided by client
magic_require.luau(3,15): TypeError: Unknown require: non_existent_module.lua
magic_require.luau(5,24): TypeError: Key 'world' not found in table '{ x: number, y: number }'

Without magic, require never errors:

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

Comment thread generated/secondlife.d.luau Outdated
declare function require(target: string): any
declare function select(i: string | number, ...any): any
-- declare function require(target: string): any -- magic type
-- declare function select(i: string | number, ...any): any -- magic type

@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.

--!strict
local _s: number = select(2, 5, "hello")

Magic knows the return type of select for each argument:

$ luau-lsp analyze --platform standard magic_select.luau 
[WARN] No definitions file provided by client
magic_select.luau(2,1): TypeError: Expected this to be 'number', but got 'string'

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

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

Comment thread generated/secondlife.d.luau Outdated
toup: (q: quaternion) -> vector,
}

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

@tapple tapple May 27, 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.

If it were possible, I'd also comment out the table module from secondlife.d.luau to avoid shadowing the magic type functions pack, freeze, and clone. But, that is not possible due to the addition of shrink, append, and extend

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's fine, I imagine we can special-case inside the definition loader to remove things where we want to rely on the built-in logic? Or what do you think.

@tapple tapple May 27, 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.

that would involve distributing a custom luau-lsp, which I don't think is worth it. Not with the viewer being much lower-hanging fruit where we can customize EmbeddedBuiltinDefinitions.cpp directly, and not worry about definition files.

Still, it might be worth bringing up with lute, once it supports definition files at all. Selene has this feature: standard library files are additive, even inside library tables, unlike luau-lsp definitions, where they replace

@tapple tapple marked this pull request as ready for review May 27, 2026 14:02

@HaroldCindy HaroldCindy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems reasonable to me, thanks! I'd really like for there to be some sort of way to "mix in" additions to table, string, and bit32, but that can be dealt with later.

@HaroldCindy HaroldCindy merged commit b21a2a0 into secondlife:main May 31, 2026
5 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators May 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants