diff --git a/.gitattributes b/.gitattributes index 0f70e84a..5655abb0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,6 @@ External/steam_networking/** -text External/steam_networking/** whitespace=-blank-at-eol External/miniz/** -text + +External/luau/** -text +External/luau/** whitespace=-blank-at-eol diff --git a/CMakeLists.txt b/CMakeLists.txt index f4cf838a..ab609f8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -404,6 +404,16 @@ if(DINGOSDK_BUILD_HALL_OF_MEAT_TESTS) target_link_libraries(dingosdk_hall_of_meat_skeleton_tests PRIVATE dingosdk_game_archives dingosdk_logging) add_test(NAME hall_of_meat_skeleton COMMAND dingosdk_hall_of_meat_skeleton_tests "${DINGOSDK_TEST_GAME_ROOT}") endif() +option(DINGOSDK_BUILD_PLUGIN_TESTS "Build plugin host regression tests" OFF) +if(DINGOSDK_BUILD_PLUGIN_TESTS AND WIN32) + enable_testing() + # The plugin host apart from the game: manifests, the reskate API, storage, modules, hot reload, + # and the sandbox against plugins written to break it. + add_executable(dingosdk_plugin_tests Extension/Plugins/Test/plugin_tests.cpp) + target_include_directories(dingosdk_plugin_tests PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries(dingosdk_plugin_tests PRIVATE dingosdk_plugins) + add_test(NAME plugins COMMAND dingosdk_plugin_tests) +endif() get_property(project_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) foreach(target IN LISTS project_targets) # (A build step, such as packing the built-in content, compiles nothing.) @@ -411,7 +421,7 @@ foreach(target IN LISTS project_targets) if(target_kind STREQUAL "UTILITY") continue() endif() - if(NOT target MATCHES "^(dingosdk_imgui|dingosdk_detours|dingosdk_sqlite|dingosdk_lz4|dingosdk_zstd|dingosdk_miniz|dingosdk_spotify_device)$") + if(NOT target MATCHES "^(dingosdk_imgui|dingosdk_detours|dingosdk_sqlite|dingosdk_lz4|dingosdk_zstd|dingosdk_miniz|dingosdk_luau|dingosdk_spotify_device)$") dingosdk_configure_target(${target}) endif() endforeach() diff --git a/Engine/Core/Log/types.h b/Engine/Core/Log/types.h index 9ba2021a..d996b410 100644 --- a/Engine/Core/Log/types.h +++ b/Engine/Core/Log/types.h @@ -19,13 +19,13 @@ inline constexpr std::string_view name(Context context) noexcept { enum class Channel { runtime, level, player, skater, flow, command, launcher, hooks, graphics, input, ui, profile, progression, customization, world, objects, settings, music, news, - assets, network, diagnostics, park, subworld, count + assets, network, diagnostics, park, subworld, plugins, count }; inline constexpr std::size_t channel_count = static_cast(Channel::count); inline constexpr std::array channel_names{ "Runtime", "Level", "Player", "Skater", "Flow", "Console", "Launcher", "Hooks", "Graphics", "Input", "UI", "Profile", "Progression", "Customization", "World", - "Objects", "Settings", "Music", "News", "Assets", "Network", "Diagnostics", "Park", "Subworld"}; + "Objects", "Settings", "Music", "News", "Assets", "Network", "Diagnostics", "Park", "Subworld", "Plugins"}; // Defaults for existing subsystem callsites; hooks with a different native owner // must pass their context explicitly instead of deriving it from message text. inline constexpr Context default_context(Channel channel) noexcept { diff --git a/Extension/Console/commands.cpp b/Extension/Console/commands.cpp index b449231c..13c16d16 100644 --- a/Extension/Console/commands.cpp +++ b/Extension/Console/commands.cpp @@ -233,6 +233,7 @@ const Commands &game_commands() { register_board_break_commands(*result); register_grind_exit_commands(*result); register_replay_keyframes_commands(*result); + register_plugin_commands(*result); return result; }(); return *registry; diff --git a/Extension/Console/commands.h b/Extension/Console/commands.h index 828ecd15..01761b4b 100644 --- a/Extension/Console/commands.h +++ b/Extension/Console/commands.h @@ -31,6 +31,7 @@ void register_road_rash_commands(Commands &); void register_board_break_commands(Commands &); void register_grind_exit_commands(Commands &); void register_replay_keyframes_commands(Commands &); +void register_plugin_commands(Commands &); // Runtime adapters. Invoked only by the verified game-thread dispatcher. void request_debug(overlay::DebugAction, bool enabled = false, float value = 0); void request_feature(overlay::OfflineFeatureGroup, bool enabled); diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp new file mode 100644 index 00000000..7a5f08da --- /dev/null +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -0,0 +1,468 @@ +// The plugin host apart from the game: manifests, the reskate API, storage, modules, hot reload, +// and the sandbox against plugins written to break it (loops, memory, error floods, escapes). +#include "Extension/Plugins/plugin_discovery.h" +#include "Extension/Plugins/plugin_host.h" +#include "Extension/Plugins/plugin_manifest.h" +#include "Engine/Vfs/mod_list.h" +#include +#include +#include +#include +#include +#include + +using namespace dingosdk::plugins; +namespace { +void check(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); } + +std::filesystem::path scratch; + +struct TestEnvironment final : Environment { + std::vector logs, lines; + double now = 1000; + void log(LogLevel, std::string_view text) override { logs.emplace_back(text); } + void console(std::string_view text) override { lines.emplace_back(text); } + double now_ms() override { return now; } + std::filesystem::path storage_root() override { return scratch / "storage"; } + bool builtin_command(std::string_view word) override { return word == "perf" || word == "load"; } + bool logged(std::string_view part) const { + return std::any_of(logs.begin(), logs.end(), [&](const auto& line) { return line.find(part) != std::string::npos; }); + } + bool printed(std::string_view part) const { + return std::any_of(lines.begin(), lines.end(), [&](const auto& line) { return line.find(part) != std::string::npos; }); + } + std::string all() const { + std::string text; + for (const auto& line : logs) text += line + "\n"; + return text; + } +}; + +std::string manifest(std::string permissions = "", std::string side = "client") { + return R"({"api": 1, "entry": "plugin/main.lua", "side": ")" + side + R"(", "permissions": [)" + permissions + "]}"; +} + +void write(const std::filesystem::path& file, const std::string& text) { + std::filesystem::create_directories(file.parent_path()); + std::ofstream(file, std::ios::binary | std::ios::trunc) << text; +} + +// A mod folder holding a plugin: its reskate-plugin.json and files. +Candidate package(const std::string& id, const std::map& files, std::string fingerprint = "1") { + const auto directory = scratch / "mods" / id; + std::filesystem::remove_all(directory); + for (const auto& [name, text] : files) write(directory / name, text); + return {id, directory, std::move(fingerprint), {}}; +} +Candidate script(const std::string& id, const std::string& main, const std::string& permissions = "") { + return package(id, {{manifest_file, manifest(permissions)}, {"plugin/main.lua", main}}); +} + +Status status_of(const Host& host, std::string_view id) { + for (const auto& status : host.status()) if (status.id == id) return status; + throw std::runtime_error("no plugin " + std::string(id)); +} +void ticks(Host& host, TestEnvironment& environment, int count) { + for (int i = 0; i < count; ++i) { + environment.now += 16.7; + host.tick(16.7); + } +} +void running(const Host& host, std::string_view id, const TestEnvironment& environment) { + const auto status = status_of(host, id); + check(status.state == "running", std::string(id) + " should run, and is " + status.state + ": " + status.reason + "\n" + environment.all()); +} +void stopped(const Host& host, std::string_view id, std::string_view because) { + const auto status = status_of(host, id); + check(status.state == "stopped", std::string(id) + " should have stopped, and is " + status.state); + check(status.reason.find(because) != std::string::npos, std::string(id) + " stopped for \"" + status.reason + "\", not for " + std::string(because)); +} + +void a_manifest_says_what_a_plugin_is() { + const auto good = parse_manifest(manifest(R"("console", "storage")")); + check(good.api == 1 && good.entry == "plugin/main.lua" && good.side == Side::client, "A manifest is read"); + check(good.permissions.contains("console") && good.permissions.contains("storage"), "with its permissions"); + const auto refused = [](const std::string& text, std::string_view because) { + try { + parse_manifest(text); + } catch (const std::exception& error) { + check(std::string(error.what()).find(because) != std::string::npos, "Refused for \"" + std::string(error.what()) + "\", not " + std::string(because)); + return; + } + throw std::runtime_error("A manifest that should be refused was read: " + text); + }; + refused(R"({"entry": "main.lua"})", "\"api\""); + refused(R"({"api": 2, "entry": "main.lua"})", "update ReSkate"); + refused(R"({"api": 1, "entry": "../main.lua"})", "inside the mod folder"); + refused(R"({"api": 1, "entry": "C:/main.lua"})", "inside the mod folder"); + refused(R"({"api": 1, "entry": "a\\main.lua"})", "inside the mod folder"); + refused(R"({"api": 1, "entry": "main.txt"})", ".lua"); + refused(R"({"api": 1, "entry": "main.lua", "side": "moon"})", "\"side\""); + refused(R"({"api": 1, "entry": "main.lua", "permissions": ["root"]})", "unknown permission"); + refused("not json", "not valid JSON"); + check(safe_relative_path("a/b.lua") == "a/b.lua", "A path inside the package is kept"); + for (const auto* bad : {"", "/a", "a//b", "./a", "a/../b", "..", "a\\b", "c:a", "a.", "a /b", "a\nb"}) + check(safe_relative_path(bad).empty(), std::string("A path that could leave the package is refused: ") + bad); +} + +void a_plugin_loads_ticks_runs_its_commands_and_unloads() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("hello", R"( + local ticks = 0 + reskate.on("load", function() reskate.log("loaded " .. reskate.id .. " api " .. reskate.api_version) end) + reskate.on("tick", function(dt) ticks += 1 end) + reskate.on("unload", function() reskate.log("bye after " .. ticks) end) + reskate.console.command("hello there", "Says hello", function(args) + reskate.console.print("hello " .. table.concat(args, ",") .. " after " .. ticks) + end) + print("printed", 1, true) + )", R"("console")")}); + running(host, "hello", environment); + check(environment.logged("[hello] loaded hello api 1"), "The load handler ran\n" + environment.all()); + check(environment.logged("[hello] printed\t1\ttrue"), "print goes to the log"); + ticks(host, environment, 3); + check(host.run_console("hello there a b"), "Its command runs"); + check(environment.printed("hello a,b after 3"), "with its arguments, after its ticks"); + check(host.run_console("HELLO There"), "whatever the case"); + check(!host.run_console("nothing here"), "and other text is left to ReSkate"); + check(status_of(host, "hello").commands.size() == 1, "Its command is listed"); + host.apply({}); + check(environment.logged("[hello] bye after 3"), "Unloading runs its unload handler"); + check(host.status().empty(), "and it is gone"); + check(!host.run_console("hello there"), "with its command"); +} + +void a_permission_must_be_declared() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("bare", R"( + local ok, err = pcall(reskate.console.command, "x", "x", function() end) + assert(not ok and err:find("needs the \"console\" permission"), err) + ok, err = pcall(reskate.storage.get, "x") + assert(not ok and err:find("needs the \"storage\" permission"), err) + reskate.console.print("never") + )")}); + stopped(host, "bare", "raised an error"); + check(environment.logged("needs the \"console\" permission"), "The error names the permission\n" + environment.all()); +} + +void command_names_are_checked() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("first", R"( + reskate.console.command("dom start", "Starts", function() end) + assert(not pcall(reskate.console.command, "perf mine", "x", function() end), "ReSkate's own") + assert(not pcall(reskate.console.command, "plugin mine", "x", function() end), "the plugin command") + assert(not pcall(reskate.console.command, "Bad Name", "x", function() end), "uppercase") + assert(not pcall(reskate.console.command, "a b c d", "x", function() end), "four words") + assert(not pcall(reskate.console.command, "dom start", "x", function() end), "twice") + )", R"("console")"), + script("second", R"( + local ok, err = pcall(reskate.console.command, "dom", "x", function() end) + assert(not ok and err:find("first"), err) + reskate.console.command("other", "x", function() end) + )", R"("console")")}); + running(host, "first", environment); + running(host, "second", environment); +} + +void storage_is_kept_across_loads_and_has_a_quota() { + TestEnvironment environment; + Limits limits; + limits.storage_bytes = 300; + std::filesystem::remove_all(scratch / "storage" / "keeper"); + const auto keeper = script("keeper", R"( + local runs = reskate.storage.add("runs") + reskate.storage.set("table", {a = 1, list = {1, 2, 3}, name = "x"}) + local back = reskate.storage.get("table") + assert(back.a == 1 and back.list[3] == 3 and back.name == "x") + reskate.storage.set("gone", true) + reskate.storage.set("gone", nil) + assert(reskate.storage.get("gone") == nil) + local ok, err = pcall(reskate.storage.set, "big", string.rep("x", 1000)) + assert(not ok and err:find("full"), err) + assert(reskate.storage.get("big") == nil) + assert(not pcall(reskate.storage.set, "f", print), "a function is not JSON") + assert(not pcall(reskate.storage.set, "", 1), "an empty key") + local cycle = {} cycle.me = cycle + assert(not pcall(reskate.storage.set, "c", cycle), "a cycle") + reskate.log("runs " .. runs) + )", R"("storage")"); + { + Host host(environment, Side::client, limits); + host.apply({keeper}); + running(host, "keeper", environment); + host.apply({}); + host.apply({keeper}); + running(host, "keeper", environment); + } + check(environment.logged("[keeper] runs 1") && environment.logged("[keeper] runs 2"), "Storage is kept from one load to the next\n" + environment.all()); + check(std::filesystem::exists(scratch / "storage" / "keeper" / "storage.json"), "in the plugin's own file"); +} + +void packages_json_and_modules() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({package("modules", { + {manifest_file, manifest()}, + {"data/parks.json", R"({"parks": [{"name": "bowl", "spots": 3}]})"}, + {"data/notes.txt", "hello"}, + {"plugin/lib/util.lua", "loads = (loads or 0) + 1\nreturn {double = function(x) return x * 2 end}"}, + {"plugin/rules.luau", "return {name = 'rules'}"}, + {"plugin/cycle_a.lua", "return require('cycle_b')"}, + {"plugin/cycle_b.lua", "return require('cycle_a')"}, + {"plugin/main.lua", R"( + assert(reskate.package.read("data/notes.txt") == "hello") + assert(reskate.package.read("data/missing.txt") == nil) + local parks = reskate.package.read_json("data/parks.json") + assert(parks.parks[1].name == "bowl" and parks.parks[1].spots == 3) + local listing = table.concat(reskate.package.list("data"), ",") + assert(listing == "notes.txt,parks.json", listing) + assert(table.concat(reskate.package.list(), ","):find("plugin/"), "folders end in /") + for _, bad in {"../x", "/x", "C:/x", "data\\notes.txt", "data/../../x"} do + assert(not pcall(reskate.package.read, bad), bad) + end + local util = require("./lib/util") + assert(util.double(4) == 8) + assert(require("lib/util") == util and loads == 1, "a module runs once") + assert(require("rules").name == "rules", ".luau too") + local ok, err = pcall(require, "cycle_a") + assert(not ok and err:find("requires itself"), err) + assert(not pcall(require, "missing")) + assert(not pcall(require, "../data/notes")) + local text = reskate.json.encode({a = {1, 2}, b = "x", c = true}) + local back = reskate.json.decode(text) + assert(back.a[2] == 2 and back.b == "x" and back.c == true, text) + assert(not pcall(reskate.json.decode, "{nope")) + assert(not pcall(reskate.json.encode, {[{}] = 1})) + assert(not pcall(reskate.json.encode, 0/0)) + reskate.log("modules fine") + )"}})}); + running(host, "modules", environment); + check(environment.logged("modules fine"), "Packages, JSON and modules work\n" + environment.all()); +} + +// What a plugin must not be able to reach or change. Each line checks one way out. +void the_sandbox_holds() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("escape", R"( + assert(io == nil and package == nil and dofile == nil and loadfile == nil, "no files") + assert(loadstring == nil and load == nil, "no loading code, bytecode or text") + assert(os.execute == nil and os.remove == nil and os.rename == nil and os.getenv == nil and os.exit == nil, "no os") + assert(not pcall(function() reskate.log = nil end), "reskate is read only") + assert(not pcall(function() reskate.storage.get = print end), "and its tables") + assert(not pcall(rawset, reskate, "x", 1), "even by rawset") + assert(not pcall(setmetatable, reskate, {}), "or a metatable") + assert(not pcall(function() string.rep = nil end), "the libraries are read only") + assert(not pcall(function() getmetatable("").__index = {} end), "and the string metatable") + assert(not pcall(function() _G.print = nil end) or print ~= nil, "print stays") + mine = 1 + assert(mine == 1, "its own globals work") + local function deep(n) return deep(n + 1) + 1 end + assert(not pcall(deep, 1), "endless recursion is an error, not a crash") + assert(not pcall(require, "C:/Windows/win.ini")) + assert(not pcall(reskate.package.read, "../../../Windows/win.ini")) + assert(not pcall(reskate.time.after, -1, print)) + assert(not pcall(reskate.time.every, 0, print)) + assert(not pcall(reskate.on, "made_up", print)) + assert(not pcall(reskate.log, "x", "loud")) + reskate.log("sandbox fine") + )")}); + running(host, "escape", environment); + check(environment.logged("sandbox fine"), "Every way out is closed\n" + environment.all()); +} + +void a_runaway_plugin_is_stopped_and_the_rest_go_on() { + TestEnvironment environment; + Host host(environment, Side::client); + const auto start = std::chrono::steady_clock::now(); + host.apply({ + script("bystander", R"(local n = 0 reskate.on("tick", function() n += 1 if n == 30 then reskate.log("bystander ticked 30") end end))"), + script("loop", "while true do end"), + script("catches", "while true do pcall(function() while true do end end) end"), + script("tick_loop", R"(reskate.on("tick", function() while true do end end))"), + script("memory", R"(local t = {} while true do t[#t + 1] = string.rep("x", 8000000) end)"), + script("memory_caught", R"( + pcall(function() local t = {} while true do t[#t + 1] = string.rep("y", 8000000) end end) + reskate.log("survived the memory bomb") + )"), + script("huge", R"(local s = string.rep("x", 100000000))"), + script("buffer", R"(local b = buffer.create(1e9))"), + script("errors", R"(reskate.on("tick", function() error("boom") end))"), + script("timers", R"( + local ok, err = pcall(function() for i = 1, 2000 do reskate.time.after(1000, function() end) end end) + assert(not ok and err:find("timers"), err) + reskate.log("timers capped") + )"), + script("chatty", R"(for i = 1, 1000 do reskate.log("line " .. i) end)"), + script("coroutines", R"(while true do coroutine.wrap(function() end)() end)"), + script("slow_unload", R"(reskate.on("unload", function() while true do end end))"), + }); + for (const auto* id : {"loop", "catches"}) stopped(host, id, "ran longer than 50 ms"); + stopped(host, "coroutines", ""); // its time or its memory, whichever runs out first + for (const auto* id : {"memory", "memory_caught", "huge", "buffer"}) stopped(host, id, "more than 64 MB"); + check(!environment.logged("survived the memory bomb"), "A memory bomb cannot be caught and carried on from"); + running(host, "tick_loop", environment); + ticks(host, environment, 1); + stopped(host, "tick_loop", "the tick handler ran longer than 50 ms"); + check(environment.logged("[tick_loop] stopped: the tick handler"), "A stop is logged, which the console shows"); + ticks(host, environment, 4); // five errors, with the tick above + running(host, "errors", environment); + ticks(host, environment, 1); + stopped(host, "errors", "more than 5 errors"); + running(host, "timers", environment); + check(environment.logged("timers capped"), "Timers have a limit"); + const auto chatty = std::count_if(environment.logs.begin(), environment.logs.end(), + [](const auto& line) { return line.starts_with("[chatty] line "); }); + check(chatty == 50, "50 lines a second, and the rest left out: " + std::to_string(chatty)); + ticks(host, environment, 30); + running(host, "bystander", environment); + check(environment.logged("bystander ticked 30"), "The others go on ticking"); + const auto unloading = std::chrono::steady_clock::now(); + host.apply({}); + const auto unload_ms = std::chrono::duration(std::chrono::steady_clock::now() - unloading).count(); + check(unload_ms < 500, "An unload handler that never ends is cut off: " + std::to_string(unload_ms) + " ms"); + const auto total = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + check(total < 20, "and none of it hangs the host: " + std::to_string(total) + " s"); +} + +void a_plugin_over_its_time_budget_is_stopped() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("busy", R"(reskate.on("tick", function() local s = os.clock() while os.clock() - s < 0.004 do end end))"), + script("light", R"(reskate.on("tick", function() local s = 0 for i = 1, 100 do s += i end end))")}); + ticks(host, environment, 60 * 5); + stopped(host, "busy", "ms a tick for 3 s"); + running(host, "light", environment); +} + +void a_plugin_reloads_when_its_files_change() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("live", R"(reskate.log("version 1") reskate.on("unload", function() reskate.log("unloading 1") end))")}); + running(host, "live", environment); + auto changed = script("live", R"(reskate.log("version 2"))"); + changed.fingerprint = "2"; + host.apply({changed}); + check(environment.logged("unloading 1") && environment.logged("version 2"), "A changed plugin unloads and loads again\n" + environment.all()); + host.apply({changed}); + check(std::count_if(environment.logs.begin(), environment.logs.end(), [](const auto& line) { return line.find("version 2") != std::string::npos; }) == 1, + "and an unchanged one is left alone"); + check(host.switch_off("live").find("switched off") != std::string::npos, "It can be switched off by hand"); + check(status_of(host, "live").state == "switched off", "and stays off"); + changed.fingerprint = "3"; + host.apply({changed}); + check(status_of(host, "live").state == "switched off", "even when its files change"); + check(host.switch_on("live").find("started") != std::string::npos, "until it is switched on"); + running(host, "live", environment); + check(host.reload("live").find("reloaded") != std::string::npos, "and reloaded by hand"); + check(host.reload("nobody").starts_with("error: "), "A plugin that is not there cannot be"); +} + +void level_events_reach_plugins() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("levels", R"( + assert(reskate.level.current() == nil) + reskate.on("level_loaded", function(name) reskate.log("loaded " .. name .. " / " .. reskate.level.current().name) end) + reskate.on("level_unloading", function(name) reskate.log("leaving " .. name) end) + )")}); + host.set_level("sanvan"); + host.set_level("park"); + host.set_level(""); + check(environment.logged("loaded sanvan / sanvan") && environment.logged("leaving sanvan") && + environment.logged("loaded park / park") && environment.logged("leaving park"), "Level events in order\n" + environment.all()); +} + +void timers_run_when_due() { + TestEnvironment environment; + Host host(environment, Side::client); + host.apply({script("clock", R"( + local count = 0 + reskate.time.after(100, function() reskate.log("once") end) + local id = reskate.time.every(50, function() count += 1 end) + reskate.time.after(260, function() reskate.time.cancel(id) reskate.log("every ran " .. count) end) + local cancelled = reskate.time.after(10, function() reskate.log("never") end) + reskate.time.cancel(cancelled) + )")}); + ticks(host, environment, 30); + check(environment.logged("[clock] once") && environment.logged("every ran 5") && !environment.logged("never"), + "Timers run once, repeat, and stop when cancelled\n" + environment.all()); +} + +void a_package_with_a_program_or_on_the_wrong_side_is_not_loaded() { + const auto clean = package("clean", {{manifest_file, manifest()}, {"plugin/main.lua", ""}, {"readme.txt", "A plain file"}}); + check(check_package_files(clean.directory).empty(), "A plain package passes"); + const auto named = package("named", {{manifest_file, manifest()}, {"plugin/main.lua", ""}, {"tools/helper.DLL", "x"}}); + check(check_package_files(named.directory).find("tools/helper.dll") != std::string::npos, "A DLL by its name is refused"); + const auto hidden = package("hidden", {{manifest_file, manifest()}, {"plugin/main.lua", ""}, {"data/picture.png", "MZ\x90\x00"}}); + check(!check_package_files(hidden.directory).empty(), "and a program by its first bytes, whatever its name"); + const auto script_file = package("batch", {{manifest_file, manifest()}, {"plugin/main.lua", ""}, {"run.bat", "echo"}}); + check(!check_package_files(script_file.directory).empty(), "and a script Windows runs"); + + TestEnvironment environment; + Host host(environment, Side::client); + auto refused = hidden; + refused.refusal = check_package_files(hidden.directory); + host.apply({refused, package("server_only", {{manifest_file, manifest("", "server")}, {"plugin/main.lua", ""}}), + package("broken", {{manifest_file, "{"}, {"plugin/main.lua", ""}}), + package("syntax", {{manifest_file, manifest()}, {"plugin/main.lua", "local = 1"}})}); + stopped(host, "hidden", "a program"); + check(status_of(host, "server_only").state == "not loaded", "A server plugin waits for a server"); + stopped(host, "broken", "not valid JSON"); + stopped(host, "syntax", "plugin/main.lua"); +} + +void discovery_finds_the_plugins_among_the_mods_left_on() { + const auto root = scratch / "game"; + const auto mods = root / "Mods"; + write(mods / "Alpha" / manifest_file, manifest()); + write(mods / "Alpha" / "plugin" / "main.lua", ""); + write(mods / "Beta" / manifest_file, manifest()); + write(mods / "Beta" / "plugin" / "main.lua", ""); + write(mods / "Beta" / "bin" / "tool.exe", "x"); + write(mods / "Gamma" / manifest_file, manifest()); + write(mods / "Gamma" / "plugin" / "main.lua", ""); + write(mods / "JustAMap" / "manifest.json", R"({"name": "Just a map"})"); + check(dingosdk::mods::set_mod_enabled(mods, "Gamma", false), "mods.json switches a mod off"); + FileChecks checks; + auto found = discover(root, checks); + std::sort(found.begin(), found.end(), [](const auto& a, const auto& b) { return a.id < b.id; }); + check(found.size() == 2 && found[0].id == "Alpha" && found[1].id == "Beta", "Only the plugins left on are found"); + check(found[0].refusal.empty() && found[1].refusal.find("bin/tool.exe") != std::string::npos, "with what their files say"); + const auto before = found[0].fingerprint; + write(mods / "Alpha" / "plugin" / "main.lua", "-- changed"); + auto again = discover(root, checks); + std::sort(again.begin(), again.end(), [](const auto& a, const auto& b) { return a.id < b.id; }); + check(again[0].fingerprint != before, "A changed file changes the fingerprint, which reloads it"); +} +} + +int main() { + try { + scratch = std::filesystem::temp_directory_path() / "reskate-plugin-tests"; + std::filesystem::remove_all(scratch); + a_manifest_says_what_a_plugin_is(); + a_plugin_loads_ticks_runs_its_commands_and_unloads(); + a_permission_must_be_declared(); + command_names_are_checked(); + storage_is_kept_across_loads_and_has_a_quota(); + packages_json_and_modules(); + the_sandbox_holds(); + a_runaway_plugin_is_stopped_and_the_rest_go_on(); + a_plugin_over_its_time_budget_is_stopped(); + a_plugin_reloads_when_its_files_change(); + level_events_reach_plugins(); + timers_run_when_due(); + a_package_with_a_program_or_on_the_wrong_side_is_not_loaded(); + discovery_finds_the_plugins_among_the_mods_left_on(); + std::filesystem::remove_all(scratch); + std::cout << "plugin tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "plugin test failed: " << error.what() << '\n'; + return 1; + } +} diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp new file mode 100644 index 00000000..de743398 --- /dev/null +++ b/Extension/Plugins/plugin_api.cpp @@ -0,0 +1,508 @@ +#include "plugin_api.h" +#include "lua.h" +#include "lualib.h" +#include +#include +#include +#include +#include + +// The reskate API, v1 base set plus console and storage (docs: ReSkate Plugin System Design). +// Every function checks what it is given before acting on it, and no value a plugin passes +// reaches anything outside its own VM unchecked. Errors are raised the way Lua reports them: +// luaL_error, or a C++ exception, which Luau turns into a Lua error with its message. +namespace dingosdk::plugins { +namespace { +constexpr std::array events{"load", "unload", "tick", "level_loaded", "level_unloading"}; +constexpr std::size_t maximum_key = 128; +constexpr std::size_t maximum_command = 48; +constexpr std::size_t maximum_listing = 1000; + +// A plugin that caught its own memory or time error in a pcall gets no further: nothing it asks +// for is done, and the next safe point stops it. +Plugin& plugin_of(lua_State* L) { + auto& plugin = *static_cast(lua_callbacks(L)->userdata); + if (plugin.halted()) luaL_error(L, "the plugin was stopped"); + return plugin; +} + +// The table being filled is on top of the stack. +void set_function(lua_State* L, const char* name, lua_CFunction function) { + lua_pushcfunction(L, function, name); + lua_setfield(L, -2, name); +} + +std::string text_of(lua_State* L, int index) { + std::size_t length = 0; + const char* text = luaL_tolstring(L, index, &length); + std::string result(text, length); + lua_pop(L, 1); + return result; +} + +// What turning Lua into JSON may cost outside the VM's own heap. +struct Budget { + std::size_t values = 1u << 20; + std::size_t bytes = 16u << 20; + void spend(std::size_t count, std::size_t size) { + if (count > values || size > bytes) throw std::runtime_error("the value is too large to turn into JSON"); + values -= count; + bytes -= size; + } +}; + +Json to_json(lua_State* L, int index, int depth, Budget& budget) { + if (depth > 32) throw std::runtime_error("tables nested deeper than 32 cannot be turned into JSON"); + index = lua_absindex(L, index); + budget.spend(1, 0); + switch (lua_type(L, index)) { + case LUA_TNIL: return nullptr; + case LUA_TBOOLEAN: return lua_toboolean(L, index) != 0; + case LUA_TNUMBER: { + const double value = lua_tonumber(L, index); + if (!std::isfinite(value)) throw std::runtime_error("JSON has no NaN or infinity"); + if (value == std::floor(value) && std::fabs(value) <= 9007199254740992.0) return static_cast(value); + return value; + } + case LUA_TSTRING: { + std::size_t length = 0; + const char* text = lua_tolstring(L, index, &length); + budget.spend(0, length); + return std::string(text, length); + } + case LUA_TTABLE: { + luaL_checkstack(L, 4, "tables nested too deep"); + const int length = lua_objlen(L, index); + bool list = length > 0; + int count = 0; + lua_pushnil(L); + while (lua_next(L, index)) { + ++count; + if (lua_type(L, -2) != LUA_TNUMBER) list = false; + else { + const double key = lua_tonumber(L, -2); + if (key != std::floor(key) || key < 1 || key > length) list = false; + } + lua_pop(L, 1); + } + if (list && count == length) { + auto result = Json::array(); + for (int i = 1; i <= length; ++i) { + lua_rawgeti(L, index, i); + result.push_back(to_json(L, -1, depth + 1, budget)); + lua_pop(L, 1); + } + return result; + } + auto result = Json::object(); + lua_pushnil(L); + while (lua_next(L, index)) { + if (lua_type(L, -2) != LUA_TSTRING) + throw std::runtime_error("only a list (keys 1 to n) or a table with string keys can be turned into JSON"); + std::size_t size = 0; + const char* key = lua_tolstring(L, -2, &size); + budget.spend(0, size); + result[std::string_view(key, size)] = to_json(L, -1, depth + 1, budget); + lua_pop(L, 1); + } + return result; + } + default: + throw std::runtime_error(std::string("a ") + lua_typename(L, lua_type(L, index)) + " cannot be turned into JSON"); + } +} + +void push_json_value(lua_State* L, const Json& value, int depth) { + luaL_checkstack(L, 4, "JSON nested too deep"); + if (depth > 64) throw std::runtime_error("JSON nested too deep"); + if (value.is_null()) lua_pushnil(L); + else if (value.is_boolean()) lua_pushboolean(L, value.get()); + else if (value.is_number()) lua_pushnumber(L, value.get()); + else if (value.is_string()) lua_pushlstring(L, value.string().data(), value.string().size()); + else if (value.is_array()) { + lua_createtable(L, static_cast(value.size()), 0); + int index = 0; + for (const auto& item : value) { + push_json_value(L, item, depth + 1); + lua_rawseti(L, -2, ++index); + } + } else { + lua_createtable(L, 0, static_cast(value.size())); + for (const auto& [key, item] : value.items()) { + lua_pushlstring(L, key.data(), key.size()); + push_json_value(L, item, depth + 1); + lua_rawset(L, -3); + } + } +} + +void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) { + if (!plugin.has(permission)) + luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission); +} + +std::string key_of(lua_State* L, int index) { + std::size_t length = 0; + const char* key = luaL_checklstring(L, index, &length); + if (length == 0 || length > maximum_key) luaL_error(L, "a storage key must be 1 to %d characters", static_cast(maximum_key)); + return std::string(key, length); +} + +// A file or folder of the plugin's own package, never outside it: the path is checked as text, +// then once more after links and junctions are resolved. +std::filesystem::path package_path(lua_State* L, Plugin& plugin, std::string_view path, bool folder) { + std::string relative = folder && path.empty() ? std::string{} : safe_relative_path(path); + if (relative.empty() && !(folder && path.empty())) + luaL_error(L, "\"%s\" is not a path inside the plugin's package (use / between folders, no ..)", std::string(path).c_str()); + const auto root = plugin.directory(); + const auto full = relative.empty() ? root : root / std::filesystem::path(relative); + std::error_code code; + const auto real_root = std::filesystem::weakly_canonical(root, code); + const auto real = std::filesystem::weakly_canonical(full, code); + const auto inside = real.lexically_relative(real_root); + if (code || (!relative.empty() && (inside.empty() || *inside.begin() == ".."))) luaL_error(L, "that path leads outside the package"); + return full; +} +} + +struct ApiFunctions { + static int log(lua_State* L) { + auto& plugin = plugin_of(L); + const auto message = text_of(L, 1); + const std::string level = luaL_optstring(L, 2, "info"); + if (level != "info" && level != "warn" && level != "error") luaL_argerror(L, 2, "the level must be \"info\", \"warn\" or \"error\""); + plugin.log(level == "error" ? LogLevel::error : level == "warn" ? LogLevel::warn : LogLevel::info, message); + return 0; + } + static int print(lua_State* L) { + std::string line; + const int count = lua_gettop(L); + for (int i = 1; i <= count; ++i) { + if (i > 1) line += '\t'; + line += text_of(L, i); + } + plugin_of(L).log(LogLevel::info, line); + return 0; + } + static int on(lua_State* L) { + auto& plugin = plugin_of(L); + const std::string event = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + if (std::find(events.begin(), events.end(), event) == events.end()) + luaL_error(L, "there is no event \"%s\" in this ReSkate", event.c_str()); + auto& list = plugin.handlers_[event]; + if (list.size() >= plugin.limits_.handlers) luaL_error(L, "more than %d handlers for one event", static_cast(plugin.limits_.handlers)); + lua_pushvalue(L, 2); + list.push_back(lua_ref(L, -1)); + lua_pop(L, 1); + return 0; + } + static int now(lua_State* L) { + lua_pushnumber(L, plugin_of(L).services_.now_ms()); + return 1; + } + static int timer(lua_State* L, bool repeat) { + auto& plugin = plugin_of(L); + const double ms = luaL_checknumber(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + if (!std::isfinite(ms) || ms < (repeat ? 1 : 0)) luaL_argerror(L, 1, repeat ? "must be 1 ms or more" : "must be 0 ms or more"); + if (plugin.timers_.size() >= plugin.limits_.timers) luaL_error(L, "more than %d timers", static_cast(plugin.limits_.timers)); + lua_pushvalue(L, 2); + const int function = lua_ref(L, -1); + lua_pop(L, 1); + const int id = plugin.next_timer_++; + plugin.timers_[id] = {plugin.services_.now_ms() + ms, repeat ? ms : 0, function}; + lua_pushinteger(L, id); + return 1; + } + static int after(lua_State* L) { return timer(L, false); } + static int every(lua_State* L) { return timer(L, true); } + static int cancel(lua_State* L) { + auto& plugin = plugin_of(L); + const auto found = plugin.timers_.find(luaL_checkinteger(L, 1)); + if (found != plugin.timers_.end()) { + lua_unref(L, found->second.function); + plugin.timers_.erase(found); + } + return 0; + } + static int level(lua_State* L) { + const auto name = plugin_of(L).services_.level(); + if (name.empty()) { + lua_pushnil(L); + return 1; + } + lua_createtable(L, 0, 1); + lua_pushlstring(L, name.data(), name.size()); + lua_setfield(L, -2, "name"); + return 1; + } + static int read(lua_State* L) { + auto& plugin = plugin_of(L); + const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false); + std::string error; + const auto text = read_package_file(file, plugin.limits_.file_bytes, error); + if (error == "not found") { + lua_pushnil(L); + return 1; + } + if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str()); + lua_pushlstring(L, text.data(), text.size()); + return 1; + } + static int read_json(lua_State* L) { + auto& plugin = plugin_of(L); + const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false); + std::string error; + const auto text = read_package_file(file, plugin.limits_.file_bytes, error); + if (error == "not found") { + lua_pushnil(L); + return 1; + } + if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str()); + push_json(L, Json::parse(text, {plugin.limits_.file_bytes, 64, 1u << 20})); + return 1; + } + static int list(lua_State* L) { + auto& plugin = plugin_of(L); + const auto folder = package_path(L, plugin, luaL_optstring(L, 1, ""), true); + std::vector names; + std::error_code code; + for (std::filesystem::directory_iterator it(folder, code), end; !code && it != end; it.increment(code)) { + const auto name = it->path().filename().wstring(); + if (!std::all_of(name.begin(), name.end(), [](wchar_t c) { return c >= 0x20 && c < 0x7f; })) continue; + std::string ascii; + for (const wchar_t c : name) ascii += static_cast(c); + if (it->is_directory(code)) ascii += '/'; + names.push_back(std::move(ascii)); + if (names.size() > maximum_listing) luaL_error(L, "the folder holds more than %d entries", static_cast(maximum_listing)); + } + std::sort(names.begin(), names.end()); + lua_createtable(L, static_cast(names.size()), 0); + for (std::size_t i = 0; i < names.size(); ++i) { + lua_pushlstring(L, names[i].data(), names[i].size()); + lua_rawseti(L, -2, static_cast(i + 1)); + } + return 1; + } + static int encode(lua_State* L) { + luaL_checkany(L, 1); + const auto text = lua_to_json(L, 1).dump(lua_toboolean(L, 2) ? 2 : -1); + lua_pushlstring(L, text.data(), text.size()); + return 1; + } + static int decode(lua_State* L) { + std::size_t length = 0; + const char* text = luaL_checklstring(L, 1, &length); + push_json(L, Json::parse(std::string_view(text, length), {16u << 20, 64, 1u << 20})); + return 1; + } + static int command(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "console", "reskate.console.command"); + std::size_t length = 0; + const char* text = luaL_checklstring(L, 1, &length); + const std::string help = luaL_checkstring(L, 2); + luaL_checktype(L, 3, LUA_TFUNCTION); + const std::string name(text, length); + // Lowercase words of letters, digits, _ and -, one space between them, at most three. + bool valid = !name.empty() && name.size() <= maximum_command && name.front() != ' ' && name.back() != ' ' && + name.find(" ") == std::string::npos && std::count(name.begin(), name.end(), ' ') < 3; + for (const char c : name) valid = valid && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == ' '); + if (!valid) luaL_error(L, "\"%s\" is not a command name: 1 to 3 lowercase words of a-z, 0-9, _ and -", name.c_str()); + if (plugin.commands_.size() >= plugin.limits_.commands) luaL_error(L, "more than %d commands", static_cast(plugin.limits_.commands)); + if (plugin.commands_.contains(name)) luaL_error(L, "the command \"%s\" is registered already", name.c_str()); + if (const auto refusal = plugin.services_.claim_command(plugin.id(), name); !refusal.empty()) + luaL_error(L, "%s", refusal.c_str()); + lua_pushvalue(L, 3); + plugin.commands_[name] = {help.substr(0, 200), lua_ref(L, -1)}; + lua_pop(L, 1); + return 0; + } + static int console_print(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "console", "reskate.console.print"); + plugin.print(text_of(L, 1)); + return 0; + } + static int storage_get(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "storage", "reskate.storage"); + const auto key = key_of(L, 1); + const auto found = plugin.storage_.find(key); + if (found == plugin.storage_.end()) { + lua_pushnil(L); + return 1; + } + push_json(L, *found); + return 1; + } + // Sets a key and checks the whole store against the quota; the value it had comes back on failure. + static void store(lua_State* L, Plugin& plugin, const std::string& key, Json value) { + std::optional previous; + if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) previous = *found; + if (value.is_null()) plugin.storage_.erase(key); + else plugin.storage_[key] = std::move(value); + if (const auto size = plugin.storage_.dump().size(); size > plugin.limits_.storage_bytes) { + if (previous) plugin.storage_[key] = std::move(*previous); + else plugin.storage_.erase(key); + luaL_error(L, "storage is full: that would make it %d KB, and a plugin has %d KB", static_cast(size >> 10), + static_cast(plugin.limits_.storage_bytes >> 10)); + } + plugin.storage_dirty_ = true; + } + static int storage_set(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "storage", "reskate.storage"); + const auto key = key_of(L, 1); + store(L, plugin, key, lua_to_json(L, 2)); + return 0; + } + static int storage_add(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "storage", "reskate.storage"); + const auto key = key_of(L, 1); + const double by = luaL_optnumber(L, 2, 1); + if (!std::isfinite(by)) luaL_argerror(L, 2, "must be a finite number"); + double value = 0; + if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) { + if (!found->is_number()) luaL_error(L, "storage key \"%s\" does not hold a number", key.c_str()); + value = found->get(); + } + value += by; + store(L, plugin, key, value == std::floor(value) && std::fabs(value) <= 9007199254740992.0 + ? Json(static_cast(value)) : Json(value)); + lua_pushnumber(L, value); + return 1; + } + static int storage_keys(lua_State* L) { + auto& plugin = plugin_of(L); + need(L, plugin, "storage", "reskate.storage"); + lua_createtable(L, static_cast(plugin.storage_.size()), 0); + int index = 0; + for (const auto& item : plugin.storage_.items()) { + lua_pushlstring(L, item.first.data(), item.first.size()); + lua_rawseti(L, -2, ++index); + } + return 1; + } + // Modules come from the plugin's own package, beside its entry: require("./rules") or + // require("rules") is /rules.luau or rules.lua. Each runs once. + static int require(lua_State* L) { + auto& plugin = plugin_of(L); + std::string name = luaL_checkstring(L, 1); + if (name.starts_with("./")) name.erase(0, 2); + const auto relative = safe_relative_path(name); + if (relative.empty()) luaL_error(L, "require(\"%s\"): a module is a path inside the plugin's package, with no ..", name.c_str()); + const auto in_root = [&](const std::string& path) { return plugin.code_root_.empty() ? path : plugin.code_root_ + "/" + path; }; + std::vector candidates; + if (relative.ends_with(".lua") || relative.ends_with(".luau")) candidates.push_back(in_root(relative)); + else { + candidates.push_back(in_root(relative + ".luau")); + candidates.push_back(in_root(relative + ".lua")); + } + std::string key; + std::filesystem::path file; + for (const auto& candidate : candidates) { + const auto path = package_path(L, plugin, candidate, false); + std::error_code code; + if (std::filesystem::is_regular_file(path, code)) { + key = candidate; + file = path; + break; + } + } + if (key.empty()) luaL_error(L, "require(\"%s\"): there is no %s", name.c_str(), candidates.front().c_str()); + lua_getref(L, plugin.modules_); + lua_getfield(L, -1, key.c_str()); + if (!lua_isnil(L, -1)) return 1; + lua_pop(L, 1); + if (plugin.loading_modules_.contains(key)) luaL_error(L, "require(\"%s\"): it requires itself, through another module", name.c_str()); + plugin.loading_modules_.insert(key); + struct Done { + Plugin& plugin; + std::string key; + ~Done() { plugin.loading_modules_.erase(key); } + } done{plugin, key}; + if (!plugin.load_chunk(L, key, file)) lua_error(L); + lua_call(L, 0, 1); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_pushboolean(L, 1); + } + lua_pushvalue(L, -1); + lua_setfield(L, -3, key.c_str()); + return 1; + } +}; + +Json lua_to_json(lua_State* L, int index) { + Budget budget; + return to_json(L, index, 0, budget); +} + +void push_json(lua_State* L, const Json& value) { push_json_value(L, value, 0); } + +void Api::install(Plugin& plugin, lua_State* L) { + const auto readonly = [&] { lua_setreadonly(L, -1, true); }; + lua_createtable(L, 0, 16); + lua_pushinteger(L, api_version); + lua_setfield(L, -2, "api_version"); + lua_pushlstring(L, plugin.id().data(), plugin.id().size()); + lua_setfield(L, -2, "id"); + set_function(L, "log", ApiFunctions::log); + set_function(L, "on", ApiFunctions::on); + + lua_createtable(L, 0, 4); + set_function(L, "now", ApiFunctions::now); + set_function(L, "after", ApiFunctions::after); + set_function(L, "every", ApiFunctions::every); + set_function(L, "cancel", ApiFunctions::cancel); + readonly(); + lua_setfield(L, -2, "time"); + + lua_createtable(L, 0, 1); + set_function(L, "current", ApiFunctions::level); + readonly(); + lua_setfield(L, -2, "level"); + + lua_createtable(L, 0, 3); + set_function(L, "read", ApiFunctions::read); + set_function(L, "read_json", ApiFunctions::read_json); + set_function(L, "list", ApiFunctions::list); + readonly(); + lua_setfield(L, -2, "package"); + + lua_createtable(L, 0, 2); + set_function(L, "encode", ApiFunctions::encode); + set_function(L, "decode", ApiFunctions::decode); + readonly(); + lua_setfield(L, -2, "json"); + + lua_createtable(L, 0, 2); + set_function(L, "command", ApiFunctions::command); + lua_pushcfunction(L, ApiFunctions::console_print, "print"); + lua_setfield(L, -2, "print"); + readonly(); + lua_setfield(L, -2, "console"); + + lua_createtable(L, 0, 4); + lua_pushcfunction(L, ApiFunctions::storage_get, "get"); + lua_setfield(L, -2, "get"); + lua_pushcfunction(L, ApiFunctions::storage_set, "set"); + lua_setfield(L, -2, "set"); + lua_pushcfunction(L, ApiFunctions::storage_add, "add"); + lua_setfield(L, -2, "add"); + lua_pushcfunction(L, ApiFunctions::storage_keys, "keys"); + lua_setfield(L, -2, "keys"); + readonly(); + lua_setfield(L, -2, "storage"); + + readonly(); + lua_setglobal(L, "reskate"); + lua_pushcfunction(L, ApiFunctions::print, "print"); + lua_setglobal(L, "print"); + lua_pushcfunction(L, ApiFunctions::require, "require"); + lua_setglobal(L, "require"); +} +} diff --git a/Extension/Plugins/plugin_api.h b/Extension/Plugins/plugin_api.h new file mode 100644 index 00000000..fc978ed3 --- /dev/null +++ b/Extension/Plugins/plugin_api.h @@ -0,0 +1,16 @@ +#pragma once +#include "plugin_vm.h" + +// Inside the plugin host only: the reskate API a plugin's VM gets. +namespace dingosdk::plugins { +struct Api { + // The `reskate` table, `print` and `require`, before the VM is sandboxed. + static void install(Plugin& plugin, lua_State* L); +}; +// A file's bytes when it is a regular file no larger than `most`; otherwise empty, and `error` +// says why ("not found" when there is none). +std::string read_package_file(const std::filesystem::path& file, std::size_t most, std::string& error); +// JSON and Lua values, both ways. Throws std::runtime_error on a value JSON cannot hold. +Json lua_to_json(lua_State* L, int index); +void push_json(lua_State* L, const Json& value); +} diff --git a/Extension/Plugins/plugin_commands.cpp b/Extension/Plugins/plugin_commands.cpp new file mode 100644 index 00000000..659e9718 --- /dev/null +++ b/Extension/Plugins/plugin_commands.cpp @@ -0,0 +1,40 @@ +#include "plugin_runtime.h" +#include "Extension/Console/commands.h" + +// `plugin list`, `plugin commands`, `plugin reload|off|on `: the plugins' state, and a +// plugin restarted or switched off by hand without touching its mod. +namespace dingosdk::console { +namespace { +void write_lines(const Output& out, const std::string& text) { + std::size_t start = 0; + while (start <= text.size()) { + const auto end = std::min(text.find('\n', start), text.size()); + out(text.substr(start, end - start)); + start = end + 1; + } +} +// No completion of ids: completion may run off the client thread, which alone reads the plugins. +Argument plugin_id() { return argument("id"); } +} +void register_plugin_commands(Commands& registry) { + auto list = action("plugin list", "List the plugins: running, stopped and why, memory and time a tick", Group::console); + list.run = [](const Model&, const Values&, const Output& out) { write_lines(out, plugins::describe()); }; + registry.add(std::move(list)); + + auto commands = action("plugin commands", "List the console commands plugins added", Group::console); + commands.run = [](const Model&, const Values&, const Output& out) { write_lines(out, plugins::list_commands()); }; + registry.add(std::move(commands)); + + auto reload = action("plugin reload", "Unload a plugin and load it again from its files", Group::console, {plugin_id()}); + reload.run = [](const Model&, const Values& args, const Output& out) { out(plugins::reload(std::get(args[0]))); }; + registry.add(std::move(reload)); + + auto off = action("plugin off", "Switch a plugin off until plugin on, or the next launch", Group::console, {plugin_id()}); + off.run = [](const Model&, const Values& args, const Output& out) { out(plugins::switch_off(std::get(args[0]))); }; + registry.add(std::move(off)); + + auto on = action("plugin on", "Start a plugin that was switched off or stopped", Group::console, {plugin_id()}); + on.run = [](const Model&, const Values& args, const Output& out) { out(plugins::switch_on(std::get(args[0]))); }; + registry.add(std::move(on)); +} +} // namespace dingosdk::console diff --git a/Extension/Plugins/plugin_discovery.cpp b/Extension/Plugins/plugin_discovery.cpp new file mode 100644 index 00000000..1fe5ed3e --- /dev/null +++ b/Extension/Plugins/plugin_discovery.cpp @@ -0,0 +1,65 @@ +#include "plugin_discovery.h" +#include "plugin_manifest.h" +#include "Engine/Vfs/mod_list.h" +#include +#include +#include + +namespace dingosdk::plugins { +namespace { +constexpr std::array runnable{".exe", ".dll", ".sys", ".scr", ".com", ".cpl", ".msi", ".msp", + ".bat", ".cmd", ".ps1", ".psm1", ".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".hta", ".lnk", ".pif", ".reg"}; +constexpr std::size_t most_files = 20000; + +// path::string() throws on names outside the ANSI code page; a name here is only shown or compared. +std::string ascii_lowercase(const std::filesystem::path& path) { + std::string text; + for (const auto c : path.generic_wstring()) + text += c < 0x80 ? static_cast(c >= L'A' && c <= L'Z' ? c + 32 : c) : '?'; + return text; +} +} + +std::string check_package_files(const std::filesystem::path& directory) { + std::error_code code; + std::size_t files = 0; + for (std::filesystem::recursive_directory_iterator it(directory, std::filesystem::directory_options::skip_permission_denied, code), end; + !code && it != end; it.increment(code)) { + std::error_code entry_code; + if (!it->is_regular_file(entry_code)) continue; + if (++files > most_files) return "it holds more than 20000 files"; + const auto shown = ascii_lowercase(std::filesystem::relative(it->path(), directory, entry_code)); + const auto extension = ascii_lowercase(it->path().extension()); + bool program = std::find(runnable.begin(), runnable.end(), extension) != runnable.end(); + if (!program) { + std::ifstream in(it->path(), std::ios::binary); + char magic[2]{}; + program = in.read(magic, 2) && magic[0] == 'M' && magic[1] == 'Z'; + } + if (program) return "it holds " + shown + ", a program or a script Windows runs. A plugin never ships one"; + } + return {}; +} + +std::vector discover(const std::filesystem::path& data_root, FileChecks& checks) { + std::vector result; + const auto list = mods::scan_mods(data_root); + // A mods.json the game cannot read loads no mod at all, plugins included. + if (!list.present || !list.issue.empty()) return result; + FileChecks seen; + for (const auto& entry : list.entries) { + if (!entry.enabled) continue; + const auto& directory = entry.mod.directory; + std::error_code code; + if (!std::filesystem::is_regular_file(directory / manifest_file, code)) continue; + Candidate candidate{entry.mod.name, directory, mods::mod_fingerprint(directory), {}}; + const auto key = candidate.id + "|" + candidate.fingerprint; + const auto known = checks.find(key); + candidate.refusal = known != checks.end() ? known->second : check_package_files(directory); + seen.emplace(key, candidate.refusal); + result.push_back(std::move(candidate)); + } + checks = std::move(seen); + return result; +} +} diff --git a/Extension/Plugins/plugin_discovery.h b/Extension/Plugins/plugin_discovery.h new file mode 100644 index 00000000..20bb4f99 --- /dev/null +++ b/Extension/Plugins/plugin_discovery.h @@ -0,0 +1,18 @@ +#pragma once +#include "plugin_host.h" +#include +#include +#include +#include + +// Which mods are plugins: the mods mods.json leaves on (Engine/Vfs/mod_list.h) that have a +// reskate-plugin.json. Reads the disk; run it off the game thread. +namespace dingosdk::plugins { +// What check_package_files said about a package, by its fingerprint, so an unchanged package is +// not read again. +using FileChecks = std::map>; +std::vector discover(const std::filesystem::path& data_root, FileChecks& checks); +// Why a package may not load, from its files alone, or empty. A plugin never ships a program: no +// Windows executable (by its first bytes, whatever its name) and no script Windows runs. +std::string check_package_files(const std::filesystem::path& directory); +} diff --git a/Extension/Plugins/plugin_host.cpp b/Extension/Plugins/plugin_host.cpp new file mode 100644 index 00000000..e7a1abfb --- /dev/null +++ b/Extension/Plugins/plugin_host.cpp @@ -0,0 +1,227 @@ +#include "plugin_host.h" +#include "plugin_api.h" +#include "Engine/Core/Console/console_core.h" +#include +#include + +namespace dingosdk::plugins { +namespace { +std::string lowercase(std::string text) { + for (auto& c : text) if (c >= 'A' && c <= 'Z') c = static_cast(c + 32); + return text; +} +} + +Host::Host(Environment& environment, Side side, Limits limits) : environment_(environment), side_(side), limits_(limits) {} + +Host::~Host() { shutdown(); } + +Host::Entry* Host::find(std::string_view id) { + const auto found = entries_.find(id); + return found == entries_.end() ? nullptr : &found->second; +} + +void Host::load(Entry& entry) { + entry.failure.clear(); + entry.plugin.reset(); + const auto& id = entry.candidate.id; + if (!entry.candidate.refusal.empty()) { + entry.failure = entry.candidate.refusal; + environment_.log(LogLevel::warn, std::format("Plugin {} was not loaded: {}", id, entry.failure)); + return; + } + Manifest manifest; + try { + std::string error; + const auto text = read_package_file(entry.candidate.directory / manifest_file, maximum_manifest_bytes, error); + if (!error.empty()) throw std::runtime_error(std::string(manifest_file) + " " + error); + manifest = parse_manifest(text); + } catch (const std::exception& error) { + entry.failure = error.what(); + environment_.log(LogLevel::warn, std::format("Plugin {} was not loaded: {}", id, entry.failure)); + return; + } + const bool here = manifest.side == Side::both || manifest.side == side_; + if (!here) { + entry.failure = std::format("it is a {} plugin", side_name(manifest.side)); + environment_.log(LogLevel::info, std::format("Plugin {} was not loaded here: {}", id, entry.failure)); + return; + } + std::string permissions; + for (const auto& permission : manifest.permissions) permissions += (permissions.empty() ? "" : ", ") + permission; + Services& services = *this; + entry.plugin = std::make_unique(id, entry.candidate.directory, std::move(manifest), services, limits_); + if (!entry.plugin->load()) { + entry.failure = entry.plugin->reason(); + entry.plugin.reset(); + release_commands(id); // its own log says why it stopped + return; + } + environment_.log(LogLevel::info, std::format("Plugin {} loaded ({}).", id, permissions.empty() ? "no permissions" : permissions)); +} + +void Host::unload(Entry& entry) { + if (entry.plugin) { + entry.plugin->unload(); + entry.plugin.reset(); + } + release_commands(entry.candidate.id); +} + +void Host::collect(Entry& entry) { + if (!entry.plugin || entry.plugin->state() != State::stopped) return; + entry.failure = entry.plugin->reason(); + entry.plugin->close_if_stopped(); + entry.plugin.reset(); + release_commands(entry.candidate.id); +} + +void Host::release_commands(std::string_view id) { + std::erase_if(commands_, [&](const auto& item) { return item.second == id; }); +} + +void Host::apply(const std::vector& found) { + std::map> wanted; + for (const auto& candidate : found) wanted.emplace(candidate.id, &candidate); + for (auto it = entries_.begin(); it != entries_.end();) { + if (wanted.contains(it->first)) { + ++it; + continue; + } + if (it->second.plugin) environment_.log(LogLevel::info, std::format("Plugin {} unloaded: its mod was switched off or removed.", it->first)); + unload(it->second); + it = entries_.erase(it); + } + for (const auto& [id, candidate] : wanted) { + auto* entry = find(id); + if (!entry) { + entry = &entries_.emplace(id, Entry{*candidate, {}, {}, false}).first->second; + load(*entry); + continue; + } + if (entry->candidate == *candidate) continue; + entry->candidate = *candidate; + if (entry->held) continue; + environment_.log(LogLevel::info, std::format("Plugin {} reloading: its files changed.", id)); + unload(*entry); + load(*entry); + } +} + +void Host::tick(double dt_ms) { + const double now = environment_.now_ms(); + const bool save = now >= next_save_; + if (save) next_save_ = now + 2000; + for (auto& [id, entry] : entries_) { + if (!entry.plugin) continue; + entry.plugin->tick(dt_ms); + collect(entry); + if (save && entry.plugin) entry.plugin->save_storage(); + } +} + +void Host::set_level(std::string level) { + if (level == level_) return; + const auto previous = std::exchange(level_, std::move(level)); + for (auto& [id, entry] : entries_) { + if (!entry.plugin) continue; + entry.plugin->level_changed(previous, level_); + collect(entry); + } +} + +bool Host::run_console(std::string_view text) { + if (commands_.empty()) return false; + const auto parsed = parse_console_command(text); + if (!parsed || parsed.arguments.empty()) return false; + const auto& words = parsed.arguments; + // The longest command name the text starts with. + for (std::size_t count = std::min(3, words.size()); count > 0; --count) { + std::string name; + for (std::size_t i = 0; i < count; ++i) name += (i ? " " : "") + lowercase(words[i]); + const auto found = commands_.find(name); + if (found == commands_.end()) continue; + auto* entry = find(found->second); + if (!entry || !entry->plugin) return false; + entry->plugin->run_command(name, {words.begin() + static_cast(count), words.end()}); + collect(*entry); + return true; + } + return false; +} + +std::vector Host::status() const { + std::vector result; + for (const auto& [id, entry] : entries_) { + Status status{id}; + if (entry.plugin) { + status.state = "running"; + status.memory = entry.plugin->memory(); + status.tick_ms = entry.plugin->tick_ms(); + status.commands = entry.plugin->commands(); + } else if (entry.held) { + status.state = "switched off"; + status.reason = "plugin on " + id + " starts it again"; + } else { + status.state = entry.failure.starts_with("it is a ") ? "not loaded" : "stopped"; + status.reason = entry.failure; + } + result.push_back(std::move(status)); + } + return result; +} + +std::string Host::reload(std::string_view id) { + auto* entry = find(id); + if (!entry) return std::format("error: There is no plugin {}.", id); + entry->held = false; + unload(*entry); + load(*entry); + return entry->plugin ? std::format("Plugin {} reloaded.", id) : std::format("error: Plugin {} did not start: {}", id, entry->failure); +} + +std::string Host::switch_off(std::string_view id) { + auto* entry = find(id); + if (!entry) return std::format("error: There is no plugin {}.", id); + entry->held = true; + unload(*entry); + return std::format("Plugin {} switched off until plugin on {}, or the next launch.", id, id); +} + +std::string Host::switch_on(std::string_view id) { + auto* entry = find(id); + if (!entry) return std::format("error: There is no plugin {}.", id); + if (entry->plugin) return std::format("Plugin {} is running.", id); + entry->held = false; + load(*entry); + return entry->plugin ? std::format("Plugin {} started.", id) : std::format("error: Plugin {} did not start: {}", id, entry->failure); +} + +void Host::shutdown() { + for (auto& [id, entry] : entries_) unload(entry); + entries_.clear(); +} + +void Host::log(std::string_view plugin, LogLevel level, std::string_view text) { + environment_.log(level, std::format("[{}] {}", plugin, text)); +} +void Host::console(std::string_view text) { environment_.console(text); } +double Host::now_ms() { return environment_.now_ms(); } +std::string Host::level() { return level_; } +std::filesystem::path Host::storage_file(std::string_view plugin) { + return environment_.storage_root() / std::filesystem::path(std::string(plugin)) / "storage.json"; +} + +std::string Host::claim_command(std::string_view plugin, std::string_view name) { + const auto first = name.substr(0, name.find(' ')); + if (first == "plugin" || first == "help" || environment_.builtin_command(first)) + return std::format("ReSkate has a console command \"{}\": pick another name", first); + for (const auto& [taken, owner] : commands_) { + if (owner == plugin) continue; + const bool overlaps = taken == name || name.starts_with(taken + " ") || taken.starts_with(std::string(name) + " "); + if (overlaps) return std::format("the plugin {} has the console command \"{}\"", owner, taken); + } + commands_.emplace(std::string(name), std::string(plugin)); + return {}; +} +} diff --git a/Extension/Plugins/plugin_host.h b/Extension/Plugins/plugin_host.h new file mode 100644 index 00000000..d335a71d --- /dev/null +++ b/Extension/Plugins/plugin_host.h @@ -0,0 +1,96 @@ +#pragma once +#include "plugin_vm.h" +#include +#include +#include +#include +#include +#include + +// Every plugin the game runs: which are loaded, reloaded when their files change, switched off +// with their mod, and stopped when they go over a limit. The host is what plugins share: the +// level being played and the console's command names. One thread drives it (the client tick). +namespace dingosdk::plugins { +// A mod folder that is a plugin, as discovery found it. +struct Candidate { + std::string id; // the mod's folder name + std::filesystem::path directory; // the mod folder + std::string fingerprint; // changes when any of its files does + std::string refusal; // why it may not load, from its files alone, or empty + bool operator==(const Candidate&) const = default; +}; + +// What the host needs from where it runs: the game, or the test runner. +class Environment { +public: + virtual ~Environment() = default; + virtual void log(LogLevel level, std::string_view text) = 0; + virtual void console(std::string_view text) = 0; + virtual double now_ms() = 0; + // //storage.json holds each plugin's storage. + virtual std::filesystem::path storage_root() = 0; + // Whether ReSkate has a console command starting with this word. + virtual bool builtin_command(std::string_view word) = 0; +}; + +struct Status { + std::string id; + std::string state; // "running", "stopped", "switched off", "not loaded" + std::string reason; // why it is not running + std::size_t memory{}; + double tick_ms{}; + std::vector> commands; +}; + +class Host final : private Services { +public: + // A client host runs client plugins and those for both sides; a server host, server ones and both. + Host(Environment& environment, Side side, Limits limits = {}); + ~Host() override; + Host(const Host&) = delete; + Host& operator=(const Host&) = delete; + + // The plugins discovery found now: new ones load, changed ones reload, missing ones unload. + void apply(const std::vector& found); + void tick(double dt_ms); + void set_level(std::string level); + // Runs a plugin's console command when the text starts with one; false when none matched. + bool run_console(std::string_view text); + std::vector status() const; + // The `plugin` console commands. Each returns what to print. + std::string reload(std::string_view id); + std::string switch_off(std::string_view id); + std::string switch_on(std::string_view id); + // Unloads every plugin (their unload handlers run). + void shutdown(); + +private: + struct Entry { + Candidate candidate; + std::unique_ptr plugin; + std::string failure; // why it is not running + bool held{}; // switched off with `plugin off` until `plugin on` or the game restarts + }; + void load(Entry& entry); + void unload(Entry& entry); + // A plugin that stopped itself (a limit, its errors) is closed and its commands released. + void collect(Entry& entry); + void release_commands(std::string_view id); + Entry* find(std::string_view id); + + void log(std::string_view plugin, LogLevel level, std::string_view text) override; + void console(std::string_view text) override; + double now_ms() override; + std::string level() override; + std::filesystem::path storage_file(std::string_view plugin) override; + std::string claim_command(std::string_view plugin, std::string_view name) override; + + Environment& environment_; + Side side_; + Limits limits_; + std::map> entries_; + std::map> commands_; // command name: plugin id + std::string level_; + double next_save_{}; +}; +} diff --git a/Extension/Plugins/plugin_manifest.cpp b/Extension/Plugins/plugin_manifest.cpp new file mode 100644 index 00000000..ffe5be78 --- /dev/null +++ b/Extension/Plugins/plugin_manifest.cpp @@ -0,0 +1,87 @@ +#include "plugin_manifest.h" +#include "Engine/Core/Json/json.h" +#include +#include + +namespace dingosdk::plugins { +namespace { +constexpr std::array permissions{ + "console", "storage", "overlay", "menu", "ui.screen", "input", "hud", "events.tricks", "events.skater", + "skater.read", "skater.teleport", "skater.control", "skater.animation", "trainer", "camera", + "world.raycast", "world.objects", "world.environment", "world.ai", "travel", "session.read", + "session.skaters", "session.nametags", "net.session", "net.http", "voice.control", "chat.read", + "chat.send", "throwdowns", "replay", "music", "progression.read", "settings.read", "audio.ui", "mods.read"}; +} + +bool known_permission(std::string_view name) noexcept { + for (const auto known : permissions) if (known == name) return true; + return false; +} + +std::string safe_relative_path(std::string_view path) { + if (path.empty() || path.size() > 260 || path.front() == '/') return {}; + for (const unsigned char c : path) + if (c < 0x20 || c > 0x7e || c == '\\' || c == ':') return {}; + std::size_t start = 0; + while (start <= path.size()) { + const auto end = std::min(path.find('/', start), path.size()); + const auto segment = path.substr(start, end - start); + if (segment.empty() || segment == "." || segment == "..") return {}; + // Windows drops trailing dots and spaces, so "a." would open "a". + if (segment.back() == '.' || segment.back() == ' ') return {}; + start = end + 1; + } + return std::string(path); +} + +const char* side_name(Side side) noexcept { + return side == Side::server ? "server" : side == Side::both ? "both" : "client"; +} + +Manifest parse_manifest(std::string_view text) { + if (text.size() > maximum_manifest_bytes) throw std::runtime_error("reskate-plugin.json is larger than 64 KB"); + Json json; + try { + json = Json::parse(text, {maximum_manifest_bytes, 8, 4096}); + } catch (const std::exception& error) { + throw std::runtime_error(std::string("reskate-plugin.json is not valid JSON: ") + error.what()); + } + if (!json.is_object()) throw std::runtime_error("reskate-plugin.json must hold a JSON object"); + Manifest result; + if (!json.contains("api") || !json.at("api").is_number_integer()) + throw std::runtime_error("reskate-plugin.json needs \"api\": the API version it was written for (1)"); + const auto api = json.at("api").get(); + if (api < 1) throw std::runtime_error("\"api\" must be 1 or more"); + if (api > api_version) + throw std::runtime_error("written for plugin API " + std::to_string(api) + ", and this ReSkate has API " + + std::to_string(api_version) + ": update ReSkate"); + result.api = static_cast(api); + if (!json.contains("entry") || !json.at("entry").is_string()) + throw std::runtime_error("reskate-plugin.json needs \"entry\": the Lua file to run, such as \"plugin/main.lua\""); + result.entry = safe_relative_path(json.at("entry").string()); + if (result.entry.empty()) + throw std::runtime_error("\"entry\" must be a path inside the mod folder, with / between folders and no .."); + if (!result.entry.ends_with(".lua") && !result.entry.ends_with(".luau")) + throw std::runtime_error("\"entry\" must be a .lua or .luau file"); + if (json.contains("side")) { + const auto& side = json.at("side"); + const auto name = side.is_string() ? side.string() : std::string{}; + if (name == "client") result.side = Side::client; + else if (name == "server") result.side = Side::server; + else if (name == "both") result.side = Side::both; + else throw std::runtime_error("\"side\" must be \"client\", \"server\" or \"both\""); + } + if (json.contains("permissions")) { + const auto& list = json.at("permissions"); + if (!list.is_array()) throw std::runtime_error("\"permissions\" must be a list of names"); + for (const auto& item : list) { + if (!item.is_string()) throw std::runtime_error("\"permissions\" must be a list of names"); + const auto& name = item.string(); + if (!known_permission(name)) + throw std::runtime_error("unknown permission \"" + name + "\": check the spelling, or update ReSkate"); + result.permissions.insert(name); + } + } + return result; +} +} diff --git a/Extension/Plugins/plugin_manifest.h b/Extension/Plugins/plugin_manifest.h new file mode 100644 index 00000000..aeb591a6 --- /dev/null +++ b/Extension/Plugins/plugin_manifest.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include + +// reskate-plugin.json, beside a mod's manifest.json, is what makes a mod a plugin: +// {"api": 1, "entry": "plugin/main.lua", "side": "client", "permissions": ["console", "storage"]} +// Thunderstore's manifest.json keeps its own rules; this file is ReSkate's alone. +namespace dingosdk::plugins { +inline constexpr char manifest_file[] = "reskate-plugin.json"; +// The API this ReSkate gives plugins. A plugin written for a newer one is refused. +inline constexpr int api_version = 1; +inline constexpr std::size_t maximum_manifest_bytes = 64 * 1024; + +enum class Side { client, server, both }; +struct Manifest { + int api{}; + std::string entry; // inside the package, as safe_relative_path leaves it + Side side{Side::client}; + std::set> permissions; +}; +// Every permission a manifest may name, including those whose APIs a later ReSkate adds, so a +// manifest written for the whole plan stays valid. The base set needs none. +bool known_permission(std::string_view name) noexcept; +// Throws std::runtime_error with a message a modder can act on. +Manifest parse_manifest(std::string_view text); +// A path a plugin names (its entry, a require, a package read) in generic form, or empty when +// it could leave the package: absolute, a drive or a stream (':'), a backslash, an empty, "." or +// ".." segment, or a character outside printable ASCII. +std::string safe_relative_path(std::string_view path); +const char* side_name(Side side) noexcept; +} diff --git a/Extension/Plugins/plugin_runtime.cpp b/Extension/Plugins/plugin_runtime.cpp new file mode 100644 index 00000000..b0349267 --- /dev/null +++ b/Extension/Plugins/plugin_runtime.cpp @@ -0,0 +1,173 @@ +#include "plugin_runtime.h" +#include "plugin_discovery.h" +#include "plugin_host.h" +#include "Engine/Core/Log/logging.h" +#include "Extension/Console/commands.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dingosdk::plugins { +namespace { +class GameEnvironment final : public Environment { +public: + explicit GameEnvironment(std::filesystem::path storage) : storage_(std::move(storage)) {} + void log(LogLevel level, std::string_view text) override { + logging::write(level == LogLevel::error ? logging::Level::error : level == LogLevel::warn ? logging::Level::warning : logging::Level::info, + logging::Channel::plugins, text); + } + // As the console's own lines are written (console_line): an "error: " line is shown as one. + void console(std::string_view text) override { + const bool error = text.starts_with("error: "); + logging::write(error ? logging::Level::error : logging::Level::info, logging::Channel::command, error ? text.substr(7) : text); + } + double now_ms() override { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + } + std::filesystem::path storage_root() override { return storage_; } + bool builtin_command(std::string_view word) override { + const auto& commands = console::game_commands(); + return commands.find(word) || !commands.under(word).empty(); + } + +private: + std::filesystem::path storage_; +}; + +std::filesystem::path data_root; +// Left to the process's end on purpose: unloading plugins from a static destructor would run +// their scripts while the game shuts down. +GameEnvironment* environment{}; +Host* host{}; +std::mutex found_mutex; +std::vector found; // under found_mutex +std::uint64_t found_generation{}; // under found_mutex +std::uint64_t applied_generation{}; +std::chrono::steady_clock::time_point last_tick{}; + +void watch() { + FileChecks checks; + for (;;) { + try { + auto plugins = discover(data_root, checks); + std::lock_guard lock(found_mutex); + if (plugins != found || !found_generation) { + found = std::move(plugins); + ++found_generation; + } + } catch (const std::exception& error) { + logging::log(logging::Level::warning, logging::Channel::plugins, "Looking for plugins failed: {}", error.what()); + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } +} + +std::filesystem::path local_storage() { + PWSTR raw{}; + std::filesystem::path result; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &raw)) && raw) + result = std::filesystem::path(raw) / L"ReSkate" / L"Plugins"; + CoTaskMemFree(raw); + return result; +} +} + +bool start() noexcept { + try { + if (host) return true; + std::array path{}; + const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (!length || length >= path.size()) return false; + data_root = std::filesystem::path(std::wstring(path.data(), length)).parent_path(); + auto storage = local_storage(); + if (storage.empty()) storage = data_root / L"ReSkate.Plugins"; + environment = new GameEnvironment(std::move(storage)); + host = new Host(*environment, Side::client); + std::thread(watch).detach(); + logging::write(logging::Level::info, logging::Channel::plugins, "Plugins: looking in Mods for reskate-plugin.json every 2 s."); + return true; + } catch (const std::exception& error) { + logging::log(logging::Level::error, logging::Channel::plugins, "Plugins could not start: {}", error.what()); + return false; + } +} + +void on_client_tick() noexcept { + if (!host) return; + try { + { + std::vector latest; + bool changed = false; + { + std::lock_guard lock(found_mutex); + if (found_generation != applied_generation) { + latest = found; + applied_generation = found_generation; + changed = true; + } + } + if (changed) host->apply(latest); + } + const auto now = std::chrono::steady_clock::now(); + const double dt = last_tick == std::chrono::steady_clock::time_point{} ? 0 + : std::chrono::duration(now - last_tick).count(); + last_tick = now; + host->tick(dt); + } catch (const std::exception& error) { + logging::log(logging::Level::error, logging::Channel::plugins, "Plugin tick failed: {}", error.what()); + } +} + +void set_level(std::string_view level) noexcept { + if (!host) return; + try { host->set_level(std::string(level)); } + catch (const std::exception& error) { + logging::log(logging::Level::error, logging::Channel::plugins, "Plugin level change failed: {}", error.what()); + } +} + +bool run_console(const std::string& text) noexcept { + if (!host) return false; + try { return host->run_console(text); } + catch (const std::exception& error) { + logging::log(logging::Level::error, logging::Channel::plugins, "Plugin command failed: {}", error.what()); + return true; + } +} + +std::string describe() { + if (!host) return "error: Plugins did not start."; + const auto plugins = host->status(); + if (plugins.empty()) return "No plugins. A plugin is a mod in Mods with a reskate-plugin.json."; + std::string text; + for (const auto& plugin : plugins) { + if (!text.empty()) text += '\n'; + text += std::format("{}: {}", plugin.id, plugin.state); + if (plugin.state == "running") + text += std::format(", {:.1f} MB, {:.3f} ms a tick, {} command{}", plugin.memory / 1048576.0, plugin.tick_ms, + plugin.commands.size(), plugin.commands.size() == 1 ? "" : "s"); + else if (!plugin.reason.empty()) text += " (" + plugin.reason + ")"; + } + return text; +} + +std::string list_commands() { + if (!host) return "error: Plugins did not start."; + std::string text; + for (const auto& plugin : host->status()) + for (const auto& [name, help] : plugin.commands) { + if (!text.empty()) text += '\n'; + text += std::format("{} - {} ({})", name, help, plugin.id); + } + return text.empty() ? "No plugin has a console command." : text; +} + +std::string reload(std::string_view id) { return host ? host->reload(id) : "error: Plugins did not start."; } +std::string switch_off(std::string_view id) { return host ? host->switch_off(id) : "error: Plugins did not start."; } +std::string switch_on(std::string_view id) { return host ? host->switch_on(id) : "error: Plugins did not start."; } +} diff --git a/Extension/Plugins/plugin_runtime.h b/Extension/Plugins/plugin_runtime.h new file mode 100644 index 00000000..0f7f30ec --- /dev/null +++ b/Extension/Plugins/plugin_runtime.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +// Plugins in the game (Extension/Plugins): Luau scripts in mods, each in its own sandboxed VM +// (plugin_vm.h), run on the client tick. A thread looks for plugins every 2 s, so a mod switched +// on or off, or a plugin's files changed, takes effect without a restart. +// +// Every function but start is for the client thread, the one that runs console commands. +namespace dingosdk::plugins { +// Once, at startup. Plugins load at the first client tick after discovery finds them. +bool start() noexcept; +void on_client_tick() noexcept; +// The level being played, or empty without one: plugins hear level_loaded and level_unloading. +void set_level(std::string_view level) noexcept; +// Runs a plugin's console command when the text starts with one. False when none matched, and +// the text goes to ReSkate's own commands. +bool run_console(const std::string& text) noexcept; +// The `plugin` console commands. +std::string describe(); +std::string list_commands(); +std::string reload(std::string_view id); +std::string switch_off(std::string_view id); +std::string switch_on(std::string_view id); +} diff --git a/Extension/Plugins/plugin_vm.cpp b/Extension/Plugins/plugin_vm.cpp new file mode 100644 index 00000000..a8d29e18 --- /dev/null +++ b/Extension/Plugins/plugin_vm.cpp @@ -0,0 +1,384 @@ +#include "plugin_vm.h" +#include "plugin_api.h" +#include "lua.h" +#include "luacode.h" +#include "lualib.h" +#include +#include +#include + +namespace dingosdk::plugins { +namespace { +double elapsed_ms(std::chrono::steady_clock::time_point since) { + return std::chrono::duration(std::chrono::steady_clock::now() - since).count(); +} +// One log line: an error message can be as long as a script makes it. +std::string shortened(std::string_view text) { + constexpr std::size_t most = 2000; + return text.size() <= most ? std::string(text) : std::string(text.substr(0, most)) + " ..."; +} +// The traceback is added where the error was raised, while its stack is still there. +int traceback(lua_State* L) { + std::string text = lua_isstring(L, 1) ? lua_tostring(L, 1) : "an error that is not a string"; + if (const char* trace = lua_debugtrace(L); trace && *trace) { + text += "\n"; + text += trace; + } + lua_pushlstring(L, text.data(), text.size()); + return 1; +} +} + +std::string read_package_file(const std::filesystem::path& file, std::size_t most, std::string& error) { + error.clear(); + std::error_code code; + if (!std::filesystem::is_regular_file(file, code)) { error = "not found"; return {}; } + const auto size = std::filesystem::file_size(file, code); + if (code) { error = "cannot be read"; return {}; } + if (size > most) { error = std::format("is larger than {} MB", most >> 20); return {}; } + std::ifstream in(file, std::ios::binary); + std::string text(static_cast(size), '\0'); + if (!in || !in.read(text.data(), static_cast(size))) { error = "cannot be read"; return {}; } + return text; +} + +Plugin::Plugin(std::string id, std::filesystem::path directory, Manifest manifest, Services& services, Limits limits) + : id_(std::move(id)), directory_(std::move(directory)), manifest_(std::move(manifest)), services_(services), + limits_(limits) { + const auto entry = std::filesystem::path(manifest_.entry); + code_root_ = entry.has_parent_path() ? entry.parent_path().generic_string() : std::string{}; +} + +Plugin::~Plugin() { close(); } + +void* Plugin::allocate(void* ud, void* ptr, std::size_t old_size, std::size_t new_size) { + auto& self = *static_cast(ud); + if (!ptr) old_size = 0; + if (new_size == 0) { + std::free(ptr); + self.memory_ -= old_size; + return nullptr; + } + if (new_size > old_size && self.memory_ - old_size + new_size > self.limits_.memory) { + self.memory_exceeded_ = true; + return nullptr; + } + void* block = std::realloc(ptr, new_size); + if (!block) return nullptr; + self.memory_ = self.memory_ - old_size + new_size; + return block; +} + +// Called at Luau's safe points: loop back edges, calls and returns. Once a plugin is over a limit +// every safe point raises again, so a pcall in the script cannot catch it and carry on. +void Plugin::interrupt(lua_State* L, int gc) { + if (gc >= 0) return; // inside the collector, where raising is not allowed + auto& self = *static_cast(lua_callbacks(L)->userdata); + if (self.halt_ == Halt::none) { + if (self.memory_exceeded_) self.halt_ = Halt::memory; + else if (self.timed_ && (++self.interrupts_ & 63) == 0 && Clock::now() > self.deadline_) self.halt_ = Halt::time; + } + if (self.halt_ != Halt::none) luaL_error(L, "the plugin was stopped"); +} + +bool Plugin::load(std::string entry) { + if (L_) return state_ == State::running; + state_ = State::loading; + reason_.clear(); + halt_ = Halt::none; + memory_exceeded_ = false; + if (!storage_loaded_) { + storage_loaded_ = true; + const auto file = services_.storage_file(id_); + std::string error; + const auto text = read_package_file(file, limits_.storage_bytes * 2, error); + if (error.empty()) { + try { + auto stored = Json::parse(text, {limits_.storage_bytes * 2, 33, 1u << 20}); + if (!stored.is_object()) throw std::runtime_error("it does not hold an object"); + storage_ = std::move(stored); + } catch (const std::exception& problem) { + // Kept aside, not overwritten by the next save. + std::error_code code; + std::filesystem::rename(file, std::filesystem::path(file) += ".bad", code); + log(LogLevel::warn, std::string("its storage could not be read and was set aside: ") + problem.what()); + } + } + } + L_ = lua_newstate(&Plugin::allocate, this); + if (!L_) { + stop("its VM could not be created"); + return false; + } + auto* callbacks = lua_callbacks(L_); + callbacks->userdata = this; + callbacks->interrupt = &Plugin::interrupt; + try { + luaL_openlibs(L_); + Api::install(*this, L_); + luaL_sandbox(L_); + lua_State* thread = lua_newthread(L_); + luaL_sandboxthread(thread); + script_thread_ = lua_ref(L_, -1); + lua_pop(L_, 1); + lua_newtable(L_); + modules_ = lua_ref(L_, -1); + lua_pop(L_, 1); + } catch (const std::exception& error) { + stop(std::string("its VM could not be set up: ") + error.what()); + close(); + return false; + } + state_ = State::running; + const auto relative = entry.empty() ? manifest_.entry : entry; + if (!load_chunk(L_, relative, directory_ / std::filesystem::path(relative))) { + const std::string message = lua_isstring(L_, -1) ? lua_tostring(L_, -1) : "it could not be loaded"; + lua_pop(L_, 1); + stop(relative + " " + message); + close(); + return false; + } + if (!call(0, relative, limits_.call_ms) && state_ == State::running) stop(relative + " raised an error as it ran"); + if (state_ == State::running) fire("load", {}); + if (state_ != State::running) { + close(); + return false; + } + return true; +} + +bool Plugin::load_chunk(lua_State* L, const std::string& relative, const std::filesystem::path& file) { + std::string error; + const auto source = read_package_file(file, limits_.source_bytes, error); + if (!error.empty()) { + lua_pushstring(L, error.c_str()); + return false; + } + lua_CompileOptions options{}; + options.optimizationLevel = 1; + options.debugLevel = 1; + std::size_t size = 0; + // Only text is ever loaded: the bytecode handed to the VM is the compiler's own. + char* bytecode = luau_compile(source.data(), source.size(), &options, &size); + if (!bytecode) { + lua_pushstring(L, "could not be compiled"); + return false; + } + lua_getref(L, script_thread_); + lua_State* thread = lua_tothread(L, -1); + lua_pop(L, 1); + const std::string chunk = "@" + relative; + const int status = luau_load(thread, chunk.c_str(), bytecode, size, 0); + std::free(bytecode); + lua_xmove(thread, L, 1); + return status == 0; +} + +bool Plugin::call(int count, std::string_view what, double limit_ms) { + const int function = lua_gettop(L_) - count; + if (state_ != State::running) { + lua_settop(L_, function - 1); + return false; + } + lua_pushcfunction(L_, traceback, "traceback"); + lua_insert(L_, function); + const auto start = Clock::now(); + deadline_ = start + std::chrono::duration_cast(std::chrono::duration(limit_ms)); + timed_ = true; + interrupts_ = 0; + const int status = lua_pcall(L_, count, 0, function); + timed_ = false; + spent_ms_ += elapsed_ms(start); + // Caught in the script or not, going over its memory stops it. + if (memory_exceeded_ && halt_ == Halt::none) halt_ = Halt::memory; + if (status == LUA_OK && halt_ == Halt::none) { + lua_settop(L_, function - 1); + return true; + } + if (halt_ == Halt::none && (memory_exceeded_ || status == LUA_ERRMEM)) halt_ = Halt::memory; + std::string message; + if (halt_ == Halt::none) message = lua_isstring(L_, -1) ? lua_tostring(L_, -1) : "an error that is not a string"; + lua_settop(L_, function - 1); + if (halt_ == Halt::time) stop(std::format("{} ran longer than {:g} ms", what, limit_ms)); + else if (halt_ == Halt::memory) stop(std::format("it used more than {} MB of memory", limits_.memory >> 20)); + else if (halt_ == Halt::none) report(what, message); + return false; +} + +void Plugin::report(std::string_view what, std::string_view message) { + services_.log(id_, LogLevel::error, shortened(std::string(what) + ": " + std::string(message))); + const double now = services_.now_ms(); + errors_.push_back(now); + std::erase_if(errors_, [&](double at) { return now - at > limits_.error_window_ms; }); + if (static_cast(errors_.size()) > limits_.errors) + stop(std::format("more than {} errors in {} s", limits_.errors, limits_.error_window_ms / 1000)); +} + +bool Plugin::count_line() { + const double now = services_.now_ms(); + if (now - log_second_ >= 1000) { + log_second_ = now; + log_lines_ = 0; + } + if (++log_lines_ <= limits_.log_lines_per_second) return true; + if (log_lines_ == limits_.log_lines_per_second + 1) + services_.log(id_, LogLevel::warn, std::format("more than {} lines in a second: the rest of this second's are left out", + limits_.log_lines_per_second)); + return false; +} + +void Plugin::log(LogLevel level, std::string_view text) { + if (count_line()) services_.log(id_, level, shortened(text)); +} + +void Plugin::print(std::string_view text) { + if (count_line()) services_.console(shortened(text)); +} + +void Plugin::stop(std::string reason) { + if (state_ == State::stopped) return; + state_ = State::stopped; + reason_ = std::move(reason); + if (halt_ == Halt::none) halt_ = Halt::stopped; + services_.log(id_, LogLevel::error, "stopped: " + reason_); +} + +void Plugin::close_if_stopped() { + if (state_ == State::stopped && L_) close(); +} + +void Plugin::close() { + save_storage(); + handlers_.clear(); + timers_.clear(); + commands_.clear(); + loading_modules_.clear(); + if (L_) { + lua_close(L_); + L_ = nullptr; + } + script_thread_ = modules_ = 0; +} + +void Plugin::unload() { + if (!L_) return; + if (state_ == State::running) { + const auto start = Clock::now(); + const auto found = handlers_.find("unload"); + const auto handlers = found == handlers_.end() ? std::vector{} : found->second; + for (const int handler : handlers) { + const double left = limits_.unload_ms - elapsed_ms(start); + if (left <= 0 || state_ != State::running) break; + lua_getref(L_, handler); + call(0, "the unload handler", left); + } + if (state_ == State::running) { + state_ = State::stopped; + reason_ = "unloaded"; + } + } + close(); +} + +void Plugin::fire(std::string_view event, const std::vector& arguments) { + if (state_ != State::running) return; + const auto found = handlers_.find(event); + if (found == handlers_.end()) return; + const auto handlers = found->second; // a handler may add others + const auto what = std::format("the {} handler", event); + for (const int handler : handlers) { + if (state_ != State::running) return; + lua_getref(L_, handler); + for (const auto& argument : arguments) lua_pushlstring(L_, argument.data(), argument.size()); + call(static_cast(arguments.size()), what, limits_.call_ms); + } +} + +void Plugin::tick(double dt_ms) { + if (state_ != State::running) return; + const double now = services_.now_ms(); + std::vector due; + for (const auto& [id, timer] : timers_) + if (timer.due <= now) due.push_back(id); + for (const int id : due) { + if (state_ != State::running) break; + const auto found = timers_.find(id); + if (found == timers_.end()) continue; // cancelled by an earlier one + const int function = found->second.function; + const bool repeat = found->second.every > 0; + if (repeat) found->second.due = now + found->second.every; + else timers_.erase(found); + lua_getref(L_, function); + if (!repeat) lua_unref(L_, function); + call(0, "a timer", limits_.call_ms); + } + if (const auto found = handlers_.find("tick"); found != handlers_.end() && state_ == State::running) { + const auto handlers = found->second; + for (const int handler : handlers) { + if (state_ != State::running) break; + lua_getref(L_, handler); + lua_pushnumber(L_, dt_ms); + call(1, "the tick handler", limits_.call_ms); + } + } + // The time budget: everything it ran since the last tick counts towards this one. + window_spent_ += spent_ms_; + spent_ms_ = 0; + ++window_ticks_; + if (!window_started_) { + window_started_ = true; + window_start_ = now; + } else if (now - window_start_ >= 1000) { + last_window_ms_ = window_spent_ / window_ticks_; + over_windows_ = last_window_ms_ > limits_.tick_ms ? over_windows_ + 1 : 0; + window_start_ = now; + window_spent_ = 0; + window_ticks_ = 0; + if (over_windows_ >= limits_.over_windows && state_ == State::running) + stop(std::format("it took {:.2f} ms a tick for {} s, over its {} ms", last_window_ms_, limits_.over_windows, + limits_.tick_ms)); + } +} + +void Plugin::level_changed(const std::string& previous, const std::string& current) { + if (!previous.empty()) fire("level_unloading", {previous}); + if (!current.empty()) fire("level_loaded", {current}); +} + +void Plugin::run_command(const std::string& name, const std::vector& arguments) { + if (state_ != State::running) return; + const auto found = commands_.find(name); + if (found == commands_.end()) return; + lua_getref(L_, found->second.function); + lua_createtable(L_, static_cast(arguments.size()), 0); + for (std::size_t i = 0; i < arguments.size(); ++i) { + lua_pushlstring(L_, arguments[i].data(), arguments[i].size()); + lua_rawseti(L_, -2, static_cast(i + 1)); + } + call(1, "the command " + name, limits_.call_ms); +} + +std::vector> Plugin::commands() const { + std::vector> result; + for (const auto& [name, command] : commands_) result.emplace_back(name, command.help); + return result; +} + +void Plugin::save_storage() { + if (!storage_dirty_) return; + storage_dirty_ = false; + const auto file = services_.storage_file(id_); + try { + std::filesystem::create_directories(file.parent_path()); + auto temporary = std::filesystem::path(file) += ".tmp"; + { + std::ofstream out(temporary, std::ios::binary | std::ios::trunc); + const auto text = storage_.dump(2); + if (!out || !out.write(text.data(), static_cast(text.size()))) throw std::runtime_error("cannot write it"); + } + std::filesystem::rename(temporary, file); + } catch (const std::exception& error) { + services_.log(id_, LogLevel::warn, std::string("its storage could not be saved: ") + error.what()); + } +} +} diff --git a/Extension/Plugins/plugin_vm.h b/Extension/Plugins/plugin_vm.h new file mode 100644 index 00000000..ea766901 --- /dev/null +++ b/Extension/Plugins/plugin_vm.h @@ -0,0 +1,151 @@ +#pragma once +#include "plugin_manifest.h" +#include "Engine/Core/Json/json.h" +#include +#include +#include +#include +#include +#include +#include +#include + +struct lua_State; + +// One plugin: its own Luau VM, the reskate API registered in it, and the limits that keep it from +// harming the game. Nothing a plugin does reaches the game except through the API, and every +// limit it goes over stops it, not the game. +// +// Not thread safe: a plugin is loaded, ticked and unloaded on one thread (the client tick). +namespace dingosdk::plugins { +enum class LogLevel { info, warn, error }; + +struct Limits { + std::size_t memory = 64u << 20; // the VM's heap + double call_ms = 50; // one call into the plugin (load, a handler, a command) + double unload_ms = 20; // its unload handlers, together + double tick_ms = 2; // the average time a tick, over a window of 1 s... + int over_windows = 3; // ...over it this many windows in a row stops it + int errors = 5; // more errors than this... + double error_window_ms = 10'000; // ...in this long stops it + int log_lines_per_second = 50; + std::size_t storage_bytes = 1u << 20; + std::size_t file_bytes = 16u << 20; // a package file read + std::size_t source_bytes = 1u << 20; // a source file + std::size_t handlers = 256; // per event + std::size_t timers = 1024; + std::size_t commands = 64; +}; + +// What a plugin's VM can reach outside itself. The host implements it. +class Services { +public: + virtual ~Services() = default; + virtual void log(std::string_view plugin, LogLevel level, std::string_view text) = 0; + virtual void console(std::string_view text) = 0; + virtual double now_ms() = 0; // a steady clock, for reskate.time and the windows + virtual std::string level() = 0; // the level being played, or empty + virtual std::filesystem::path storage_file(std::string_view plugin) = 0; + // Empty when the plugin may have the console command; otherwise why not. + virtual std::string claim_command(std::string_view plugin, std::string_view name) = 0; +}; + +enum class State { loading, running, stopped }; + +class Plugin { +public: + // `directory` is the mod folder; `id` its folder name. + Plugin(std::string id, std::filesystem::path directory, Manifest manifest, Services& services, Limits limits); + ~Plugin(); + Plugin(const Plugin&) = delete; + Plugin& operator=(const Plugin&) = delete; + + // Creates the VM, runs the entry (or `entry` in its place, for the test runner), then the load + // handlers. False when it stopped; reason() says why. + bool load(std::string entry = {}); + // The unload handlers (while it runs), its storage saved, the VM closed. Safe to call twice. + void unload(); + // Stops it at the next safe point: the VM is closed by close_if_stopped(). + void stop(std::string reason); + void close_if_stopped(); + + void tick(double dt_ms); + void level_changed(const std::string& previous, const std::string& current); + // A console command it registered: the words after the command's name. + void run_command(const std::string& name, const std::vector& arguments); + void save_storage(); + + const std::string& id() const noexcept { return id_; } + const std::filesystem::path& directory() const noexcept { return directory_; } + const Manifest& manifest() const noexcept { return manifest_; } + State state() const noexcept { return state_; } + const std::string& reason() const noexcept { return reason_; } + std::size_t memory() const noexcept { return memory_; } + double tick_ms() const noexcept { return last_window_ms_; } + // Its console commands and their help. + std::vector> commands() const; + bool has(std::string_view permission) const { return manifest_.permissions.contains(permission); } + // Over a limit or stopped: the API refuses every call until the VM is closed. + bool halted() const noexcept { return halt_ != Halt::none || memory_exceeded_; } + +private: + friend struct Api; + friend struct ApiFunctions; + struct Command { std::string help; int function{}; }; + struct Timer { double due{}, every{}; int function{}; }; + using Clock = std::chrono::steady_clock; + enum class Halt { none, time, memory, stopped }; + + static void* allocate(void* ud, void* ptr, std::size_t old_size, std::size_t new_size); + static void interrupt(lua_State* L, int gc); + // Calls the function under the `count` arguments on top of the main stack; false (and the + // error reported) when it raised one. Stops the plugin past its limits. + bool call(int count, std::string_view what, double limit_ms); + void report(std::string_view what, std::string_view message); + // reskate.log and console.print: at most limits_.log_lines_per_second lines between them. + bool count_line(); + void log(LogLevel level, std::string_view text); + void print(std::string_view text); + // The compiled chunk of a source file on the main stack, or false and the error on it. + bool load_chunk(lua_State* L, const std::string& relative, const std::filesystem::path& file); + void fire(std::string_view event, const std::vector& arguments); + void close(); + + std::string id_; + std::filesystem::path directory_; + std::string code_root_; // the entry's folder in the package: where require looks + Manifest manifest_; + Services& services_; + Limits limits_; + lua_State* L_{}; + int script_thread_{}, modules_{}; + State state_{State::loading}; + std::string reason_; + + std::size_t memory_{}; + Halt halt_{Halt::none}; + bool memory_exceeded_{}; + Clock::time_point deadline_{}; + bool timed_{}; + unsigned interrupts_{}; + double spent_ms_{}; + + bool window_started_{}; + double window_start_{}; + double window_spent_{}; + int window_ticks_{}, over_windows_{}; + double last_window_ms_{}; + std::vector errors_; + double log_second_{}; + int log_lines_{}; + + std::map, std::less<>> handlers_; + std::map timers_; + int next_timer_{1}; + std::map> commands_; + std::set> loading_modules_; + + Json storage_ = Json::object(); + bool storage_loaded_{}, storage_dirty_{}; +}; +} diff --git a/External/luau/Ast/include/Luau/Allocator.h b/External/luau/Ast/include/Luau/Allocator.h new file mode 100644 index 00000000..726ff65b --- /dev/null +++ b/External/luau/Ast/include/Luau/Allocator.h @@ -0,0 +1,48 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/Location.h" +#include "Luau/DenseHash.h" +#include "Luau/Common.h" + +#include + +namespace Luau +{ + +class Allocator +{ +public: + Allocator(); + Allocator(Allocator&&); + + Allocator& operator=(Allocator&&) = delete; + + ~Allocator(); + + void* allocate(size_t size); + + template + T* alloc(Args&&... args) + { + static_assert(std::is_trivially_destructible::value, "Objects allocated with this allocator will never have their destructors run!"); + + T* t = static_cast(allocate(sizeof(T))); + new (t) T(std::forward(args)...); + return t; + } + +private: + struct Page + { + Page* next; + + alignas(8) char data[8192]; + }; + + Page* root; + size_t offset; +}; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/Ast.h b/External/luau/Ast/include/Luau/Ast.h new file mode 100644 index 00000000..3cb0fc01 --- /dev/null +++ b/External/luau/Ast/include/Luau/Ast.h @@ -0,0 +1,1792 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" +#include "Luau/Location.h" +#include "Luau/Variant.h" + +#include +#include +#include +#include + +#include +#include + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) + +namespace Luau +{ + +struct AstName +{ + const char* value; + + AstName() + : value(nullptr) + { + } + + explicit AstName(const char* value) + : value(value) + { + } + + bool operator==(const AstName& rhs) const + { + return value == rhs.value; + } + + bool operator!=(const AstName& rhs) const + { + return value != rhs.value; + } + + bool operator==(const char* rhs) const + { + return value && strcmp(value, rhs) == 0; + } + + bool operator!=(const char* rhs) const + { + return !value || strcmp(value, rhs) != 0; + } + + bool operator<(const AstName& rhs) const + { + return (value && rhs.value) ? strcmp(value, rhs.value) < 0 : value < rhs.value; + } +}; + +class AstType; +class AstVisitor; +class AstStat; +class AstStatBlock; +class AstExpr; +class AstTypePack; +class AstAttr; +class AstExprTable; + +struct AstLocal +{ + AstName name; + Location location; + AstLocal* shadow; + size_t functionDepth; + size_t loopDepth; + bool isConst; + // exported is only a property set after construction + bool isExported = false; + + AstType* annotation; + + AstLocal( + const AstName& name, + const Location& location, + AstLocal* shadow, + size_t functionDepth, + size_t loopDepth, + AstType* annotation, + bool isConst = false + ) + : name(name) + , location(location) + , shadow(shadow) + , functionDepth(functionDepth) + , loopDepth(loopDepth) + , isConst(isConst) + , annotation(annotation) + { + } +}; + +template +struct AstArray +{ + T* data = nullptr; + size_t size = 0; + + const T* begin() const + { + return data; + } + + const T* end() const + { + return data + size; + } + + std::reverse_iterator rbegin() const + { + return std::make_reverse_iterator(end()); + } + + std::reverse_iterator rend() const + { + return std::make_reverse_iterator(begin()); + } +}; + +struct AstTypeList +{ + AstArray types; + // Null indicates no tail, not an untyped tail. + AstTypePack* tailType = nullptr; +}; + +// Don't have Luau::Variant available, it's a bit of an overhead, but a plain struct is nice to use +struct AstTypeOrPack +{ + AstType* type = nullptr; + AstTypePack* typePack = nullptr; +}; + +using AstArgumentName = std::pair; // TODO: remove and replace when we get a common struct for this pair instead of AstName + +extern int gAstRttiIndex; + +template +struct AstRtti +{ + static const int value; +}; + +template +const int AstRtti::value = ++gAstRttiIndex; + +#define LUAU_RTTI(Class) \ + static int ClassIndex() \ + { \ + return AstRtti::value; \ + } + +class AstNode +{ +public: + explicit AstNode(int classIndex, const Location& location) + : classIndex(classIndex) + , location(location) + { + } + + virtual void visit(AstVisitor* visitor) = 0; + + virtual AstExpr* asExpr() + { + return nullptr; + } + virtual AstStat* asStat() + { + return nullptr; + } + virtual AstType* asType() + { + return nullptr; + } + virtual AstAttr* asAttr() + { + return nullptr; + } + + template + bool is() const + { + return classIndex == T::ClassIndex(); + } + template + T* as() + { + return classIndex == T::ClassIndex() ? static_cast(this) : nullptr; + } + template + const T* as() const + { + return classIndex == T::ClassIndex() ? static_cast(this) : nullptr; + } + + const int classIndex; + Location location; +}; + +class AstAttr : public AstNode +{ +public: + LUAU_RTTI(AstAttr) + + enum class Type + { + Checked, + Native, + Deprecated, + DebugNoinline, + Unknown + }; + + struct DeprecatedInfo + { + bool deprecated = false; + std::optional use; + std::optional reason; + }; + + AstAttr(const Location& location, Type type, AstArray args); + AstAttr(const Location& location, Type type, AstArray args, AstName name); + + AstAttr* asAttr() override + { + return this; + } + + void visit(AstVisitor* visitor) override; + + DeprecatedInfo deprecatedInfo() const; + + Type type; + AstArray args; + AstName name; +}; + +class AstExpr : public AstNode +{ +public: + explicit AstExpr(int classIndex, const Location& location) + : AstNode(classIndex, location) + { + } + + AstExpr* asExpr() override + { + return this; + } +}; + +class AstStat : public AstNode +{ +public: + explicit AstStat(int classIndex, const Location& location) + : AstNode(classIndex, location) + , hasSemicolon(false) + { + } + + AstStat* asStat() override + { + return this; + } + + bool hasSemicolon; +}; + +class AstGenericType : public AstNode +{ +public: + LUAU_RTTI(AstGenericType) + + explicit AstGenericType(const Location& location, AstName name, AstType* defaultValue = nullptr); + + void visit(AstVisitor* visitor) override; + + AstName name; + AstType* defaultValue = nullptr; +}; + +class AstGenericTypePack : public AstNode +{ +public: + LUAU_RTTI(AstGenericTypePack) + + explicit AstGenericTypePack(const Location& location, AstName name, AstTypePack* defaultValue = nullptr); + + void visit(AstVisitor* visitor) override; + + AstName name; + AstTypePack* defaultValue = nullptr; +}; + +class AstExprGroup : public AstExpr +{ +public: + LUAU_RTTI(AstExprGroup) + + explicit AstExprGroup(const Location& location, AstExpr* expr); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; +}; + +class AstExprConstantNil : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantNil) + + explicit AstExprConstantNil(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + +class AstExprConstantBool : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantBool) + + AstExprConstantBool(const Location& location, bool value); + + void visit(AstVisitor* visitor) override; + + bool value; +}; + +enum class ConstantNumberParseResult +{ + Ok, + Imprecise, + Malformed, + BinOverflow, + HexOverflow, + IntOverflow, +}; + +class AstExprConstantNumber : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantNumber) + + AstExprConstantNumber(const Location& location, double value, ConstantNumberParseResult parseResult = ConstantNumberParseResult::Ok); + + void visit(AstVisitor* visitor) override; + + double value; + ConstantNumberParseResult parseResult; +}; + +class AstExprConstantInteger : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantInteger) + + AstExprConstantInteger(const Location& location, int64_t value, ConstantNumberParseResult parseResult = ConstantNumberParseResult::Ok); + + void visit(AstVisitor* visitor) override; + + int64_t value; + ConstantNumberParseResult parseResult; +}; +class AstExprConstantString : public AstExpr +{ +public: + LUAU_RTTI(AstExprConstantString) + + enum class QuoteStyle + { + // A string created using double quotes or an interpolated string, + // as in: + // + // "foo", `My name is {protagonist}! / And I'm {antagonist}!` + // + QuotedSimple, + // A string created using single quotes, as in: + // + // 'bar' + // + QuotedSingle, + // A string created using `[[ ... ]]` as in: + // + // [[ Gee, this sure is a long string. + // it even has a new line in it! ]] + // + QuotedRaw, + // A "string" in the context of a table literal, as in: + // + // { foo = 42 } -- `foo` here is a "constant string" + // + Unquoted, + }; + + AstExprConstantString(const Location& location, const AstArray& value, QuoteStyle quoteStyle); + + void visit(AstVisitor* visitor) override; + bool isQuoted() const; + + AstArray value; + QuoteStyle quoteStyle; +}; + +class AstExprLocal : public AstExpr +{ +public: + LUAU_RTTI(AstExprLocal) + + AstExprLocal(const Location& location, AstLocal* local, bool upvalue); + + void visit(AstVisitor* visitor) override; + + AstLocal* local; + bool upvalue; +}; + +class AstExprGlobal : public AstExpr +{ +public: + LUAU_RTTI(AstExprGlobal) + + AstExprGlobal(const Location& location, const AstName& name); + + void visit(AstVisitor* visitor) override; + + AstName name; +}; + +class AstExprVarargs : public AstExpr +{ +public: + LUAU_RTTI(AstExprVarargs) + + AstExprVarargs(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + +class AstExprCall : public AstExpr +{ +public: + LUAU_RTTI(AstExprCall) + + AstExprCall( + const Location& location, + AstExpr* func, + const AstArray& args, + bool self, + const AstArray& explicitTypes, + const Location& argLocation + ); + + void visit(AstVisitor* visitor) override; + + AstExpr* func; + // These will only be filled in specifically `t:f<>()`. + // In `f<>()`, this is parsed as `f<>` as an expression, + // which is then called. + AstArray typeArguments; + AstArray args; + bool self; + Location argLocation; +}; + +class AstExprIndexName : public AstExpr +{ +public: + LUAU_RTTI(AstExprIndexName) + + AstExprIndexName( + const Location& location, + AstExpr* expr, + const AstName& index, + const Location& indexLocation, + const Position& opPosition, + char op + ); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; + AstName index; + Location indexLocation; + Position opPosition; + char op = '.'; +}; + +class AstExprIndexExpr : public AstExpr +{ +public: + LUAU_RTTI(AstExprIndexExpr) + + AstExprIndexExpr(const Location& location, AstExpr* expr, AstExpr* index); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; + AstExpr* index; +}; + +class AstExprFunction : public AstExpr +{ +public: + LUAU_RTTI(AstExprFunction) + + AstExprFunction( + const Location& location, + const AstArray& attributes, + const AstArray& generics, + const AstArray& genericPacks, + AstLocal* self, + const AstArray& args, + bool vararg, + const Location& varargLocation, + AstStatBlock* body, + size_t functionDepth, + const AstName& debugname, + AstTypePack* returnAnnotation, + AstTypePack* varargAnnotation = nullptr, + const std::optional& argLocation = std::nullopt + ); + + void visit(AstVisitor* visitor) override; + + bool hasNativeAttribute() const; + bool hasAttribute(AstAttr::Type attributeType) const; + AstAttr* getAttribute(AstAttr::Type attributeType) const; + + AstArray attributes; + AstArray generics; + AstArray genericPacks; + AstLocal* self; + AstArray args; + AstTypePack* returnAnnotation = nullptr; + bool vararg = false; + Location varargLocation; + AstTypePack* varargAnnotation; + + AstStatBlock* body; + + size_t functionDepth; + + AstName debugname; + + std::optional argLocation; +}; + +class AstExprTable : public AstExpr +{ +public: + LUAU_RTTI(AstExprTable) + + struct Item + { + enum class Kind + { + List, // foo, in which case key is a nullptr + Record, // foo=bar, in which case key is a AstExprConstantString + General, // [foo]=bar + }; + + Kind kind; + + AstExpr* key; // can be nullptr! + AstExpr* value; + }; + + AstExprTable(const Location& location, const AstArray& items); + + void visit(AstVisitor* visitor) override; + + std::optional getRecord(const char* key) const; + + AstArray items; +}; + +class AstExprUnary : public AstExpr +{ +public: + LUAU_RTTI(AstExprUnary) + + enum class Op + { + Not, + Minus, + Len + }; + + AstExprUnary(const Location& location, Op op, AstExpr* expr); + + void visit(AstVisitor* visitor) override; + + Op op; + AstExpr* expr; +}; + +std::string toString(AstExprUnary::Op op); + +class AstExprBinary : public AstExpr +{ +public: + LUAU_RTTI(AstExprBinary) + + enum Op + { + Add, + Sub, + Mul, + Div, + FloorDiv, + Mod, + Pow, + Concat, + CompareNe, + CompareEq, + CompareLt, + CompareLe, + CompareGt, + CompareGe, + And, + Or, + + Op__Count + }; + + AstExprBinary(const Location& location, Op op, AstExpr* left, AstExpr* right); + + void visit(AstVisitor* visitor) override; + + Op op; + AstExpr* left; + AstExpr* right; +}; + +std::string toString(AstExprBinary::Op op); + +class AstExprTypeAssertion : public AstExpr +{ +public: + LUAU_RTTI(AstExprTypeAssertion) + + AstExprTypeAssertion(const Location& location, AstExpr* expr, AstType* annotation); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; + AstType* annotation; +}; + +class AstExprIfElse : public AstExpr +{ +public: + LUAU_RTTI(AstExprIfElse) + + AstExprIfElse(const Location& location, AstExpr* condition, bool hasThen, AstExpr* trueExpr, bool hasElse, AstExpr* falseExpr); + + AstExprIfElse( + const Location& location, + AstExpr* condition, + bool hasThen, + AstExpr* trueExpr, + bool hasElse, + AstExpr* falseExpr, + AstLocal* conditionLocal, + bool conditionIsConst, + const std::optional& conditionKeywordLocation, + const std::optional& conditionEqualsLocation + ); + + void visit(AstVisitor* visitor) override; + + AstExpr* condition; + bool hasThen; + AstExpr* trueExpr; + bool hasElse; + AstExpr* falseExpr; + + // Active for 'if local' and 'if const' expressions; conditionLocal is bound to `condition` and in scope for trueExpr only + AstLocal* conditionLocal = nullptr; + bool conditionIsConst = false; + std::optional conditionKeywordLocation; + + // Location of the `=` in an `if local`/`if const` binding + std::optional conditionEqualsLocation; +}; + +class AstExprInterpString : public AstExpr +{ +public: + LUAU_RTTI(AstExprInterpString) + + AstExprInterpString(const Location& location, const AstArray>& strings, const AstArray& expressions); + + void visit(AstVisitor* visitor) override; + + /// An interpolated string such as `foo{bar}baz` is represented as + /// an array of strings for "foo" and "bar", and an array of expressions for "baz". + /// `strings` will always have one more element than `expressions`. + AstArray> strings; + AstArray expressions; +}; + +// f<> +class AstExprInstantiate : public AstExpr +{ +public: + LUAU_RTTI(AstExprInstantiate) + + AstExprInstantiate(const Location& location, AstExpr* expr, AstArray types); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; + AstArray typeArguments; +}; + +class AstStatBlock : public AstStat +{ +public: + LUAU_RTTI(AstStatBlock) + + AstStatBlock(const Location& location, const AstArray& body, bool hasEnd = true); + + void visit(AstVisitor* visitor) override; + + AstArray body; + + /* Indicates whether or not this block has been terminated in a + * syntactically valid way. + * + * This is usually but not always done with the 'end' keyword. AstStatIf + * and AstStatRepeat are the two main exceptions to this. + * + * The 'then' clause of an if statement can properly be closed by the + * keywords 'else' or 'elseif'. A 'repeat' loop's body is closed with the + * 'until' keyword. + */ + bool hasEnd = false; +}; + +class AstStatIf : public AstStat +{ +public: + LUAU_RTTI(AstStatIf) + + AstStatIf( + const Location& location, + AstExpr* condition, + AstStatBlock* thenbody, + AstStat* elsebody, + const std::optional& thenLocation, + const std::optional& elseLocation + ); + + AstStatIf( + const Location& location, + AstExpr* condition, + AstStatBlock* thenbody, + AstStat* elsebody, + const std::optional& thenLocation, + const std::optional& elseLocation, + AstLocal* conditionLocal, + bool conditionIsConst, + const std::optional& conditionKeywordLocation, + const std::optional& conditionEqualsLocation + ); + + void visit(AstVisitor* visitor) override; + + AstExpr* condition; + AstStatBlock* thenbody; + AstStat* elsebody; + + std::optional thenLocation; + + // Active for 'elseif' as well + std::optional elseLocation; + + // Active for 'if local' and 'if const' statements + AstLocal* conditionLocal = nullptr; + bool conditionIsConst = false; + std::optional conditionKeywordLocation; + + // Location of the `=` in an `if local`/`if const` binding + std::optional conditionEqualsLocation; +}; + +class AstStatWhile : public AstStat +{ +public: + LUAU_RTTI(AstStatWhile) + + AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation); + + void visit(AstVisitor* visitor) override; + + AstExpr* condition; + AstStatBlock* body; + + bool hasDo = false; + Location doLocation; +}; + +class AstStatRepeat : public AstStat +{ +public: + LUAU_RTTI(AstStatRepeat) + + AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil); + + void visit(AstVisitor* visitor) override; + + AstExpr* condition; + AstStatBlock* body; + + bool DEPRECATED_hasUntil = false; +}; + +class AstStatBreak : public AstStat +{ +public: + LUAU_RTTI(AstStatBreak) + + AstStatBreak(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + +class AstStatContinue : public AstStat +{ +public: + LUAU_RTTI(AstStatContinue) + + AstStatContinue(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + +class AstStatReturn : public AstStat +{ +public: + LUAU_RTTI(AstStatReturn) + + AstStatReturn(const Location& location, const AstArray& list); + + void visit(AstVisitor* visitor) override; + + AstArray list; +}; + +class AstStatExpr : public AstStat +{ +public: + LUAU_RTTI(AstStatExpr) + + AstStatExpr(const Location& location, AstExpr* expr); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; +}; + +class AstStatLocal : public AstStat +{ +public: + LUAU_RTTI(AstStatLocal) + + AstStatLocal( + const Location& location, + const AstArray& vars, + const AstArray& values, + const std::optional& equalsSignLocation, + bool isConst = false + ); + + void visit(AstVisitor* visitor) override; + + AstArray vars; + AstArray values; + + bool isConst = false; + bool isExported = false; + + // if the StatLocal is being exported, this is the location of `const` or `local` + std::optional keywordLocation; + std::optional equalsSignLocation; +}; + +class AstStatFor : public AstStat +{ +public: + LUAU_RTTI(AstStatFor) + + AstStatFor( + const Location& location, + AstLocal* var, + AstExpr* from, + AstExpr* to, + AstExpr* step, + AstStatBlock* body, + bool hasDo, + const Location& doLocation + ); + + void visit(AstVisitor* visitor) override; + + AstLocal* var; + AstExpr* from; + AstExpr* to; + AstExpr* step; + AstStatBlock* body; + + bool hasDo = false; + Location doLocation; +}; + +class AstStatForIn : public AstStat +{ +public: + LUAU_RTTI(AstStatForIn) + + AstStatForIn( + const Location& location, + const AstArray& vars, + const AstArray& values, + AstStatBlock* body, + bool hasIn, + const Location& inLocation, + bool hasDo, + const Location& doLocation + ); + + void visit(AstVisitor* visitor) override; + + AstArray vars; + AstArray values; + AstStatBlock* body; + + bool hasIn = false; + Location inLocation; + + bool hasDo = false; + Location doLocation; +}; + +class AstStatAssign : public AstStat +{ +public: + LUAU_RTTI(AstStatAssign) + + AstStatAssign(const Location& location, const AstArray& vars, const AstArray& values); + + void visit(AstVisitor* visitor) override; + + AstArray vars; + AstArray values; +}; + +class AstStatCompoundAssign : public AstStat +{ +public: + LUAU_RTTI(AstStatCompoundAssign) + + AstStatCompoundAssign(const Location& location, AstExprBinary::Op op, AstExpr* var, AstExpr* value); + + void visit(AstVisitor* visitor) override; + + AstExprBinary::Op op; + AstExpr* var; + AstExpr* value; +}; + +class AstStatFunction : public AstStat +{ +public: + LUAU_RTTI(AstStatFunction) + + AstStatFunction(const Location& location, AstExpr* name, AstExprFunction* func); + + void visit(AstVisitor* visitor) override; + + AstExpr* name; + AstExprFunction* func; +}; + +class AstStatLocalFunction : public AstStat +{ +public: + LUAU_RTTI(AstStatLocalFunction) + + AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func, bool isConst, Position constKeywordBegin); + + void visit(AstVisitor* visitor) override; + + AstLocal* name; + AstExprFunction* func; + bool isConst; + // Position of the `const` keyword; Position::missing() when isConst is false. + Position constKeywordBegin; +}; + +class AstStatTypeAlias : public AstStat +{ +public: + LUAU_RTTI(AstStatTypeAlias) + + AstStatTypeAlias( + const Location& location, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + AstType* type, + bool exported + ); + + void visit(AstVisitor* visitor) override; + + AstName name; + Location nameLocation; + AstArray generics; + AstArray genericPacks; + AstType* type; + bool exported; +}; + +class AstStatTypeFunction : public AstStat +{ +public: + LUAU_RTTI(AstStatTypeFunction); + + AstStatTypeFunction( + const Location& location, + const AstName& name, + const Location& nameLocation, + AstExprFunction* body, + bool exported, + bool hasErrors + ); + + void visit(AstVisitor* visitor) override; + + AstName name; + Location nameLocation; + AstExprFunction* body = nullptr; + bool exported = false; + bool hasErrors = false; +}; + +class AstStatDeclareGlobal : public AstStat +{ +public: + LUAU_RTTI(AstStatDeclareGlobal) + + AstStatDeclareGlobal(const Location& location, const AstName& name, const Location& nameLocation, AstType* type); + + void visit(AstVisitor* visitor) override; + + AstName name; + Location nameLocation; + AstType* type; +}; + +class AstStatDeclareFunction : public AstStat +{ +public: + LUAU_RTTI(AstStatDeclareFunction) + + AstStatDeclareFunction( + const Location& location, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& params, + const AstArray& paramNames, + bool vararg, + const Location& varargLocation, + AstTypePack* retTypes + ); + + AstStatDeclareFunction( + const Location& location, + const AstArray& attributes, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& params, + const AstArray& paramNames, + bool vararg, + const Location& varargLocation, + AstTypePack* retTypes + ); + + void visit(AstVisitor* visitor) override; + + bool isCheckedFunction() const; + bool hasAttribute(AstAttr::Type attributeType) const; + AstAttr* getAttribute(AstAttr::Type attributeType) const; + + AstArray attributes; + AstName name; + Location nameLocation; + AstArray generics; + AstArray genericPacks; + AstTypeList params; + AstArray paramNames; + bool vararg = false; + Location varargLocation; + AstTypePack* retTypes; +}; + +enum class AstTableAccess +{ + Read = 0b01, + Write = 0b10, + ReadWrite = 0b11, +}; + +struct AstDeclaredExternTypeProperty +{ + AstName name; + Location nameLocation; + AstType* ty = nullptr; + bool isMethod = false; + Location location; + AstTableAccess access = AstTableAccess::ReadWrite; +}; + +struct AstClassProperty +{ + Location qualifierLocation; + AstName name; + Location nameLocation; + std::optional typeColonLocation = std::nullopt; + AstType* ty = nullptr; +}; + +struct AstClassMethod +{ + std::optional qualifierLocation; + Location keywordLocation; + AstName functionName; + Location nameLocation; + AstExprFunction* function; +}; + +using AstClassMember = Variant; + +class AstStatClass : public AstStat +{ +public: + LUAU_RTTI(AstStatClass) + + AstLocal* name; + AstExpr* super; + AstArray members; + bool exported; + bool open; + + AstStatClass(const Location& location, AstLocal* name, AstExpr* super, AstArray members, bool exported, bool open); + + void visit(AstVisitor* visitor) override; +}; + +struct AstTableIndexer +{ + AstType* indexType; + AstType* resultType; + Location location; + + AstTableAccess access = AstTableAccess::ReadWrite; + std::optional accessLocation; +}; + +class AstStatDeclareExternType : public AstStat +{ +public: + LUAU_RTTI(AstStatDeclareExternType) + + AstStatDeclareExternType( + const Location& location, + const AstName& name, + std::optional superName, + const AstArray& props, + AstTableIndexer* indexer = nullptr + ); + + void visit(AstVisitor* visitor) override; + + AstName name; + std::optional superName; + + AstArray props; + AstTableIndexer* indexer; +}; + +class AstType : public AstNode +{ +public: + AstType(int classIndex, const Location& location) + : AstNode(classIndex, location) + { + } + + AstType* asType() override + { + return this; + } +}; + +class AstTypeReference : public AstType +{ +public: + LUAU_RTTI(AstTypeReference) + + AstTypeReference( + const Location& location, + std::optional prefix, + AstName name, + std::optional prefixLocation, + const Location& nameLocation, + bool hasParameterList = false, + const AstArray& parameters = {}, + AstLocal* prefixLocal = nullptr + ); + + void visit(AstVisitor* visitor) override; + + bool hasParameterList; + std::optional prefix; + std::optional prefixLocation; + AstLocal* prefixLocal = nullptr; + AstName name; + Location nameLocation; + AstArray parameters; +}; + +struct AstTableProp +{ + AstName name; + Location location; + AstType* type; + AstTableAccess access = AstTableAccess::ReadWrite; + std::optional accessLocation; +}; + +class AstTypeTable : public AstType +{ +public: + LUAU_RTTI(AstTypeTable) + + AstTypeTable(const Location& location, const AstArray& props, AstTableIndexer* indexer = nullptr, bool isExact = false); + + void visit(AstVisitor* visitor) override; + + AstArray props; + AstTableIndexer* indexer; + bool isExact = false; +}; + +class AstTypeFunction : public AstType +{ +public: + LUAU_RTTI(AstTypeFunction) + + AstTypeFunction( + const Location& location, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& argTypes, + const AstArray>& argNames, + AstTypePack* returnTypes + ); + + AstTypeFunction( + const Location& location, + const AstArray& attributes, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& argTypes, + const AstArray>& argNames, + AstTypePack* returnTypes + ); + + void visit(AstVisitor* visitor) override; + + bool isCheckedFunction() const; + bool hasAttribute(AstAttr::Type attributeType) const; + AstAttr* getAttribute(AstAttr::Type attributeType) const; + + AstArray attributes; + AstArray generics; + AstArray genericPacks; + AstTypeList argTypes; + AstArray> argNames; + AstTypePack* returnTypes; +}; + +class AstTypeTypeof : public AstType +{ +public: + LUAU_RTTI(AstTypeTypeof) + + AstTypeTypeof(const Location& location, AstExpr* expr); + + void visit(AstVisitor* visitor) override; + + AstExpr* expr; +}; + +class AstTypeOptional : public AstType +{ +public: + LUAU_RTTI(AstTypeOptional) + + AstTypeOptional(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + +class AstTypeUnion : public AstType +{ +public: + LUAU_RTTI(AstTypeUnion) + + AstTypeUnion(const Location& location, const AstArray& types); + + void visit(AstVisitor* visitor) override; + + AstArray types; +}; + +class AstTypeIntersection : public AstType +{ +public: + LUAU_RTTI(AstTypeIntersection) + + AstTypeIntersection(const Location& location, const AstArray& types); + + void visit(AstVisitor* visitor) override; + + AstArray types; +}; + +class AstExprError : public AstExpr +{ +public: + LUAU_RTTI(AstExprError) + + AstExprError(const Location& location, const AstArray& expressions, unsigned messageIndex); + + void visit(AstVisitor* visitor) override; + + AstArray expressions; + unsigned messageIndex; +}; + +class AstStatError : public AstStat +{ +public: + LUAU_RTTI(AstStatError) + + AstStatError(const Location& location, const AstArray& expressions, const AstArray& statements, unsigned messageIndex); + + void visit(AstVisitor* visitor) override; + + AstArray expressions; + AstArray statements; + unsigned messageIndex; +}; + +class AstTypeError : public AstType +{ +public: + LUAU_RTTI(AstTypeError) + + AstTypeError(const Location& location, const AstArray& types, bool isMissing, unsigned messageIndex); + + void visit(AstVisitor* visitor) override; + + AstArray types; + bool isMissing; + unsigned messageIndex; +}; + +class AstTypeSingletonBool : public AstType +{ +public: + LUAU_RTTI(AstTypeSingletonBool) + + AstTypeSingletonBool(const Location& location, bool value); + + void visit(AstVisitor* visitor) override; + + bool value; +}; + +class AstTypeSingletonString : public AstType +{ +public: + LUAU_RTTI(AstTypeSingletonString) + + AstTypeSingletonString(const Location& location, const AstArray& value); + + void visit(AstVisitor* visitor) override; + + const AstArray value; +}; + +class AstTypeGroup : public AstType +{ +public: + LUAU_RTTI(AstTypeGroup) + + explicit AstTypeGroup(const Location& location, AstType* type); + + void visit(AstVisitor* visitor) override; + + AstType* type; +}; + +class AstTypePack : public AstNode +{ +public: + AstTypePack(int classIndex, const Location& location) + : AstNode(classIndex, location) + { + } +}; + +class AstTypePackExplicit : public AstTypePack +{ +public: + LUAU_RTTI(AstTypePackExplicit) + + AstTypePackExplicit(const Location& location, AstTypeList typeList); + + void visit(AstVisitor* visitor) override; + + AstTypeList typeList; +}; + +class AstTypePackVariadic : public AstTypePack +{ +public: + LUAU_RTTI(AstTypePackVariadic) + + AstTypePackVariadic(const Location& location, AstType* variadicType); + + void visit(AstVisitor* visitor) override; + + AstType* variadicType; +}; + +class AstTypePackGeneric : public AstTypePack +{ +public: + LUAU_RTTI(AstTypePackGeneric) + + AstTypePackGeneric(const Location& location, AstName name); + + void visit(AstVisitor* visitor) override; + + AstName genericName; +}; + +class AstVisitor +{ +public: + virtual ~AstVisitor() {} + + virtual bool visit(class AstNode*) + { + return true; + } + + virtual bool visit(class AstAttr* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstGenericType* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstGenericTypePack* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstExpr* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstExprGroup* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprConstantNil* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprConstantBool* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprConstantNumber* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprConstantInteger* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprConstantString* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprLocal* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprGlobal* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprVarargs* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprCall* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprIndexName* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprIndexExpr* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprTable* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprUnary* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprBinary* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprTypeAssertion* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprIfElse* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprInterpString* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprInstantiate* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstExprError* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstStat* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstStatBlock* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatIf* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatWhile* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatRepeat* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatBreak* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatContinue* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatReturn* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatExpr* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatLocal* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatFor* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatForIn* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatAssign* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatCompoundAssign* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatLocalFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatTypeAlias* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatTypeFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatDeclareFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatDeclareGlobal* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatClass* node) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + return visit(static_cast(node)); + } + virtual bool visit(class AstStatDeclareExternType* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstStatError* node) + { + return visit(static_cast(node)); + } + + // By default visiting type annotations is disabled; override this in your visitor if you need to! + virtual bool visit(class AstType* node) + { + return false; + } + + virtual bool visit(class AstTypeReference* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeTable* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeFunction* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeTypeof* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeOptional* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeUnion* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeIntersection* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeSingletonBool* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeSingletonString* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeGroup* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypeError* node) + { + return visit(static_cast(node)); + } + + virtual bool visit(class AstTypePack* node) + { + return false; + } + virtual bool visit(class AstTypePackExplicit* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypePackVariadic* node) + { + return visit(static_cast(node)); + } + virtual bool visit(class AstTypePackGeneric* node) + { + return visit(static_cast(node)); + } +}; + +bool isLValue(const AstExpr*); +bool isConstantLiteral(const AstExpr*); +bool isLiteralTable(const AstExpr*); +AstName getIdentifier(AstExpr*); +Location getLocation(const AstTypeList& typeList); + +template // AstNode, AstExpr, AstLocal, etc +Location getLocation(AstArray array) +{ + if (0 == array.size) + return {}; + + return Location{array.data[0]->location.begin, array.data[array.size - 1]->location.end}; +} + +#undef LUAU_RTTI + +} // namespace Luau + +namespace std +{ + +template<> +struct hash +{ + size_t operator()(const Luau::AstName& value) const + { + // note: since operator== uses pointer identity, hashing function uses it as well + // the hasher is the same as DenseHashPointer (DenseHash.h) + return (uintptr_t(value.value) >> 4) ^ (uintptr_t(value.value) >> 9); + } +}; + +} // namespace std diff --git a/External/luau/Ast/include/Luau/Confusables.h b/External/luau/Ast/include/Luau/Confusables.h new file mode 100644 index 00000000..0e6c2622 --- /dev/null +++ b/External/luau/Ast/include/Luau/Confusables.h @@ -0,0 +1,9 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +namespace Luau +{ +const char* findConfusable(uint32_t codepoint); +} diff --git a/External/luau/Ast/include/Luau/Cst.h b/External/luau/Ast/include/Luau/Cst.h new file mode 100644 index 00000000..cd0f79cf --- /dev/null +++ b/External/luau/Ast/include/Luau/Cst.h @@ -0,0 +1,605 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/Location.h" + +namespace Luau +{ + +extern int gCstRttiIndex; + +template +struct CstRtti +{ + static const int value; +}; + +template +const int CstRtti::value = ++gCstRttiIndex; + +#define LUAU_CST_RTTI(Class) \ + static int CstClassIndex() \ + { \ + return CstRtti::value; \ + } + +class CstNode +{ +public: + explicit CstNode(int classIndex) + : classIndex(classIndex) + { + } + + template + bool is() const + { + return classIndex == T::CstClassIndex(); + } + template + T* as() + { + return classIndex == T::CstClassIndex() ? static_cast(this) : nullptr; + } + template + const T* as() const + { + return classIndex == T::CstClassIndex() ? static_cast(this) : nullptr; + } + + const int classIndex; +}; + +class CstAttr : public CstNode +{ +public: + LUAU_CST_RTTI(CstAttr) + + explicit CstAttr(bool hasAt); + + bool hasAt; // false when inside an attribute list, ie @[native checked] +}; + +class CstParametrizedAttr : public CstNode +{ +public: + LUAU_CST_RTTI(CstParametrizedAttr) + + explicit CstParametrizedAttr(Position openParenPosition, Position closeParenPosition, AstArray argsCommaPositions); + + Position openParenPosition; // for `@x(args)` form + Position closeParenPosition; + + // Commas inside the `(a, b, c)` arg list + AstArray argsCommaPositions; +}; + +struct CstAttrList +{ + explicit CstAttrList(Position atBracketPosition, Position closeBracketPosition, AstArray commaPositions); + + Position atBracketPosition; + Position closeBracketPosition; + AstArray commaPositions; +}; + +class CstExprGroup : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprGroup) + + explicit CstExprGroup(Position closePosition); + + Position closePosition; +}; + +class CstExprConstantNumber : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprConstantNumber) + + explicit CstExprConstantNumber(const AstArray& value); + + AstArray value; +}; + +class CstExprConstantInteger : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprConstantInteger) + + explicit CstExprConstantInteger(const AstArray& value); + + AstArray value; +}; + +class CstExprConstantString : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprConstantString) + + enum class QuoteStyle + { + QuotedSingle, + QuotedDouble, + QuotedRaw, + QuotedInterp, + }; + + CstExprConstantString(AstArray sourceString, QuoteStyle quoteStyle, unsigned int blockDepth); + + AstArray sourceString; + QuoteStyle quoteStyle; + unsigned int blockDepth; +}; + +// Shared between the expression and call nodes +struct CstTypeInstantiation +{ + Position leftArrow1Position = Position::missing(); + Position leftArrow2Position = Position::missing(); + + AstArray commaPositions = {}; + + Position rightArrow1Position = Position::missing(); + Position rightArrow2Position = Position::missing(); +}; + +class CstExprCall : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprCall) + + CstExprCall(Position openParens, Position closeParens, AstArray commaPositions); + + Position openParens; + Position closeParens; + AstArray commaPositions; + CstTypeInstantiation* explicitTypes = nullptr; +}; + +class CstExprIndexExpr : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprIndexExpr) + + CstExprIndexExpr(Position openBracketPosition, Position closeBracketPosition); + + Position openBracketPosition; + Position closeBracketPosition; +}; + +class CstExprFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprFunction) + + CstExprFunction(); + + AstArray attrLists = {}; + Position functionKeywordPosition = Position::missing(); + Position openGenericsPosition = Position::missing(); + AstArray genericsCommaPositions; + Position closeGenericsPosition = Position::missing(); + AstArray argsAnnotationColonPositions; + AstArray argsCommaPositions; + Position varargAnnotationColonPosition = Position::missing(); + Position returnSpecifierPosition = Position::missing(); +}; + +class CstExprTable : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprTable) + + enum class Separator + { + Comma, + Semicolon, + Missing + }; + + struct Item + { + Position indexerOpenPosition; // '[', only if Kind == General + Position indexerClosePosition; // ']', only if Kind == General + Position equalsPosition; // only if Kind != List + Separator separator; // may be missing for last Item + Position separatorPosition; + }; + + explicit CstExprTable(const AstArray& items); + + AstArray items; +}; + +// TODO: Shared between unary and binary, should we split? +class CstExprOp : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprOp) + + explicit CstExprOp(Position opPosition); + + Position opPosition; +}; + +class CstExprTypeAssertion : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprTypeAssertion) + + explicit CstExprTypeAssertion(Position opPosition); + + Position opPosition; +}; + +class CstExprIfElse : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprIfElse) + + CstExprIfElse(Position thenPosition, Position elsePosition, bool isElseIf, Position annotationColonPosition = Position::missing()); + + Position thenPosition; + Position elsePosition; + bool isElseIf; + + // Colon before the `if local`/`if const` binding's type annotation + Position annotationColonPosition; +}; + +class CstExprInterpString : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprInterpString) + + explicit CstExprInterpString(AstArray> sourceStrings, AstArray stringPositions); + + AstArray> sourceStrings; + AstArray stringPositions; +}; + +class CstExprExplicitTypeInstantiation : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprExplicitTypeInstantiation) + + explicit CstExprExplicitTypeInstantiation(CstTypeInstantiation instantiation); + + CstTypeInstantiation instantiation; +}; + +class CstStatDo : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatDo) + + explicit CstStatDo(Position statsStartPosition, Position endPosition); + + Position statsStartPosition; + Position endPosition; +}; + +class CstStatRepeat : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatRepeat) + + explicit CstStatRepeat(Position untilPosition); + + Position untilPosition; +}; + +class CstStatReturn : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatReturn) + + explicit CstStatReturn(AstArray commaPositions); + + AstArray commaPositions; +}; + +class CstStatLocal : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatLocal) + + CstStatLocal(AstArray varsAnnotationColonPositions, AstArray varsCommaPositions, AstArray valuesCommaPositions); + + AstArray varsAnnotationColonPositions; + AstArray varsCommaPositions; + AstArray valuesCommaPositions; +}; + +class CstStatFor : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatFor) + + CstStatFor(Position annotationColonPosition, Position equalsPosition, Position endCommaPosition, Position stepCommaPosition); + + Position annotationColonPosition; + Position equalsPosition; + Position endCommaPosition; + Position stepCommaPosition; +}; + +class CstStatForIn : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatForIn) + + CstStatForIn(AstArray varsAnnotationColonPositions, AstArray varsCommaPositions, AstArray valuesCommaPositions); + + AstArray varsAnnotationColonPositions; + AstArray varsCommaPositions; + AstArray valuesCommaPositions; +}; + +class CstStatIf : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatIf) + + explicit CstStatIf(Position annotationColonPosition); + + // Colon before the `if local`/`if const` binding's type annotation + Position annotationColonPosition; +}; + +class CstStatAssign : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatAssign) + + CstStatAssign(AstArray varsCommaPositions, Position equalsPosition, AstArray valuesCommaPositions); + + AstArray varsCommaPositions; + Position equalsPosition; + AstArray valuesCommaPositions; +}; + +class CstStatCompoundAssign : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatCompoundAssign) + + explicit CstStatCompoundAssign(Position opPosition); + + Position opPosition; +}; + +class CstStatFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatFunction) + + explicit CstStatFunction(Position functionKeywordPosition); + explicit CstStatFunction(AstArray attrLists, Position functionKeywordPosition); + + AstArray attrLists; + Position functionKeywordPosition; +}; + +class CstStatLocalFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatLocalFunction) + + explicit CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition); + explicit CstStatLocalFunction(AstArray attrLists, Position localKeywordPosition, Position functionKeywordPosition); + + AstArray attrLists; + Position localKeywordPosition; + Position functionKeywordPosition; +}; + +class CstGenericType : public CstNode +{ +public: + LUAU_CST_RTTI(CstGenericType) + + CstGenericType(Position defaultEqualsPosition); + + Position defaultEqualsPosition; +}; + +class CstGenericTypePack : public CstNode +{ +public: + LUAU_CST_RTTI(CstGenericTypePack) + + CstGenericTypePack(Position ellipsisPosition, Position defaultEqualsPosition); + + Position ellipsisPosition; + Position defaultEqualsPosition; +}; + +class CstStatTypeAlias : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatTypeAlias) + + CstStatTypeAlias( + Position typeKeywordPosition, + Position genericsOpenPosition, + AstArray genericsCommaPositions, + Position genericsClosePosition, + Position equalsPosition + ); + + Position typeKeywordPosition; + Position genericsOpenPosition; + AstArray genericsCommaPositions; + Position genericsClosePosition; + Position equalsPosition; +}; + +class CstStatTypeFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatTypeFunction) + + CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition); + + Position typeKeywordPosition; + Position functionKeywordPosition; +}; + +class CstTypeReference : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeReference) + + CstTypeReference( + Position prefixPointPosition, + Position openParametersPosition, + AstArray parametersCommaPositions, + Position closeParametersPosition + ); + + Position prefixPointPosition; + Position openParametersPosition; + AstArray parametersCommaPositions; + Position closeParametersPosition; +}; + +class CstTypeTable : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeTable) + + struct Item + { + enum class Kind + { + Indexer, + Property, + StringProperty, + }; + + Kind kind; + Position indexerOpenPosition; // '[', only if Kind != Property + Position indexerClosePosition; // ']' only if Kind != Property + Position colonPosition; + CstExprTable::Separator separator; // may be missing for last Item + Position separatorPosition; + + CstExprConstantString* stringInfo = nullptr; // only if Kind == StringProperty + Position stringPosition{0, 0}; // only if Kind == StringProperty + }; + + CstTypeTable(AstArray items, bool isArray); + + AstArray items; + bool isArray = false; +}; + +class CstTypeFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeFunction) + + CstTypeFunction( + Position openGenericsPosition, + AstArray genericsCommaPositions, + Position closeGenericsPosition, + Position openArgsPosition, + AstArray argumentNameColonPositions, + AstArray argumentsCommaPositions, + Position closeArgsPosition, + Position returnArrowPosition + ); + + Position openGenericsPosition; + AstArray genericsCommaPositions; + Position closeGenericsPosition; + Position openArgsPosition; + AstArray argumentNameColonPositions; + AstArray argumentsCommaPositions; + Position closeArgsPosition; + Position returnArrowPosition; +}; + +class CstTypeTypeof : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeTypeof) + + CstTypeTypeof(Position openPosition, Position closePosition); + + Position openPosition; + Position closePosition; +}; + +class CstTypeUnion : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeUnion) + + CstTypeUnion(Position leadingPosition, AstArray separatorPositions); + + Position leadingPosition; + AstArray separatorPositions; +}; + +class CstTypeIntersection : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeIntersection) + + explicit CstTypeIntersection(Position leadingPosition, AstArray separatorPositions); + + Position leadingPosition; + AstArray separatorPositions; +}; + +class CstTypeSingletonString : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeSingletonString) + + CstTypeSingletonString(AstArray sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth); + + AstArray sourceString; + CstExprConstantString::QuoteStyle quoteStyle; + unsigned int blockDepth; +}; + +class CstTypeGroup : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeGroup) + + CstTypeGroup(Position closePosition); + + Position closePosition; +}; + +class CstTypePackExplicit : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypePackExplicit) + + explicit CstTypePackExplicit(); + explicit CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions); + + Position openParenthesesPosition; + Position closeParenthesesPosition; + AstArray commaPositions; +}; + +class CstTypePackGeneric : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypePackGeneric) + + explicit CstTypePackGeneric(Position ellipsisPosition); + + Position ellipsisPosition; +}; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/Lexer.h b/External/luau/Ast/include/Luau/Lexer.h new file mode 100644 index 00000000..b5fb7ab7 --- /dev/null +++ b/External/luau/Ast/include/Luau/Lexer.h @@ -0,0 +1,264 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Allocator.h" +#include "Luau/Ast.h" +#include "Luau/Location.h" +#include "Luau/DenseHash.h" +#include "Luau/Common.h" + +#include + +namespace Luau +{ + +struct Lexeme +{ + enum Type + { + Eof = 0, + + // 1..255 means actual character values + Char_END = 256, + + Equal, + LessEqual, + GreaterEqual, + NotEqual, + Dot2, + Dot3, + SkinnyArrow, + DoubleColon, + FloorDiv, + + InterpStringBegin, + InterpStringMid, + InterpStringEnd, + // An interpolated string with no expressions (like `x`) + InterpStringSimple, + + AddAssign, + SubAssign, + MulAssign, + DivAssign, + FloorDivAssign, + ModAssign, + PowAssign, + ConcatAssign, + + RawString, + QuotedString, + Number, + Name, + + Comment, + BlockComment, + + Attribute, + AttributeOpen, + + BrokenString, + BrokenComment, + BrokenUnicode, + BrokenInterpDoubleBrace, + Error, + + Reserved_BEGIN, + ReservedAnd = Reserved_BEGIN, + ReservedBreak, + ReservedDo, + ReservedElse, + ReservedElseif, + ReservedEnd, + ReservedFalse, + ReservedFor, + ReservedFunction, + ReservedIf, + ReservedIn, + ReservedLocal, + ReservedNil, + ReservedNot, + ReservedOr, + ReservedRepeat, + ReservedReturn, + ReservedThen, + ReservedTrue, + ReservedUntil, + ReservedWhile, + Reserved_END + }; + + enum struct QuoteStyle + { + Single, + Double, + }; + + Type type; + Location location; + + // Field declared here, before the union, to ensure that Lexeme size is 32 bytes. +private: + // length is used to extract a slice from the input buffer. + // This field is only valid for certain lexeme types which don't duplicate portions of input + // but instead store a pointer to a location in the input buffer and the length of lexeme. + unsigned int length; + +public: + union + { + const char* data; // String, Number, Comment + const char* name; // Name + unsigned int codepoint; // BrokenUnicode + }; + + Lexeme(const Location& location, Type type); + Lexeme(const Location& location, char character); + Lexeme(const Location& location, Type type, const char* data, size_t size); + Lexeme(const Location& location, Type type, const char* name); + + unsigned int getLength() const; + unsigned int getBlockDepth() const; + QuoteStyle getQuoteStyle() const; + + std::string toString() const; +}; + +static_assert(sizeof(Lexeme) <= 32, "Size of `Lexeme` struct should be up to 32 bytes."); + +class AstNameTable +{ +public: + AstNameTable(Allocator& allocator); + + AstName addStatic(const char* name, Lexeme::Type type = Lexeme::Name); + + std::pair getOrAddWithType(const char* name, size_t length); + std::pair getWithType(const char* name, size_t length) const; + + AstName getOrAdd(const char* name, size_t len); + AstName getOrAdd(const char* name); + AstName get(const char* name) const; + +private: + struct Entry + { + AstName value; + uint32_t length; + Lexeme::Type type; + + bool operator==(const Entry& other) const; + }; + + struct EntryHash + { + size_t operator()(const Entry& e) const; + }; + + DenseHashSet data; + + Allocator& allocator; +}; + +class Lexer +{ +public: + Lexer(const char* buffer, std::size_t bufferSize, AstNameTable& names, Position startPosition = {0, 0}); + + void setSkipComments(bool skip); + void setReadNames(bool read); + + const Location& previousLocation() const + { + return prevLocation; + } + + const Lexeme& next(); + const Lexeme& next(bool skipComments, bool updatePrevLocation); + void nextline(); + + Lexeme lookahead(); + + const Lexeme& current() const + { + return lexeme; + } + + static bool isReserved(const std::string& word); + + static bool fixupQuotedString(std::string& data); + static void fixupMultilineString(std::string& data); + + unsigned int getOffset() const + { + return offset; + } + + enum class BraceType + { + InterpolatedString, + Normal + }; + + std::optional peekBraceStackTop(); + +private: + char peekch() const; + char peekch(unsigned int lookahead) const; + + Position position() const; + + // consume() assumes current character is not a newline for performance; when that is not known, consumeAny() should be used instead. + void consume(); + void consumeAny(); + + Lexeme readCommentBody(); + + // Given a sequence [===[ or ]===], returns: + // 1. number of equal signs (or 0 if none present) between the brackets + // 2. -1 if this is not a long comment/string separator + // 3. -N if this is a malformed separator + // Does *not* consume the closing brace. + int skipLongSeparator(); + + Lexeme readLongString(const Position& start, int sep, Lexeme::Type ok, Lexeme::Type broken); + Lexeme readQuotedString(); + + Lexeme readInterpolatedStringBegin(); + Lexeme readInterpolatedStringSection(Position start, Lexeme::Type formatType, Lexeme::Type endType); + + void readBackslashInString(); + + std::pair readName(); + + Lexeme readNumber(const Position& start, unsigned int startOffset); + + Lexeme readUtf8Error(); + Lexeme readNext(); + + const char* buffer; + std::size_t bufferSize; + + unsigned int offset; + + unsigned int line; + unsigned int lineOffset; + + Lexeme lexeme; + + Location prevLocation; + + AstNameTable& names; + + bool skipComments; + bool readNames; + + std::vector braceStack; +}; + +inline bool isSpace(char ch) +{ + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\v' || ch == '\f'; +} + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/Location.h b/External/luau/Ast/include/Luau/Location.h new file mode 100644 index 00000000..8a493127 --- /dev/null +++ b/External/luau/Ast/include/Luau/Location.h @@ -0,0 +1,109 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +namespace Luau +{ + +struct Position +{ + unsigned int line, column; + + Position(unsigned int line, unsigned int column) + : line(line) + , column(column) + { + } + + static Position missing() + { + return {UINT_MAX, UINT_MAX}; + } + + bool operator==(const Position& rhs) const + { + return this->column == rhs.column && this->line == rhs.line; + } + + bool operator!=(const Position& rhs) const + { + return !(*this == rhs); + } + bool operator<(const Position& rhs) const + { + if (line == rhs.line) + return column < rhs.column; + else + return line < rhs.line; + } + bool operator>(const Position& rhs) const + { + if (line == rhs.line) + return column > rhs.column; + else + return line > rhs.line; + } + bool operator<=(const Position& rhs) const + { + return *this == rhs || *this < rhs; + } + bool operator>=(const Position& rhs) const + { + return *this == rhs || *this > rhs; + } + + void shift(const Position& start, const Position& oldEnd, const Position& newEnd); + + bool hasValue() const + { + return line != UINT_MAX || column != UINT_MAX; + } +}; + +struct Location +{ + Position begin, end; + + Location() + : begin(0, 0) + , end(0, 0) + { + } + + Location(const Position& begin, const Position& end) + : begin(begin) + , end(end) + { + } + + Location(const Position& begin, unsigned int length) + : begin(begin) + , end(begin.line, begin.column + length) + { + } + + Location(const Location& begin, const Location& end) + : begin(begin.begin) + , end(end.end) + { + } + + bool operator==(const Location& rhs) const + { + return this->begin == rhs.begin && this->end == rhs.end; + } + bool operator!=(const Location& rhs) const + { + return !(*this == rhs); + } + + bool encloses(const Location& l) const; + bool overlaps(const Location& l) const; + bool contains(const Position& p) const; + bool containsClosed(const Position& p) const; + void extend(const Location& other); + void shift(const Position& start, const Position& oldEnd, const Position& newEnd); +}; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/ParseOptions.h b/External/luau/Ast/include/Luau/ParseOptions.h new file mode 100644 index 00000000..6a22ebd9 --- /dev/null +++ b/External/luau/Ast/include/Luau/ParseOptions.h @@ -0,0 +1,36 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" + +#include + +namespace Luau +{ + +enum class Mode +{ + NoCheck, // Do not perform any inference + Nonstrict, // Unannotated symbols are any + Strict, // Unannotated symbols are inferred + Definition, // Type definition module, has special parsing rules +}; + +struct FragmentParseResumeSettings +{ + DenseHashMap localMap; + std::vector localStack; + Position resumePosition; +}; + +struct ParseOptions +{ + bool allowDeclarationSyntax = false; + bool captureComments = false; + std::optional parseFragment = std::nullopt; + bool storeCstData = false; + bool noErrorLimit = false; +}; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/ParseResult.h b/External/luau/Ast/include/Luau/ParseResult.h new file mode 100644 index 00000000..4e9ff73b --- /dev/null +++ b/External/luau/Ast/include/Luau/ParseResult.h @@ -0,0 +1,90 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" +#include "Luau/Location.h" +#include "Luau/Lexer.h" +#include "Luau/StringUtils.h" + +namespace Luau +{ + +class AstStatBlock; +class CstNode; + +class ParseError : public std::exception +{ +public: + ParseError(const Location& location, std::string message); + + const char* what() const throw() override; + + const Location& getLocation() const; + const std::string& getMessage() const; + + static LUAU_NORETURN void raise(const Location& location, const char* format, ...) LUAU_PRINTF_ATTR(2, 3); + +private: + Location location; + std::string message; +}; + +class ParseErrors : public std::exception +{ +public: + ParseErrors(std::vector errors); + + const char* what() const throw() override; + + const std::vector& getErrors() const; + +private: + std::vector errors; + std::string message; +}; + +struct HotComment +{ + bool header; + Location location; + std::string content; +}; + +struct Comment +{ + Lexeme::Type type; // Comment, BlockComment, or BrokenComment + Location location; +}; + +using CstNodeMap = DenseHashMap; + +struct ParseResult +{ + AstStatBlock* root; + size_t lines = 0; + + std::vector hotcomments; + std::vector errors; + + std::vector commentLocations; + + CstNodeMap cstNodeMap; +}; + +template +struct ParseNodeResult +{ + Node* root; + size_t lines = 0; + + std::vector hotcomments; + std::vector errors; + + std::vector commentLocations; + + CstNodeMap cstNodeMap; +}; + +inline constexpr const char* kParseNameError = "%error-id%"; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/Parser.h b/External/luau/Ast/include/Luau/Parser.h new file mode 100644 index 00000000..fdecdccd --- /dev/null +++ b/External/luau/Ast/include/Luau/Parser.h @@ -0,0 +1,599 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/Lexer.h" +#include "Luau/ParseOptions.h" +#include "Luau/ParseResult.h" +#include "Luau/DenseHash.h" +#include "Luau/Common.h" +#include "Luau/Cst.h" + +#include +#include +#include + +namespace Luau +{ + +template +class TempVector +{ +public: + explicit TempVector(std::vector& storage); + + ~TempVector(); + + const T& operator[](std::size_t index) const; + + const T& front() const; + + const T& back() const; + + bool empty() const; + + std::size_t size() const; + + void push_back(const T& item); + + typename std::vector::const_iterator begin() const + { + return storage.begin() + offset; + } + typename std::vector::const_iterator end() const + { + return storage.begin() + offset + size_; + } + +private: + std::vector& storage; + size_t offset; + size_t size_; +}; + +class Parser +{ + template + static ParseNodeResult runParse( + const char* buffer, + size_t bufferSize, + AstNameTable& names, + Allocator& allocator, + ParseOptions options, + F f + ); + +public: + static ParseResult parse( + const char* buffer, + std::size_t bufferSize, + AstNameTable& names, + Allocator& allocator, + ParseOptions options = ParseOptions() + ); + + static ParseNodeResult parseExpr( + const char* buffer, + std::size_t bufferSize, + AstNameTable& names, + Allocator& allocator, + ParseOptions options = ParseOptions() + ); + + static ParseNodeResult parseType( + const char* buffer, + std::size_t bufferSize, + AstNameTable& names, + Allocator& allocator, + ParseOptions options = {} + ); + +private: + struct Name; + struct Binding; + + Parser(const char* buffer, std::size_t bufferSize, AstNameTable& names, Allocator& allocator, const ParseOptions& options); + + bool blockFollow(const Lexeme& l); + + AstStatBlock* parseChunk(); + + // chunk ::= {stat [`;']} [laststat [`;']] + // block ::= chunk + AstStatBlock* parseBlock(); + + AstStatBlock* parseBlockNoScope(); + + // stat ::= + // varlist `=' explist | + // 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 Name `=' exp `,' exp [`,' exp] do block end | + // for namelist in explist do block end | + // [attributes] function funcname funcbody | + // [attributes] local function Name funcbody | + // local namelist [`=' explist] + // laststat ::= return [explist] | break + AstStat* parseStat(); + + // if exp then block {elseif exp then block} [else block] end + AstStat* parseIf(); + + // (`if' | `elseif') (`local' | `const') binding `=' exp then block ... end -- parses an entire `if local`/`if const` + AstStat* parseIfLocalCondition(const Location& start); + + // Parse the trailing `{elseif exp then block} [else block] end` shared by `parseIf` and `parseIfLocalCondition` + AstStat* parseElseBody(const Location& start, const Lexeme& matchThen, AstStatBlock* thenbody, Location& end, std::optional& elseLocation); + + // while exp do block end + AstStat* parseWhile(); + + // repeat block until exp + AstStat* parseRepeat(); + + // do block end + AstStat* parseDo(); + + // break + AstStat* parseBreak(); + + // continue + AstStat* parseContinue(const Location& start); + + // for Name `=' exp `,' exp [`,' exp] do block end | + // for namelist in explist do block end | + AstStat* parseFor(); + + // funcname ::= Name {`.' Name} [`:' Name] + AstExpr* parseFunctionName(bool& hasself, AstName& debugname); + + // function funcname funcbody + LUAU_FORCEINLINE AstStatFunction* parseFunctionStat(const AstArray& attributes, TempVector* cstAttrLists = nullptr); + + std::optional validateAttribute( + Location loc, + const char* attributeName, + const TempVector& attributes, + const AstArray& args + ); + + Location getAttributeStartLocation( + const AstArray& attributes, + const TempVector* cstAttrLists, + const Location& defaultLocation + ); + + // attrlist = '@[' parattr {',' parattr} ']' + void parseAttrList(TempVector& attributes, TempVector* cstAttrLists); + + // attribute ::= '@' NAME | attrlist + void parseAttribute(TempVector& attribute); + + // attributes ::= {attribute} + AstArray parseAttributes(TempVector* cstAttrLists = nullptr); + + // attributes local function Name funcbody + // attributes function funcname funcbody + // attributes `declare function' Name`(' [parlist] `)' [`:` Type] + // declare Name '{' Name ':' attributes `(' [parlist] `)' [`:` Type] '}' + AstStat* parseAttributeStat(); + + // local function Name funcbody | + // local namelist [`=' explist] + AstStat* parseLocal( + const Location start, + const Position keywordPosition, + const AstArray& attributes, + bool isConst, + TempVector* cstAttrLists = nullptr + ); + + // return [explist] + AstStat* parseReturn(); + + // type Name `=' Type + AstStat* parseTypeAlias(const Location& start, bool exported, Position typeKeywordPosition); + + AstStat* parseClassStat(const Location& start, bool exported, bool open); + + // type function Name ... end + AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition); + + AstDeclaredExternTypeProperty parseDeclaredExternTypeMethod(const AstArray& attributes); + AstDeclaredExternTypeProperty parseDeclaredExternTypeMethod_DEPRECATED(); + + + // `declare global' Name: Type | + // `declare function' Name`(' [parlist] `)' [`:` Type] + AstStat* parseDeclaration(const Location& start, const AstArray& attributes); + + // varlist `=' explist + AstStat* parseAssignment(AstExpr* initial); + + AstStat* parseExportValue( + const Location& start, + const Position keywordPosition, + const AstArray& attributes, + TempVector* cstAttrLists = nullptr + ); + + // var [`+=' | `-=' | `*=' | `/=' | `%=' | `^=' | `..='] exp + AstStat* parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op); + + std::pair> prepareFunctionArguments(const Location& start, bool hasself, const TempVector& args); + + // funcbodyhead ::= `(' [namelist [`,' `...'] | `...'] `)' [`:` Type] + // funcbody ::= funcbodyhead block end + std::pair parseFunctionBody( + bool hasself, + const Lexeme& matchFunction, + const AstName& debugname, + const Name* localName, + const AstArray& attributes, + const bool isConst = false, + TempVector* cstAttrLists = nullptr + ); + + // explist ::= {exp `,'} exp + void parseExprList(TempVector& result, TempVector* commaPositions = nullptr); + + // binding ::= Name [`:` Type] + Binding parseBinding(bool isConst = false); + AstArray extractAnnotationColonPositions(const TempVector& bindings); + + // bindinglist ::= (binding | `...') {`,' bindinglist} + // Returns the location of the vararg ..., or std::nullopt if the function is not vararg. + std::tuple parseBindingList( + TempVector& result, + bool allowDot3 = false, + AstArray* commaPositions = nullptr, + Position* initialCommaPosition = nullptr, + Position* varargAnnotationColonPosition = nullptr, + bool isConst = false + ); + + AstType* parseOptionalType(); + + // TypeList ::= Type [`,' TypeList] + // ReturnType ::= Type | `(' TypeList `)' + // TableProp ::= Name `:' Type + // TableIndexer ::= `[' Type `]' `:' Type + // PropList ::= (TableProp | TableIndexer) [`,' PropList] + // Type + // ::= Name + // | `nil` + // | `{' [PropList] `}' + // | `(' [TypeList] `)' `->` ReturnType + + // Returns the variadic annotation, if it exists. + AstTypePack* parseTypeList( + TempVector& result, + TempVector>& resultNames, + TempVector* commaPositions = nullptr, + TempVector* nameColonPositions = nullptr + ); + + AstTypePack* parseOptionalReturnType(Position* returnSpecifierPosition = nullptr); + AstTypePack* parseReturnType(); + + struct TableIndexerResult + { + AstTableIndexer* node; + Position indexerOpenPosition; + Position indexerClosePosition; + Position colonPosition; + }; + + TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional accessLocation, Lexeme begin); + + AstTypeOrPack parseFunctionType(bool allowPack, const AstArray& attributes); + AstType* parseFunctionTypeTail( + const Lexeme& begin, + const AstArray& attributes, + AstArray generics, + AstArray genericPacks, + AstArray params, + AstArray> paramNames, + AstTypePack* varargAnnotation + ); + + AstType* parseTableType(bool inDeclarationContext = false); + AstTypeOrPack parseSimpleType(bool allowPack, bool inDeclarationContext = false); + + AstTypeOrPack parseSimpleTypeOrPack(); + AstType* parseType(bool inDeclarationContext = false); + + AstTypePack* parseTypePack(); + AstTypePack* parseVariadicArgumentTypePack(); + + AstType* parseTypeSuffix(AstType* type, const Location& begin); + + static std::optional parseUnaryOp(const Lexeme& l); + static std::optional parseBinaryOp(const Lexeme& l); + static std::optional parseCompoundOp(const Lexeme& l); + + struct BinaryOpPriority + { + unsigned char left, right; + }; + + std::optional checkUnaryConfusables(); + std::optional checkBinaryConfusables(const BinaryOpPriority binaryPriority[], unsigned int limit); + + // subexpr -> (asexp | unop subexpr) { binop subexpr } + // where `binop' is any binary operator with a priority higher than `limit' + AstExpr* parseExpr(unsigned int limit = 0); + + // NAME + AstExpr* parseNameExpr(const char* context = nullptr); + + // prefixexp -> NAME | '(' expr ')' + AstExpr* parsePrefixExpr(); + + // primaryexp -> prefixexp { `.' NAME | `[' exp `]' | TypeInstantiation | `:' NAME [TypeInstantiation] funcargs | funcargs } + AstExpr* parsePrimaryExpr(bool asStatement); + AstExpr* parseIndexExpr(Position start, AstExpr* expr); + AstExpr* parseMethodCall(Position start, AstExpr* expr); + + // asexp -> simpleexp [`::' Type] + AstExpr* parseAssertionExpr(); + + // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | [attributes] FUNCTION body | primaryexp + AstExpr* parseSimpleExpr(); + + AstExpr* parseAttributedFunction(const Location& start); + + std::tuple, Location, Location> parseCallList(TempVector* commaPositions, Position* closeParenPosition = nullptr); + // args ::= `(' [explist] `)' | tableconstructor | String + AstExpr* parseFunctionArgs(AstExpr* func, bool self); + + CstExprTable::Separator tableSeparator(); + + // tableconstructor ::= `{' [fieldlist] `}' + // fieldlist ::= field {fieldsep field} [fieldsep] + // field ::= `[' exp `]' `=' exp | Name `=' exp | exp + // fieldsep ::= `,' | `;' + AstExpr* parseTableConstructor(); + + // TODO: Add grammar rules here? + AstExpr* parseIfElseExpr(); + + // (`if' | `elseif') (`local' | `const') binding `=' exp then exp ... else exp -- parses an entire `if local`/`if const` expression + AstExpr* parseIfElseExprLocalCondition(const Location& start); + + // Parse the trailing `else exp` / `elseif ...` shared by `parseIfElseExpr` and `parseIfElseExprLocalCondition` + AstExpr* parseIfElseExprTail(bool& hasElse, bool& isElseIf); + + // stringinterp ::= exp { exp} + AstExpr* parseInterpString(); + + // TypeInstantiation ::= `<' `<' [TypeList] `>' `>' + AstArray parseTypeInstantiationExpr(CstTypeInstantiation* cstNodeOut = nullptr, Location* endLocationOut = nullptr); + + AstExpr* parseExplicitTypeInstantiationExpr(Position start, AstExpr& basedOnExpr); + + AstExpr* parseClassRefExpr(); + + // Name + std::optional parseNameOpt(const char* context = nullptr); + Name parseName(const char* context = nullptr); + Name parseIndexName(const char* context, const Position& previous); + + // `<' namelist `>' + std::pair, AstArray> parseGenericTypeList( + bool withDefaultValues, + Position* openPosition = nullptr, + AstArray* commaPositions = nullptr, + Position* closePosition = nullptr + ); + + // `<' Type[, ...] `>' + AstArray parseTypeParams( + Position* openingPosition = nullptr, + TempVector* commaPositions = nullptr, + Position* closingPosition = nullptr + ); + + std::optional> parseCharArray(AstArray* originalString = nullptr); + AstExpr* parseString(); + AstExpr* parseNumber(); + + AstLocal* pushLocal(const Binding& binding); + + unsigned int saveLocals(); + + void restoreLocals(unsigned int offset); + + /// Returns string quote style and block depth + std::pair extractStringDetails(); + + // check that parser is at lexeme/symbol, move to next lexeme/symbol on success, report failure and continue on failure + bool expectAndConsume(char value, const char* context = nullptr); + bool expectAndConsume(Lexeme::Type type, const char* context = nullptr); + bool expectAndConsumeFailWithLookahead(Lexeme::Type type, const char* context); + void expectAndConsumeFail(Lexeme::Type type, const char* context); + + struct MatchLexeme + { + MatchLexeme(const Lexeme& l) + : type(l.type) + , position(l.location.begin) + { + } + + Lexeme::Type type; + Position position; + }; + + bool expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing = false); + void expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra = nullptr); + bool expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing); + + bool expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin); + bool expectMatchEndAndConsumeFailWithLookahead(Lexeme::Type type, const MatchLexeme& begin); + + template + AstArray copy(const T* data, std::size_t size); + + template + AstArray copy(const TempVector& data); + + template + AstArray copy(std::initializer_list data); + + AstArray copy(const std::string& data); + + void incrementRecursionCounter(const char* context); + + void report(const Location& location, const char* format, va_list args); + void report(const Location& location, const char* format, ...) LUAU_PRINTF_ATTR(3, 4); + + void reportNameError(const char* context); + + AstStatError* reportStatError( + const Location& location, + const AstArray& expressions, + const AstArray& statements, + const char* format, + ... + ) LUAU_PRINTF_ATTR(5, 6); + AstExprError* reportExprError(const Location& location, const AstArray& expressions, const char* format, ...) LUAU_PRINTF_ATTR(4, 5); + AstStatClass* getMatchingClass(AstExpr* expr); + bool isExprLValue(AstExpr* expr); + AstExprError* reportLValueError(AstExpr* expr); + AstTypeError* reportTypeError(const Location& location, const AstArray& types, const char* format, ...) LUAU_PRINTF_ATTR(4, 5); + // `parseErrorLocation` is associated with the parser error + // `astErrorLocation` is associated with the AstTypeError created + // It can be useful to have different error locations so that the parse error can include the next lexeme, while the AstTypeError can precisely + // define the location (possibly of zero size) where a type annotation is expected. + AstTypeError* reportMissingTypeError( + const Location& parseErrorLocation, + const Location& astErrorLocation, + const char* format, + ... + ) LUAU_PRINTF_ATTR(4, 5); + + AstExpr* reportFunctionArgsError(AstExpr* func, bool self); + void reportAmbiguousCallError(); + + void nextLexeme(); + + struct Function + { + bool vararg; + unsigned int loopDepth; + + Function() + : vararg(false) + , loopDepth(0) + { + } + }; + + struct Local + { + AstLocal* local; + unsigned int offset; + + Local() + : local(nullptr) + , offset(0) + { + } + }; + + struct Name + { + AstName name; + Location location; + + Name(const AstName& name, const Location& location) + : name(name) + , location(location) + { + } + }; + + struct Binding + { + Name name; + AstType* annotation; + Position colonPosition; + bool isConst; + + explicit Binding(const Name& name, AstType* annotation = nullptr, Position colonPosition = {0, 0}, bool isConst = false) + : name(name) + , annotation(annotation) + , colonPosition(colonPosition) + , isConst(isConst) + { + } + }; + + ParseOptions options; + + Lexer lexer; + Allocator& allocator; + + std::vector commentLocations; + std::vector hotcomments; + + bool hotcommentHeader = true; + + unsigned int recursionCounter; + + AstName nameSelf; + AstName nameNumber; + AstName nameError; + AstName nameNil; + + MatchLexeme endMismatchSuspect; + + std::vector functionStack; + size_t typeFunctionDepth = 0; + + DenseHashMap localMap; + std::vector localStack; + DenseHashMap classesWithinModule; + + std::vector parseErrors; + + std::vector matchRecoveryStopOnToken; + + DenseHashMap declaredExportBindings; + bool hasModuleReturn = false; + + std::vector scratchAttr; + std::vector scratchStat; + std::vector> scratchString; + std::vector> scratchString2; + std::vector scratchExpr; + std::vector scratchExprAux; + std::vector scratchName; + std::vector scratchPackName; + std::vector scratchBinding; + std::vector scratchLocal; + std::vector scratchTableTypeProps; + std::vector scratchCstTableTypeProps; + std::vector scratchType; + std::vector scratchTypeOrPack; + std::vector scratchDeclaredClassProps; + std::vector scratchClassDeclarations; + std::vector scratchItem; + std::vector scratchCstItem; + std::vector scratchArgName; + std::vector scratchGenericTypes; + std::vector scratchGenericTypePacks; + std::vector> scratchOptArgName; + std::vector scratchPosition; + std::vector scratchPosition2; + std::vector scratchCstAttrList; + std::string scratchData; + + CstNodeMap cstNodeMap; +}; + +} // namespace Luau diff --git a/External/luau/Ast/include/Luau/PrettyPrinter.h b/External/luau/Ast/include/Luau/PrettyPrinter.h new file mode 100644 index 00000000..5660f0f0 --- /dev/null +++ b/External/luau/Ast/include/Luau/PrettyPrinter.h @@ -0,0 +1,34 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Location.h" +#include "Luau/ParseOptions.h" +#include "Luau/ParseResult.h" + +#include + +namespace Luau +{ +class AstNode; +class AstStatBlock; + +struct PrettyPrintResult +{ + std::string code; + Location errorLocation; + std::string parseError; // Nonempty if the transpile failed +}; + +std::string toString(AstNode* node); +void dump(AstNode* node); + +// Never fails on a well-formed AST +std::string prettyPrint(AstStatBlock& block); +std::string prettyPrint(AstStatBlock& block, const CstNodeMap& cstNodeMap); +std::string prettyPrintWithTypes(AstStatBlock& block); +std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap); + +// Only fails when parsing fails and we're not ignoring parse errors. +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options = ParseOptions{}, bool withTypes = false, bool ignoreParseErrors = false); + +} // namespace Luau diff --git a/External/luau/Ast/src/Allocator.cpp b/External/luau/Ast/src/Allocator.cpp new file mode 100644 index 00000000..f1381a2d --- /dev/null +++ b/External/luau/Ast/src/Allocator.cpp @@ -0,0 +1,66 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/Allocator.h" + +namespace Luau +{ + +Allocator::Allocator() + : root(static_cast(operator new(sizeof(Page)))) + , offset(0) +{ + root->next = nullptr; +} + +Allocator::Allocator(Allocator&& rhs) + : root(rhs.root) + , offset(rhs.offset) +{ + rhs.root = nullptr; + rhs.offset = 0; +} + +Allocator::~Allocator() +{ + Page* page = root; + + while (page) + { + Page* next = page->next; + + operator delete(page); + + page = next; + } +} + +void* Allocator::allocate(size_t size) +{ + constexpr size_t align = alignof(void*) > alignof(double) ? alignof(void*) : alignof(double); + + if (root) + { + uintptr_t data = reinterpret_cast(root->data); + uintptr_t result = (data + offset + align - 1) & ~(align - 1); + if (result + size <= data + sizeof(root->data)) + { + offset = result - data + size; + return reinterpret_cast(result); + } + } + + // allocate new page + size_t pageSize = size > sizeof(root->data) ? size : sizeof(root->data); + void* pageData = operator new(offsetof(Page, data) + pageSize); + + Page* page = static_cast(pageData); + + page->next = root; + + root = page; + offset = size; + + return page->data; +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Ast.cpp b/External/luau/Ast/src/Ast.cpp new file mode 100644 index 00000000..b894c63e --- /dev/null +++ b/External/luau/Ast/src/Ast.cpp @@ -0,0 +1,1506 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Ast.h" + +#include "Luau/Common.h" + +LUAU_FASTFLAG(LuauExperimentalIfLocalSyntax) + +namespace Luau +{ + +static AstAttr* findAttributeInArray(const AstArray attributes, AstAttr::Type attributeType) +{ + for (const auto attribute : attributes) + { + if (attribute->type == attributeType) + return attribute; + } + + return nullptr; +} + +static bool hasAttributeInArray(const AstArray attributes, AstAttr::Type attributeType) +{ + return findAttributeInArray(attributes, attributeType) != nullptr; +} + +static void visitTypeList(AstVisitor* visitor, const AstTypeList& list) +{ + for (AstType* ty : list.types) + ty->visit(visitor); + + if (list.tailType) + list.tailType->visit(visitor); +} + +static void visitTypeOrPackArray(AstVisitor* visitor, const AstArray& arrayOfTypeOrPack) +{ + for (const AstTypeOrPack& param : arrayOfTypeOrPack) + { + if (param.type) + param.type->visit(visitor); + else + param.typePack->visit(visitor); + } +} + +AstAttr::AstAttr(const Location& location, Type type, AstArray args) + : AstNode(ClassIndex(), location) + , type(type) + , args(args) +{ +} + +AstAttr::AstAttr(const Location& location, Type type, AstArray args, AstName name) + : AstNode(ClassIndex(), location) + , type(type) + , args(args) + , name(name) +{ +} + +void AstAttr::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstAttr::DeprecatedInfo AstAttr::deprecatedInfo() const +{ + AstAttr::DeprecatedInfo info; + info.deprecated = type == AstAttr::Type::Deprecated; + + if (info.deprecated && args.size > 0 && args.data[0]->is()) + { + AstExprTable* table = args.data[0]->as(); + if (auto useValue = table->getRecord("use")) + { + if ((*useValue)->is()) + { + AstArray use = (*useValue)->as()->value; + info.use = {{use.data, use.size}}; + } + } + if (auto reasonValue = table->getRecord("reason")) + { + if ((*reasonValue)->is()) + { + AstArray reason = (*reasonValue)->as()->value; + info.reason = {{reason.data, reason.size}}; + } + } + } + + return info; +} + +int gAstRttiIndex = 0; + +AstGenericType::AstGenericType(const Location& location, AstName name, AstType* defaultValue) + : AstNode(ClassIndex(), location) + , name(name) + , defaultValue(defaultValue) +{ +} + +void AstGenericType::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + if (defaultValue) + defaultValue->visit(visitor); + } +} + +AstGenericTypePack::AstGenericTypePack(const Location& location, AstName name, AstTypePack* defaultValue) + : AstNode(ClassIndex(), location) + , name(name) + , defaultValue(defaultValue) +{ +} + +void AstGenericTypePack::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + if (defaultValue) + defaultValue->visit(visitor); + } +} + +AstExprGroup::AstExprGroup(const Location& location, AstExpr* expr) + : AstExpr(ClassIndex(), location) + , expr(expr) +{ +} + +void AstExprGroup::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + expr->visit(visitor); +} + +AstExprConstantNil::AstExprConstantNil(const Location& location) + : AstExpr(ClassIndex(), location) +{ +} + +void AstExprConstantNil::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprConstantBool::AstExprConstantBool(const Location& location, bool value) + : AstExpr(ClassIndex(), location) + , value(value) +{ +} + +void AstExprConstantBool::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprConstantNumber::AstExprConstantNumber(const Location& location, double value, ConstantNumberParseResult parseResult) + : AstExpr(ClassIndex(), location) + , value(value) + , parseResult(parseResult) +{ +} + +void AstExprConstantNumber::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprConstantInteger::AstExprConstantInteger(const Location& location, int64_t value, ConstantNumberParseResult parseResult) + : AstExpr(ClassIndex(), location) + , value(value) + , parseResult(parseResult) +{ +} + +void AstExprConstantInteger::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprConstantString::AstExprConstantString(const Location& location, const AstArray& value, QuoteStyle quoteStyle) + : AstExpr(ClassIndex(), location) + , value(value) + , quoteStyle(quoteStyle) +{ +} + +void AstExprConstantString::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +bool AstExprConstantString::isQuoted() const +{ + return quoteStyle == QuoteStyle::QuotedSimple || quoteStyle == QuoteStyle::QuotedRaw; +} + +AstExprLocal::AstExprLocal(const Location& location, AstLocal* local, bool upvalue) + : AstExpr(ClassIndex(), location) + , local(local) + , upvalue(upvalue) +{ +} + +void AstExprLocal::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprGlobal::AstExprGlobal(const Location& location, const AstName& name) + : AstExpr(ClassIndex(), location) + , name(name) +{ +} + +void AstExprGlobal::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprVarargs::AstExprVarargs(const Location& location) + : AstExpr(ClassIndex(), location) +{ +} + +void AstExprVarargs::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstExprCall::AstExprCall( + const Location& location, + AstExpr* func, + const AstArray& args, + bool self, + const AstArray& explicitTypes, + const Location& argLocation +) + : AstExpr(ClassIndex(), location) + , func(func) + , typeArguments(explicitTypes) + , args(args) + , self(self) + , argLocation(argLocation) +{ +} + +void AstExprCall::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + func->visit(visitor); + + for (AstExpr* arg : args) + arg->visit(visitor); + } +} + +AstExprIndexName::AstExprIndexName( + const Location& location, + AstExpr* expr, + const AstName& index, + const Location& indexLocation, + const Position& opPosition, + char op +) + : AstExpr(ClassIndex(), location) + , expr(expr) + , index(index) + , indexLocation(indexLocation) + , opPosition(opPosition) + , op(op) +{ +} + +void AstExprIndexName::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + expr->visit(visitor); +} + +AstExprIndexExpr::AstExprIndexExpr(const Location& location, AstExpr* expr, AstExpr* index) + : AstExpr(ClassIndex(), location) + , expr(expr) + , index(index) +{ +} + +void AstExprIndexExpr::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + expr->visit(visitor); + index->visit(visitor); + } +} + +AstExprFunction::AstExprFunction( + const Location& location, + const AstArray& attributes, + const AstArray& generics, + const AstArray& genericPacks, + AstLocal* self, + const AstArray& args, + bool vararg, + const Location& varargLocation, + AstStatBlock* body, + size_t functionDepth, + const AstName& debugname, + AstTypePack* returnAnnotation, + AstTypePack* varargAnnotation, + const std::optional& argLocation +) + : AstExpr(ClassIndex(), location) + , attributes(attributes) + , generics(generics) + , genericPacks(genericPacks) + , self(self) + , args(args) + , returnAnnotation(returnAnnotation) + , vararg(vararg) + , varargLocation(varargLocation) + , varargAnnotation(varargAnnotation) + , body(body) + , functionDepth(functionDepth) + , debugname(debugname) + , argLocation(argLocation) +{ +} + +void AstExprFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstLocal* arg : args) + { + if (arg->annotation) + arg->annotation->visit(visitor); + } + + if (varargAnnotation) + varargAnnotation->visit(visitor); + + if (returnAnnotation) + returnAnnotation->visit(visitor); + + body->visit(visitor); + } +} + +bool AstExprFunction::hasNativeAttribute() const +{ + for (const auto attribute : attributes) + { + if (attribute->type == AstAttr::Type::Native) + return true; + } + return false; +} + +bool AstExprFunction::hasAttribute(const AstAttr::Type attributeType) const +{ + return hasAttributeInArray(attributes, attributeType); +} + +AstAttr* AstExprFunction::getAttribute(const AstAttr::Type attributeType) const +{ + return findAttributeInArray(attributes, attributeType); +} + +AstExprTable::AstExprTable(const Location& location, const AstArray& items) + : AstExpr(ClassIndex(), location) + , items(items) +{ +} + +void AstExprTable::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (const Item& item : items) + { + if (item.key) + item.key->visit(visitor); + + item.value->visit(visitor); + } + } +} + +std::optional AstExprTable::getRecord(const char* key) const +{ + for (const AstExprTable::Item& item : items) + { + if (item.kind == AstExprTable::Item::Kind::Record) + { + if (strcmp(item.key->as()->value.data, key) == 0) + return item.value; + } + } + return {}; +} + +AstExprUnary::AstExprUnary(const Location& location, Op op, AstExpr* expr) + : AstExpr(ClassIndex(), location) + , op(op) + , expr(expr) +{ +} + +void AstExprUnary::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + expr->visit(visitor); +} + +std::string toString(AstExprUnary::Op op) +{ + switch (op) + { + case AstExprUnary::Op::Minus: + return "-"; + case AstExprUnary::Op::Not: + return "not"; + case AstExprUnary::Op::Len: + return "#"; + default: + LUAU_ASSERT(false); + return ""; // MSVC requires this even though the switch/case is exhaustive + } +} + +AstExprBinary::AstExprBinary(const Location& location, Op op, AstExpr* left, AstExpr* right) + : AstExpr(ClassIndex(), location) + , op(op) + , left(left) + , right(right) +{ +} + +void AstExprBinary::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + left->visit(visitor); + right->visit(visitor); + } +} + +std::string toString(AstExprBinary::Op op) +{ + switch (op) + { + case AstExprBinary::Add: + return "+"; + case AstExprBinary::Sub: + return "-"; + case AstExprBinary::Mul: + return "*"; + case AstExprBinary::Div: + return "/"; + case AstExprBinary::FloorDiv: + return "//"; + case AstExprBinary::Mod: + return "%"; + case AstExprBinary::Pow: + return "^"; + case AstExprBinary::Concat: + return ".."; + case AstExprBinary::CompareNe: + return "~="; + case AstExprBinary::CompareEq: + return "=="; + case AstExprBinary::CompareLt: + return "<"; + case AstExprBinary::CompareLe: + return "<="; + case AstExprBinary::CompareGt: + return ">"; + case AstExprBinary::CompareGe: + return ">="; + case AstExprBinary::And: + return "and"; + case AstExprBinary::Or: + return "or"; + default: + LUAU_ASSERT(false); + return ""; // MSVC requires this even though the switch/case is exhaustive + } +} + +AstExprTypeAssertion::AstExprTypeAssertion(const Location& location, AstExpr* expr, AstType* annotation) + : AstExpr(ClassIndex(), location) + , expr(expr) + , annotation(annotation) +{ +} + +void AstExprTypeAssertion::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + expr->visit(visitor); + annotation->visit(visitor); + } +} + +AstExprIfElse::AstExprIfElse(const Location& location, AstExpr* condition, bool hasThen, AstExpr* trueExpr, bool hasElse, AstExpr* falseExpr) + : AstExpr(ClassIndex(), location) + , condition(condition) + , hasThen(hasThen) + , trueExpr(trueExpr) + , hasElse(hasElse) + , falseExpr(falseExpr) +{ +} + +AstExprIfElse::AstExprIfElse( + const Location& location, + AstExpr* condition, + bool hasThen, + AstExpr* trueExpr, + bool hasElse, + AstExpr* falseExpr, + AstLocal* conditionLocal, + bool conditionIsConst, + const std::optional& conditionKeywordLocation, + const std::optional& conditionEqualsLocation +) + : AstExpr(ClassIndex(), location) + , condition(condition) + , hasThen(hasThen) + , trueExpr(trueExpr) + , hasElse(hasElse) + , falseExpr(falseExpr) + , conditionLocal(conditionLocal) + , conditionIsConst(conditionIsConst) + , conditionKeywordLocation(conditionKeywordLocation) + , conditionEqualsLocation(conditionEqualsLocation) +{ +} + +void AstExprIfElse::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + if (FFlag::LuauExperimentalIfLocalSyntax && conditionLocal && conditionLocal->annotation) + conditionLocal->annotation->visit(visitor); + + condition->visit(visitor); + trueExpr->visit(visitor); + falseExpr->visit(visitor); + } +} + +AstExprError::AstExprError(const Location& location, const AstArray& expressions, unsigned messageIndex) + : AstExpr(ClassIndex(), location) + , expressions(expressions) + , messageIndex(messageIndex) +{ +} + +AstExprInterpString::AstExprInterpString(const Location& location, const AstArray>& strings, const AstArray& expressions) + : AstExpr(ClassIndex(), location) + , strings(strings) + , expressions(expressions) +{ +} + +void AstExprInterpString::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstExpr* expr : expressions) + expr->visit(visitor); + } +} + +AstExprInstantiate::AstExprInstantiate(const Location& location, AstExpr* expr, AstArray types) + : AstExpr(ClassIndex(), location) + , expr(expr) + , typeArguments(types) +{ +} + +void AstExprInstantiate::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + expr->visit(visitor); + visitTypeOrPackArray(visitor, typeArguments); + } +} + + +void AstExprError::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstExpr* expression : expressions) + expression->visit(visitor); + } +} + +AstStatBlock::AstStatBlock(const Location& location, const AstArray& body, bool hasEnd) + : AstStat(ClassIndex(), location) + , body(body) + , hasEnd(hasEnd) +{ +} + +void AstStatBlock::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstStat* stat : body) + stat->visit(visitor); + } +} + +AstStatIf::AstStatIf( + const Location& location, + AstExpr* condition, + AstStatBlock* thenbody, + AstStat* elsebody, + const std::optional& thenLocation, + const std::optional& elseLocation +) + : AstStat(ClassIndex(), location) + , condition(condition) + , thenbody(thenbody) + , elsebody(elsebody) + , thenLocation(thenLocation) + , elseLocation(elseLocation) +{ +} + +AstStatIf::AstStatIf( + const Location& location, + AstExpr* condition, + AstStatBlock* thenbody, + AstStat* elsebody, + const std::optional& thenLocation, + const std::optional& elseLocation, + AstLocal* conditionLocal, + bool conditionIsConst, + const std::optional& conditionKeywordLocation, + const std::optional& conditionEqualsLocation +) + : AstStat(ClassIndex(), location) + , condition(condition) + , thenbody(thenbody) + , elsebody(elsebody) + , thenLocation(thenLocation) + , elseLocation(elseLocation) + , conditionLocal(conditionLocal) + , conditionIsConst(conditionIsConst) + , conditionKeywordLocation(conditionKeywordLocation) + , conditionEqualsLocation(conditionEqualsLocation) +{ +} + +void AstStatIf::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + if (FFlag::LuauExperimentalIfLocalSyntax && conditionLocal && conditionLocal->annotation) + conditionLocal->annotation->visit(visitor); + + condition->visit(visitor); + thenbody->visit(visitor); + + if (elsebody) + elsebody->visit(visitor); + } +} + +AstStatWhile::AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation) + : AstStat(ClassIndex(), location) + , condition(condition) + , body(body) + , hasDo(hasDo) + , doLocation(doLocation) +{ +} + +void AstStatWhile::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + condition->visit(visitor); + body->visit(visitor); + } +} + +AstStatRepeat::AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil) + : AstStat(ClassIndex(), location) + , condition(condition) + , body(body) + , DEPRECATED_hasUntil(DEPRECATED_hasUntil) +{ +} + +void AstStatRepeat::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + body->visit(visitor); + condition->visit(visitor); + } +} + +AstStatBreak::AstStatBreak(const Location& location) + : AstStat(ClassIndex(), location) +{ +} + +void AstStatBreak::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstStatContinue::AstStatContinue(const Location& location) + : AstStat(ClassIndex(), location) +{ +} + +void AstStatContinue::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstStatReturn::AstStatReturn(const Location& location, const AstArray& list) + : AstStat(ClassIndex(), location) + , list(list) +{ +} + +void AstStatReturn::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstExpr* expr : list) + expr->visit(visitor); + } +} + +AstStatExpr::AstStatExpr(const Location& location, AstExpr* expr) + : AstStat(ClassIndex(), location) + , expr(expr) +{ +} + +void AstStatExpr::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + expr->visit(visitor); +} + +AstStatLocal::AstStatLocal( + const Location& location, + const AstArray& vars, + const AstArray& values, + const std::optional& equalsSignLocation, + bool isConst +) + : AstStat(ClassIndex(), location) + , vars(vars) + , values(values) + , isConst(isConst) + , equalsSignLocation(equalsSignLocation) +{ +} + +void AstStatLocal::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstLocal* var : vars) + { + if (var->annotation) + var->annotation->visit(visitor); + } + + for (AstExpr* expr : values) + expr->visit(visitor); + } +} + +AstStatFor::AstStatFor( + const Location& location, + AstLocal* var, + AstExpr* from, + AstExpr* to, + AstExpr* step, + AstStatBlock* body, + bool hasDo, + const Location& doLocation +) + : AstStat(ClassIndex(), location) + , var(var) + , from(from) + , to(to) + , step(step) + , body(body) + , hasDo(hasDo) + , doLocation(doLocation) +{ +} + +void AstStatFor::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + if (var->annotation) + var->annotation->visit(visitor); + + from->visit(visitor); + to->visit(visitor); + + if (step) + step->visit(visitor); + + body->visit(visitor); + } +} + +AstStatForIn::AstStatForIn( + const Location& location, + const AstArray& vars, + const AstArray& values, + AstStatBlock* body, + bool hasIn, + const Location& inLocation, + bool hasDo, + const Location& doLocation +) + : AstStat(ClassIndex(), location) + , vars(vars) + , values(values) + , body(body) + , hasIn(hasIn) + , inLocation(inLocation) + , hasDo(hasDo) + , doLocation(doLocation) +{ +} + +void AstStatForIn::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstLocal* var : vars) + { + if (var->annotation) + var->annotation->visit(visitor); + } + + for (AstExpr* expr : values) + expr->visit(visitor); + + body->visit(visitor); + } +} + +AstStatAssign::AstStatAssign(const Location& location, const AstArray& vars, const AstArray& values) + : AstStat(ClassIndex(), location) + , vars(vars) + , values(values) +{ +} + +void AstStatAssign::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstExpr* lvalue : vars) + lvalue->visit(visitor); + + for (AstExpr* expr : values) + expr->visit(visitor); + } +} + +AstStatCompoundAssign::AstStatCompoundAssign(const Location& location, AstExprBinary::Op op, AstExpr* var, AstExpr* value) + : AstStat(ClassIndex(), location) + , op(op) + , var(var) + , value(value) +{ +} + +void AstStatCompoundAssign::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + var->visit(visitor); + value->visit(visitor); + } +} + +AstStatFunction::AstStatFunction(const Location& location, AstExpr* name, AstExprFunction* func) + : AstStat(ClassIndex(), location) + , name(name) + , func(func) +{ +} + +void AstStatFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + name->visit(visitor); + func->visit(visitor); + } +} + +AstStatLocalFunction::AstStatLocalFunction(const Location& location, AstLocal* name, AstExprFunction* func, bool isConst, Position constKeywordBegin) + : AstStat(ClassIndex(), location) + , name(name) + , func(func) + , isConst(isConst) + , constKeywordBegin(constKeywordBegin) +{ +} + +void AstStatLocalFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + func->visit(visitor); +} + +AstStatTypeAlias::AstStatTypeAlias( + const Location& location, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + AstType* type, + bool exported +) + : AstStat(ClassIndex(), location) + , name(name) + , nameLocation(nameLocation) + , generics(generics) + , genericPacks(genericPacks) + , type(type) + , exported(exported) +{ +} + +void AstStatTypeAlias::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstGenericType* el : generics) + { + el->visit(visitor); + } + + for (AstGenericTypePack* el : genericPacks) + { + el->visit(visitor); + } + + type->visit(visitor); + } +} + +AstStatTypeFunction::AstStatTypeFunction( + const Location& location, + const AstName& name, + const Location& nameLocation, + AstExprFunction* body, + bool exported, + bool hasErrors +) + : AstStat(ClassIndex(), location) + , name(name) + , nameLocation(nameLocation) + , body(body) + , exported(exported) + , hasErrors(hasErrors) +{ +} + +void AstStatTypeFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + body->visit(visitor); +} + +AstStatDeclareGlobal::AstStatDeclareGlobal(const Location& location, const AstName& name, const Location& nameLocation, AstType* type) + : AstStat(ClassIndex(), location) + , name(name) + , nameLocation(nameLocation) + , type(type) +{ +} + +void AstStatDeclareGlobal::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + type->visit(visitor); +} + +AstStatDeclareFunction::AstStatDeclareFunction( + const Location& location, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& params, + const AstArray& paramNames, + bool vararg, + const Location& varargLocation, + AstTypePack* retTypes +) + : AstStat(ClassIndex(), location) + , attributes() + , name(name) + , nameLocation(nameLocation) + , generics(generics) + , genericPacks(genericPacks) + , params(params) + , paramNames(paramNames) + , vararg(vararg) + , varargLocation(varargLocation) + , retTypes(retTypes) +{ +} + +AstStatClass::AstStatClass(const Location& location, AstLocal* name, AstExpr* super, AstArray members, bool exported, bool open) + : AstStat(ClassIndex(), location) + , name(name) + , super(super) + , members(members) + , exported(exported) + , open(open) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); +} + +void AstStatClass::visit(AstVisitor* visitor) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + if (visitor->visit(this)) + { + if (super) + super->visit(visitor); + + for (const auto& member : members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& prop) + { + if (prop.ty) + prop.ty->visit(visitor); + }, + [&](const AstClassMethod& method) + { + method.function->visit(visitor); + } + }, + member + ); + } + } +} + +AstStatDeclareFunction::AstStatDeclareFunction( + const Location& location, + const AstArray& attributes, + const AstName& name, + const Location& nameLocation, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& params, + const AstArray& paramNames, + bool vararg, + const Location& varargLocation, + AstTypePack* retTypes +) + : AstStat(ClassIndex(), location) + , attributes(attributes) + , name(name) + , nameLocation(nameLocation) + , generics(generics) + , genericPacks(genericPacks) + , params(params) + , paramNames(paramNames) + , vararg(vararg) + , varargLocation(varargLocation) + , retTypes(retTypes) +{ +} + +void AstStatDeclareFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + visitTypeList(visitor, params); + retTypes->visit(visitor); + } +} + +bool AstStatDeclareFunction::isCheckedFunction() const +{ + for (const AstAttr* attr : attributes) + { + if (attr->type == AstAttr::Type::Checked) + return true; + } + + return false; +} + +bool AstStatDeclareFunction::hasAttribute(AstAttr::Type attributeType) const +{ + return hasAttributeInArray(attributes, attributeType); +} + +AstAttr* AstStatDeclareFunction::getAttribute(const AstAttr::Type attributeType) const +{ + return findAttributeInArray(attributes, attributeType); +} + +AstStatDeclareExternType::AstStatDeclareExternType( + const Location& location, + const AstName& name, + std::optional superName, + const AstArray& props, + AstTableIndexer* indexer +) + : AstStat(ClassIndex(), location) + , name(name) + , superName(superName) + , props(props) + , indexer(indexer) +{ +} + +void AstStatDeclareExternType::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (const AstDeclaredExternTypeProperty& prop : props) + prop.ty->visit(visitor); + } +} + +AstStatError::AstStatError( + const Location& location, + const AstArray& expressions, + const AstArray& statements, + unsigned messageIndex +) + : AstStat(ClassIndex(), location) + , expressions(expressions) + , statements(statements) + , messageIndex(messageIndex) +{ +} + +void AstStatError::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstNode* expression : expressions) + expression->visit(visitor); + + for (AstNode* statement : statements) + statement->visit(visitor); + } +} + +AstTypeReference::AstTypeReference( + const Location& location, + std::optional prefix, + AstName name, + std::optional prefixLocation, + const Location& nameLocation, + bool hasParameterList, + const AstArray& parameters, + AstLocal* prefixLocal +) + : AstType(ClassIndex(), location) + , hasParameterList(hasParameterList) + , prefix(prefix) + , prefixLocation(prefixLocation) + , prefixLocal(prefixLocal) + , name(name) + , nameLocation(nameLocation) + , parameters(parameters) +{ +} + +void AstTypeReference::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + visitTypeOrPackArray(visitor, parameters); + } +} + +AstTypeTable::AstTypeTable(const Location& location, const AstArray& props, AstTableIndexer* indexer, bool isExact) + : AstType(ClassIndex(), location) + , props(props) + , indexer(indexer) + , isExact(isExact) +{ +} + +void AstTypeTable::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (const AstTableProp& prop : props) + prop.type->visit(visitor); + + if (indexer) + { + indexer->indexType->visit(visitor); + indexer->resultType->visit(visitor); + } + } +} + +AstTypeFunction::AstTypeFunction( + const Location& location, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& argTypes, + const AstArray>& argNames, + AstTypePack* returnTypes +) + : AstType(ClassIndex(), location) + , attributes() + , generics(generics) + , genericPacks(genericPacks) + , argTypes(argTypes) + , argNames(argNames) + , returnTypes(returnTypes) +{ + LUAU_ASSERT(argNames.size == 0 || argNames.size == argTypes.types.size); +} + +AstTypeFunction::AstTypeFunction( + const Location& location, + const AstArray& attributes, + const AstArray& generics, + const AstArray& genericPacks, + const AstTypeList& argTypes, + const AstArray>& argNames, + AstTypePack* returnTypes +) + : AstType(ClassIndex(), location) + , attributes(attributes) + , generics(generics) + , genericPacks(genericPacks) + , argTypes(argTypes) + , argNames(argNames) + , returnTypes(returnTypes) +{ + LUAU_ASSERT(argNames.size == 0 || argNames.size == argTypes.types.size); +} + +void AstTypeFunction::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + visitTypeList(visitor, argTypes); + returnTypes->visit(visitor); + } +} + +bool AstTypeFunction::isCheckedFunction() const +{ + for (const AstAttr* attr : attributes) + { + if (attr->type == AstAttr::Type::Checked) + return true; + } + + return false; +} + +bool AstTypeFunction::hasAttribute(AstAttr::Type attributeType) const +{ + return hasAttributeInArray(attributes, attributeType); +} + +AstAttr* AstTypeFunction::getAttribute(AstAttr::Type attributeType) const +{ + return findAttributeInArray(attributes, attributeType); +} + +AstTypeTypeof::AstTypeTypeof(const Location& location, AstExpr* expr) + : AstType(ClassIndex(), location) + , expr(expr) +{ +} + +void AstTypeTypeof::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + expr->visit(visitor); +} + +AstTypeOptional::AstTypeOptional(const Location& location) + : AstType(ClassIndex(), location) +{ +} + +void AstTypeOptional::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstTypeUnion::AstTypeUnion(const Location& location, const AstArray& types) + : AstType(ClassIndex(), location) + , types(types) +{ +} + +void AstTypeUnion::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstType* type : types) + type->visit(visitor); + } +} + +AstTypeIntersection::AstTypeIntersection(const Location& location, const AstArray& types) + : AstType(ClassIndex(), location) + , types(types) +{ +} + +void AstTypeIntersection::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstType* type : types) + type->visit(visitor); + } +} + +AstTypeSingletonBool::AstTypeSingletonBool(const Location& location, bool value) + : AstType(ClassIndex(), location) + , value(value) +{ +} + +void AstTypeSingletonBool::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstTypeSingletonString::AstTypeSingletonString(const Location& location, const AstArray& value) + : AstType(ClassIndex(), location) + , value(value) +{ +} + +void AstTypeSingletonString::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +AstTypeGroup::AstTypeGroup(const Location& location, AstType* type) + : AstType(ClassIndex(), location) + , type(type) +{ +} + +void AstTypeGroup::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + type->visit(visitor); +} + +AstTypeError::AstTypeError(const Location& location, const AstArray& types, bool isMissing, unsigned messageIndex) + : AstType(ClassIndex(), location) + , types(types) + , isMissing(isMissing) + , messageIndex(messageIndex) +{ +} + +void AstTypeError::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstType* type : types) + type->visit(visitor); + } +} + +AstTypePackExplicit::AstTypePackExplicit(const Location& location, AstTypeList typeList) + : AstTypePack(ClassIndex(), location) + , typeList(typeList) +{ +} + +void AstTypePackExplicit::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + { + for (AstType* type : typeList.types) + type->visit(visitor); + + if (typeList.tailType) + typeList.tailType->visit(visitor); + } +} + +AstTypePackVariadic::AstTypePackVariadic(const Location& location, AstType* variadicType) + : AstTypePack(ClassIndex(), location) + , variadicType(variadicType) +{ +} + +void AstTypePackVariadic::visit(AstVisitor* visitor) +{ + if (visitor->visit(this)) + variadicType->visit(visitor); +} + +AstTypePackGeneric::AstTypePackGeneric(const Location& location, AstName name) + : AstTypePack(ClassIndex(), location) + , genericName(name) +{ +} + +void AstTypePackGeneric::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + +bool isLValue(const AstExpr* expr) +{ + return expr->is() || expr->is() || expr->is() || expr->is(); +} + +bool isConstantLiteral(const AstExpr* expr) +{ + return expr->is() || expr->is() || expr->is() || + expr->is(); +} + +bool isLiteralTable(const AstExpr* expr) +{ + if (!expr->is()) + return false; + + for (const AstExprTable::Item& item : expr->as()->items) + { + switch (item.kind) + { + case AstExprTable::Item::Kind::General: + return false; + break; + case AstExprTable::Item::Kind::Record: + case AstExprTable::Item::Kind::List: + if (!isConstantLiteral(item.value) && !isLiteralTable(item.value)) + return false; + break; + } + } + return true; +} + +AstName getIdentifier(AstExpr* node) +{ + if (AstExprGlobal* expr = node->as()) + return expr->name; + + if (AstExprLocal* expr = node->as()) + return expr->local->name; + + return AstName(); +} + +Location getLocation(const AstTypeList& typeList) +{ + Location result; + if (typeList.types.size) + { + result = Location{typeList.types.data[0]->location, typeList.types.data[typeList.types.size - 1]->location}; + } + if (typeList.tailType) + result.end = typeList.tailType->location.end; + + return result; +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Confusables.cpp b/External/luau/Ast/src/Confusables.cpp new file mode 100644 index 00000000..3cd5a89f --- /dev/null +++ b/External/luau/Ast/src/Confusables.cpp @@ -0,0 +1,1824 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Confusables.h" + +#include +#include + +namespace Luau +{ + +struct Confusable +{ + unsigned codepoint : 24; + char text[5]; +}; + +// Derived from http://www.unicode.org/Public/security/10.0.0/confusables.txt; sorted by codepoint +// clang-format off +static const Confusable kConfusables[] = +{ + {34, "\""}, // MA#* ( " → '' ) QUOTATION MARK → APOSTROPHE, APOSTROPHE# # Converted to a quote. + {48, "O"}, // MA# ( 0 → O ) DIGIT ZERO → LATIN CAPITAL LETTER O# + {49, "l"}, // MA# ( 1 → l ) DIGIT ONE → LATIN SMALL LETTER L# + {73, "l"}, // MA# ( I → l ) LATIN CAPITAL LETTER I → LATIN SMALL LETTER L# + {96, "'"}, // MA#* ( ` → ' ) GRAVE ACCENT → APOSTROPHE# →ˋ→→`→→‘→ + {109, "rn"}, // MA# ( m → rn ) LATIN SMALL LETTER M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# + {124, "l"}, // MA#* ( | → l ) VERTICAL LINE → LATIN SMALL LETTER L# + {160, " "}, // MA#* (   → ) NO-BREAK SPACE → SPACE# + {180, "'"}, // MA#* ( ´ → ' ) ACUTE ACCENT → APOSTROPHE# →΄→→ʹ→ + {184, ","}, // MA#* ( ¸ → , ) CEDILLA → COMMA# + {198, "AE"}, // MA# ( Æ → AE ) LATIN CAPITAL LETTER AE → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER E# + {215, "x"}, // MA#* ( × → x ) MULTIPLICATION SIGN → LATIN SMALL LETTER X# + {230, "ae"}, // MA# ( æ → ae ) LATIN SMALL LETTER AE → LATIN SMALL LETTER A, LATIN SMALL LETTER E# + {305, "i"}, // MA# ( ı → i ) LATIN SMALL LETTER DOTLESS I → LATIN SMALL LETTER I# + {306, "lJ"}, // MA# ( IJ → lJ ) LATIN CAPITAL LIGATURE IJ → LATIN SMALL LETTER L, LATIN CAPITAL LETTER J# →IJ→ + {307, "ij"}, // MA# ( ij → ij ) LATIN SMALL LIGATURE IJ → LATIN SMALL LETTER I, LATIN SMALL LETTER J# + {329, "'n"}, // MA# ( ʼn → 'n ) LATIN SMALL LETTER N PRECEDED BY APOSTROPHE → APOSTROPHE, LATIN SMALL LETTER N# →ʼn→ + {338, "OE"}, // MA# ( Œ → OE ) LATIN CAPITAL LIGATURE OE → LATIN CAPITAL LETTER O, LATIN CAPITAL LETTER E# + {339, "oe"}, // MA# ( œ → oe ) LATIN SMALL LIGATURE OE → LATIN SMALL LETTER O, LATIN SMALL LETTER E# + {383, "f"}, // MA# ( ſ → f ) LATIN SMALL LETTER LONG S → LATIN SMALL LETTER F# + {385, "'B"}, // MA# ( Ɓ → 'B ) LATIN CAPITAL LETTER B WITH HOOK → APOSTROPHE, LATIN CAPITAL LETTER B# →ʽB→ + {388, "b"}, // MA# ( Ƅ → b ) LATIN CAPITAL LETTER TONE SIX → LATIN SMALL LETTER B# + {391, "C'"}, // MA# ( Ƈ → C' ) LATIN CAPITAL LETTER C WITH HOOK → LATIN CAPITAL LETTER C, APOSTROPHE# →Cʽ→ + {394, "'D"}, // MA# ( Ɗ → 'D ) LATIN CAPITAL LETTER D WITH HOOK → APOSTROPHE, LATIN CAPITAL LETTER D# →ʽD→ + {397, "g"}, // MA# ( ƍ → g ) LATIN SMALL LETTER TURNED DELTA → LATIN SMALL LETTER G# + {403, "G'"}, // MA# ( Ɠ → G' ) LATIN CAPITAL LETTER G WITH HOOK → LATIN CAPITAL LETTER G, APOSTROPHE# →Gʽ→ + {406, "l"}, // MA# ( Ɩ → l ) LATIN CAPITAL LETTER IOTA → LATIN SMALL LETTER L# + {408, "K'"}, // MA# ( Ƙ → K' ) LATIN CAPITAL LETTER K WITH HOOK → LATIN CAPITAL LETTER K, APOSTROPHE# →Kʽ→ + {416, "O'"}, // MA# ( Ơ → O' ) LATIN CAPITAL LETTER O WITH HORN → LATIN CAPITAL LETTER O, APOSTROPHE# →Oʼ→ + {417, "o'"}, // MA# ( ơ → o' ) LATIN SMALL LETTER O WITH HORN → LATIN SMALL LETTER O, APOSTROPHE# →oʼ→ + {420, "'P"}, // MA# ( Ƥ → 'P ) LATIN CAPITAL LETTER P WITH HOOK → APOSTROPHE, LATIN CAPITAL LETTER P# →ʽP→ + {422, "R"}, // MA# ( Ʀ → R ) LATIN LETTER YR → LATIN CAPITAL LETTER R# + {423, "2"}, // MA# ( Ƨ → 2 ) LATIN CAPITAL LETTER TONE TWO → DIGIT TWO# + {428, "'T"}, // MA# ( Ƭ → 'T ) LATIN CAPITAL LETTER T WITH HOOK → APOSTROPHE, LATIN CAPITAL LETTER T# →ʽT→ + {435, "'Y"}, // MA# ( Ƴ → 'Y ) LATIN CAPITAL LETTER Y WITH HOOK → APOSTROPHE, LATIN CAPITAL LETTER Y# →ʽY→ + {439, "3"}, // MA# ( Ʒ → 3 ) LATIN CAPITAL LETTER EZH → DIGIT THREE# + {444, "5"}, // MA# ( Ƽ → 5 ) LATIN CAPITAL LETTER TONE FIVE → DIGIT FIVE# + {445, "s"}, // MA# ( ƽ → s ) LATIN SMALL LETTER TONE FIVE → LATIN SMALL LETTER S# + {448, "l"}, // MA# ( ǀ → l ) LATIN LETTER DENTAL CLICK → LATIN SMALL LETTER L# + {449, "ll"}, // MA# ( ǁ → ll ) LATIN LETTER LATERAL CLICK → LATIN SMALL LETTER L, LATIN SMALL LETTER L# →‖→→∥→→||→ + {451, "!"}, // MA# ( ǃ → ! ) LATIN LETTER RETROFLEX CLICK → EXCLAMATION MARK# + {455, "LJ"}, // MA# ( LJ → LJ ) LATIN CAPITAL LETTER LJ → LATIN CAPITAL LETTER L, LATIN CAPITAL LETTER J# + {456, "Lj"}, // MA# ( Lj → Lj ) LATIN CAPITAL LETTER L WITH SMALL LETTER J → LATIN CAPITAL LETTER L, LATIN SMALL LETTER J# + {457, "lj"}, // MA# ( lj → lj ) LATIN SMALL LETTER LJ → LATIN SMALL LETTER L, LATIN SMALL LETTER J# + {458, "NJ"}, // MA# ( NJ → NJ ) LATIN CAPITAL LETTER NJ → LATIN CAPITAL LETTER N, LATIN CAPITAL LETTER J# + {459, "Nj"}, // MA# ( Nj → Nj ) LATIN CAPITAL LETTER N WITH SMALL LETTER J → LATIN CAPITAL LETTER N, LATIN SMALL LETTER J# + {460, "nj"}, // MA# ( nj → nj ) LATIN SMALL LETTER NJ → LATIN SMALL LETTER N, LATIN SMALL LETTER J# + {497, "DZ"}, // MA# ( DZ → DZ ) LATIN CAPITAL LETTER DZ → LATIN CAPITAL LETTER D, LATIN CAPITAL LETTER Z# + {498, "Dz"}, // MA# ( Dz → Dz ) LATIN CAPITAL LETTER D WITH SMALL LETTER Z → LATIN CAPITAL LETTER D, LATIN SMALL LETTER Z# + {499, "dz"}, // MA# ( dz → dz ) LATIN SMALL LETTER DZ → LATIN SMALL LETTER D, LATIN SMALL LETTER Z# + {540, "3"}, // MA# ( Ȝ → 3 ) LATIN CAPITAL LETTER YOGH → DIGIT THREE# →Ʒ→ + {546, "8"}, // MA# ( Ȣ → 8 ) LATIN CAPITAL LETTER OU → DIGIT EIGHT# + {547, "8"}, // MA# ( ȣ → 8 ) LATIN SMALL LETTER OU → DIGIT EIGHT# + {577, "?"}, // MA# ( Ɂ → ? ) LATIN CAPITAL LETTER GLOTTAL STOP → QUESTION MARK# →ʔ→ + {593, "a"}, // MA# ( ɑ → a ) LATIN SMALL LETTER ALPHA → LATIN SMALL LETTER A# + {609, "g"}, // MA# ( ɡ → g ) LATIN SMALL LETTER SCRIPT G → LATIN SMALL LETTER G# + {611, "y"}, // MA# ( ɣ → y ) LATIN SMALL LETTER GAMMA → LATIN SMALL LETTER Y# →γ→ + {617, "i"}, // MA# ( ɩ → i ) LATIN SMALL LETTER IOTA → LATIN SMALL LETTER I# + {618, "i"}, // MA# ( ɪ → i ) LATIN LETTER SMALL CAPITAL I → LATIN SMALL LETTER I# →ı→ + {623, "w"}, // MA# ( ɯ → w ) LATIN SMALL LETTER TURNED M → LATIN SMALL LETTER W# + {651, "u"}, // MA# ( ʋ → u ) LATIN SMALL LETTER V WITH HOOK → LATIN SMALL LETTER U# + {655, "y"}, // MA# ( ʏ → y ) LATIN LETTER SMALL CAPITAL Y → LATIN SMALL LETTER Y# →ү→→γ→ + {660, "?"}, // MA# ( ʔ → ? ) LATIN LETTER GLOTTAL STOP → QUESTION MARK# + {675, "dz"}, // MA# ( ʣ → dz ) LATIN SMALL LETTER DZ DIGRAPH → LATIN SMALL LETTER D, LATIN SMALL LETTER Z# + {678, "ts"}, // MA# ( ʦ → ts ) LATIN SMALL LETTER TS DIGRAPH → LATIN SMALL LETTER T, LATIN SMALL LETTER S# + {682, "ls"}, // MA# ( ʪ → ls ) LATIN SMALL LETTER LS DIGRAPH → LATIN SMALL LETTER L, LATIN SMALL LETTER S# + {683, "lz"}, // MA# ( ʫ → lz ) LATIN SMALL LETTER LZ DIGRAPH → LATIN SMALL LETTER L, LATIN SMALL LETTER Z# + {697, "'"}, // MA# ( ʹ → ' ) MODIFIER LETTER PRIME → APOSTROPHE# + {698, "\""}, // MA# ( ʺ → '' ) MODIFIER LETTER DOUBLE PRIME → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {699, "'"}, // MA# ( ʻ → ' ) MODIFIER LETTER TURNED COMMA → APOSTROPHE# →‘→ + {700, "'"}, // MA# ( ʼ → ' ) MODIFIER LETTER APOSTROPHE → APOSTROPHE# →′→ + {701, "'"}, // MA# ( ʽ → ' ) MODIFIER LETTER REVERSED COMMA → APOSTROPHE# →‘→ + {702, "'"}, // MA# ( ʾ → ' ) MODIFIER LETTER RIGHT HALF RING → APOSTROPHE# →ʼ→→′→ + {706, "<"}, // MA#* ( ˂ → < ) MODIFIER LETTER LEFT ARROWHEAD → LESS-THAN SIGN# + {707, ">"}, // MA#* ( ˃ → > ) MODIFIER LETTER RIGHT ARROWHEAD → GREATER-THAN SIGN# + {708, "^"}, // MA#* ( ˄ → ^ ) MODIFIER LETTER UP ARROWHEAD → CIRCUMFLEX ACCENT# + {710, "^"}, // MA# ( ˆ → ^ ) MODIFIER LETTER CIRCUMFLEX ACCENT → CIRCUMFLEX ACCENT# + {712, "'"}, // MA# ( ˈ → ' ) MODIFIER LETTER VERTICAL LINE → APOSTROPHE# + {714, "'"}, // MA# ( ˊ → ' ) MODIFIER LETTER ACUTE ACCENT → APOSTROPHE# →ʹ→→′→ + {715, "'"}, // MA# ( ˋ → ' ) MODIFIER LETTER GRAVE ACCENT → APOSTROPHE# →`→→‘→ + {720, ":"}, // MA# ( ː → : ) MODIFIER LETTER TRIANGULAR COLON → COLON# + {727, "-"}, // MA#* ( ˗ → - ) MODIFIER LETTER MINUS SIGN → HYPHEN-MINUS# + {731, "i"}, // MA#* ( ˛ → i ) OGONEK → LATIN SMALL LETTER I# →ͺ→→ι→→ι→ + {732, "~"}, // MA#* ( ˜ → ~ ) SMALL TILDE → TILDE# + {733, "\""}, // MA#* ( ˝ → '' ) DOUBLE ACUTE ACCENT → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {750, "\""}, // MA# ( ˮ → '' ) MODIFIER LETTER DOUBLE APOSTROPHE → APOSTROPHE, APOSTROPHE# →″→→"→# Converted to a quote. + {756, "'"}, // MA#* ( ˴ → ' ) MODIFIER LETTER MIDDLE GRAVE ACCENT → APOSTROPHE# →ˋ→→`→→‘→ + {758, "\""}, // MA#* ( ˶ → '' ) MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT → APOSTROPHE, APOSTROPHE# →˝→→"→# Converted to a quote. + {760, ":"}, // MA#* ( ˸ → : ) MODIFIER LETTER RAISED COLON → COLON# + {884, "'"}, // MA# ( ʹ → ' ) GREEK NUMERAL SIGN → APOSTROPHE# →′→ + {890, "i"}, // MA#* ( ͺ → i ) GREEK YPOGEGRAMMENI → LATIN SMALL LETTER I# →ι→→ι→ + {894, ";"}, // MA#* ( ; → ; ) GREEK QUESTION MARK → SEMICOLON# + {895, "J"}, // MA# ( Ϳ → J ) GREEK CAPITAL LETTER YOT → LATIN CAPITAL LETTER J# + {900, "'"}, // MA#* ( ΄ → ' ) GREEK TONOS → APOSTROPHE# →ʹ→ + {913, "A"}, // MA# ( Α → A ) GREEK CAPITAL LETTER ALPHA → LATIN CAPITAL LETTER A# + {914, "B"}, // MA# ( Β → B ) GREEK CAPITAL LETTER BETA → LATIN CAPITAL LETTER B# + {917, "E"}, // MA# ( Ε → E ) GREEK CAPITAL LETTER EPSILON → LATIN CAPITAL LETTER E# + {918, "Z"}, // MA# ( Ζ → Z ) GREEK CAPITAL LETTER ZETA → LATIN CAPITAL LETTER Z# + {919, "H"}, // MA# ( Η → H ) GREEK CAPITAL LETTER ETA → LATIN CAPITAL LETTER H# + {921, "l"}, // MA# ( Ι → l ) GREEK CAPITAL LETTER IOTA → LATIN SMALL LETTER L# + {922, "K"}, // MA# ( Κ → K ) GREEK CAPITAL LETTER KAPPA → LATIN CAPITAL LETTER K# + {924, "M"}, // MA# ( Μ → M ) GREEK CAPITAL LETTER MU → LATIN CAPITAL LETTER M# + {925, "N"}, // MA# ( Ν → N ) GREEK CAPITAL LETTER NU → LATIN CAPITAL LETTER N# + {927, "O"}, // MA# ( Ο → O ) GREEK CAPITAL LETTER OMICRON → LATIN CAPITAL LETTER O# + {929, "P"}, // MA# ( Ρ → P ) GREEK CAPITAL LETTER RHO → LATIN CAPITAL LETTER P# + {932, "T"}, // MA# ( Τ → T ) GREEK CAPITAL LETTER TAU → LATIN CAPITAL LETTER T# + {933, "Y"}, // MA# ( Υ → Y ) GREEK CAPITAL LETTER UPSILON → LATIN CAPITAL LETTER Y# + {935, "X"}, // MA# ( Χ → X ) GREEK CAPITAL LETTER CHI → LATIN CAPITAL LETTER X# + {945, "a"}, // MA# ( α → a ) GREEK SMALL LETTER ALPHA → LATIN SMALL LETTER A# + {947, "y"}, // MA# ( γ → y ) GREEK SMALL LETTER GAMMA → LATIN SMALL LETTER Y# + {953, "i"}, // MA# ( ι → i ) GREEK SMALL LETTER IOTA → LATIN SMALL LETTER I# + {957, "v"}, // MA# ( ν → v ) GREEK SMALL LETTER NU → LATIN SMALL LETTER V# + {959, "o"}, // MA# ( ο → o ) GREEK SMALL LETTER OMICRON → LATIN SMALL LETTER O# + {961, "p"}, // MA# ( ρ → p ) GREEK SMALL LETTER RHO → LATIN SMALL LETTER P# + {963, "o"}, // MA# ( σ → o ) GREEK SMALL LETTER SIGMA → LATIN SMALL LETTER O# + {965, "u"}, // MA# ( υ → u ) GREEK SMALL LETTER UPSILON → LATIN SMALL LETTER U# →ʋ→ + {978, "Y"}, // MA# ( ϒ → Y ) GREEK UPSILON WITH HOOK SYMBOL → LATIN CAPITAL LETTER Y# + {988, "F"}, // MA# ( Ϝ → F ) GREEK LETTER DIGAMMA → LATIN CAPITAL LETTER F# + {1000, "2"}, // MA# ( Ϩ → 2 ) COPTIC CAPITAL LETTER HORI → DIGIT TWO# →Ƨ→ + {1009, "p"}, // MA# ( ϱ → p ) GREEK RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {1010, "c"}, // MA# ( ϲ → c ) GREEK LUNATE SIGMA SYMBOL → LATIN SMALL LETTER C# + {1011, "j"}, // MA# ( ϳ → j ) GREEK LETTER YOT → LATIN SMALL LETTER J# + {1017, "C"}, // MA# ( Ϲ → C ) GREEK CAPITAL LUNATE SIGMA SYMBOL → LATIN CAPITAL LETTER C# + {1018, "M"}, // MA# ( Ϻ → M ) GREEK CAPITAL LETTER SAN → LATIN CAPITAL LETTER M# + {1029, "S"}, // MA# ( Ѕ → S ) CYRILLIC CAPITAL LETTER DZE → LATIN CAPITAL LETTER S# + {1030, "l"}, // MA# ( І → l ) CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I → LATIN SMALL LETTER L# + {1032, "J"}, // MA# ( Ј → J ) CYRILLIC CAPITAL LETTER JE → LATIN CAPITAL LETTER J# + {1040, "A"}, // MA# ( А → A ) CYRILLIC CAPITAL LETTER A → LATIN CAPITAL LETTER A# + {1042, "B"}, // MA# ( В → B ) CYRILLIC CAPITAL LETTER VE → LATIN CAPITAL LETTER B# + {1045, "E"}, // MA# ( Е → E ) CYRILLIC CAPITAL LETTER IE → LATIN CAPITAL LETTER E# + {1047, "3"}, // MA# ( З → 3 ) CYRILLIC CAPITAL LETTER ZE → DIGIT THREE# + {1050, "K"}, // MA# ( К → K ) CYRILLIC CAPITAL LETTER KA → LATIN CAPITAL LETTER K# + {1052, "M"}, // MA# ( М → M ) CYRILLIC CAPITAL LETTER EM → LATIN CAPITAL LETTER M# + {1053, "H"}, // MA# ( Н → H ) CYRILLIC CAPITAL LETTER EN → LATIN CAPITAL LETTER H# + {1054, "O"}, // MA# ( О → O ) CYRILLIC CAPITAL LETTER O → LATIN CAPITAL LETTER O# + {1056, "P"}, // MA# ( Р → P ) CYRILLIC CAPITAL LETTER ER → LATIN CAPITAL LETTER P# + {1057, "C"}, // MA# ( С → C ) CYRILLIC CAPITAL LETTER ES → LATIN CAPITAL LETTER C# + {1058, "T"}, // MA# ( Т → T ) CYRILLIC CAPITAL LETTER TE → LATIN CAPITAL LETTER T# + {1059, "Y"}, // MA# ( У → Y ) CYRILLIC CAPITAL LETTER U → LATIN CAPITAL LETTER Y# + {1061, "X"}, // MA# ( Х → X ) CYRILLIC CAPITAL LETTER HA → LATIN CAPITAL LETTER X# + {1067, "bl"}, // MA# ( Ы → bl ) CYRILLIC CAPITAL LETTER YERU → LATIN SMALL LETTER B, LATIN SMALL LETTER L# →ЬІ→→Ь1→ + {1068, "b"}, // MA# ( Ь → b ) CYRILLIC CAPITAL LETTER SOFT SIGN → LATIN SMALL LETTER B# →Ƅ→ + {1070, "lO"}, // MA# ( Ю → lO ) CYRILLIC CAPITAL LETTER YU → LATIN SMALL LETTER L, LATIN CAPITAL LETTER O# →IO→ + {1072, "a"}, // MA# ( а → a ) CYRILLIC SMALL LETTER A → LATIN SMALL LETTER A# + {1073, "6"}, // MA# ( б → 6 ) CYRILLIC SMALL LETTER BE → DIGIT SIX# + {1075, "r"}, // MA# ( г → r ) CYRILLIC SMALL LETTER GHE → LATIN SMALL LETTER R# + {1077, "e"}, // MA# ( е → e ) CYRILLIC SMALL LETTER IE → LATIN SMALL LETTER E# + {1086, "o"}, // MA# ( о → o ) CYRILLIC SMALL LETTER O → LATIN SMALL LETTER O# + {1088, "p"}, // MA# ( р → p ) CYRILLIC SMALL LETTER ER → LATIN SMALL LETTER P# + {1089, "c"}, // MA# ( с → c ) CYRILLIC SMALL LETTER ES → LATIN SMALL LETTER C# + {1091, "y"}, // MA# ( у → y ) CYRILLIC SMALL LETTER U → LATIN SMALL LETTER Y# + {1093, "x"}, // MA# ( х → x ) CYRILLIC SMALL LETTER HA → LATIN SMALL LETTER X# + {1109, "s"}, // MA# ( ѕ → s ) CYRILLIC SMALL LETTER DZE → LATIN SMALL LETTER S# + {1110, "i"}, // MA# ( і → i ) CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I → LATIN SMALL LETTER I# + {1112, "j"}, // MA# ( ј → j ) CYRILLIC SMALL LETTER JE → LATIN SMALL LETTER J# + {1121, "w"}, // MA# ( ѡ → w ) CYRILLIC SMALL LETTER OMEGA → LATIN SMALL LETTER W# + {1140, "V"}, // MA# ( Ѵ → V ) CYRILLIC CAPITAL LETTER IZHITSA → LATIN CAPITAL LETTER V# + {1141, "v"}, // MA# ( ѵ → v ) CYRILLIC SMALL LETTER IZHITSA → LATIN SMALL LETTER V# + {1169, "r'"}, // MA# ( ґ → r' ) CYRILLIC SMALL LETTER GHE WITH UPTURN → LATIN SMALL LETTER R, APOSTROPHE# →гˈ→ + {1198, "Y"}, // MA# ( Ү → Y ) CYRILLIC CAPITAL LETTER STRAIGHT U → LATIN CAPITAL LETTER Y# + {1199, "y"}, // MA# ( ү → y ) CYRILLIC SMALL LETTER STRAIGHT U → LATIN SMALL LETTER Y# →γ→ + {1211, "h"}, // MA# ( һ → h ) CYRILLIC SMALL LETTER SHHA → LATIN SMALL LETTER H# + {1213, "e"}, // MA# ( ҽ → e ) CYRILLIC SMALL LETTER ABKHASIAN CHE → LATIN SMALL LETTER E# + {1216, "l"}, // MA# ( Ӏ → l ) CYRILLIC LETTER PALOCHKA → LATIN SMALL LETTER L# + {1231, "i"}, // MA# ( ӏ → i ) CYRILLIC SMALL LETTER PALOCHKA → LATIN SMALL LETTER I# →ı→ + {1236, "AE"}, // MA# ( Ӕ → AE ) CYRILLIC CAPITAL LIGATURE A IE → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER E# →Æ→ + {1237, "ae"}, // MA# ( ӕ → ae ) CYRILLIC SMALL LIGATURE A IE → LATIN SMALL LETTER A, LATIN SMALL LETTER E# →ае→ + {1248, "3"}, // MA# ( Ӡ → 3 ) CYRILLIC CAPITAL LETTER ABKHASIAN DZE → DIGIT THREE# →Ʒ→ + {1281, "d"}, // MA# ( ԁ → d ) CYRILLIC SMALL LETTER KOMI DE → LATIN SMALL LETTER D# + {1292, "G"}, // MA# ( Ԍ → G ) CYRILLIC CAPITAL LETTER KOMI SJE → LATIN CAPITAL LETTER G# + {1307, "q"}, // MA# ( ԛ → q ) CYRILLIC SMALL LETTER QA → LATIN SMALL LETTER Q# + {1308, "W"}, // MA# ( Ԝ → W ) CYRILLIC CAPITAL LETTER WE → LATIN CAPITAL LETTER W# + {1309, "w"}, // MA# ( ԝ → w ) CYRILLIC SMALL LETTER WE → LATIN SMALL LETTER W# + {1357, "U"}, // MA# ( Ս → U ) ARMENIAN CAPITAL LETTER SEH → LATIN CAPITAL LETTER U# + {1359, "S"}, // MA# ( Տ → S ) ARMENIAN CAPITAL LETTER TIWN → LATIN CAPITAL LETTER S# + {1365, "O"}, // MA# ( Օ → O ) ARMENIAN CAPITAL LETTER OH → LATIN CAPITAL LETTER O# + {1370, "'"}, // MA#* ( ՚ → ' ) ARMENIAN APOSTROPHE → APOSTROPHE# →’→ + {1373, "'"}, // MA#* ( ՝ → ' ) ARMENIAN COMMA → APOSTROPHE# →ˋ→→`→→‘→ + {1377, "w"}, // MA# ( ա → w ) ARMENIAN SMALL LETTER AYB → LATIN SMALL LETTER W# →ɯ→ + {1379, "q"}, // MA# ( գ → q ) ARMENIAN SMALL LETTER GIM → LATIN SMALL LETTER Q# + {1382, "q"}, // MA# ( զ → q ) ARMENIAN SMALL LETTER ZA → LATIN SMALL LETTER Q# + {1392, "h"}, // MA# ( հ → h ) ARMENIAN SMALL LETTER HO → LATIN SMALL LETTER H# + {1400, "n"}, // MA# ( ո → n ) ARMENIAN SMALL LETTER VO → LATIN SMALL LETTER N# + {1404, "n"}, // MA# ( ռ → n ) ARMENIAN SMALL LETTER RA → LATIN SMALL LETTER N# + {1405, "u"}, // MA# ( ս → u ) ARMENIAN SMALL LETTER SEH → LATIN SMALL LETTER U# + {1409, "g"}, // MA# ( ց → g ) ARMENIAN SMALL LETTER CO → LATIN SMALL LETTER G# + {1412, "f"}, // MA# ( ք → f ) ARMENIAN SMALL LETTER KEH → LATIN SMALL LETTER F# + {1413, "o"}, // MA# ( օ → o ) ARMENIAN SMALL LETTER OH → LATIN SMALL LETTER O# + {1417, ":"}, // MA#* ( ։ → : ) ARMENIAN FULL STOP → COLON# + {1472, "l"}, // MA#* ( ‎׀‎ → l ) HEBREW PUNCTUATION PASEQ → LATIN SMALL LETTER L# →|→ + {1475, ":"}, // MA#* ( ‎׃‎ → : ) HEBREW PUNCTUATION SOF PASUQ → COLON# + {1493, "l"}, // MA# ( ‎ו‎ → l ) HEBREW LETTER VAV → LATIN SMALL LETTER L# + {1496, "v"}, // MA# ( ‎ט‎ → v ) HEBREW LETTER TET → LATIN SMALL LETTER V# + {1497, "'"}, // MA# ( ‎י‎ → ' ) HEBREW LETTER YOD → APOSTROPHE# + {1503, "l"}, // MA# ( ‎ן‎ → l ) HEBREW LETTER FINAL NUN → LATIN SMALL LETTER L# + {1505, "o"}, // MA# ( ‎ס‎ → o ) HEBREW LETTER SAMEKH → LATIN SMALL LETTER O# + {1520, "ll"}, // MA# ( ‎װ‎ → ll ) HEBREW LIGATURE YIDDISH DOUBLE VAV → LATIN SMALL LETTER L, LATIN SMALL LETTER L# →‎וו‎→ + {1521, "l'"}, // MA# ( ‎ױ‎ → l' ) HEBREW LIGATURE YIDDISH VAV YOD → LATIN SMALL LETTER L, APOSTROPHE# →‎וי‎→ + {1522, "\""}, // MA# ( ‎ײ‎ → '' ) HEBREW LIGATURE YIDDISH DOUBLE YOD → APOSTROPHE, APOSTROPHE# →‎יי‎→# Converted to a quote. + {1523, "'"}, // MA#* ( ‎׳‎ → ' ) HEBREW PUNCTUATION GERESH → APOSTROPHE# + {1524, "\""}, // MA#* ( ‎״‎ → '' ) HEBREW PUNCTUATION GERSHAYIM → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {1549, ","}, // MA#* ( ‎؍‎ → , ) ARABIC DATE SEPARATOR → COMMA# →‎٫‎→ + {1575, "l"}, // MA# ( ‎ا‎ → l ) ARABIC LETTER ALEF → LATIN SMALL LETTER L# →1→ + {1607, "o"}, // MA# ( ‎ه‎ → o ) ARABIC LETTER HEH → LATIN SMALL LETTER O# + {1632, "."}, // MA# ( ‎٠‎ → . ) ARABIC-INDIC DIGIT ZERO → FULL STOP# + {1633, "l"}, // MA# ( ‎١‎ → l ) ARABIC-INDIC DIGIT ONE → LATIN SMALL LETTER L# →1→ + {1637, "o"}, // MA# ( ‎٥‎ → o ) ARABIC-INDIC DIGIT FIVE → LATIN SMALL LETTER O# + {1639, "V"}, // MA# ( ‎٧‎ → V ) ARABIC-INDIC DIGIT SEVEN → LATIN CAPITAL LETTER V# + {1643, ","}, // MA#* ( ‎٫‎ → , ) ARABIC DECIMAL SEPARATOR → COMMA# + {1645, "*"}, // MA#* ( ‎٭‎ → * ) ARABIC FIVE POINTED STAR → ASTERISK# + {1726, "o"}, // MA# ( ‎ھ‎ → o ) ARABIC LETTER HEH DOACHASHMEE → LATIN SMALL LETTER O# →‎ه‎→ + {1729, "o"}, // MA# ( ‎ہ‎ → o ) ARABIC LETTER HEH GOAL → LATIN SMALL LETTER O# →‎ه‎→ + {1748, "-"}, // MA#* ( ‎۔‎ → - ) ARABIC FULL STOP → HYPHEN-MINUS# →‐→ + {1749, "o"}, // MA# ( ‎ە‎ → o ) ARABIC LETTER AE → LATIN SMALL LETTER O# →‎ه‎→ + {1776, "."}, // MA# ( ۰ → . ) EXTENDED ARABIC-INDIC DIGIT ZERO → FULL STOP# →‎٠‎→ + {1777, "l"}, // MA# ( ۱ → l ) EXTENDED ARABIC-INDIC DIGIT ONE → LATIN SMALL LETTER L# →1→ + {1781, "o"}, // MA# ( ۵ → o ) EXTENDED ARABIC-INDIC DIGIT FIVE → LATIN SMALL LETTER O# →‎٥‎→ + {1783, "V"}, // MA# ( ۷ → V ) EXTENDED ARABIC-INDIC DIGIT SEVEN → LATIN CAPITAL LETTER V# →‎٧‎→ + {1793, "."}, // MA#* ( ‎܁‎ → . ) SYRIAC SUPRALINEAR FULL STOP → FULL STOP# + {1794, "."}, // MA#* ( ‎܂‎ → . ) SYRIAC SUBLINEAR FULL STOP → FULL STOP# + {1795, ":"}, // MA#* ( ‎܃‎ → : ) SYRIAC SUPRALINEAR COLON → COLON# + {1796, ":"}, // MA#* ( ‎܄‎ → : ) SYRIAC SUBLINEAR COLON → COLON# + {1984, "O"}, // MA# ( ‎߀‎ → O ) NKO DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {1994, "l"}, // MA# ( ‎ߊ‎ → l ) NKO LETTER A → LATIN SMALL LETTER L# →∣→→ǀ→ + {2036, "'"}, // MA# ( ‎ߴ‎ → ' ) NKO HIGH TONE APOSTROPHE → APOSTROPHE# →’→ + {2037, "'"}, // MA# ( ‎ߵ‎ → ' ) NKO LOW TONE APOSTROPHE → APOSTROPHE# →‘→ + {2042, "_"}, // MA# ( ‎ߺ‎ → _ ) NKO LAJANYALAN → LOW LINE# + {2307, ":"}, // MA# ( ः → : ) DEVANAGARI SIGN VISARGA → COLON# + {2406, "o"}, // MA# ( ० → o ) DEVANAGARI DIGIT ZERO → LATIN SMALL LETTER O# + {2429, "?"}, // MA# ( ॽ → ? ) DEVANAGARI LETTER GLOTTAL STOP → QUESTION MARK# + {2534, "O"}, // MA# ( ০ → O ) BENGALI DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {2538, "8"}, // MA# ( ৪ → 8 ) BENGALI DIGIT FOUR → DIGIT EIGHT# + {2541, "9"}, // MA# ( ৭ → 9 ) BENGALI DIGIT SEVEN → DIGIT NINE# + {2662, "o"}, // MA# ( ੦ → o ) GURMUKHI DIGIT ZERO → LATIN SMALL LETTER O# + {2663, "9"}, // MA# ( ੧ → 9 ) GURMUKHI DIGIT ONE → DIGIT NINE# + {2666, "8"}, // MA# ( ੪ → 8 ) GURMUKHI DIGIT FOUR → DIGIT EIGHT# + {2691, ":"}, // MA# ( ઃ → : ) GUJARATI SIGN VISARGA → COLON# + {2790, "o"}, // MA# ( ૦ → o ) GUJARATI DIGIT ZERO → LATIN SMALL LETTER O# + {2819, "8"}, // MA# ( ଃ → 8 ) ORIYA SIGN VISARGA → DIGIT EIGHT# + {2848, "O"}, // MA# ( ଠ → O ) ORIYA LETTER TTHA → LATIN CAPITAL LETTER O# →୦→→0→ + {2918, "O"}, // MA# ( ୦ → O ) ORIYA DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {2920, "9"}, // MA# ( ୨ → 9 ) ORIYA DIGIT TWO → DIGIT NINE# + {3046, "o"}, // MA# ( ௦ → o ) TAMIL DIGIT ZERO → LATIN SMALL LETTER O# + {3074, "o"}, // MA# ( ం → o ) TELUGU SIGN ANUSVARA → LATIN SMALL LETTER O# + {3174, "o"}, // MA# ( ౦ → o ) TELUGU DIGIT ZERO → LATIN SMALL LETTER O# + {3202, "o"}, // MA# ( ಂ → o ) KANNADA SIGN ANUSVARA → LATIN SMALL LETTER O# + {3302, "o"}, // MA# ( ೦ → o ) KANNADA DIGIT ZERO → LATIN SMALL LETTER O# →౦→ + {3330, "o"}, // MA# ( ം → o ) MALAYALAM SIGN ANUSVARA → LATIN SMALL LETTER O# + {3360, "o"}, // MA# ( ഠ → o ) MALAYALAM LETTER TTHA → LATIN SMALL LETTER O# + {3430, "o"}, // MA# ( ൦ → o ) MALAYALAM DIGIT ZERO → LATIN SMALL LETTER O# + {3437, "9"}, // MA# ( ൭ → 9 ) MALAYALAM DIGIT SEVEN → DIGIT NINE# + {3458, "o"}, // MA# ( ං → o ) SINHALA SIGN ANUSVARAYA → LATIN SMALL LETTER O# + {3664, "o"}, // MA# ( ๐ → o ) THAI DIGIT ZERO → LATIN SMALL LETTER O# + {3792, "o"}, // MA# ( ໐ → o ) LAO DIGIT ZERO → LATIN SMALL LETTER O# + {4125, "o"}, // MA# ( ဝ → o ) MYANMAR LETTER WA → LATIN SMALL LETTER O# + {4160, "o"}, // MA# ( ၀ → o ) MYANMAR DIGIT ZERO → LATIN SMALL LETTER O# + {4327, "y"}, // MA# ( ყ → y ) GEORGIAN LETTER QAR → LATIN SMALL LETTER Y# + {4351, "o"}, // MA# ( ჿ → o ) GEORGIAN LETTER LABIAL SIGN → LATIN SMALL LETTER O# + {4608, "U"}, // MA# ( ሀ → U ) ETHIOPIC SYLLABLE HA → LATIN CAPITAL LETTER U# →Ս→ + {4816, "O"}, // MA# ( ዐ → O ) ETHIOPIC SYLLABLE PHARYNGEAL A → LATIN CAPITAL LETTER O# →Օ→ + {5024, "D"}, // MA# ( Ꭰ → D ) CHEROKEE LETTER A → LATIN CAPITAL LETTER D# + {5025, "R"}, // MA# ( Ꭱ → R ) CHEROKEE LETTER E → LATIN CAPITAL LETTER R# + {5026, "T"}, // MA# ( Ꭲ → T ) CHEROKEE LETTER I → LATIN CAPITAL LETTER T# + {5028, "O'"}, // MA# ( Ꭴ → O' ) CHEROKEE LETTER U → LATIN CAPITAL LETTER O, APOSTROPHE# →Ơ→→Oʼ→ + {5029, "i"}, // MA# ( Ꭵ → i ) CHEROKEE LETTER V → LATIN SMALL LETTER I# + {5033, "Y"}, // MA# ( Ꭹ → Y ) CHEROKEE LETTER GI → LATIN CAPITAL LETTER Y# + {5034, "A"}, // MA# ( Ꭺ → A ) CHEROKEE LETTER GO → LATIN CAPITAL LETTER A# + {5035, "J"}, // MA# ( Ꭻ → J ) CHEROKEE LETTER GU → LATIN CAPITAL LETTER J# + {5036, "E"}, // MA# ( Ꭼ → E ) CHEROKEE LETTER GV → LATIN CAPITAL LETTER E# + {5038, "?"}, // MA# ( Ꭾ → ? ) CHEROKEE LETTER HE → QUESTION MARK# →Ɂ→→ʔ→ + {5043, "W"}, // MA# ( Ꮃ → W ) CHEROKEE LETTER LA → LATIN CAPITAL LETTER W# + {5047, "M"}, // MA# ( Ꮇ → M ) CHEROKEE LETTER LU → LATIN CAPITAL LETTER M# + {5051, "H"}, // MA# ( Ꮋ → H ) CHEROKEE LETTER MI → LATIN CAPITAL LETTER H# + {5053, "Y"}, // MA# ( Ꮍ → Y ) CHEROKEE LETTER MU → LATIN CAPITAL LETTER Y# →Ꭹ→ + {5056, "G"}, // MA# ( Ꮐ → G ) CHEROKEE LETTER NAH → LATIN CAPITAL LETTER G# + {5058, "h"}, // MA# ( Ꮒ → h ) CHEROKEE LETTER NI → LATIN SMALL LETTER H# + {5059, "Z"}, // MA# ( Ꮓ → Z ) CHEROKEE LETTER NO → LATIN CAPITAL LETTER Z# + {5070, "4"}, // MA# ( Ꮞ → 4 ) CHEROKEE LETTER SE → DIGIT FOUR# + {5071, "b"}, // MA# ( Ꮟ → b ) CHEROKEE LETTER SI → LATIN SMALL LETTER B# + {5074, "R"}, // MA# ( Ꮢ → R ) CHEROKEE LETTER SV → LATIN CAPITAL LETTER R# + {5076, "W"}, // MA# ( Ꮤ → W ) CHEROKEE LETTER TA → LATIN CAPITAL LETTER W# + {5077, "S"}, // MA# ( Ꮥ → S ) CHEROKEE LETTER DE → LATIN CAPITAL LETTER S# + {5081, "V"}, // MA# ( Ꮩ → V ) CHEROKEE LETTER DO → LATIN CAPITAL LETTER V# + {5082, "S"}, // MA# ( Ꮪ → S ) CHEROKEE LETTER DU → LATIN CAPITAL LETTER S# + {5086, "L"}, // MA# ( Ꮮ → L ) CHEROKEE LETTER TLE → LATIN CAPITAL LETTER L# + {5087, "C"}, // MA# ( Ꮯ → C ) CHEROKEE LETTER TLI → LATIN CAPITAL LETTER C# + {5090, "P"}, // MA# ( Ꮲ → P ) CHEROKEE LETTER TLV → LATIN CAPITAL LETTER P# + {5094, "K"}, // MA# ( Ꮶ → K ) CHEROKEE LETTER TSO → LATIN CAPITAL LETTER K# + {5095, "d"}, // MA# ( Ꮷ → d ) CHEROKEE LETTER TSU → LATIN SMALL LETTER D# + {5102, "6"}, // MA# ( Ꮾ → 6 ) CHEROKEE LETTER WV → DIGIT SIX# + {5107, "G"}, // MA# ( Ᏻ → G ) CHEROKEE LETTER YU → LATIN CAPITAL LETTER G# + {5108, "B"}, // MA# ( Ᏼ → B ) CHEROKEE LETTER YV → LATIN CAPITAL LETTER B# + {5120, "="}, // MA#* ( ᐀ → = ) CANADIAN SYLLABICS HYPHEN → EQUALS SIGN# + {5167, "V"}, // MA# ( ᐯ → V ) CANADIAN SYLLABICS PE → LATIN CAPITAL LETTER V# + {5171, ">"}, // MA# ( ᐳ → > ) CANADIAN SYLLABICS PO → GREATER-THAN SIGN# + {5176, "<"}, // MA# ( ᐸ → < ) CANADIAN SYLLABICS PA → LESS-THAN SIGN# + {5194, "'"}, // MA# ( ᑊ → ' ) CANADIAN SYLLABICS WEST-CREE P → APOSTROPHE# →ˈ→ + {5196, "U"}, // MA# ( ᑌ → U ) CANADIAN SYLLABICS TE → LATIN CAPITAL LETTER U# + {5223, "U'"}, // MA# ( ᑧ → U' ) CANADIAN SYLLABICS TTE → LATIN CAPITAL LETTER U, APOSTROPHE# →ᑌᑊ→→ᑌ'→ + {5229, "P"}, // MA# ( ᑭ → P ) CANADIAN SYLLABICS KI → LATIN CAPITAL LETTER P# + {5231, "d"}, // MA# ( ᑯ → d ) CANADIAN SYLLABICS KO → LATIN SMALL LETTER D# + {5254, "P'"}, // MA# ( ᒆ → P' ) CANADIAN SYLLABICS SOUTH-SLAVEY KIH → LATIN CAPITAL LETTER P, APOSTROPHE# →ᑭᑊ→ + {5255, "d'"}, // MA# ( ᒇ → d' ) CANADIAN SYLLABICS SOUTH-SLAVEY KOH → LATIN SMALL LETTER D, APOSTROPHE# →ᑯᑊ→ + {5261, "J"}, // MA# ( ᒍ → J ) CANADIAN SYLLABICS CO → LATIN CAPITAL LETTER J# + {5290, "L"}, // MA# ( ᒪ → L ) CANADIAN SYLLABICS MA → LATIN CAPITAL LETTER L# + {5311, "2"}, // MA# ( ᒿ → 2 ) CANADIAN SYLLABICS SAYISI M → DIGIT TWO# + {5441, "x"}, // MA# ( ᕁ → x ) CANADIAN SYLLABICS SAYISI YI → LATIN SMALL LETTER X# →᙮→ + {5500, "H"}, // MA# ( ᕼ → H ) CANADIAN SYLLABICS NUNAVUT H → LATIN CAPITAL LETTER H# + {5501, "x"}, // MA# ( ᕽ → x ) CANADIAN SYLLABICS HK → LATIN SMALL LETTER X# →ᕁ→→᙮→ + {5511, "R"}, // MA# ( ᖇ → R ) CANADIAN SYLLABICS TLHI → LATIN CAPITAL LETTER R# + {5551, "b"}, // MA# ( ᖯ → b ) CANADIAN SYLLABICS AIVILIK B → LATIN SMALL LETTER B# + {5556, "F"}, // MA# ( ᖴ → F ) CANADIAN SYLLABICS BLACKFOOT WE → LATIN CAPITAL LETTER F# + {5573, "A"}, // MA# ( ᗅ → A ) CANADIAN SYLLABICS CARRIER GHO → LATIN CAPITAL LETTER A# + {5598, "D"}, // MA# ( ᗞ → D ) CANADIAN SYLLABICS CARRIER THE → LATIN CAPITAL LETTER D# + {5610, "D"}, // MA# ( ᗪ → D ) CANADIAN SYLLABICS CARRIER PE → LATIN CAPITAL LETTER D# →ᗞ→ + {5616, "M"}, // MA# ( ᗰ → M ) CANADIAN SYLLABICS CARRIER GO → LATIN CAPITAL LETTER M# + {5623, "B"}, // MA# ( ᗷ → B ) CANADIAN SYLLABICS CARRIER KHE → LATIN CAPITAL LETTER B# + {5741, "X"}, // MA#* ( ᙭ → X ) CANADIAN SYLLABICS CHI SIGN → LATIN CAPITAL LETTER X# + {5742, "x"}, // MA#* ( ᙮ → x ) CANADIAN SYLLABICS FULL STOP → LATIN SMALL LETTER X# + {5760, " "}, // MA#* (   → ) OGHAM SPACE MARK → SPACE# + {5810, "<"}, // MA# ( ᚲ → < ) RUNIC LETTER KAUNA → LESS-THAN SIGN# + {5815, "X"}, // MA# ( ᚷ → X ) RUNIC LETTER GEBO GYFU G → LATIN CAPITAL LETTER X# + {5825, "l"}, // MA# ( ᛁ → l ) RUNIC LETTER ISAZ IS ISS I → LATIN SMALL LETTER L# →I→ + {5836, "'"}, // MA# ( ᛌ → ' ) RUNIC LETTER SHORT-TWIG-SOL S → APOSTROPHE# + {5845, "K"}, // MA# ( ᛕ → K ) RUNIC LETTER OPEN-P → LATIN CAPITAL LETTER K# + {5846, "M"}, // MA# ( ᛖ → M ) RUNIC LETTER EHWAZ EH E → LATIN CAPITAL LETTER M# + {5868, ":"}, // MA#* ( ᛬ → : ) RUNIC MULTIPLE PUNCTUATION → COLON# + {5869, "+"}, // MA#* ( ᛭ → + ) RUNIC CROSS PUNCTUATION → PLUS SIGN# + {5941, "/"}, // MA#* ( ᜵ → / ) PHILIPPINE SINGLE PUNCTUATION → SOLIDUS# + {6147, ":"}, // MA#* ( ᠃ → : ) MONGOLIAN FULL STOP → COLON# + {6153, ":"}, // MA#* ( ᠉ → : ) MONGOLIAN MANCHU FULL STOP → COLON# + {7379, "\""}, // MA#* ( ᳓ → '' ) VEDIC SIGN NIHSHVASA → APOSTROPHE, APOSTROPHE# →″→→"→# Converted to a quote. + {7428, "c"}, // MA# ( ᴄ → c ) LATIN LETTER SMALL CAPITAL C → LATIN SMALL LETTER C# + {7439, "o"}, // MA# ( ᴏ → o ) LATIN LETTER SMALL CAPITAL O → LATIN SMALL LETTER O# + {7441, "o"}, // MA# ( ᴑ → o ) LATIN SMALL LETTER SIDEWAYS O → LATIN SMALL LETTER O# + {7452, "u"}, // MA# ( ᴜ → u ) LATIN LETTER SMALL CAPITAL U → LATIN SMALL LETTER U# + {7456, "v"}, // MA# ( ᴠ → v ) LATIN LETTER SMALL CAPITAL V → LATIN SMALL LETTER V# + {7457, "w"}, // MA# ( ᴡ → w ) LATIN LETTER SMALL CAPITAL W → LATIN SMALL LETTER W# + {7458, "z"}, // MA# ( ᴢ → z ) LATIN LETTER SMALL CAPITAL Z → LATIN SMALL LETTER Z# + {7462, "r"}, // MA# ( ᴦ → r ) GREEK LETTER SMALL CAPITAL GAMMA → LATIN SMALL LETTER R# →г→ + {7531, "ue"}, // MA# ( ᵫ → ue ) LATIN SMALL LETTER UE → LATIN SMALL LETTER U, LATIN SMALL LETTER E# + {7555, "g"}, // MA# ( ᶃ → g ) LATIN SMALL LETTER G WITH PALATAL HOOK → LATIN SMALL LETTER G# + {7564, "y"}, // MA# ( ᶌ → y ) LATIN SMALL LETTER V WITH PALATAL HOOK → LATIN SMALL LETTER Y# + {7837, "f"}, // MA# ( ẝ → f ) LATIN SMALL LETTER LONG S WITH HIGH STROKE → LATIN SMALL LETTER F# + {7935, "y"}, // MA# ( ỿ → y ) LATIN SMALL LETTER Y WITH LOOP → LATIN SMALL LETTER Y# + {8125, "'"}, // MA#* ( ᾽ → ' ) GREEK KORONIS → APOSTROPHE# →’→ + {8126, "i"}, // MA# ( ι → i ) GREEK PROSGEGRAMMENI → LATIN SMALL LETTER I# →ι→ + {8127, "'"}, // MA#* ( ᾿ → ' ) GREEK PSILI → APOSTROPHE# →’→ + {8128, "~"}, // MA#* ( ῀ → ~ ) GREEK PERISPOMENI → TILDE# →˜→ + {8175, "'"}, // MA#* ( ` → ' ) GREEK VARIA → APOSTROPHE# →ˋ→→`→→‘→ + {8189, "'"}, // MA#* ( ´ → ' ) GREEK OXIA → APOSTROPHE# →´→→΄→→ʹ→ + {8190, "'"}, // MA#* ( ῾ → ' ) GREEK DASIA → APOSTROPHE# →‛→→′→ + {8192, " "}, // MA#* (   → ) EN QUAD → SPACE# + {8193, " "}, // MA#* (   → ) EM QUAD → SPACE# + {8194, " "}, // MA#* (   → ) EN SPACE → SPACE# + {8195, " "}, // MA#* (   → ) EM SPACE → SPACE# + {8196, " "}, // MA#* (   → ) THREE-PER-EM SPACE → SPACE# + {8197, " "}, // MA#* (   → ) FOUR-PER-EM SPACE → SPACE# + {8198, " "}, // MA#* (   → ) SIX-PER-EM SPACE → SPACE# + {8199, " "}, // MA#* (   → ) FIGURE SPACE → SPACE# + {8200, " "}, // MA#* (   → ) PUNCTUATION SPACE → SPACE# + {8201, " "}, // MA#* (   → ) THIN SPACE → SPACE# + {8202, " "}, // MA#* (   → ) HAIR SPACE → SPACE# + {8208, "-"}, // MA#* ( ‐ → - ) HYPHEN → HYPHEN-MINUS# + {8209, "-"}, // MA#* ( ‑ → - ) NON-BREAKING HYPHEN → HYPHEN-MINUS# + {8210, "-"}, // MA#* ( ‒ → - ) FIGURE DASH → HYPHEN-MINUS# + {8211, "-"}, // MA#* ( – → - ) EN DASH → HYPHEN-MINUS# + {8214, "ll"}, // MA#* ( ‖ → ll ) DOUBLE VERTICAL LINE → LATIN SMALL LETTER L, LATIN SMALL LETTER L# →∥→→||→ + {8216, "'"}, // MA#* ( ‘ → ' ) LEFT SINGLE QUOTATION MARK → APOSTROPHE# + {8217, "'"}, // MA#* ( ’ → ' ) RIGHT SINGLE QUOTATION MARK → APOSTROPHE# + {8218, ","}, // MA#* ( ‚ → , ) SINGLE LOW-9 QUOTATION MARK → COMMA# + {8219, "'"}, // MA#* ( ‛ → ' ) SINGLE HIGH-REVERSED-9 QUOTATION MARK → APOSTROPHE# →′→ + {8220, "\""}, // MA#* ( “ → '' ) LEFT DOUBLE QUOTATION MARK → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {8221, "\""}, // MA#* ( ” → '' ) RIGHT DOUBLE QUOTATION MARK → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {8223, "\""}, // MA#* ( ‟ → '' ) DOUBLE HIGH-REVERSED-9 QUOTATION MARK → APOSTROPHE, APOSTROPHE# →“→→"→# Converted to a quote. + {8228, "."}, // MA#* ( ․ → . ) ONE DOT LEADER → FULL STOP# + {8229, ".."}, // MA#* ( ‥ → .. ) TWO DOT LEADER → FULL STOP, FULL STOP# + {8230, "..."}, // MA#* ( … → ... ) HORIZONTAL ELLIPSIS → FULL STOP, FULL STOP, FULL STOP# + {8232, " "}, // MA#* ( → ) LINE SEPARATOR → SPACE# + {8233, " "}, // MA#* ( → ) PARAGRAPH SEPARATOR → SPACE# + {8239, " "}, // MA#* (   → ) NARROW NO-BREAK SPACE → SPACE# + {8242, "'"}, // MA#* ( ′ → ' ) PRIME → APOSTROPHE# + {8243, "\""}, // MA#* ( ″ → '' ) DOUBLE PRIME → APOSTROPHE, APOSTROPHE# →"→# Converted to a quote. + {8244, "'''"}, // MA#* ( ‴ → ''' ) TRIPLE PRIME → APOSTROPHE, APOSTROPHE, APOSTROPHE# →′′′→ + {8245, "'"}, // MA#* ( ‵ → ' ) REVERSED PRIME → APOSTROPHE# →ʽ→→‘→ + {8246, "\""}, // MA#* ( ‶ → '' ) REVERSED DOUBLE PRIME → APOSTROPHE, APOSTROPHE# →‵‵→# Converted to a quote. + {8247, "'''"}, // MA#* ( ‷ → ''' ) REVERSED TRIPLE PRIME → APOSTROPHE, APOSTROPHE, APOSTROPHE# →‵‵‵→ + {8249, "<"}, // MA#* ( ‹ → < ) SINGLE LEFT-POINTING ANGLE QUOTATION MARK → LESS-THAN SIGN# + {8250, ">"}, // MA#* ( › → > ) SINGLE RIGHT-POINTING ANGLE QUOTATION MARK → GREATER-THAN SIGN# + {8252, "!!"}, // MA#* ( ‼ → !! ) DOUBLE EXCLAMATION MARK → EXCLAMATION MARK, EXCLAMATION MARK# + {8257, "/"}, // MA#* ( ⁁ → / ) CARET INSERTION POINT → SOLIDUS# + {8259, "-"}, // MA#* ( ⁃ → - ) HYPHEN BULLET → HYPHEN-MINUS# →‐→ + {8260, "/"}, // MA#* ( ⁄ → / ) FRACTION SLASH → SOLIDUS# + {8263, "??"}, // MA#* ( ⁇ → ?? ) DOUBLE QUESTION MARK → QUESTION MARK, QUESTION MARK# + {8264, "?!"}, // MA#* ( ⁈ → ?! ) QUESTION EXCLAMATION MARK → QUESTION MARK, EXCLAMATION MARK# + {8265, "!?"}, // MA#* ( ⁉ → !? ) EXCLAMATION QUESTION MARK → EXCLAMATION MARK, QUESTION MARK# + {8270, "*"}, // MA#* ( ⁎ → * ) LOW ASTERISK → ASTERISK# + {8275, "~"}, // MA#* ( ⁓ → ~ ) SWUNG DASH → TILDE# + {8279, "''''"}, // MA#* ( ⁗ → '''' ) QUADRUPLE PRIME → APOSTROPHE, APOSTROPHE, APOSTROPHE, APOSTROPHE# →′′′′→ + {8282, ":"}, // MA#* ( ⁚ → : ) TWO DOT PUNCTUATION → COLON# + {8287, " "}, // MA#* (   → ) MEDIUM MATHEMATICAL SPACE → SPACE# + {8360, "Rs"}, // MA#* ( ₨ → Rs ) RUPEE SIGN → LATIN CAPITAL LETTER R, LATIN SMALL LETTER S# + {8374, "lt"}, // MA#* ( ₶ → lt ) LIVRE TOURNOIS SIGN → LATIN SMALL LETTER L, LATIN SMALL LETTER T# + {8448, "a/c"}, // MA#* ( ℀ → a/c ) ACCOUNT OF → LATIN SMALL LETTER A, SOLIDUS, LATIN SMALL LETTER C# + {8449, "a/s"}, // MA#* ( ℁ → a/s ) ADDRESSED TO THE SUBJECT → LATIN SMALL LETTER A, SOLIDUS, LATIN SMALL LETTER S# + {8450, "C"}, // MA# ( ℂ → C ) DOUBLE-STRUCK CAPITAL C → LATIN CAPITAL LETTER C# + {8453, "c/o"}, // MA#* ( ℅ → c/o ) CARE OF → LATIN SMALL LETTER C, SOLIDUS, LATIN SMALL LETTER O# + {8454, "c/u"}, // MA#* ( ℆ → c/u ) CADA UNA → LATIN SMALL LETTER C, SOLIDUS, LATIN SMALL LETTER U# + {8458, "g"}, // MA# ( ℊ → g ) SCRIPT SMALL G → LATIN SMALL LETTER G# + {8459, "H"}, // MA# ( ℋ → H ) SCRIPT CAPITAL H → LATIN CAPITAL LETTER H# + {8460, "H"}, // MA# ( ℌ → H ) BLACK-LETTER CAPITAL H → LATIN CAPITAL LETTER H# + {8461, "H"}, // MA# ( ℍ → H ) DOUBLE-STRUCK CAPITAL H → LATIN CAPITAL LETTER H# + {8462, "h"}, // MA# ( ℎ → h ) PLANCK CONSTANT → LATIN SMALL LETTER H# + {8464, "l"}, // MA# ( ℐ → l ) SCRIPT CAPITAL I → LATIN SMALL LETTER L# →I→ + {8465, "l"}, // MA# ( ℑ → l ) BLACK-LETTER CAPITAL I → LATIN SMALL LETTER L# →I→ + {8466, "L"}, // MA# ( ℒ → L ) SCRIPT CAPITAL L → LATIN CAPITAL LETTER L# + {8467, "l"}, // MA# ( ℓ → l ) SCRIPT SMALL L → LATIN SMALL LETTER L# + {8469, "N"}, // MA# ( ℕ → N ) DOUBLE-STRUCK CAPITAL N → LATIN CAPITAL LETTER N# + {8470, "No"}, // MA#* ( № → No ) NUMERO SIGN → LATIN CAPITAL LETTER N, LATIN SMALL LETTER O# + {8473, "P"}, // MA# ( ℙ → P ) DOUBLE-STRUCK CAPITAL P → LATIN CAPITAL LETTER P# + {8474, "Q"}, // MA# ( ℚ → Q ) DOUBLE-STRUCK CAPITAL Q → LATIN CAPITAL LETTER Q# + {8475, "R"}, // MA# ( ℛ → R ) SCRIPT CAPITAL R → LATIN CAPITAL LETTER R# + {8476, "R"}, // MA# ( ℜ → R ) BLACK-LETTER CAPITAL R → LATIN CAPITAL LETTER R# + {8477, "R"}, // MA# ( ℝ → R ) DOUBLE-STRUCK CAPITAL R → LATIN CAPITAL LETTER R# + {8481, "TEL"}, // MA#* ( ℡ → TEL ) TELEPHONE SIGN → LATIN CAPITAL LETTER T, LATIN CAPITAL LETTER E, LATIN CAPITAL LETTER L# + {8484, "Z"}, // MA# ( ℤ → Z ) DOUBLE-STRUCK CAPITAL Z → LATIN CAPITAL LETTER Z# + {8488, "Z"}, // MA# ( ℨ → Z ) BLACK-LETTER CAPITAL Z → LATIN CAPITAL LETTER Z# + {8490, "K"}, // MA# ( K → K ) KELVIN SIGN → LATIN CAPITAL LETTER K# + {8492, "B"}, // MA# ( ℬ → B ) SCRIPT CAPITAL B → LATIN CAPITAL LETTER B# + {8493, "C"}, // MA# ( ℭ → C ) BLACK-LETTER CAPITAL C → LATIN CAPITAL LETTER C# + {8494, "e"}, // MA# ( ℮ → e ) ESTIMATED SYMBOL → LATIN SMALL LETTER E# + {8495, "e"}, // MA# ( ℯ → e ) SCRIPT SMALL E → LATIN SMALL LETTER E# + {8496, "E"}, // MA# ( ℰ → E ) SCRIPT CAPITAL E → LATIN CAPITAL LETTER E# + {8497, "F"}, // MA# ( ℱ → F ) SCRIPT CAPITAL F → LATIN CAPITAL LETTER F# + {8499, "M"}, // MA# ( ℳ → M ) SCRIPT CAPITAL M → LATIN CAPITAL LETTER M# + {8500, "o"}, // MA# ( ℴ → o ) SCRIPT SMALL O → LATIN SMALL LETTER O# + {8505, "i"}, // MA# ( ℹ → i ) INFORMATION SOURCE → LATIN SMALL LETTER I# + {8507, "FAX"}, // MA#* ( ℻ → FAX ) FACSIMILE SIGN → LATIN CAPITAL LETTER F, LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER X# + {8509, "y"}, // MA# ( ℽ → y ) DOUBLE-STRUCK SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {8517, "D"}, // MA# ( ⅅ → D ) DOUBLE-STRUCK ITALIC CAPITAL D → LATIN CAPITAL LETTER D# + {8518, "d"}, // MA# ( ⅆ → d ) DOUBLE-STRUCK ITALIC SMALL D → LATIN SMALL LETTER D# + {8519, "e"}, // MA# ( ⅇ → e ) DOUBLE-STRUCK ITALIC SMALL E → LATIN SMALL LETTER E# + {8520, "i"}, // MA# ( ⅈ → i ) DOUBLE-STRUCK ITALIC SMALL I → LATIN SMALL LETTER I# + {8521, "j"}, // MA# ( ⅉ → j ) DOUBLE-STRUCK ITALIC SMALL J → LATIN SMALL LETTER J# + {8544, "l"}, // MA# ( Ⅰ → l ) ROMAN NUMERAL ONE → LATIN SMALL LETTER L# →Ӏ→ + {8545, "ll"}, // MA# ( Ⅱ → ll ) ROMAN NUMERAL TWO → LATIN SMALL LETTER L, LATIN SMALL LETTER L# →II→ + {8546, "lll"}, // MA# ( Ⅲ → lll ) ROMAN NUMERAL THREE → LATIN SMALL LETTER L, LATIN SMALL LETTER L, LATIN SMALL LETTER L# →III→ + {8547, "lV"}, // MA# ( Ⅳ → lV ) ROMAN NUMERAL FOUR → LATIN SMALL LETTER L, LATIN CAPITAL LETTER V# →IV→ + {8548, "V"}, // MA# ( Ⅴ → V ) ROMAN NUMERAL FIVE → LATIN CAPITAL LETTER V# + {8549, "Vl"}, // MA# ( Ⅵ → Vl ) ROMAN NUMERAL SIX → LATIN CAPITAL LETTER V, LATIN SMALL LETTER L# →VI→ + {8550, "Vll"}, // MA# ( Ⅶ → Vll ) ROMAN NUMERAL SEVEN → LATIN CAPITAL LETTER V, LATIN SMALL LETTER L, LATIN SMALL LETTER L# →VII→ + {8551, "Vlll"}, // MA# ( Ⅷ → Vlll ) ROMAN NUMERAL EIGHT → LATIN CAPITAL LETTER V, LATIN SMALL LETTER L, LATIN SMALL LETTER L, LATIN SMALL LETTER L# →VIII→ + {8552, "lX"}, // MA# ( Ⅸ → lX ) ROMAN NUMERAL NINE → LATIN SMALL LETTER L, LATIN CAPITAL LETTER X# →IX→ + {8553, "X"}, // MA# ( Ⅹ → X ) ROMAN NUMERAL TEN → LATIN CAPITAL LETTER X# + {8554, "Xl"}, // MA# ( Ⅺ → Xl ) ROMAN NUMERAL ELEVEN → LATIN CAPITAL LETTER X, LATIN SMALL LETTER L# →XI→ + {8555, "Xll"}, // MA# ( Ⅻ → Xll ) ROMAN NUMERAL TWELVE → LATIN CAPITAL LETTER X, LATIN SMALL LETTER L, LATIN SMALL LETTER L# →XII→ + {8556, "L"}, // MA# ( Ⅼ → L ) ROMAN NUMERAL FIFTY → LATIN CAPITAL LETTER L# + {8557, "C"}, // MA# ( Ⅽ → C ) ROMAN NUMERAL ONE HUNDRED → LATIN CAPITAL LETTER C# + {8558, "D"}, // MA# ( Ⅾ → D ) ROMAN NUMERAL FIVE HUNDRED → LATIN CAPITAL LETTER D# + {8559, "M"}, // MA# ( Ⅿ → M ) ROMAN NUMERAL ONE THOUSAND → LATIN CAPITAL LETTER M# + {8560, "i"}, // MA# ( ⅰ → i ) SMALL ROMAN NUMERAL ONE → LATIN SMALL LETTER I# + {8561, "ii"}, // MA# ( ⅱ → ii ) SMALL ROMAN NUMERAL TWO → LATIN SMALL LETTER I, LATIN SMALL LETTER I# + {8562, "iii"}, // MA# ( ⅲ → iii ) SMALL ROMAN NUMERAL THREE → LATIN SMALL LETTER I, LATIN SMALL LETTER I, LATIN SMALL LETTER I# + {8563, "iv"}, // MA# ( ⅳ → iv ) SMALL ROMAN NUMERAL FOUR → LATIN SMALL LETTER I, LATIN SMALL LETTER V# + {8564, "v"}, // MA# ( ⅴ → v ) SMALL ROMAN NUMERAL FIVE → LATIN SMALL LETTER V# + {8565, "vi"}, // MA# ( ⅵ → vi ) SMALL ROMAN NUMERAL SIX → LATIN SMALL LETTER V, LATIN SMALL LETTER I# + {8566, "vii"}, // MA# ( ⅶ → vii ) SMALL ROMAN NUMERAL SEVEN → LATIN SMALL LETTER V, LATIN SMALL LETTER I, LATIN SMALL LETTER I# + {8567, "viii"}, // MA# ( ⅷ → viii ) SMALL ROMAN NUMERAL EIGHT → LATIN SMALL LETTER V, LATIN SMALL LETTER I, LATIN SMALL LETTER I, LATIN SMALL LETTER I# + {8568, "ix"}, // MA# ( ⅸ → ix ) SMALL ROMAN NUMERAL NINE → LATIN SMALL LETTER I, LATIN SMALL LETTER X# + {8569, "x"}, // MA# ( ⅹ → x ) SMALL ROMAN NUMERAL TEN → LATIN SMALL LETTER X# + {8570, "xi"}, // MA# ( ⅺ → xi ) SMALL ROMAN NUMERAL ELEVEN → LATIN SMALL LETTER X, LATIN SMALL LETTER I# + {8571, "xii"}, // MA# ( ⅻ → xii ) SMALL ROMAN NUMERAL TWELVE → LATIN SMALL LETTER X, LATIN SMALL LETTER I, LATIN SMALL LETTER I# + {8572, "l"}, // MA# ( ⅼ → l ) SMALL ROMAN NUMERAL FIFTY → LATIN SMALL LETTER L# + {8573, "c"}, // MA# ( ⅽ → c ) SMALL ROMAN NUMERAL ONE HUNDRED → LATIN SMALL LETTER C# + {8574, "d"}, // MA# ( ⅾ → d ) SMALL ROMAN NUMERAL FIVE HUNDRED → LATIN SMALL LETTER D# + {8575, "rn"}, // MA# ( ⅿ → rn ) SMALL ROMAN NUMERAL ONE THOUSAND → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {8722, "-"}, // MA#* ( − → - ) MINUS SIGN → HYPHEN-MINUS# + {8725, "/"}, // MA#* ( ∕ → / ) DIVISION SLASH → SOLIDUS# + {8726, "\\"}, // MA#* ( ∖ → \ ) SET MINUS → REVERSE SOLIDUS# + {8727, "*"}, // MA#* ( ∗ → * ) ASTERISK OPERATOR → ASTERISK# + {8734, "oo"}, // MA#* ( ∞ → oo ) INFINITY → LATIN SMALL LETTER O, LATIN SMALL LETTER O# →ꝏ→ + {8739, "l"}, // MA#* ( ∣ → l ) DIVIDES → LATIN SMALL LETTER L# →ǀ→ + {8741, "ll"}, // MA#* ( ∥ → ll ) PARALLEL TO → LATIN SMALL LETTER L, LATIN SMALL LETTER L# →||→ + {8744, "v"}, // MA#* ( ∨ → v ) LOGICAL OR → LATIN SMALL LETTER V# + {8746, "U"}, // MA#* ( ∪ → U ) UNION → LATIN CAPITAL LETTER U# →ᑌ→ + {8758, ":"}, // MA#* ( ∶ → : ) RATIO → COLON# + {8764, "~"}, // MA#* ( ∼ → ~ ) TILDE OPERATOR → TILDE# + {8810, "<<"}, // MA#* ( ≪ → << ) MUCH LESS-THAN → LESS-THAN SIGN, LESS-THAN SIGN# + {8811, ">>"}, // MA#* ( ≫ → >> ) MUCH GREATER-THAN → GREATER-THAN SIGN, GREATER-THAN SIGN# + {8868, "T"}, // MA#* ( ⊤ → T ) DOWN TACK → LATIN CAPITAL LETTER T# + {8897, "v"}, // MA#* ( ⋁ → v ) N-ARY LOGICAL OR → LATIN SMALL LETTER V# →∨→ + {8899, "U"}, // MA#* ( ⋃ → U ) N-ARY UNION → LATIN CAPITAL LETTER U# →∪→→ᑌ→ + {8920, "<<<"}, // MA#* ( ⋘ → <<< ) VERY MUCH LESS-THAN → LESS-THAN SIGN, LESS-THAN SIGN, LESS-THAN SIGN# + {8921, ">>>"}, // MA#* ( ⋙ → >>> ) VERY MUCH GREATER-THAN → GREATER-THAN SIGN, GREATER-THAN SIGN, GREATER-THAN SIGN# + {8959, "E"}, // MA#* ( ⋿ → E ) Z NOTATION BAG MEMBERSHIP → LATIN CAPITAL LETTER E# + {9075, "i"}, // MA#* ( ⍳ → i ) APL FUNCTIONAL SYMBOL IOTA → LATIN SMALL LETTER I# →ι→ + {9076, "p"}, // MA#* ( ⍴ → p ) APL FUNCTIONAL SYMBOL RHO → LATIN SMALL LETTER P# →ρ→ + {9082, "a"}, // MA#* ( ⍺ → a ) APL FUNCTIONAL SYMBOL ALPHA → LATIN SMALL LETTER A# →α→ + {9213, "l"}, // MA#* ( ⏽ → l ) POWER ON SYMBOL → LATIN SMALL LETTER L# →I→ + {9290, "\\\\"}, // MA#* ( ⑊ → \\ ) OCR DOUBLE BACKSLASH → REVERSE SOLIDUS, REVERSE SOLIDUS# + {9332, "(l)"}, // MA#* ( ⑴ → (l) ) PARENTHESIZED DIGIT ONE → LEFT PARENTHESIS, LATIN SMALL LETTER L, RIGHT PARENTHESIS# →(1)→ + {9333, "(2)"}, // MA#* ( ⑵ → (2) ) PARENTHESIZED DIGIT TWO → LEFT PARENTHESIS, DIGIT TWO, RIGHT PARENTHESIS# + {9334, "(3)"}, // MA#* ( ⑶ → (3) ) PARENTHESIZED DIGIT THREE → LEFT PARENTHESIS, DIGIT THREE, RIGHT PARENTHESIS# + {9335, "(4)"}, // MA#* ( ⑷ → (4) ) PARENTHESIZED DIGIT FOUR → LEFT PARENTHESIS, DIGIT FOUR, RIGHT PARENTHESIS# + {9336, "(5)"}, // MA#* ( ⑸ → (5) ) PARENTHESIZED DIGIT FIVE → LEFT PARENTHESIS, DIGIT FIVE, RIGHT PARENTHESIS# + {9337, "(6)"}, // MA#* ( ⑹ → (6) ) PARENTHESIZED DIGIT SIX → LEFT PARENTHESIS, DIGIT SIX, RIGHT PARENTHESIS# + {9338, "(7)"}, // MA#* ( ⑺ → (7) ) PARENTHESIZED DIGIT SEVEN → LEFT PARENTHESIS, DIGIT SEVEN, RIGHT PARENTHESIS# + {9339, "(8)"}, // MA#* ( ⑻ → (8) ) PARENTHESIZED DIGIT EIGHT → LEFT PARENTHESIS, DIGIT EIGHT, RIGHT PARENTHESIS# + {9340, "(9)"}, // MA#* ( ⑼ → (9) ) PARENTHESIZED DIGIT NINE → LEFT PARENTHESIS, DIGIT NINE, RIGHT PARENTHESIS# + {9341, "(lO)"}, // MA#* ( ⑽ → (lO) ) PARENTHESIZED NUMBER TEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS# →(10)→ + {9342, "(ll)"}, // MA#* ( ⑾ → (ll) ) PARENTHESIZED NUMBER ELEVEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, LATIN SMALL LETTER L, RIGHT PARENTHESIS# →(11)→ + {9343, "(l2)"}, // MA#* ( ⑿ → (l2) ) PARENTHESIZED NUMBER TWELVE → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT TWO, RIGHT PARENTHESIS# →(12)→ + {9344, "(l3)"}, // MA#* ( ⒀ → (l3) ) PARENTHESIZED NUMBER THIRTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT THREE, RIGHT PARENTHESIS# →(13)→ + {9345, "(l4)"}, // MA#* ( ⒁ → (l4) ) PARENTHESIZED NUMBER FOURTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT FOUR, RIGHT PARENTHESIS# →(14)→ + {9346, "(l5)"}, // MA#* ( ⒂ → (l5) ) PARENTHESIZED NUMBER FIFTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT FIVE, RIGHT PARENTHESIS# →(15)→ + {9347, "(l6)"}, // MA#* ( ⒃ → (l6) ) PARENTHESIZED NUMBER SIXTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT SIX, RIGHT PARENTHESIS# →(16)→ + {9348, "(l7)"}, // MA#* ( ⒄ → (l7) ) PARENTHESIZED NUMBER SEVENTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT SEVEN, RIGHT PARENTHESIS# →(17)→ + {9349, "(l8)"}, // MA#* ( ⒅ → (l8) ) PARENTHESIZED NUMBER EIGHTEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT EIGHT, RIGHT PARENTHESIS# →(18)→ + {9350, "(l9)"}, // MA#* ( ⒆ → (l9) ) PARENTHESIZED NUMBER NINETEEN → LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT NINE, RIGHT PARENTHESIS# →(19)→ + {9351, "(2O)"}, // MA#* ( ⒇ → (2O) ) PARENTHESIZED NUMBER TWENTY → LEFT PARENTHESIS, DIGIT TWO, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS# →(20)→ + {9352, "l."}, // MA#* ( ⒈ → l. ) DIGIT ONE FULL STOP → LATIN SMALL LETTER L, FULL STOP# →1.→ + {9353, "2."}, // MA#* ( ⒉ → 2. ) DIGIT TWO FULL STOP → DIGIT TWO, FULL STOP# + {9354, "3."}, // MA#* ( ⒊ → 3. ) DIGIT THREE FULL STOP → DIGIT THREE, FULL STOP# + {9355, "4."}, // MA#* ( ⒋ → 4. ) DIGIT FOUR FULL STOP → DIGIT FOUR, FULL STOP# + {9356, "5."}, // MA#* ( ⒌ → 5. ) DIGIT FIVE FULL STOP → DIGIT FIVE, FULL STOP# + {9357, "6."}, // MA#* ( ⒍ → 6. ) DIGIT SIX FULL STOP → DIGIT SIX, FULL STOP# + {9358, "7."}, // MA#* ( ⒎ → 7. ) DIGIT SEVEN FULL STOP → DIGIT SEVEN, FULL STOP# + {9359, "8."}, // MA#* ( ⒏ → 8. ) DIGIT EIGHT FULL STOP → DIGIT EIGHT, FULL STOP# + {9360, "9."}, // MA#* ( ⒐ → 9. ) DIGIT NINE FULL STOP → DIGIT NINE, FULL STOP# + {9361, "lO."}, // MA#* ( ⒑ → lO. ) NUMBER TEN FULL STOP → LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, FULL STOP# →10.→ + {9362, "ll."}, // MA#* ( ⒒ → ll. ) NUMBER ELEVEN FULL STOP → LATIN SMALL LETTER L, LATIN SMALL LETTER L, FULL STOP# →11.→ + {9363, "l2."}, // MA#* ( ⒓ → l2. ) NUMBER TWELVE FULL STOP → LATIN SMALL LETTER L, DIGIT TWO, FULL STOP# →12.→ + {9364, "l3."}, // MA#* ( ⒔ → l3. ) NUMBER THIRTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT THREE, FULL STOP# →13.→ + {9365, "l4."}, // MA#* ( ⒕ → l4. ) NUMBER FOURTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT FOUR, FULL STOP# →14.→ + {9366, "l5."}, // MA#* ( ⒖ → l5. ) NUMBER FIFTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT FIVE, FULL STOP# →15.→ + {9367, "l6."}, // MA#* ( ⒗ → l6. ) NUMBER SIXTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT SIX, FULL STOP# →16.→ + {9368, "l7."}, // MA#* ( ⒘ → l7. ) NUMBER SEVENTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT SEVEN, FULL STOP# →17.→ + {9369, "l8."}, // MA#* ( ⒙ → l8. ) NUMBER EIGHTEEN FULL STOP → LATIN SMALL LETTER L, DIGIT EIGHT, FULL STOP# →18.→ + {9370, "l9."}, // MA#* ( ⒚ → l9. ) NUMBER NINETEEN FULL STOP → LATIN SMALL LETTER L, DIGIT NINE, FULL STOP# →19.→ + {9371, "2O."}, // MA#* ( ⒛ → 2O. ) NUMBER TWENTY FULL STOP → DIGIT TWO, LATIN CAPITAL LETTER O, FULL STOP# →20.→ + {9372, "(a)"}, // MA#* ( ⒜ → (a) ) PARENTHESIZED LATIN SMALL LETTER A → LEFT PARENTHESIS, LATIN SMALL LETTER A, RIGHT PARENTHESIS# + {9373, "(b)"}, // MA#* ( ⒝ → (b) ) PARENTHESIZED LATIN SMALL LETTER B → LEFT PARENTHESIS, LATIN SMALL LETTER B, RIGHT PARENTHESIS# + {9374, "(c)"}, // MA#* ( ⒞ → (c) ) PARENTHESIZED LATIN SMALL LETTER C → LEFT PARENTHESIS, LATIN SMALL LETTER C, RIGHT PARENTHESIS# + {9375, "(d)"}, // MA#* ( ⒟ → (d) ) PARENTHESIZED LATIN SMALL LETTER D → LEFT PARENTHESIS, LATIN SMALL LETTER D, RIGHT PARENTHESIS# + {9376, "(e)"}, // MA#* ( ⒠ → (e) ) PARENTHESIZED LATIN SMALL LETTER E → LEFT PARENTHESIS, LATIN SMALL LETTER E, RIGHT PARENTHESIS# + {9377, "(f)"}, // MA#* ( ⒡ → (f) ) PARENTHESIZED LATIN SMALL LETTER F → LEFT PARENTHESIS, LATIN SMALL LETTER F, RIGHT PARENTHESIS# + {9378, "(g)"}, // MA#* ( ⒢ → (g) ) PARENTHESIZED LATIN SMALL LETTER G → LEFT PARENTHESIS, LATIN SMALL LETTER G, RIGHT PARENTHESIS# + {9379, "(h)"}, // MA#* ( ⒣ → (h) ) PARENTHESIZED LATIN SMALL LETTER H → LEFT PARENTHESIS, LATIN SMALL LETTER H, RIGHT PARENTHESIS# + {9380, "(i)"}, // MA#* ( ⒤ → (i) ) PARENTHESIZED LATIN SMALL LETTER I → LEFT PARENTHESIS, LATIN SMALL LETTER I, RIGHT PARENTHESIS# + {9381, "(j)"}, // MA#* ( ⒥ → (j) ) PARENTHESIZED LATIN SMALL LETTER J → LEFT PARENTHESIS, LATIN SMALL LETTER J, RIGHT PARENTHESIS# + {9382, "(k)"}, // MA#* ( ⒦ → (k) ) PARENTHESIZED LATIN SMALL LETTER K → LEFT PARENTHESIS, LATIN SMALL LETTER K, RIGHT PARENTHESIS# + {9383, "(l)"}, // MA#* ( ⒧ → (l) ) PARENTHESIZED LATIN SMALL LETTER L → LEFT PARENTHESIS, LATIN SMALL LETTER L, RIGHT PARENTHESIS# + {9384, "(rn)"}, // MA#* ( ⒨ → (rn) ) PARENTHESIZED LATIN SMALL LETTER M → LEFT PARENTHESIS, LATIN SMALL LETTER R, LATIN SMALL LETTER N, RIGHT PARENTHESIS# →(m)→ + {9385, "(n)"}, // MA#* ( ⒩ → (n) ) PARENTHESIZED LATIN SMALL LETTER N → LEFT PARENTHESIS, LATIN SMALL LETTER N, RIGHT PARENTHESIS# + {9386, "(o)"}, // MA#* ( ⒪ → (o) ) PARENTHESIZED LATIN SMALL LETTER O → LEFT PARENTHESIS, LATIN SMALL LETTER O, RIGHT PARENTHESIS# + {9387, "(p)"}, // MA#* ( ⒫ → (p) ) PARENTHESIZED LATIN SMALL LETTER P → LEFT PARENTHESIS, LATIN SMALL LETTER P, RIGHT PARENTHESIS# + {9388, "(q)"}, // MA#* ( ⒬ → (q) ) PARENTHESIZED LATIN SMALL LETTER Q → LEFT PARENTHESIS, LATIN SMALL LETTER Q, RIGHT PARENTHESIS# + {9389, "(r)"}, // MA#* ( ⒭ → (r) ) PARENTHESIZED LATIN SMALL LETTER R → LEFT PARENTHESIS, LATIN SMALL LETTER R, RIGHT PARENTHESIS# + {9390, "(s)"}, // MA#* ( ⒮ → (s) ) PARENTHESIZED LATIN SMALL LETTER S → LEFT PARENTHESIS, LATIN SMALL LETTER S, RIGHT PARENTHESIS# + {9391, "(t)"}, // MA#* ( ⒯ → (t) ) PARENTHESIZED LATIN SMALL LETTER T → LEFT PARENTHESIS, LATIN SMALL LETTER T, RIGHT PARENTHESIS# + {9392, "(u)"}, // MA#* ( ⒰ → (u) ) PARENTHESIZED LATIN SMALL LETTER U → LEFT PARENTHESIS, LATIN SMALL LETTER U, RIGHT PARENTHESIS# + {9393, "(v)"}, // MA#* ( ⒱ → (v) ) PARENTHESIZED LATIN SMALL LETTER V → LEFT PARENTHESIS, LATIN SMALL LETTER V, RIGHT PARENTHESIS# + {9394, "(w)"}, // MA#* ( ⒲ → (w) ) PARENTHESIZED LATIN SMALL LETTER W → LEFT PARENTHESIS, LATIN SMALL LETTER W, RIGHT PARENTHESIS# + {9395, "(x)"}, // MA#* ( ⒳ → (x) ) PARENTHESIZED LATIN SMALL LETTER X → LEFT PARENTHESIS, LATIN SMALL LETTER X, RIGHT PARENTHESIS# + {9396, "(y)"}, // MA#* ( ⒴ → (y) ) PARENTHESIZED LATIN SMALL LETTER Y → LEFT PARENTHESIS, LATIN SMALL LETTER Y, RIGHT PARENTHESIS# + {9397, "(z)"}, // MA#* ( ⒵ → (z) ) PARENTHESIZED LATIN SMALL LETTER Z → LEFT PARENTHESIS, LATIN SMALL LETTER Z, RIGHT PARENTHESIS# + {9585, "/"}, // MA#* ( ╱ → / ) BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT → SOLIDUS# + {9587, "X"}, // MA#* ( ╳ → X ) BOX DRAWINGS LIGHT DIAGONAL CROSS → LATIN CAPITAL LETTER X# + {10088, "("}, // MA#* ( ❨ → ( ) MEDIUM LEFT PARENTHESIS ORNAMENT → LEFT PARENTHESIS# + {10089, ")"}, // MA#* ( ❩ → ) ) MEDIUM RIGHT PARENTHESIS ORNAMENT → RIGHT PARENTHESIS# + {10094, "<"}, // MA#* ( ❮ → < ) HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT → LESS-THAN SIGN# →‹→ + {10095, ">"}, // MA#* ( ❯ → > ) HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT → GREATER-THAN SIGN# →›→ + {10098, "("}, // MA#* ( ❲ → ( ) LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT → LEFT PARENTHESIS# →〔→ + {10099, ")"}, // MA#* ( ❳ → ) ) LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT → RIGHT PARENTHESIS# →〕→ + {10100, "{"}, // MA#* ( ❴ → { ) MEDIUM LEFT CURLY BRACKET ORNAMENT → LEFT CURLY BRACKET# + {10101, "}"}, // MA#* ( ❵ → } ) MEDIUM RIGHT CURLY BRACKET ORNAMENT → RIGHT CURLY BRACKET# + {10133, "+"}, // MA#* ( ➕ → + ) HEAVY PLUS SIGN → PLUS SIGN# + {10134, "-"}, // MA#* ( ➖ → - ) HEAVY MINUS SIGN → HYPHEN-MINUS# →−→ + {10187, "/"}, // MA#* ( ⟋ → / ) MATHEMATICAL RISING DIAGONAL → SOLIDUS# + {10189, "\\"}, // MA#* ( ⟍ → \ ) MATHEMATICAL FALLING DIAGONAL → REVERSE SOLIDUS# + {10201, "T"}, // MA#* ( ⟙ → T ) LARGE DOWN TACK → LATIN CAPITAL LETTER T# + {10539, "x"}, // MA#* ( ⤫ → x ) RISING DIAGONAL CROSSING FALLING DIAGONAL → LATIN SMALL LETTER X# + {10540, "x"}, // MA#* ( ⤬ → x ) FALLING DIAGONAL CROSSING RISING DIAGONAL → LATIN SMALL LETTER X# + {10741, "\\"}, // MA#* ( ⧵ → \ ) REVERSE SOLIDUS OPERATOR → REVERSE SOLIDUS# + {10744, "/"}, // MA#* ( ⧸ → / ) BIG SOLIDUS → SOLIDUS# + {10745, "\\"}, // MA#* ( ⧹ → \ ) BIG REVERSE SOLIDUS → REVERSE SOLIDUS# + {10784, ">>"}, // MA#* ( ⨠ → >> ) Z NOTATION SCHEMA PIPING → GREATER-THAN SIGN, GREATER-THAN SIGN# →≫→ + {10799, "x"}, // MA#* ( ⨯ → x ) VECTOR OR CROSS PRODUCT → LATIN SMALL LETTER X# →×→ + {10868, "::="}, // MA#* ( ⩴ → ::= ) DOUBLE COLON EQUAL → COLON, COLON, EQUALS SIGN# + {10869, "=="}, // MA#* ( ⩵ → == ) TWO CONSECUTIVE EQUALS SIGNS → EQUALS SIGN, EQUALS SIGN# + {10870, "==="}, // MA#* ( ⩶ → === ) THREE CONSECUTIVE EQUALS SIGNS → EQUALS SIGN, EQUALS SIGN, EQUALS SIGN# + {10917, "><"}, // MA#* ( ⪥ → >< ) GREATER-THAN BESIDE LESS-THAN → GREATER-THAN SIGN, LESS-THAN SIGN# + {11003, "///"}, // MA#* ( ⫻ → /// ) TRIPLE SOLIDUS BINARY RELATION → SOLIDUS, SOLIDUS, SOLIDUS# + {11005, "//"}, // MA#* ( ⫽ → // ) DOUBLE SOLIDUS OPERATOR → SOLIDUS, SOLIDUS# + {11397, "r"}, // MA# ( ⲅ → r ) COPTIC SMALL LETTER GAMMA → LATIN SMALL LETTER R# →г→ + {11406, "H"}, // MA# ( Ⲏ → H ) COPTIC CAPITAL LETTER HATE → LATIN CAPITAL LETTER H# →Η→ + {11410, "l"}, // MA# ( Ⲓ → l ) COPTIC CAPITAL LETTER IAUDA → LATIN SMALL LETTER L# →Ӏ→ + {11412, "K"}, // MA# ( Ⲕ → K ) COPTIC CAPITAL LETTER KAPA → LATIN CAPITAL LETTER K# →Κ→ + {11416, "M"}, // MA# ( Ⲙ → M ) COPTIC CAPITAL LETTER MI → LATIN CAPITAL LETTER M# + {11418, "N"}, // MA# ( Ⲛ → N ) COPTIC CAPITAL LETTER NI → LATIN CAPITAL LETTER N# + {11422, "O"}, // MA# ( Ⲟ → O ) COPTIC CAPITAL LETTER O → LATIN CAPITAL LETTER O# + {11423, "o"}, // MA# ( ⲟ → o ) COPTIC SMALL LETTER O → LATIN SMALL LETTER O# + {11426, "P"}, // MA# ( Ⲣ → P ) COPTIC CAPITAL LETTER RO → LATIN CAPITAL LETTER P# + {11427, "p"}, // MA# ( ⲣ → p ) COPTIC SMALL LETTER RO → LATIN SMALL LETTER P# →ρ→ + {11428, "C"}, // MA# ( Ⲥ → C ) COPTIC CAPITAL LETTER SIMA → LATIN CAPITAL LETTER C# →Ϲ→ + {11429, "c"}, // MA# ( ⲥ → c ) COPTIC SMALL LETTER SIMA → LATIN SMALL LETTER C# →ϲ→ + {11430, "T"}, // MA# ( Ⲧ → T ) COPTIC CAPITAL LETTER TAU → LATIN CAPITAL LETTER T# + {11432, "Y"}, // MA# ( Ⲩ → Y ) COPTIC CAPITAL LETTER UA → LATIN CAPITAL LETTER Y# + {11436, "X"}, // MA# ( Ⲭ → X ) COPTIC CAPITAL LETTER KHI → LATIN CAPITAL LETTER X# →Х→ + {11450, "-"}, // MA# ( Ⲻ → - ) COPTIC CAPITAL LETTER DIALECT-P NI → HYPHEN-MINUS# →‒→ + {11462, "/"}, // MA# ( Ⳇ → / ) COPTIC CAPITAL LETTER OLD COPTIC ESH → SOLIDUS# + {11466, "9"}, // MA# ( Ⳋ → 9 ) COPTIC CAPITAL LETTER DIALECT-P HORI → DIGIT NINE# + {11468, "3"}, // MA# ( Ⳍ → 3 ) COPTIC CAPITAL LETTER OLD COPTIC HORI → DIGIT THREE# →Ȝ→→Ʒ→ + {11472, "L"}, // MA# ( Ⳑ → L ) COPTIC CAPITAL LETTER L-SHAPED HA → LATIN CAPITAL LETTER L# + {11474, "6"}, // MA# ( Ⳓ → 6 ) COPTIC CAPITAL LETTER OLD COPTIC HEI → DIGIT SIX# + {11513, "\\\\"}, // MA#* ( ⳹ → \\ ) COPTIC OLD NUBIAN FULL STOP → REVERSE SOLIDUS, REVERSE SOLIDUS# + {11576, "V"}, // MA# ( ⴸ → V ) TIFINAGH LETTER YADH → LATIN CAPITAL LETTER V# + {11577, "E"}, // MA# ( ⴹ → E ) TIFINAGH LETTER YADD → LATIN CAPITAL LETTER E# + {11599, "l"}, // MA# ( ⵏ → l ) TIFINAGH LETTER YAN → LATIN SMALL LETTER L# →Ӏ→ + {11601, "!"}, // MA# ( ⵑ → ! ) TIFINAGH LETTER TUAREG YANG → EXCLAMATION MARK# + {11604, "O"}, // MA# ( ⵔ → O ) TIFINAGH LETTER YAR → LATIN CAPITAL LETTER O# + {11605, "Q"}, // MA# ( ⵕ → Q ) TIFINAGH LETTER YARR → LATIN CAPITAL LETTER Q# + {11613, "X"}, // MA# ( ⵝ → X ) TIFINAGH LETTER YATH → LATIN CAPITAL LETTER X# + {11816, "(("}, // MA#* ( ⸨ → (( ) LEFT DOUBLE PARENTHESIS → LEFT PARENTHESIS, LEFT PARENTHESIS# + {11817, "))"}, // MA#* ( ⸩ → )) ) RIGHT DOUBLE PARENTHESIS → RIGHT PARENTHESIS, RIGHT PARENTHESIS# + {11840, "="}, // MA#* ( ⹀ → = ) DOUBLE HYPHEN → EQUALS SIGN# + {12034, "\\"}, // MA#* ( ⼂ → \ ) KANGXI RADICAL DOT → REVERSE SOLIDUS# + {12035, "/"}, // MA#* ( ⼃ → / ) KANGXI RADICAL SLASH → SOLIDUS# + {12291, "\""}, // MA#* ( 〃 → '' ) DITTO MARK → APOSTROPHE, APOSTROPHE# →″→→"→# Converted to a quote. + {12295, "O"}, // MA# ( 〇 → O ) IDEOGRAPHIC NUMBER ZERO → LATIN CAPITAL LETTER O# + {12308, "("}, // MA#* ( 〔 → ( ) LEFT TORTOISE SHELL BRACKET → LEFT PARENTHESIS# + {12309, ")"}, // MA#* ( 〕 → ) ) RIGHT TORTOISE SHELL BRACKET → RIGHT PARENTHESIS# + {12339, "/"}, // MA# ( 〳 → / ) VERTICAL KANA REPEAT MARK UPPER HALF → SOLIDUS# + {12448, "="}, // MA#* ( ゠ → = ) KATAKANA-HIRAGANA DOUBLE HYPHEN → EQUALS SIGN# + {12494, "/"}, // MA# ( ノ → / ) KATAKANA LETTER NO → SOLIDUS# →⼃→ + {12755, "/"}, // MA#* ( ㇓ → / ) CJK STROKE SP → SOLIDUS# →⼃→ + {12756, "\\"}, // MA#* ( ㇔ → \ ) CJK STROKE D → REVERSE SOLIDUS# →⼂→ + {20022, "\\"}, // MA# ( 丶 → \ ) CJK UNIFIED IDEOGRAPH-4E36 → REVERSE SOLIDUS# →⼂→ + {20031, "/"}, // MA# ( 丿 → / ) CJK UNIFIED IDEOGRAPH-4E3F → SOLIDUS# →⼃→ + {42192, "B"}, // MA# ( ꓐ → B ) LISU LETTER BA → LATIN CAPITAL LETTER B# + {42193, "P"}, // MA# ( ꓑ → P ) LISU LETTER PA → LATIN CAPITAL LETTER P# + {42194, "d"}, // MA# ( ꓒ → d ) LISU LETTER PHA → LATIN SMALL LETTER D# + {42195, "D"}, // MA# ( ꓓ → D ) LISU LETTER DA → LATIN CAPITAL LETTER D# + {42196, "T"}, // MA# ( ꓔ → T ) LISU LETTER TA → LATIN CAPITAL LETTER T# + {42198, "G"}, // MA# ( ꓖ → G ) LISU LETTER GA → LATIN CAPITAL LETTER G# + {42199, "K"}, // MA# ( ꓗ → K ) LISU LETTER KA → LATIN CAPITAL LETTER K# + {42201, "J"}, // MA# ( ꓙ → J ) LISU LETTER JA → LATIN CAPITAL LETTER J# + {42202, "C"}, // MA# ( ꓚ → C ) LISU LETTER CA → LATIN CAPITAL LETTER C# + {42204, "Z"}, // MA# ( ꓜ → Z ) LISU LETTER DZA → LATIN CAPITAL LETTER Z# + {42205, "F"}, // MA# ( ꓝ → F ) LISU LETTER TSA → LATIN CAPITAL LETTER F# + {42207, "M"}, // MA# ( ꓟ → M ) LISU LETTER MA → LATIN CAPITAL LETTER M# + {42208, "N"}, // MA# ( ꓠ → N ) LISU LETTER NA → LATIN CAPITAL LETTER N# + {42209, "L"}, // MA# ( ꓡ → L ) LISU LETTER LA → LATIN CAPITAL LETTER L# + {42210, "S"}, // MA# ( ꓢ → S ) LISU LETTER SA → LATIN CAPITAL LETTER S# + {42211, "R"}, // MA# ( ꓣ → R ) LISU LETTER ZHA → LATIN CAPITAL LETTER R# + {42214, "V"}, // MA# ( ꓦ → V ) LISU LETTER HA → LATIN CAPITAL LETTER V# + {42215, "H"}, // MA# ( ꓧ → H ) LISU LETTER XA → LATIN CAPITAL LETTER H# + {42218, "W"}, // MA# ( ꓪ → W ) LISU LETTER WA → LATIN CAPITAL LETTER W# + {42219, "X"}, // MA# ( ꓫ → X ) LISU LETTER SHA → LATIN CAPITAL LETTER X# + {42220, "Y"}, // MA# ( ꓬ → Y ) LISU LETTER YA → LATIN CAPITAL LETTER Y# + {42222, "A"}, // MA# ( ꓮ → A ) LISU LETTER A → LATIN CAPITAL LETTER A# + {42224, "E"}, // MA# ( ꓰ → E ) LISU LETTER E → LATIN CAPITAL LETTER E# + {42226, "l"}, // MA# ( ꓲ → l ) LISU LETTER I → LATIN SMALL LETTER L# →I→ + {42227, "O"}, // MA# ( ꓳ → O ) LISU LETTER O → LATIN CAPITAL LETTER O# + {42228, "U"}, // MA# ( ꓴ → U ) LISU LETTER U → LATIN CAPITAL LETTER U# + {42232, "."}, // MA# ( ꓸ → . ) LISU LETTER TONE MYA TI → FULL STOP# + {42233, ","}, // MA# ( ꓹ → , ) LISU LETTER TONE NA PO → COMMA# + {42234, ".."}, // MA# ( ꓺ → .. ) LISU LETTER TONE MYA CYA → FULL STOP, FULL STOP# + {42235, ".,"}, // MA# ( ꓻ → ., ) LISU LETTER TONE MYA BO → FULL STOP, COMMA# + {42237, ":"}, // MA# ( ꓽ → : ) LISU LETTER TONE MYA JEU → COLON# + {42238, "-."}, // MA#* ( ꓾ → -. ) LISU PUNCTUATION COMMA → HYPHEN-MINUS, FULL STOP# + {42239, "="}, // MA#* ( ꓿ → = ) LISU PUNCTUATION FULL STOP → EQUALS SIGN# + {42510, "."}, // MA#* ( ꘎ → . ) VAI FULL STOP → FULL STOP# + {42564, "2"}, // MA# ( Ꙅ → 2 ) CYRILLIC CAPITAL LETTER REVERSED DZE → DIGIT TWO# →Ƨ→ + {42567, "i"}, // MA# ( ꙇ → i ) CYRILLIC SMALL LETTER IOTA → LATIN SMALL LETTER I# →ι→ + {42648, "OO"}, // MA# ( Ꚙ → OO ) CYRILLIC CAPITAL LETTER DOUBLE O → LATIN CAPITAL LETTER O, LATIN CAPITAL LETTER O# + {42649, "oo"}, // MA# ( ꚙ → oo ) CYRILLIC SMALL LETTER DOUBLE O → LATIN SMALL LETTER O, LATIN SMALL LETTER O# + {42719, "V"}, // MA# ( ꛟ → V ) BAMUM LETTER KO → LATIN CAPITAL LETTER V# + {42731, "?"}, // MA# ( ꛫ → ? ) BAMUM LETTER NTUU → QUESTION MARK# →ʔ→ + {42735, "2"}, // MA# ( ꛯ → 2 ) BAMUM LETTER KOGHOM → DIGIT TWO# →Ƨ→ + {42792, "T3"}, // MA# ( Ꜩ → T3 ) LATIN CAPITAL LETTER TZ → LATIN CAPITAL LETTER T, DIGIT THREE# →TƷ→ + {42801, "s"}, // MA# ( ꜱ → s ) LATIN LETTER SMALL CAPITAL S → LATIN SMALL LETTER S# + {42802, "AA"}, // MA# ( Ꜳ → AA ) LATIN CAPITAL LETTER AA → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER A# + {42803, "aa"}, // MA# ( ꜳ → aa ) LATIN SMALL LETTER AA → LATIN SMALL LETTER A, LATIN SMALL LETTER A# + {42804, "AO"}, // MA# ( Ꜵ → AO ) LATIN CAPITAL LETTER AO → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER O# + {42805, "ao"}, // MA# ( ꜵ → ao ) LATIN SMALL LETTER AO → LATIN SMALL LETTER A, LATIN SMALL LETTER O# + {42806, "AU"}, // MA# ( Ꜷ → AU ) LATIN CAPITAL LETTER AU → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER U# + {42807, "au"}, // MA# ( ꜷ → au ) LATIN SMALL LETTER AU → LATIN SMALL LETTER A, LATIN SMALL LETTER U# + {42808, "AV"}, // MA# ( Ꜹ → AV ) LATIN CAPITAL LETTER AV → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER V# + {42809, "av"}, // MA# ( ꜹ → av ) LATIN SMALL LETTER AV → LATIN SMALL LETTER A, LATIN SMALL LETTER V# + {42810, "AV"}, // MA# ( Ꜻ → AV ) LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER V# + {42811, "av"}, // MA# ( ꜻ → av ) LATIN SMALL LETTER AV WITH HORIZONTAL BAR → LATIN SMALL LETTER A, LATIN SMALL LETTER V# + {42812, "AY"}, // MA# ( Ꜽ → AY ) LATIN CAPITAL LETTER AY → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER Y# + {42813, "ay"}, // MA# ( ꜽ → ay ) LATIN SMALL LETTER AY → LATIN SMALL LETTER A, LATIN SMALL LETTER Y# + {42830, "OO"}, // MA# ( Ꝏ → OO ) LATIN CAPITAL LETTER OO → LATIN CAPITAL LETTER O, LATIN CAPITAL LETTER O# + {42831, "oo"}, // MA# ( ꝏ → oo ) LATIN SMALL LETTER OO → LATIN SMALL LETTER O, LATIN SMALL LETTER O# + {42842, "2"}, // MA# ( Ꝛ → 2 ) LATIN CAPITAL LETTER R ROTUNDA → DIGIT TWO# + {42858, "3"}, // MA# ( Ꝫ → 3 ) LATIN CAPITAL LETTER ET → DIGIT THREE# + {42862, "9"}, // MA# ( Ꝯ → 9 ) LATIN CAPITAL LETTER CON → DIGIT NINE# + {42871, "tf"}, // MA# ( ꝷ → tf ) LATIN SMALL LETTER TUM → LATIN SMALL LETTER T, LATIN SMALL LETTER F# + {42872, "&"}, // MA# ( ꝸ → & ) LATIN SMALL LETTER UM → AMPERSAND# + {42889, ":"}, // MA#* ( ꞉ → : ) MODIFIER LETTER COLON → COLON# + {42892, "'"}, // MA# ( ꞌ → ' ) LATIN SMALL LETTER SALTILLO → APOSTROPHE# + {42904, "F"}, // MA# ( Ꞙ → F ) LATIN CAPITAL LETTER F WITH STROKE → LATIN CAPITAL LETTER F# + {42905, "f"}, // MA# ( ꞙ → f ) LATIN SMALL LETTER F WITH STROKE → LATIN SMALL LETTER F# + {42911, "u"}, // MA# ( ꞟ → u ) LATIN SMALL LETTER VOLAPUK UE → LATIN SMALL LETTER U# + {42923, "3"}, // MA# ( Ɜ → 3 ) LATIN CAPITAL LETTER REVERSED OPEN E → DIGIT THREE# + {42930, "J"}, // MA# ( Ʝ → J ) LATIN CAPITAL LETTER J WITH CROSSED-TAIL → LATIN CAPITAL LETTER J# + {42931, "X"}, // MA# ( Ꭓ → X ) LATIN CAPITAL LETTER CHI → LATIN CAPITAL LETTER X# + {42932, "B"}, // MA# ( Ꞵ → B ) LATIN CAPITAL LETTER BETA → LATIN CAPITAL LETTER B# + {43826, "e"}, // MA# ( ꬲ → e ) LATIN SMALL LETTER BLACKLETTER E → LATIN SMALL LETTER E# + {43829, "f"}, // MA# ( ꬵ → f ) LATIN SMALL LETTER LENIS F → LATIN SMALL LETTER F# + {43837, "o"}, // MA# ( ꬽ → o ) LATIN SMALL LETTER BLACKLETTER O → LATIN SMALL LETTER O# + {43847, "r"}, // MA# ( ꭇ → r ) LATIN SMALL LETTER R WITHOUT HANDLE → LATIN SMALL LETTER R# + {43848, "r"}, // MA# ( ꭈ → r ) LATIN SMALL LETTER DOUBLE R → LATIN SMALL LETTER R# + {43854, "u"}, // MA# ( ꭎ → u ) LATIN SMALL LETTER U WITH SHORT RIGHT LEG → LATIN SMALL LETTER U# + {43858, "u"}, // MA# ( ꭒ → u ) LATIN SMALL LETTER U WITH LEFT HOOK → LATIN SMALL LETTER U# + {43866, "y"}, // MA# ( ꭚ → y ) LATIN SMALL LETTER Y WITH SHORT RIGHT LEG → LATIN SMALL LETTER Y# + {43875, "uo"}, // MA# ( ꭣ → uo ) LATIN SMALL LETTER UO → LATIN SMALL LETTER U, LATIN SMALL LETTER O# + {43893, "i"}, // MA# ( ꭵ → i ) CHEROKEE SMALL LETTER V → LATIN SMALL LETTER I# + {43905, "r"}, // MA# ( ꮁ → r ) CHEROKEE SMALL LETTER HU → LATIN SMALL LETTER R# →ᴦ→→г→ + {43907, "w"}, // MA# ( ꮃ → w ) CHEROKEE SMALL LETTER LA → LATIN SMALL LETTER W# →ᴡ→ + {43923, "z"}, // MA# ( ꮓ → z ) CHEROKEE SMALL LETTER NO → LATIN SMALL LETTER Z# →ᴢ→ + {43945, "v"}, // MA# ( ꮩ → v ) CHEROKEE SMALL LETTER DO → LATIN SMALL LETTER V# →ᴠ→ + {43946, "s"}, // MA# ( ꮪ → s ) CHEROKEE SMALL LETTER DU → LATIN SMALL LETTER S# →ꜱ→ + {43951, "c"}, // MA# ( ꮯ → c ) CHEROKEE SMALL LETTER TLI → LATIN SMALL LETTER C# →ᴄ→ + {64256, "ff"}, // MA# ( ff → ff ) LATIN SMALL LIGATURE FF → LATIN SMALL LETTER F, LATIN SMALL LETTER F# + {64257, "fi"}, // MA# ( fi → fi ) LATIN SMALL LIGATURE FI → LATIN SMALL LETTER F, LATIN SMALL LETTER I# + {64258, "fl"}, // MA# ( fl → fl ) LATIN SMALL LIGATURE FL → LATIN SMALL LETTER F, LATIN SMALL LETTER L# + {64259, "ffi"}, // MA# ( ffi → ffi ) LATIN SMALL LIGATURE FFI → LATIN SMALL LETTER F, LATIN SMALL LETTER F, LATIN SMALL LETTER I# + {64260, "ffl"}, // MA# ( ffl → ffl ) LATIN SMALL LIGATURE FFL → LATIN SMALL LETTER F, LATIN SMALL LETTER F, LATIN SMALL LETTER L# + {64262, "st"}, // MA# ( st → st ) LATIN SMALL LIGATURE ST → LATIN SMALL LETTER S, LATIN SMALL LETTER T# + {64422, "o"}, // MA# ( ‎ﮦ‎ → o ) ARABIC LETTER HEH GOAL ISOLATED FORM → LATIN SMALL LETTER O# →‎ه‎→ + {64423, "o"}, // MA# ( ‎ﮧ‎ → o ) ARABIC LETTER HEH GOAL FINAL FORM → LATIN SMALL LETTER O# →‎ہ‎→→‎ه‎→ + {64424, "o"}, // MA# ( ‎ﮨ‎ → o ) ARABIC LETTER HEH GOAL INITIAL FORM → LATIN SMALL LETTER O# →‎ہ‎→→‎ه‎→ + {64425, "o"}, // MA# ( ‎ﮩ‎ → o ) ARABIC LETTER HEH GOAL MEDIAL FORM → LATIN SMALL LETTER O# →‎ہ‎→→‎ه‎→ + {64426, "o"}, // MA# ( ‎ﮪ‎ → o ) ARABIC LETTER HEH DOACHASHMEE ISOLATED FORM → LATIN SMALL LETTER O# →‎ه‎→ + {64427, "o"}, // MA# ( ‎ﮫ‎ → o ) ARABIC LETTER HEH DOACHASHMEE FINAL FORM → LATIN SMALL LETTER O# →‎ﻪ‎→→‎ه‎→ + {64428, "o"}, // MA# ( ‎ﮬ‎ → o ) ARABIC LETTER HEH DOACHASHMEE INITIAL FORM → LATIN SMALL LETTER O# →‎ﻫ‎→→‎ه‎→ + {64429, "o"}, // MA# ( ‎ﮭ‎ → o ) ARABIC LETTER HEH DOACHASHMEE MEDIAL FORM → LATIN SMALL LETTER O# →‎ﻬ‎→→‎ه‎→ + {64830, "("}, // MA#* ( ﴾ → ( ) ORNATE LEFT PARENTHESIS → LEFT PARENTHESIS# + {64831, ")"}, // MA#* ( ﴿ → ) ) ORNATE RIGHT PARENTHESIS → RIGHT PARENTHESIS# + {65072, ":"}, // MA#* ( ︰ → : ) PRESENTATION FORM FOR VERTICAL TWO DOT LEADER → COLON# + {65101, "_"}, // MA# ( ﹍ → _ ) DASHED LOW LINE → LOW LINE# + {65102, "_"}, // MA# ( ﹎ → _ ) CENTRELINE LOW LINE → LOW LINE# + {65103, "_"}, // MA# ( ﹏ → _ ) WAVY LOW LINE → LOW LINE# + {65112, "-"}, // MA#* ( ﹘ → - ) SMALL EM DASH → HYPHEN-MINUS# + {65128, "\\"}, // MA#* ( ﹨ → \ ) SMALL REVERSE SOLIDUS → REVERSE SOLIDUS# →∖→ + {65165, "l"}, // MA# ( ‎ﺍ‎ → l ) ARABIC LETTER ALEF ISOLATED FORM → LATIN SMALL LETTER L# →‎ا‎→→1→ + {65166, "l"}, // MA# ( ‎ﺎ‎ → l ) ARABIC LETTER ALEF FINAL FORM → LATIN SMALL LETTER L# →‎ا‎→→1→ + {65257, "o"}, // MA# ( ‎ﻩ‎ → o ) ARABIC LETTER HEH ISOLATED FORM → LATIN SMALL LETTER O# →‎ه‎→ + {65258, "o"}, // MA# ( ‎ﻪ‎ → o ) ARABIC LETTER HEH FINAL FORM → LATIN SMALL LETTER O# →‎ه‎→ + {65259, "o"}, // MA# ( ‎ﻫ‎ → o ) ARABIC LETTER HEH INITIAL FORM → LATIN SMALL LETTER O# →‎ه‎→ + {65260, "o"}, // MA# ( ‎ﻬ‎ → o ) ARABIC LETTER HEH MEDIAL FORM → LATIN SMALL LETTER O# →‎ه‎→ + {65281, "!"}, // MA#* ( ! → ! ) FULLWIDTH EXCLAMATION MARK → EXCLAMATION MARK# →ǃ→ + {65282, "\""}, // MA#* ( " → '' ) FULLWIDTH QUOTATION MARK → APOSTROPHE, APOSTROPHE# →”→→"→# Converted to a quote. + {65287, "'"}, // MA#* ( ' → ' ) FULLWIDTH APOSTROPHE → APOSTROPHE# →’→ + {65306, ":"}, // MA#* ( : → : ) FULLWIDTH COLON → COLON# →︰→ + {65313, "A"}, // MA# ( A → A ) FULLWIDTH LATIN CAPITAL LETTER A → LATIN CAPITAL LETTER A# →А→ + {65314, "B"}, // MA# ( B → B ) FULLWIDTH LATIN CAPITAL LETTER B → LATIN CAPITAL LETTER B# →Β→ + {65315, "C"}, // MA# ( C → C ) FULLWIDTH LATIN CAPITAL LETTER C → LATIN CAPITAL LETTER C# →С→ + {65317, "E"}, // MA# ( E → E ) FULLWIDTH LATIN CAPITAL LETTER E → LATIN CAPITAL LETTER E# →Ε→ + {65320, "H"}, // MA# ( H → H ) FULLWIDTH LATIN CAPITAL LETTER H → LATIN CAPITAL LETTER H# →Η→ + {65321, "l"}, // MA# ( I → l ) FULLWIDTH LATIN CAPITAL LETTER I → LATIN SMALL LETTER L# →Ӏ→ + {65322, "J"}, // MA# ( J → J ) FULLWIDTH LATIN CAPITAL LETTER J → LATIN CAPITAL LETTER J# →Ј→ + {65323, "K"}, // MA# ( K → K ) FULLWIDTH LATIN CAPITAL LETTER K → LATIN CAPITAL LETTER K# →Κ→ + {65325, "M"}, // MA# ( M → M ) FULLWIDTH LATIN CAPITAL LETTER M → LATIN CAPITAL LETTER M# →Μ→ + {65326, "N"}, // MA# ( N → N ) FULLWIDTH LATIN CAPITAL LETTER N → LATIN CAPITAL LETTER N# →Ν→ + {65327, "O"}, // MA# ( O → O ) FULLWIDTH LATIN CAPITAL LETTER O → LATIN CAPITAL LETTER O# →О→ + {65328, "P"}, // MA# ( P → P ) FULLWIDTH LATIN CAPITAL LETTER P → LATIN CAPITAL LETTER P# →Р→ + {65331, "S"}, // MA# ( S → S ) FULLWIDTH LATIN CAPITAL LETTER S → LATIN CAPITAL LETTER S# →Ѕ→ + {65332, "T"}, // MA# ( T → T ) FULLWIDTH LATIN CAPITAL LETTER T → LATIN CAPITAL LETTER T# →Т→ + {65336, "X"}, // MA# ( X → X ) FULLWIDTH LATIN CAPITAL LETTER X → LATIN CAPITAL LETTER X# →Х→ + {65337, "Y"}, // MA# ( Y → Y ) FULLWIDTH LATIN CAPITAL LETTER Y → LATIN CAPITAL LETTER Y# →Υ→ + {65338, "Z"}, // MA# ( Z → Z ) FULLWIDTH LATIN CAPITAL LETTER Z → LATIN CAPITAL LETTER Z# →Ζ→ + {65339, "("}, // MA#* ( [ → ( ) FULLWIDTH LEFT SQUARE BRACKET → LEFT PARENTHESIS# →〔→ + {65340, "\\"}, // MA#* ( \ → \ ) FULLWIDTH REVERSE SOLIDUS → REVERSE SOLIDUS# →∖→ + {65341, ")"}, // MA#* ( ] → ) ) FULLWIDTH RIGHT SQUARE BRACKET → RIGHT PARENTHESIS# →〕→ + {65344, "'"}, // MA#* ( ` → ' ) FULLWIDTH GRAVE ACCENT → APOSTROPHE# →‘→ + {65345, "a"}, // MA# ( a → a ) FULLWIDTH LATIN SMALL LETTER A → LATIN SMALL LETTER A# →а→ + {65347, "c"}, // MA# ( c → c ) FULLWIDTH LATIN SMALL LETTER C → LATIN SMALL LETTER C# →с→ + {65349, "e"}, // MA# ( e → e ) FULLWIDTH LATIN SMALL LETTER E → LATIN SMALL LETTER E# →е→ + {65351, "g"}, // MA# ( g → g ) FULLWIDTH LATIN SMALL LETTER G → LATIN SMALL LETTER G# →ɡ→ + {65352, "h"}, // MA# ( h → h ) FULLWIDTH LATIN SMALL LETTER H → LATIN SMALL LETTER H# →һ→ + {65353, "i"}, // MA# ( i → i ) FULLWIDTH LATIN SMALL LETTER I → LATIN SMALL LETTER I# →і→ + {65354, "j"}, // MA# ( j → j ) FULLWIDTH LATIN SMALL LETTER J → LATIN SMALL LETTER J# →ϳ→ + {65356, "l"}, // MA# ( l → l ) FULLWIDTH LATIN SMALL LETTER L → LATIN SMALL LETTER L# →Ⅰ→→Ӏ→ + {65359, "o"}, // MA# ( o → o ) FULLWIDTH LATIN SMALL LETTER O → LATIN SMALL LETTER O# →о→ + {65360, "p"}, // MA# ( p → p ) FULLWIDTH LATIN SMALL LETTER P → LATIN SMALL LETTER P# →р→ + {65363, "s"}, // MA# ( s → s ) FULLWIDTH LATIN SMALL LETTER S → LATIN SMALL LETTER S# →ѕ→ + {65366, "v"}, // MA# ( v → v ) FULLWIDTH LATIN SMALL LETTER V → LATIN SMALL LETTER V# →ν→ + {65368, "x"}, // MA# ( x → x ) FULLWIDTH LATIN SMALL LETTER X → LATIN SMALL LETTER X# →х→ + {65369, "y"}, // MA# ( y → y ) FULLWIDTH LATIN SMALL LETTER Y → LATIN SMALL LETTER Y# →у→ + {65512, "l"}, // MA#* ( │ → l ) HALFWIDTH FORMS LIGHT VERTICAL → LATIN SMALL LETTER L# →|→ + {66178, "B"}, // MA# ( 𐊂 → B ) LYCIAN LETTER B → LATIN CAPITAL LETTER B# + {66182, "E"}, // MA# ( 𐊆 → E ) LYCIAN LETTER I → LATIN CAPITAL LETTER E# + {66183, "F"}, // MA# ( 𐊇 → F ) LYCIAN LETTER W → LATIN CAPITAL LETTER F# + {66186, "l"}, // MA# ( 𐊊 → l ) LYCIAN LETTER J → LATIN SMALL LETTER L# →I→ + {66192, "X"}, // MA# ( 𐊐 → X ) LYCIAN LETTER MM → LATIN CAPITAL LETTER X# + {66194, "O"}, // MA# ( 𐊒 → O ) LYCIAN LETTER U → LATIN CAPITAL LETTER O# + {66197, "P"}, // MA# ( 𐊕 → P ) LYCIAN LETTER R → LATIN CAPITAL LETTER P# + {66198, "S"}, // MA# ( 𐊖 → S ) LYCIAN LETTER S → LATIN CAPITAL LETTER S# + {66199, "T"}, // MA# ( 𐊗 → T ) LYCIAN LETTER T → LATIN CAPITAL LETTER T# + {66203, "+"}, // MA# ( 𐊛 → + ) LYCIAN LETTER H → PLUS SIGN# + {66208, "A"}, // MA# ( 𐊠 → A ) CARIAN LETTER A → LATIN CAPITAL LETTER A# + {66209, "B"}, // MA# ( 𐊡 → B ) CARIAN LETTER P2 → LATIN CAPITAL LETTER B# + {66210, "C"}, // MA# ( 𐊢 → C ) CARIAN LETTER D → LATIN CAPITAL LETTER C# + {66213, "F"}, // MA# ( 𐊥 → F ) CARIAN LETTER R → LATIN CAPITAL LETTER F# + {66219, "O"}, // MA# ( 𐊫 → O ) CARIAN LETTER O → LATIN CAPITAL LETTER O# + {66224, "M"}, // MA# ( 𐊰 → M ) CARIAN LETTER S → LATIN CAPITAL LETTER M# + {66225, "T"}, // MA# ( 𐊱 → T ) CARIAN LETTER C-18 → LATIN CAPITAL LETTER T# + {66226, "Y"}, // MA# ( 𐊲 → Y ) CARIAN LETTER U → LATIN CAPITAL LETTER Y# + {66228, "X"}, // MA# ( 𐊴 → X ) CARIAN LETTER X → LATIN CAPITAL LETTER X# + {66255, "H"}, // MA# ( 𐋏 → H ) CARIAN LETTER E2 → LATIN CAPITAL LETTER H# + {66293, "Z"}, // MA#* ( 𐋵 → Z ) COPTIC EPACT NUMBER THREE HUNDRED → LATIN CAPITAL LETTER Z# + {66305, "B"}, // MA# ( 𐌁 → B ) OLD ITALIC LETTER BE → LATIN CAPITAL LETTER B# + {66306, "C"}, // MA# ( 𐌂 → C ) OLD ITALIC LETTER KE → LATIN CAPITAL LETTER C# + {66313, "l"}, // MA# ( 𐌉 → l ) OLD ITALIC LETTER I → LATIN SMALL LETTER L# →I→ + {66321, "M"}, // MA# ( 𐌑 → M ) OLD ITALIC LETTER SHE → LATIN CAPITAL LETTER M# + {66325, "T"}, // MA# ( 𐌕 → T ) OLD ITALIC LETTER TE → LATIN CAPITAL LETTER T# + {66327, "X"}, // MA# ( 𐌗 → X ) OLD ITALIC LETTER EKS → LATIN CAPITAL LETTER X# + {66330, "8"}, // MA# ( 𐌚 → 8 ) OLD ITALIC LETTER EF → DIGIT EIGHT# + {66335, "*"}, // MA# ( 𐌟 → * ) OLD ITALIC LETTER ESS → ASTERISK# + {66336, "l"}, // MA#* ( 𐌠 → l ) OLD ITALIC NUMERAL ONE → LATIN SMALL LETTER L# →𐌉→→I→ + {66338, "X"}, // MA#* ( 𐌢 → X ) OLD ITALIC NUMERAL TEN → LATIN CAPITAL LETTER X# →𐌗→ + {66564, "O"}, // MA# ( 𐐄 → O ) DESERET CAPITAL LETTER LONG O → LATIN CAPITAL LETTER O# + {66581, "C"}, // MA# ( 𐐕 → C ) DESERET CAPITAL LETTER CHEE → LATIN CAPITAL LETTER C# + {66587, "L"}, // MA# ( 𐐛 → L ) DESERET CAPITAL LETTER ETH → LATIN CAPITAL LETTER L# + {66592, "S"}, // MA# ( 𐐠 → S ) DESERET CAPITAL LETTER ZHEE → LATIN CAPITAL LETTER S# + {66604, "o"}, // MA# ( 𐐬 → o ) DESERET SMALL LETTER LONG O → LATIN SMALL LETTER O# + {66621, "c"}, // MA# ( 𐐽 → c ) DESERET SMALL LETTER CHEE → LATIN SMALL LETTER C# + {66632, "s"}, // MA# ( 𐑈 → s ) DESERET SMALL LETTER ZHEE → LATIN SMALL LETTER S# + {66740, "R"}, // MA# ( 𐒴 → R ) OSAGE CAPITAL LETTER BRA → LATIN CAPITAL LETTER R# →Ʀ→ + {66754, "O"}, // MA# ( 𐓂 → O ) OSAGE CAPITAL LETTER O → LATIN CAPITAL LETTER O# + {66766, "U"}, // MA# ( 𐓎 → U ) OSAGE CAPITAL LETTER U → LATIN CAPITAL LETTER U# + {66770, "7"}, // MA# ( 𐓒 → 7 ) OSAGE CAPITAL LETTER ZA → DIGIT SEVEN# + {66794, "o"}, // MA# ( 𐓪 → o ) OSAGE SMALL LETTER O → LATIN SMALL LETTER O# + {66806, "u"}, // MA# ( 𐓶 → u ) OSAGE SMALL LETTER U → LATIN SMALL LETTER U# →ᴜ→ + {66835, "N"}, // MA# ( 𐔓 → N ) ELBASAN LETTER NE → LATIN CAPITAL LETTER N# + {66838, "O"}, // MA# ( 𐔖 → O ) ELBASAN LETTER O → LATIN CAPITAL LETTER O# + {66840, "K"}, // MA# ( 𐔘 → K ) ELBASAN LETTER QE → LATIN CAPITAL LETTER K# + {66844, "C"}, // MA# ( 𐔜 → C ) ELBASAN LETTER SHE → LATIN CAPITAL LETTER C# + {66845, "V"}, // MA# ( 𐔝 → V ) ELBASAN LETTER TE → LATIN CAPITAL LETTER V# + {66853, "F"}, // MA# ( 𐔥 → F ) ELBASAN LETTER GHE → LATIN CAPITAL LETTER F# + {66854, "L"}, // MA# ( 𐔦 → L ) ELBASAN LETTER GHAMMA → LATIN CAPITAL LETTER L# + {66855, "X"}, // MA# ( 𐔧 → X ) ELBASAN LETTER KHE → LATIN CAPITAL LETTER X# + {68176, "."}, // MA#* ( ‎𐩐‎ → . ) KHAROSHTHI PUNCTUATION DOT → FULL STOP# + {70864, "O"}, // MA# ( 𑓐 → O ) TIRHUTA DIGIT ZERO → LATIN CAPITAL LETTER O# →০→→0→ + {71424, "rn"}, // MA# ( 𑜀 → rn ) AHOM LETTER KA → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {71430, "v"}, // MA# ( 𑜆 → v ) AHOM LETTER PA → LATIN SMALL LETTER V# + {71434, "w"}, // MA# ( 𑜊 → w ) AHOM LETTER JA → LATIN SMALL LETTER W# + {71438, "w"}, // MA# ( 𑜎 → w ) AHOM LETTER LA → LATIN SMALL LETTER W# + {71439, "w"}, // MA# ( 𑜏 → w ) AHOM LETTER SA → LATIN SMALL LETTER W# + {71840, "V"}, // MA# ( 𑢠 → V ) WARANG CITI CAPITAL LETTER NGAA → LATIN CAPITAL LETTER V# + {71842, "F"}, // MA# ( 𑢢 → F ) WARANG CITI CAPITAL LETTER WI → LATIN CAPITAL LETTER F# + {71843, "L"}, // MA# ( 𑢣 → L ) WARANG CITI CAPITAL LETTER YU → LATIN CAPITAL LETTER L# + {71844, "Y"}, // MA# ( 𑢤 → Y ) WARANG CITI CAPITAL LETTER YA → LATIN CAPITAL LETTER Y# + {71846, "E"}, // MA# ( 𑢦 → E ) WARANG CITI CAPITAL LETTER II → LATIN CAPITAL LETTER E# + {71849, "Z"}, // MA# ( 𑢩 → Z ) WARANG CITI CAPITAL LETTER O → LATIN CAPITAL LETTER Z# + {71852, "9"}, // MA# ( 𑢬 → 9 ) WARANG CITI CAPITAL LETTER KO → DIGIT NINE# + {71854, "E"}, // MA# ( 𑢮 → E ) WARANG CITI CAPITAL LETTER YUJ → LATIN CAPITAL LETTER E# + {71855, "4"}, // MA# ( 𑢯 → 4 ) WARANG CITI CAPITAL LETTER UC → DIGIT FOUR# + {71858, "L"}, // MA# ( 𑢲 → L ) WARANG CITI CAPITAL LETTER TTE → LATIN CAPITAL LETTER L# + {71861, "O"}, // MA# ( 𑢵 → O ) WARANG CITI CAPITAL LETTER AT → LATIN CAPITAL LETTER O# + {71864, "U"}, // MA# ( 𑢸 → U ) WARANG CITI CAPITAL LETTER PU → LATIN CAPITAL LETTER U# + {71867, "5"}, // MA# ( 𑢻 → 5 ) WARANG CITI CAPITAL LETTER HORR → DIGIT FIVE# + {71868, "T"}, // MA# ( 𑢼 → T ) WARANG CITI CAPITAL LETTER HAR → LATIN CAPITAL LETTER T# + {71872, "v"}, // MA# ( 𑣀 → v ) WARANG CITI SMALL LETTER NGAA → LATIN SMALL LETTER V# + {71873, "s"}, // MA# ( 𑣁 → s ) WARANG CITI SMALL LETTER A → LATIN SMALL LETTER S# + {71874, "F"}, // MA# ( 𑣂 → F ) WARANG CITI SMALL LETTER WI → LATIN CAPITAL LETTER F# + {71875, "i"}, // MA# ( 𑣃 → i ) WARANG CITI SMALL LETTER YU → LATIN SMALL LETTER I# →ι→ + {71876, "z"}, // MA# ( 𑣄 → z ) WARANG CITI SMALL LETTER YA → LATIN SMALL LETTER Z# + {71878, "7"}, // MA# ( 𑣆 → 7 ) WARANG CITI SMALL LETTER II → DIGIT SEVEN# + {71880, "o"}, // MA# ( 𑣈 → o ) WARANG CITI SMALL LETTER E → LATIN SMALL LETTER O# + {71882, "3"}, // MA# ( 𑣊 → 3 ) WARANG CITI SMALL LETTER ANG → DIGIT THREE# + {71884, "9"}, // MA# ( 𑣌 → 9 ) WARANG CITI SMALL LETTER KO → DIGIT NINE# + {71893, "6"}, // MA# ( 𑣕 → 6 ) WARANG CITI SMALL LETTER AT → DIGIT SIX# + {71894, "9"}, // MA# ( 𑣖 → 9 ) WARANG CITI SMALL LETTER AM → DIGIT NINE# + {71895, "o"}, // MA# ( 𑣗 → o ) WARANG CITI SMALL LETTER BU → LATIN SMALL LETTER O# + {71896, "u"}, // MA# ( 𑣘 → u ) WARANG CITI SMALL LETTER PU → LATIN SMALL LETTER U# →υ→→ʋ→ + {71900, "y"}, // MA# ( 𑣜 → y ) WARANG CITI SMALL LETTER HAR → LATIN SMALL LETTER Y# →ɣ→→γ→ + {71904, "O"}, // MA# ( 𑣠 → O ) WARANG CITI DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {71907, "rn"}, // MA# ( 𑣣 → rn ) WARANG CITI DIGIT THREE → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {71909, "Z"}, // MA# ( 𑣥 → Z ) WARANG CITI DIGIT FIVE → LATIN CAPITAL LETTER Z# + {71910, "W"}, // MA# ( 𑣦 → W ) WARANG CITI DIGIT SIX → LATIN CAPITAL LETTER W# + {71913, "C"}, // MA# ( 𑣩 → C ) WARANG CITI DIGIT NINE → LATIN CAPITAL LETTER C# + {71916, "X"}, // MA#* ( 𑣬 → X ) WARANG CITI NUMBER THIRTY → LATIN CAPITAL LETTER X# + {71919, "W"}, // MA#* ( 𑣯 → W ) WARANG CITI NUMBER SIXTY → LATIN CAPITAL LETTER W# + {71922, "C"}, // MA#* ( 𑣲 → C ) WARANG CITI NUMBER NINETY → LATIN CAPITAL LETTER C# + {93960, "V"}, // MA# ( 𖼈 → V ) MIAO LETTER VA → LATIN CAPITAL LETTER V# + {93962, "T"}, // MA# ( 𖼊 → T ) MIAO LETTER TA → LATIN CAPITAL LETTER T# + {93974, "L"}, // MA# ( 𖼖 → L ) MIAO LETTER LA → LATIN CAPITAL LETTER L# + {93992, "l"}, // MA# ( 𖼨 → l ) MIAO LETTER GHA → LATIN SMALL LETTER L# →I→ + {94005, "R"}, // MA# ( 𖼵 → R ) MIAO LETTER ZHA → LATIN CAPITAL LETTER R# + {94010, "S"}, // MA# ( 𖼺 → S ) MIAO LETTER SA → LATIN CAPITAL LETTER S# + {94011, "3"}, // MA# ( 𖼻 → 3 ) MIAO LETTER ZA → DIGIT THREE# →Ʒ→ + {94015, ">"}, // MA# ( 𖼿 → > ) MIAO LETTER ARCHAIC ZZA → GREATER-THAN SIGN# + {94016, "A"}, // MA# ( 𖽀 → A ) MIAO LETTER ZZYA → LATIN CAPITAL LETTER A# + {94018, "U"}, // MA# ( 𖽂 → U ) MIAO LETTER WA → LATIN CAPITAL LETTER U# + {94019, "Y"}, // MA# ( 𖽃 → Y ) MIAO LETTER AH → LATIN CAPITAL LETTER Y# + {94033, "'"}, // MA# ( 𖽑 → ' ) MIAO SIGN ASPIRATION → APOSTROPHE# →ʼ→→′→ + {94034, "'"}, // MA# ( 𖽒 → ' ) MIAO SIGN REFORMED VOICING → APOSTROPHE# →ʻ→→‘→ + {119060, "{"}, // MA#* ( 𝄔 → { ) MUSICAL SYMBOL BRACE → LEFT CURLY BRACKET# + {119149, "."}, // MA# ( 𝅭 → . ) MUSICAL SYMBOL COMBINING AUGMENTATION DOT → FULL STOP# + {119302, "3"}, // MA#* ( 𝈆 → 3 ) GREEK VOCAL NOTATION SYMBOL-7 → DIGIT THREE# + {119309, "V"}, // MA#* ( 𝈍 → V ) GREEK VOCAL NOTATION SYMBOL-14 → LATIN CAPITAL LETTER V# + {119311, "\\"}, // MA#* ( 𝈏 → \ ) GREEK VOCAL NOTATION SYMBOL-16 → REVERSE SOLIDUS# + {119314, "7"}, // MA#* ( 𝈒 → 7 ) GREEK VOCAL NOTATION SYMBOL-19 → DIGIT SEVEN# + {119315, "F"}, // MA#* ( 𝈓 → F ) GREEK VOCAL NOTATION SYMBOL-20 → LATIN CAPITAL LETTER F# →Ϝ→ + {119318, "R"}, // MA#* ( 𝈖 → R ) GREEK VOCAL NOTATION SYMBOL-23 → LATIN CAPITAL LETTER R# + {119338, "L"}, // MA#* ( 𝈪 → L ) GREEK INSTRUMENTAL NOTATION SYMBOL-23 → LATIN CAPITAL LETTER L# + {119350, "<"}, // MA#* ( 𝈶 → < ) GREEK INSTRUMENTAL NOTATION SYMBOL-40 → LESS-THAN SIGN# + {119351, ">"}, // MA#* ( 𝈷 → > ) GREEK INSTRUMENTAL NOTATION SYMBOL-42 → GREATER-THAN SIGN# + {119354, "/"}, // MA#* ( 𝈺 → / ) GREEK INSTRUMENTAL NOTATION SYMBOL-47 → SOLIDUS# + {119355, "\\"}, // MA#* ( 𝈻 → \ ) GREEK INSTRUMENTAL NOTATION SYMBOL-48 → REVERSE SOLIDUS# →𝈏→ + {119808, "A"}, // MA# ( 𝐀 → A ) MATHEMATICAL BOLD CAPITAL A → LATIN CAPITAL LETTER A# + {119809, "B"}, // MA# ( 𝐁 → B ) MATHEMATICAL BOLD CAPITAL B → LATIN CAPITAL LETTER B# + {119810, "C"}, // MA# ( 𝐂 → C ) MATHEMATICAL BOLD CAPITAL C → LATIN CAPITAL LETTER C# + {119811, "D"}, // MA# ( 𝐃 → D ) MATHEMATICAL BOLD CAPITAL D → LATIN CAPITAL LETTER D# + {119812, "E"}, // MA# ( 𝐄 → E ) MATHEMATICAL BOLD CAPITAL E → LATIN CAPITAL LETTER E# + {119813, "F"}, // MA# ( 𝐅 → F ) MATHEMATICAL BOLD CAPITAL F → LATIN CAPITAL LETTER F# + {119814, "G"}, // MA# ( 𝐆 → G ) MATHEMATICAL BOLD CAPITAL G → LATIN CAPITAL LETTER G# + {119815, "H"}, // MA# ( 𝐇 → H ) MATHEMATICAL BOLD CAPITAL H → LATIN CAPITAL LETTER H# + {119816, "l"}, // MA# ( 𝐈 → l ) MATHEMATICAL BOLD CAPITAL I → LATIN SMALL LETTER L# →I→ + {119817, "J"}, // MA# ( 𝐉 → J ) MATHEMATICAL BOLD CAPITAL J → LATIN CAPITAL LETTER J# + {119818, "K"}, // MA# ( 𝐊 → K ) MATHEMATICAL BOLD CAPITAL K → LATIN CAPITAL LETTER K# + {119819, "L"}, // MA# ( 𝐋 → L ) MATHEMATICAL BOLD CAPITAL L → LATIN CAPITAL LETTER L# + {119820, "M"}, // MA# ( 𝐌 → M ) MATHEMATICAL BOLD CAPITAL M → LATIN CAPITAL LETTER M# + {119821, "N"}, // MA# ( 𝐍 → N ) MATHEMATICAL BOLD CAPITAL N → LATIN CAPITAL LETTER N# + {119822, "O"}, // MA# ( 𝐎 → O ) MATHEMATICAL BOLD CAPITAL O → LATIN CAPITAL LETTER O# + {119823, "P"}, // MA# ( 𝐏 → P ) MATHEMATICAL BOLD CAPITAL P → LATIN CAPITAL LETTER P# + {119824, "Q"}, // MA# ( 𝐐 → Q ) MATHEMATICAL BOLD CAPITAL Q → LATIN CAPITAL LETTER Q# + {119825, "R"}, // MA# ( 𝐑 → R ) MATHEMATICAL BOLD CAPITAL R → LATIN CAPITAL LETTER R# + {119826, "S"}, // MA# ( 𝐒 → S ) MATHEMATICAL BOLD CAPITAL S → LATIN CAPITAL LETTER S# + {119827, "T"}, // MA# ( 𝐓 → T ) MATHEMATICAL BOLD CAPITAL T → LATIN CAPITAL LETTER T# + {119828, "U"}, // MA# ( 𝐔 → U ) MATHEMATICAL BOLD CAPITAL U → LATIN CAPITAL LETTER U# + {119829, "V"}, // MA# ( 𝐕 → V ) MATHEMATICAL BOLD CAPITAL V → LATIN CAPITAL LETTER V# + {119830, "W"}, // MA# ( 𝐖 → W ) MATHEMATICAL BOLD CAPITAL W → LATIN CAPITAL LETTER W# + {119831, "X"}, // MA# ( 𝐗 → X ) MATHEMATICAL BOLD CAPITAL X → LATIN CAPITAL LETTER X# + {119832, "Y"}, // MA# ( 𝐘 → Y ) MATHEMATICAL BOLD CAPITAL Y → LATIN CAPITAL LETTER Y# + {119833, "Z"}, // MA# ( 𝐙 → Z ) MATHEMATICAL BOLD CAPITAL Z → LATIN CAPITAL LETTER Z# + {119834, "a"}, // MA# ( 𝐚 → a ) MATHEMATICAL BOLD SMALL A → LATIN SMALL LETTER A# + {119835, "b"}, // MA# ( 𝐛 → b ) MATHEMATICAL BOLD SMALL B → LATIN SMALL LETTER B# + {119836, "c"}, // MA# ( 𝐜 → c ) MATHEMATICAL BOLD SMALL C → LATIN SMALL LETTER C# + {119837, "d"}, // MA# ( 𝐝 → d ) MATHEMATICAL BOLD SMALL D → LATIN SMALL LETTER D# + {119838, "e"}, // MA# ( 𝐞 → e ) MATHEMATICAL BOLD SMALL E → LATIN SMALL LETTER E# + {119839, "f"}, // MA# ( 𝐟 → f ) MATHEMATICAL BOLD SMALL F → LATIN SMALL LETTER F# + {119840, "g"}, // MA# ( 𝐠 → g ) MATHEMATICAL BOLD SMALL G → LATIN SMALL LETTER G# + {119841, "h"}, // MA# ( 𝐡 → h ) MATHEMATICAL BOLD SMALL H → LATIN SMALL LETTER H# + {119842, "i"}, // MA# ( 𝐢 → i ) MATHEMATICAL BOLD SMALL I → LATIN SMALL LETTER I# + {119843, "j"}, // MA# ( 𝐣 → j ) MATHEMATICAL BOLD SMALL J → LATIN SMALL LETTER J# + {119844, "k"}, // MA# ( 𝐤 → k ) MATHEMATICAL BOLD SMALL K → LATIN SMALL LETTER K# + {119845, "l"}, // MA# ( 𝐥 → l ) MATHEMATICAL BOLD SMALL L → LATIN SMALL LETTER L# + {119846, "rn"}, // MA# ( 𝐦 → rn ) MATHEMATICAL BOLD SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {119847, "n"}, // MA# ( 𝐧 → n ) MATHEMATICAL BOLD SMALL N → LATIN SMALL LETTER N# + {119848, "o"}, // MA# ( 𝐨 → o ) MATHEMATICAL BOLD SMALL O → LATIN SMALL LETTER O# + {119849, "p"}, // MA# ( 𝐩 → p ) MATHEMATICAL BOLD SMALL P → LATIN SMALL LETTER P# + {119850, "q"}, // MA# ( 𝐪 → q ) MATHEMATICAL BOLD SMALL Q → LATIN SMALL LETTER Q# + {119851, "r"}, // MA# ( 𝐫 → r ) MATHEMATICAL BOLD SMALL R → LATIN SMALL LETTER R# + {119852, "s"}, // MA# ( 𝐬 → s ) MATHEMATICAL BOLD SMALL S → LATIN SMALL LETTER S# + {119853, "t"}, // MA# ( 𝐭 → t ) MATHEMATICAL BOLD SMALL T → LATIN SMALL LETTER T# + {119854, "u"}, // MA# ( 𝐮 → u ) MATHEMATICAL BOLD SMALL U → LATIN SMALL LETTER U# + {119855, "v"}, // MA# ( 𝐯 → v ) MATHEMATICAL BOLD SMALL V → LATIN SMALL LETTER V# + {119856, "w"}, // MA# ( 𝐰 → w ) MATHEMATICAL BOLD SMALL W → LATIN SMALL LETTER W# + {119857, "x"}, // MA# ( 𝐱 → x ) MATHEMATICAL BOLD SMALL X → LATIN SMALL LETTER X# + {119858, "y"}, // MA# ( 𝐲 → y ) MATHEMATICAL BOLD SMALL Y → LATIN SMALL LETTER Y# + {119859, "z"}, // MA# ( 𝐳 → z ) MATHEMATICAL BOLD SMALL Z → LATIN SMALL LETTER Z# + {119860, "A"}, // MA# ( 𝐴 → A ) MATHEMATICAL ITALIC CAPITAL A → LATIN CAPITAL LETTER A# + {119861, "B"}, // MA# ( 𝐵 → B ) MATHEMATICAL ITALIC CAPITAL B → LATIN CAPITAL LETTER B# + {119862, "C"}, // MA# ( 𝐶 → C ) MATHEMATICAL ITALIC CAPITAL C → LATIN CAPITAL LETTER C# + {119863, "D"}, // MA# ( 𝐷 → D ) MATHEMATICAL ITALIC CAPITAL D → LATIN CAPITAL LETTER D# + {119864, "E"}, // MA# ( 𝐸 → E ) MATHEMATICAL ITALIC CAPITAL E → LATIN CAPITAL LETTER E# + {119865, "F"}, // MA# ( 𝐹 → F ) MATHEMATICAL ITALIC CAPITAL F → LATIN CAPITAL LETTER F# + {119866, "G"}, // MA# ( 𝐺 → G ) MATHEMATICAL ITALIC CAPITAL G → LATIN CAPITAL LETTER G# + {119867, "H"}, // MA# ( 𝐻 → H ) MATHEMATICAL ITALIC CAPITAL H → LATIN CAPITAL LETTER H# + {119868, "l"}, // MA# ( 𝐼 → l ) MATHEMATICAL ITALIC CAPITAL I → LATIN SMALL LETTER L# →I→ + {119869, "J"}, // MA# ( 𝐽 → J ) MATHEMATICAL ITALIC CAPITAL J → LATIN CAPITAL LETTER J# + {119870, "K"}, // MA# ( 𝐾 → K ) MATHEMATICAL ITALIC CAPITAL K → LATIN CAPITAL LETTER K# + {119871, "L"}, // MA# ( 𝐿 → L ) MATHEMATICAL ITALIC CAPITAL L → LATIN CAPITAL LETTER L# + {119872, "M"}, // MA# ( 𝑀 → M ) MATHEMATICAL ITALIC CAPITAL M → LATIN CAPITAL LETTER M# + {119873, "N"}, // MA# ( 𝑁 → N ) MATHEMATICAL ITALIC CAPITAL N → LATIN CAPITAL LETTER N# + {119874, "O"}, // MA# ( 𝑂 → O ) MATHEMATICAL ITALIC CAPITAL O → LATIN CAPITAL LETTER O# + {119875, "P"}, // MA# ( 𝑃 → P ) MATHEMATICAL ITALIC CAPITAL P → LATIN CAPITAL LETTER P# + {119876, "Q"}, // MA# ( 𝑄 → Q ) MATHEMATICAL ITALIC CAPITAL Q → LATIN CAPITAL LETTER Q# + {119877, "R"}, // MA# ( 𝑅 → R ) MATHEMATICAL ITALIC CAPITAL R → LATIN CAPITAL LETTER R# + {119878, "S"}, // MA# ( 𝑆 → S ) MATHEMATICAL ITALIC CAPITAL S → LATIN CAPITAL LETTER S# + {119879, "T"}, // MA# ( 𝑇 → T ) MATHEMATICAL ITALIC CAPITAL T → LATIN CAPITAL LETTER T# + {119880, "U"}, // MA# ( 𝑈 → U ) MATHEMATICAL ITALIC CAPITAL U → LATIN CAPITAL LETTER U# + {119881, "V"}, // MA# ( 𝑉 → V ) MATHEMATICAL ITALIC CAPITAL V → LATIN CAPITAL LETTER V# + {119882, "W"}, // MA# ( 𝑊 → W ) MATHEMATICAL ITALIC CAPITAL W → LATIN CAPITAL LETTER W# + {119883, "X"}, // MA# ( 𝑋 → X ) MATHEMATICAL ITALIC CAPITAL X → LATIN CAPITAL LETTER X# + {119884, "Y"}, // MA# ( 𝑌 → Y ) MATHEMATICAL ITALIC CAPITAL Y → LATIN CAPITAL LETTER Y# + {119885, "Z"}, // MA# ( 𝑍 → Z ) MATHEMATICAL ITALIC CAPITAL Z → LATIN CAPITAL LETTER Z# + {119886, "a"}, // MA# ( 𝑎 → a ) MATHEMATICAL ITALIC SMALL A → LATIN SMALL LETTER A# + {119887, "b"}, // MA# ( 𝑏 → b ) MATHEMATICAL ITALIC SMALL B → LATIN SMALL LETTER B# + {119888, "c"}, // MA# ( 𝑐 → c ) MATHEMATICAL ITALIC SMALL C → LATIN SMALL LETTER C# + {119889, "d"}, // MA# ( 𝑑 → d ) MATHEMATICAL ITALIC SMALL D → LATIN SMALL LETTER D# + {119890, "e"}, // MA# ( 𝑒 → e ) MATHEMATICAL ITALIC SMALL E → LATIN SMALL LETTER E# + {119891, "f"}, // MA# ( 𝑓 → f ) MATHEMATICAL ITALIC SMALL F → LATIN SMALL LETTER F# + {119892, "g"}, // MA# ( 𝑔 → g ) MATHEMATICAL ITALIC SMALL G → LATIN SMALL LETTER G# + {119894, "i"}, // MA# ( 𝑖 → i ) MATHEMATICAL ITALIC SMALL I → LATIN SMALL LETTER I# + {119895, "j"}, // MA# ( 𝑗 → j ) MATHEMATICAL ITALIC SMALL J → LATIN SMALL LETTER J# + {119896, "k"}, // MA# ( 𝑘 → k ) MATHEMATICAL ITALIC SMALL K → LATIN SMALL LETTER K# + {119897, "l"}, // MA# ( 𝑙 → l ) MATHEMATICAL ITALIC SMALL L → LATIN SMALL LETTER L# + {119898, "rn"}, // MA# ( 𝑚 → rn ) MATHEMATICAL ITALIC SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {119899, "n"}, // MA# ( 𝑛 → n ) MATHEMATICAL ITALIC SMALL N → LATIN SMALL LETTER N# + {119900, "o"}, // MA# ( 𝑜 → o ) MATHEMATICAL ITALIC SMALL O → LATIN SMALL LETTER O# + {119901, "p"}, // MA# ( 𝑝 → p ) MATHEMATICAL ITALIC SMALL P → LATIN SMALL LETTER P# + {119902, "q"}, // MA# ( 𝑞 → q ) MATHEMATICAL ITALIC SMALL Q → LATIN SMALL LETTER Q# + {119903, "r"}, // MA# ( 𝑟 → r ) MATHEMATICAL ITALIC SMALL R → LATIN SMALL LETTER R# + {119904, "s"}, // MA# ( 𝑠 → s ) MATHEMATICAL ITALIC SMALL S → LATIN SMALL LETTER S# + {119905, "t"}, // MA# ( 𝑡 → t ) MATHEMATICAL ITALIC SMALL T → LATIN SMALL LETTER T# + {119906, "u"}, // MA# ( 𝑢 → u ) MATHEMATICAL ITALIC SMALL U → LATIN SMALL LETTER U# + {119907, "v"}, // MA# ( 𝑣 → v ) MATHEMATICAL ITALIC SMALL V → LATIN SMALL LETTER V# + {119908, "w"}, // MA# ( 𝑤 → w ) MATHEMATICAL ITALIC SMALL W → LATIN SMALL LETTER W# + {119909, "x"}, // MA# ( 𝑥 → x ) MATHEMATICAL ITALIC SMALL X → LATIN SMALL LETTER X# + {119910, "y"}, // MA# ( 𝑦 → y ) MATHEMATICAL ITALIC SMALL Y → LATIN SMALL LETTER Y# + {119911, "z"}, // MA# ( 𝑧 → z ) MATHEMATICAL ITALIC SMALL Z → LATIN SMALL LETTER Z# + {119912, "A"}, // MA# ( 𝑨 → A ) MATHEMATICAL BOLD ITALIC CAPITAL A → LATIN CAPITAL LETTER A# + {119913, "B"}, // MA# ( 𝑩 → B ) MATHEMATICAL BOLD ITALIC CAPITAL B → LATIN CAPITAL LETTER B# + {119914, "C"}, // MA# ( 𝑪 → C ) MATHEMATICAL BOLD ITALIC CAPITAL C → LATIN CAPITAL LETTER C# + {119915, "D"}, // MA# ( 𝑫 → D ) MATHEMATICAL BOLD ITALIC CAPITAL D → LATIN CAPITAL LETTER D# + {119916, "E"}, // MA# ( 𝑬 → E ) MATHEMATICAL BOLD ITALIC CAPITAL E → LATIN CAPITAL LETTER E# + {119917, "F"}, // MA# ( 𝑭 → F ) MATHEMATICAL BOLD ITALIC CAPITAL F → LATIN CAPITAL LETTER F# + {119918, "G"}, // MA# ( 𝑮 → G ) MATHEMATICAL BOLD ITALIC CAPITAL G → LATIN CAPITAL LETTER G# + {119919, "H"}, // MA# ( 𝑯 → H ) MATHEMATICAL BOLD ITALIC CAPITAL H → LATIN CAPITAL LETTER H# + {119920, "l"}, // MA# ( 𝑰 → l ) MATHEMATICAL BOLD ITALIC CAPITAL I → LATIN SMALL LETTER L# →I→ + {119921, "J"}, // MA# ( 𝑱 → J ) MATHEMATICAL BOLD ITALIC CAPITAL J → LATIN CAPITAL LETTER J# + {119922, "K"}, // MA# ( 𝑲 → K ) MATHEMATICAL BOLD ITALIC CAPITAL K → LATIN CAPITAL LETTER K# + {119923, "L"}, // MA# ( 𝑳 → L ) MATHEMATICAL BOLD ITALIC CAPITAL L → LATIN CAPITAL LETTER L# + {119924, "M"}, // MA# ( 𝑴 → M ) MATHEMATICAL BOLD ITALIC CAPITAL M → LATIN CAPITAL LETTER M# + {119925, "N"}, // MA# ( 𝑵 → N ) MATHEMATICAL BOLD ITALIC CAPITAL N → LATIN CAPITAL LETTER N# + {119926, "O"}, // MA# ( 𝑶 → O ) MATHEMATICAL BOLD ITALIC CAPITAL O → LATIN CAPITAL LETTER O# + {119927, "P"}, // MA# ( 𝑷 → P ) MATHEMATICAL BOLD ITALIC CAPITAL P → LATIN CAPITAL LETTER P# + {119928, "Q"}, // MA# ( 𝑸 → Q ) MATHEMATICAL BOLD ITALIC CAPITAL Q → LATIN CAPITAL LETTER Q# + {119929, "R"}, // MA# ( 𝑹 → R ) MATHEMATICAL BOLD ITALIC CAPITAL R → LATIN CAPITAL LETTER R# + {119930, "S"}, // MA# ( 𝑺 → S ) MATHEMATICAL BOLD ITALIC CAPITAL S → LATIN CAPITAL LETTER S# + {119931, "T"}, // MA# ( 𝑻 → T ) MATHEMATICAL BOLD ITALIC CAPITAL T → LATIN CAPITAL LETTER T# + {119932, "U"}, // MA# ( 𝑼 → U ) MATHEMATICAL BOLD ITALIC CAPITAL U → LATIN CAPITAL LETTER U# + {119933, "V"}, // MA# ( 𝑽 → V ) MATHEMATICAL BOLD ITALIC CAPITAL V → LATIN CAPITAL LETTER V# + {119934, "W"}, // MA# ( 𝑾 → W ) MATHEMATICAL BOLD ITALIC CAPITAL W → LATIN CAPITAL LETTER W# + {119935, "X"}, // MA# ( 𝑿 → X ) MATHEMATICAL BOLD ITALIC CAPITAL X → LATIN CAPITAL LETTER X# + {119936, "Y"}, // MA# ( 𝒀 → Y ) MATHEMATICAL BOLD ITALIC CAPITAL Y → LATIN CAPITAL LETTER Y# + {119937, "Z"}, // MA# ( 𝒁 → Z ) MATHEMATICAL BOLD ITALIC CAPITAL Z → LATIN CAPITAL LETTER Z# + {119938, "a"}, // MA# ( 𝒂 → a ) MATHEMATICAL BOLD ITALIC SMALL A → LATIN SMALL LETTER A# + {119939, "b"}, // MA# ( 𝒃 → b ) MATHEMATICAL BOLD ITALIC SMALL B → LATIN SMALL LETTER B# + {119940, "c"}, // MA# ( 𝒄 → c ) MATHEMATICAL BOLD ITALIC SMALL C → LATIN SMALL LETTER C# + {119941, "d"}, // MA# ( 𝒅 → d ) MATHEMATICAL BOLD ITALIC SMALL D → LATIN SMALL LETTER D# + {119942, "e"}, // MA# ( 𝒆 → e ) MATHEMATICAL BOLD ITALIC SMALL E → LATIN SMALL LETTER E# + {119943, "f"}, // MA# ( 𝒇 → f ) MATHEMATICAL BOLD ITALIC SMALL F → LATIN SMALL LETTER F# + {119944, "g"}, // MA# ( 𝒈 → g ) MATHEMATICAL BOLD ITALIC SMALL G → LATIN SMALL LETTER G# + {119945, "h"}, // MA# ( 𝒉 → h ) MATHEMATICAL BOLD ITALIC SMALL H → LATIN SMALL LETTER H# + {119946, "i"}, // MA# ( 𝒊 → i ) MATHEMATICAL BOLD ITALIC SMALL I → LATIN SMALL LETTER I# + {119947, "j"}, // MA# ( 𝒋 → j ) MATHEMATICAL BOLD ITALIC SMALL J → LATIN SMALL LETTER J# + {119948, "k"}, // MA# ( 𝒌 → k ) MATHEMATICAL BOLD ITALIC SMALL K → LATIN SMALL LETTER K# + {119949, "l"}, // MA# ( 𝒍 → l ) MATHEMATICAL BOLD ITALIC SMALL L → LATIN SMALL LETTER L# + {119950, "rn"}, // MA# ( 𝒎 → rn ) MATHEMATICAL BOLD ITALIC SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {119951, "n"}, // MA# ( 𝒏 → n ) MATHEMATICAL BOLD ITALIC SMALL N → LATIN SMALL LETTER N# + {119952, "o"}, // MA# ( 𝒐 → o ) MATHEMATICAL BOLD ITALIC SMALL O → LATIN SMALL LETTER O# + {119953, "p"}, // MA# ( 𝒑 → p ) MATHEMATICAL BOLD ITALIC SMALL P → LATIN SMALL LETTER P# + {119954, "q"}, // MA# ( 𝒒 → q ) MATHEMATICAL BOLD ITALIC SMALL Q → LATIN SMALL LETTER Q# + {119955, "r"}, // MA# ( 𝒓 → r ) MATHEMATICAL BOLD ITALIC SMALL R → LATIN SMALL LETTER R# + {119956, "s"}, // MA# ( 𝒔 → s ) MATHEMATICAL BOLD ITALIC SMALL S → LATIN SMALL LETTER S# + {119957, "t"}, // MA# ( 𝒕 → t ) MATHEMATICAL BOLD ITALIC SMALL T → LATIN SMALL LETTER T# + {119958, "u"}, // MA# ( 𝒖 → u ) MATHEMATICAL BOLD ITALIC SMALL U → LATIN SMALL LETTER U# + {119959, "v"}, // MA# ( 𝒗 → v ) MATHEMATICAL BOLD ITALIC SMALL V → LATIN SMALL LETTER V# + {119960, "w"}, // MA# ( 𝒘 → w ) MATHEMATICAL BOLD ITALIC SMALL W → LATIN SMALL LETTER W# + {119961, "x"}, // MA# ( 𝒙 → x ) MATHEMATICAL BOLD ITALIC SMALL X → LATIN SMALL LETTER X# + {119962, "y"}, // MA# ( 𝒚 → y ) MATHEMATICAL BOLD ITALIC SMALL Y → LATIN SMALL LETTER Y# + {119963, "z"}, // MA# ( 𝒛 → z ) MATHEMATICAL BOLD ITALIC SMALL Z → LATIN SMALL LETTER Z# + {119964, "A"}, // MA# ( 𝒜 → A ) MATHEMATICAL SCRIPT CAPITAL A → LATIN CAPITAL LETTER A# + {119966, "C"}, // MA# ( 𝒞 → C ) MATHEMATICAL SCRIPT CAPITAL C → LATIN CAPITAL LETTER C# + {119967, "D"}, // MA# ( 𝒟 → D ) MATHEMATICAL SCRIPT CAPITAL D → LATIN CAPITAL LETTER D# + {119970, "G"}, // MA# ( 𝒢 → G ) MATHEMATICAL SCRIPT CAPITAL G → LATIN CAPITAL LETTER G# + {119973, "J"}, // MA# ( 𝒥 → J ) MATHEMATICAL SCRIPT CAPITAL J → LATIN CAPITAL LETTER J# + {119974, "K"}, // MA# ( 𝒦 → K ) MATHEMATICAL SCRIPT CAPITAL K → LATIN CAPITAL LETTER K# + {119977, "N"}, // MA# ( 𝒩 → N ) MATHEMATICAL SCRIPT CAPITAL N → LATIN CAPITAL LETTER N# + {119978, "O"}, // MA# ( 𝒪 → O ) MATHEMATICAL SCRIPT CAPITAL O → LATIN CAPITAL LETTER O# + {119979, "P"}, // MA# ( 𝒫 → P ) MATHEMATICAL SCRIPT CAPITAL P → LATIN CAPITAL LETTER P# + {119980, "Q"}, // MA# ( 𝒬 → Q ) MATHEMATICAL SCRIPT CAPITAL Q → LATIN CAPITAL LETTER Q# + {119982, "S"}, // MA# ( 𝒮 → S ) MATHEMATICAL SCRIPT CAPITAL S → LATIN CAPITAL LETTER S# + {119983, "T"}, // MA# ( 𝒯 → T ) MATHEMATICAL SCRIPT CAPITAL T → LATIN CAPITAL LETTER T# + {119984, "U"}, // MA# ( 𝒰 → U ) MATHEMATICAL SCRIPT CAPITAL U → LATIN CAPITAL LETTER U# + {119985, "V"}, // MA# ( 𝒱 → V ) MATHEMATICAL SCRIPT CAPITAL V → LATIN CAPITAL LETTER V# + {119986, "W"}, // MA# ( 𝒲 → W ) MATHEMATICAL SCRIPT CAPITAL W → LATIN CAPITAL LETTER W# + {119987, "X"}, // MA# ( 𝒳 → X ) MATHEMATICAL SCRIPT CAPITAL X → LATIN CAPITAL LETTER X# + {119988, "Y"}, // MA# ( 𝒴 → Y ) MATHEMATICAL SCRIPT CAPITAL Y → LATIN CAPITAL LETTER Y# + {119989, "Z"}, // MA# ( 𝒵 → Z ) MATHEMATICAL SCRIPT CAPITAL Z → LATIN CAPITAL LETTER Z# + {119990, "a"}, // MA# ( 𝒶 → a ) MATHEMATICAL SCRIPT SMALL A → LATIN SMALL LETTER A# + {119991, "b"}, // MA# ( 𝒷 → b ) MATHEMATICAL SCRIPT SMALL B → LATIN SMALL LETTER B# + {119992, "c"}, // MA# ( 𝒸 → c ) MATHEMATICAL SCRIPT SMALL C → LATIN SMALL LETTER C# + {119993, "d"}, // MA# ( 𝒹 → d ) MATHEMATICAL SCRIPT SMALL D → LATIN SMALL LETTER D# + {119995, "f"}, // MA# ( 𝒻 → f ) MATHEMATICAL SCRIPT SMALL F → LATIN SMALL LETTER F# + {119997, "h"}, // MA# ( 𝒽 → h ) MATHEMATICAL SCRIPT SMALL H → LATIN SMALL LETTER H# + {119998, "i"}, // MA# ( 𝒾 → i ) MATHEMATICAL SCRIPT SMALL I → LATIN SMALL LETTER I# + {119999, "j"}, // MA# ( 𝒿 → j ) MATHEMATICAL SCRIPT SMALL J → LATIN SMALL LETTER J# + {120000, "k"}, // MA# ( 𝓀 → k ) MATHEMATICAL SCRIPT SMALL K → LATIN SMALL LETTER K# + {120001, "l"}, // MA# ( 𝓁 → l ) MATHEMATICAL SCRIPT SMALL L → LATIN SMALL LETTER L# + {120002, "rn"}, // MA# ( 𝓂 → rn ) MATHEMATICAL SCRIPT SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120003, "n"}, // MA# ( 𝓃 → n ) MATHEMATICAL SCRIPT SMALL N → LATIN SMALL LETTER N# + {120005, "p"}, // MA# ( 𝓅 → p ) MATHEMATICAL SCRIPT SMALL P → LATIN SMALL LETTER P# + {120006, "q"}, // MA# ( 𝓆 → q ) MATHEMATICAL SCRIPT SMALL Q → LATIN SMALL LETTER Q# + {120007, "r"}, // MA# ( 𝓇 → r ) MATHEMATICAL SCRIPT SMALL R → LATIN SMALL LETTER R# + {120008, "s"}, // MA# ( 𝓈 → s ) MATHEMATICAL SCRIPT SMALL S → LATIN SMALL LETTER S# + {120009, "t"}, // MA# ( 𝓉 → t ) MATHEMATICAL SCRIPT SMALL T → LATIN SMALL LETTER T# + {120010, "u"}, // MA# ( 𝓊 → u ) MATHEMATICAL SCRIPT SMALL U → LATIN SMALL LETTER U# + {120011, "v"}, // MA# ( 𝓋 → v ) MATHEMATICAL SCRIPT SMALL V → LATIN SMALL LETTER V# + {120012, "w"}, // MA# ( 𝓌 → w ) MATHEMATICAL SCRIPT SMALL W → LATIN SMALL LETTER W# + {120013, "x"}, // MA# ( 𝓍 → x ) MATHEMATICAL SCRIPT SMALL X → LATIN SMALL LETTER X# + {120014, "y"}, // MA# ( 𝓎 → y ) MATHEMATICAL SCRIPT SMALL Y → LATIN SMALL LETTER Y# + {120015, "z"}, // MA# ( 𝓏 → z ) MATHEMATICAL SCRIPT SMALL Z → LATIN SMALL LETTER Z# + {120016, "A"}, // MA# ( 𝓐 → A ) MATHEMATICAL BOLD SCRIPT CAPITAL A → LATIN CAPITAL LETTER A# + {120017, "B"}, // MA# ( 𝓑 → B ) MATHEMATICAL BOLD SCRIPT CAPITAL B → LATIN CAPITAL LETTER B# + {120018, "C"}, // MA# ( 𝓒 → C ) MATHEMATICAL BOLD SCRIPT CAPITAL C → LATIN CAPITAL LETTER C# + {120019, "D"}, // MA# ( 𝓓 → D ) MATHEMATICAL BOLD SCRIPT CAPITAL D → LATIN CAPITAL LETTER D# + {120020, "E"}, // MA# ( 𝓔 → E ) MATHEMATICAL BOLD SCRIPT CAPITAL E → LATIN CAPITAL LETTER E# + {120021, "F"}, // MA# ( 𝓕 → F ) MATHEMATICAL BOLD SCRIPT CAPITAL F → LATIN CAPITAL LETTER F# + {120022, "G"}, // MA# ( 𝓖 → G ) MATHEMATICAL BOLD SCRIPT CAPITAL G → LATIN CAPITAL LETTER G# + {120023, "H"}, // MA# ( 𝓗 → H ) MATHEMATICAL BOLD SCRIPT CAPITAL H → LATIN CAPITAL LETTER H# + {120024, "l"}, // MA# ( 𝓘 → l ) MATHEMATICAL BOLD SCRIPT CAPITAL I → LATIN SMALL LETTER L# →I→ + {120025, "J"}, // MA# ( 𝓙 → J ) MATHEMATICAL BOLD SCRIPT CAPITAL J → LATIN CAPITAL LETTER J# + {120026, "K"}, // MA# ( 𝓚 → K ) MATHEMATICAL BOLD SCRIPT CAPITAL K → LATIN CAPITAL LETTER K# + {120027, "L"}, // MA# ( 𝓛 → L ) MATHEMATICAL BOLD SCRIPT CAPITAL L → LATIN CAPITAL LETTER L# + {120028, "M"}, // MA# ( 𝓜 → M ) MATHEMATICAL BOLD SCRIPT CAPITAL M → LATIN CAPITAL LETTER M# + {120029, "N"}, // MA# ( 𝓝 → N ) MATHEMATICAL BOLD SCRIPT CAPITAL N → LATIN CAPITAL LETTER N# + {120030, "O"}, // MA# ( 𝓞 → O ) MATHEMATICAL BOLD SCRIPT CAPITAL O → LATIN CAPITAL LETTER O# + {120031, "P"}, // MA# ( 𝓟 → P ) MATHEMATICAL BOLD SCRIPT CAPITAL P → LATIN CAPITAL LETTER P# + {120032, "Q"}, // MA# ( 𝓠 → Q ) MATHEMATICAL BOLD SCRIPT CAPITAL Q → LATIN CAPITAL LETTER Q# + {120033, "R"}, // MA# ( 𝓡 → R ) MATHEMATICAL BOLD SCRIPT CAPITAL R → LATIN CAPITAL LETTER R# + {120034, "S"}, // MA# ( 𝓢 → S ) MATHEMATICAL BOLD SCRIPT CAPITAL S → LATIN CAPITAL LETTER S# + {120035, "T"}, // MA# ( 𝓣 → T ) MATHEMATICAL BOLD SCRIPT CAPITAL T → LATIN CAPITAL LETTER T# + {120036, "U"}, // MA# ( 𝓤 → U ) MATHEMATICAL BOLD SCRIPT CAPITAL U → LATIN CAPITAL LETTER U# + {120037, "V"}, // MA# ( 𝓥 → V ) MATHEMATICAL BOLD SCRIPT CAPITAL V → LATIN CAPITAL LETTER V# + {120038, "W"}, // MA# ( 𝓦 → W ) MATHEMATICAL BOLD SCRIPT CAPITAL W → LATIN CAPITAL LETTER W# + {120039, "X"}, // MA# ( 𝓧 → X ) MATHEMATICAL BOLD SCRIPT CAPITAL X → LATIN CAPITAL LETTER X# + {120040, "Y"}, // MA# ( 𝓨 → Y ) MATHEMATICAL BOLD SCRIPT CAPITAL Y → LATIN CAPITAL LETTER Y# + {120041, "Z"}, // MA# ( 𝓩 → Z ) MATHEMATICAL BOLD SCRIPT CAPITAL Z → LATIN CAPITAL LETTER Z# + {120042, "a"}, // MA# ( 𝓪 → a ) MATHEMATICAL BOLD SCRIPT SMALL A → LATIN SMALL LETTER A# + {120043, "b"}, // MA# ( 𝓫 → b ) MATHEMATICAL BOLD SCRIPT SMALL B → LATIN SMALL LETTER B# + {120044, "c"}, // MA# ( 𝓬 → c ) MATHEMATICAL BOLD SCRIPT SMALL C → LATIN SMALL LETTER C# + {120045, "d"}, // MA# ( 𝓭 → d ) MATHEMATICAL BOLD SCRIPT SMALL D → LATIN SMALL LETTER D# + {120046, "e"}, // MA# ( 𝓮 → e ) MATHEMATICAL BOLD SCRIPT SMALL E → LATIN SMALL LETTER E# + {120047, "f"}, // MA# ( 𝓯 → f ) MATHEMATICAL BOLD SCRIPT SMALL F → LATIN SMALL LETTER F# + {120048, "g"}, // MA# ( 𝓰 → g ) MATHEMATICAL BOLD SCRIPT SMALL G → LATIN SMALL LETTER G# + {120049, "h"}, // MA# ( 𝓱 → h ) MATHEMATICAL BOLD SCRIPT SMALL H → LATIN SMALL LETTER H# + {120050, "i"}, // MA# ( 𝓲 → i ) MATHEMATICAL BOLD SCRIPT SMALL I → LATIN SMALL LETTER I# + {120051, "j"}, // MA# ( 𝓳 → j ) MATHEMATICAL BOLD SCRIPT SMALL J → LATIN SMALL LETTER J# + {120052, "k"}, // MA# ( 𝓴 → k ) MATHEMATICAL BOLD SCRIPT SMALL K → LATIN SMALL LETTER K# + {120053, "l"}, // MA# ( 𝓵 → l ) MATHEMATICAL BOLD SCRIPT SMALL L → LATIN SMALL LETTER L# + {120054, "rn"}, // MA# ( 𝓶 → rn ) MATHEMATICAL BOLD SCRIPT SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120055, "n"}, // MA# ( 𝓷 → n ) MATHEMATICAL BOLD SCRIPT SMALL N → LATIN SMALL LETTER N# + {120056, "o"}, // MA# ( 𝓸 → o ) MATHEMATICAL BOLD SCRIPT SMALL O → LATIN SMALL LETTER O# + {120057, "p"}, // MA# ( 𝓹 → p ) MATHEMATICAL BOLD SCRIPT SMALL P → LATIN SMALL LETTER P# + {120058, "q"}, // MA# ( 𝓺 → q ) MATHEMATICAL BOLD SCRIPT SMALL Q → LATIN SMALL LETTER Q# + {120059, "r"}, // MA# ( 𝓻 → r ) MATHEMATICAL BOLD SCRIPT SMALL R → LATIN SMALL LETTER R# + {120060, "s"}, // MA# ( 𝓼 → s ) MATHEMATICAL BOLD SCRIPT SMALL S → LATIN SMALL LETTER S# + {120061, "t"}, // MA# ( 𝓽 → t ) MATHEMATICAL BOLD SCRIPT SMALL T → LATIN SMALL LETTER T# + {120062, "u"}, // MA# ( 𝓾 → u ) MATHEMATICAL BOLD SCRIPT SMALL U → LATIN SMALL LETTER U# + {120063, "v"}, // MA# ( 𝓿 → v ) MATHEMATICAL BOLD SCRIPT SMALL V → LATIN SMALL LETTER V# + {120064, "w"}, // MA# ( 𝔀 → w ) MATHEMATICAL BOLD SCRIPT SMALL W → LATIN SMALL LETTER W# + {120065, "x"}, // MA# ( 𝔁 → x ) MATHEMATICAL BOLD SCRIPT SMALL X → LATIN SMALL LETTER X# + {120066, "y"}, // MA# ( 𝔂 → y ) MATHEMATICAL BOLD SCRIPT SMALL Y → LATIN SMALL LETTER Y# + {120067, "z"}, // MA# ( 𝔃 → z ) MATHEMATICAL BOLD SCRIPT SMALL Z → LATIN SMALL LETTER Z# + {120068, "A"}, // MA# ( 𝔄 → A ) MATHEMATICAL FRAKTUR CAPITAL A → LATIN CAPITAL LETTER A# + {120069, "B"}, // MA# ( 𝔅 → B ) MATHEMATICAL FRAKTUR CAPITAL B → LATIN CAPITAL LETTER B# + {120071, "D"}, // MA# ( 𝔇 → D ) MATHEMATICAL FRAKTUR CAPITAL D → LATIN CAPITAL LETTER D# + {120072, "E"}, // MA# ( 𝔈 → E ) MATHEMATICAL FRAKTUR CAPITAL E → LATIN CAPITAL LETTER E# + {120073, "F"}, // MA# ( 𝔉 → F ) MATHEMATICAL FRAKTUR CAPITAL F → LATIN CAPITAL LETTER F# + {120074, "G"}, // MA# ( 𝔊 → G ) MATHEMATICAL FRAKTUR CAPITAL G → LATIN CAPITAL LETTER G# + {120077, "J"}, // MA# ( 𝔍 → J ) MATHEMATICAL FRAKTUR CAPITAL J → LATIN CAPITAL LETTER J# + {120078, "K"}, // MA# ( 𝔎 → K ) MATHEMATICAL FRAKTUR CAPITAL K → LATIN CAPITAL LETTER K# + {120079, "L"}, // MA# ( 𝔏 → L ) MATHEMATICAL FRAKTUR CAPITAL L → LATIN CAPITAL LETTER L# + {120080, "M"}, // MA# ( 𝔐 → M ) MATHEMATICAL FRAKTUR CAPITAL M → LATIN CAPITAL LETTER M# + {120081, "N"}, // MA# ( 𝔑 → N ) MATHEMATICAL FRAKTUR CAPITAL N → LATIN CAPITAL LETTER N# + {120082, "O"}, // MA# ( 𝔒 → O ) MATHEMATICAL FRAKTUR CAPITAL O → LATIN CAPITAL LETTER O# + {120083, "P"}, // MA# ( 𝔓 → P ) MATHEMATICAL FRAKTUR CAPITAL P → LATIN CAPITAL LETTER P# + {120084, "Q"}, // MA# ( 𝔔 → Q ) MATHEMATICAL FRAKTUR CAPITAL Q → LATIN CAPITAL LETTER Q# + {120086, "S"}, // MA# ( 𝔖 → S ) MATHEMATICAL FRAKTUR CAPITAL S → LATIN CAPITAL LETTER S# + {120087, "T"}, // MA# ( 𝔗 → T ) MATHEMATICAL FRAKTUR CAPITAL T → LATIN CAPITAL LETTER T# + {120088, "U"}, // MA# ( 𝔘 → U ) MATHEMATICAL FRAKTUR CAPITAL U → LATIN CAPITAL LETTER U# + {120089, "V"}, // MA# ( 𝔙 → V ) MATHEMATICAL FRAKTUR CAPITAL V → LATIN CAPITAL LETTER V# + {120090, "W"}, // MA# ( 𝔚 → W ) MATHEMATICAL FRAKTUR CAPITAL W → LATIN CAPITAL LETTER W# + {120091, "X"}, // MA# ( 𝔛 → X ) MATHEMATICAL FRAKTUR CAPITAL X → LATIN CAPITAL LETTER X# + {120092, "Y"}, // MA# ( 𝔜 → Y ) MATHEMATICAL FRAKTUR CAPITAL Y → LATIN CAPITAL LETTER Y# + {120094, "a"}, // MA# ( 𝔞 → a ) MATHEMATICAL FRAKTUR SMALL A → LATIN SMALL LETTER A# + {120095, "b"}, // MA# ( 𝔟 → b ) MATHEMATICAL FRAKTUR SMALL B → LATIN SMALL LETTER B# + {120096, "c"}, // MA# ( 𝔠 → c ) MATHEMATICAL FRAKTUR SMALL C → LATIN SMALL LETTER C# + {120097, "d"}, // MA# ( 𝔡 → d ) MATHEMATICAL FRAKTUR SMALL D → LATIN SMALL LETTER D# + {120098, "e"}, // MA# ( 𝔢 → e ) MATHEMATICAL FRAKTUR SMALL E → LATIN SMALL LETTER E# + {120099, "f"}, // MA# ( 𝔣 → f ) MATHEMATICAL FRAKTUR SMALL F → LATIN SMALL LETTER F# + {120100, "g"}, // MA# ( 𝔤 → g ) MATHEMATICAL FRAKTUR SMALL G → LATIN SMALL LETTER G# + {120101, "h"}, // MA# ( 𝔥 → h ) MATHEMATICAL FRAKTUR SMALL H → LATIN SMALL LETTER H# + {120102, "i"}, // MA# ( 𝔦 → i ) MATHEMATICAL FRAKTUR SMALL I → LATIN SMALL LETTER I# + {120103, "j"}, // MA# ( 𝔧 → j ) MATHEMATICAL FRAKTUR SMALL J → LATIN SMALL LETTER J# + {120104, "k"}, // MA# ( 𝔨 → k ) MATHEMATICAL FRAKTUR SMALL K → LATIN SMALL LETTER K# + {120105, "l"}, // MA# ( 𝔩 → l ) MATHEMATICAL FRAKTUR SMALL L → LATIN SMALL LETTER L# + {120106, "rn"}, // MA# ( 𝔪 → rn ) MATHEMATICAL FRAKTUR SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120107, "n"}, // MA# ( 𝔫 → n ) MATHEMATICAL FRAKTUR SMALL N → LATIN SMALL LETTER N# + {120108, "o"}, // MA# ( 𝔬 → o ) MATHEMATICAL FRAKTUR SMALL O → LATIN SMALL LETTER O# + {120109, "p"}, // MA# ( 𝔭 → p ) MATHEMATICAL FRAKTUR SMALL P → LATIN SMALL LETTER P# + {120110, "q"}, // MA# ( 𝔮 → q ) MATHEMATICAL FRAKTUR SMALL Q → LATIN SMALL LETTER Q# + {120111, "r"}, // MA# ( 𝔯 → r ) MATHEMATICAL FRAKTUR SMALL R → LATIN SMALL LETTER R# + {120112, "s"}, // MA# ( 𝔰 → s ) MATHEMATICAL FRAKTUR SMALL S → LATIN SMALL LETTER S# + {120113, "t"}, // MA# ( 𝔱 → t ) MATHEMATICAL FRAKTUR SMALL T → LATIN SMALL LETTER T# + {120114, "u"}, // MA# ( 𝔲 → u ) MATHEMATICAL FRAKTUR SMALL U → LATIN SMALL LETTER U# + {120115, "v"}, // MA# ( 𝔳 → v ) MATHEMATICAL FRAKTUR SMALL V → LATIN SMALL LETTER V# + {120116, "w"}, // MA# ( 𝔴 → w ) MATHEMATICAL FRAKTUR SMALL W → LATIN SMALL LETTER W# + {120117, "x"}, // MA# ( 𝔵 → x ) MATHEMATICAL FRAKTUR SMALL X → LATIN SMALL LETTER X# + {120118, "y"}, // MA# ( 𝔶 → y ) MATHEMATICAL FRAKTUR SMALL Y → LATIN SMALL LETTER Y# + {120119, "z"}, // MA# ( 𝔷 → z ) MATHEMATICAL FRAKTUR SMALL Z → LATIN SMALL LETTER Z# + {120120, "A"}, // MA# ( 𝔸 → A ) MATHEMATICAL DOUBLE-STRUCK CAPITAL A → LATIN CAPITAL LETTER A# + {120121, "B"}, // MA# ( 𝔹 → B ) MATHEMATICAL DOUBLE-STRUCK CAPITAL B → LATIN CAPITAL LETTER B# + {120123, "D"}, // MA# ( 𝔻 → D ) MATHEMATICAL DOUBLE-STRUCK CAPITAL D → LATIN CAPITAL LETTER D# + {120124, "E"}, // MA# ( 𝔼 → E ) MATHEMATICAL DOUBLE-STRUCK CAPITAL E → LATIN CAPITAL LETTER E# + {120125, "F"}, // MA# ( 𝔽 → F ) MATHEMATICAL DOUBLE-STRUCK CAPITAL F → LATIN CAPITAL LETTER F# + {120126, "G"}, // MA# ( 𝔾 → G ) MATHEMATICAL DOUBLE-STRUCK CAPITAL G → LATIN CAPITAL LETTER G# + {120128, "l"}, // MA# ( 𝕀 → l ) MATHEMATICAL DOUBLE-STRUCK CAPITAL I → LATIN SMALL LETTER L# →I→ + {120129, "J"}, // MA# ( 𝕁 → J ) MATHEMATICAL DOUBLE-STRUCK CAPITAL J → LATIN CAPITAL LETTER J# + {120130, "K"}, // MA# ( 𝕂 → K ) MATHEMATICAL DOUBLE-STRUCK CAPITAL K → LATIN CAPITAL LETTER K# + {120131, "L"}, // MA# ( 𝕃 → L ) MATHEMATICAL DOUBLE-STRUCK CAPITAL L → LATIN CAPITAL LETTER L# + {120132, "M"}, // MA# ( 𝕄 → M ) MATHEMATICAL DOUBLE-STRUCK CAPITAL M → LATIN CAPITAL LETTER M# + {120134, "O"}, // MA# ( 𝕆 → O ) MATHEMATICAL DOUBLE-STRUCK CAPITAL O → LATIN CAPITAL LETTER O# + {120138, "S"}, // MA# ( 𝕊 → S ) MATHEMATICAL DOUBLE-STRUCK CAPITAL S → LATIN CAPITAL LETTER S# + {120139, "T"}, // MA# ( 𝕋 → T ) MATHEMATICAL DOUBLE-STRUCK CAPITAL T → LATIN CAPITAL LETTER T# + {120140, "U"}, // MA# ( 𝕌 → U ) MATHEMATICAL DOUBLE-STRUCK CAPITAL U → LATIN CAPITAL LETTER U# + {120141, "V"}, // MA# ( 𝕍 → V ) MATHEMATICAL DOUBLE-STRUCK CAPITAL V → LATIN CAPITAL LETTER V# + {120142, "W"}, // MA# ( 𝕎 → W ) MATHEMATICAL DOUBLE-STRUCK CAPITAL W → LATIN CAPITAL LETTER W# + {120143, "X"}, // MA# ( 𝕏 → X ) MATHEMATICAL DOUBLE-STRUCK CAPITAL X → LATIN CAPITAL LETTER X# + {120144, "Y"}, // MA# ( 𝕐 → Y ) MATHEMATICAL DOUBLE-STRUCK CAPITAL Y → LATIN CAPITAL LETTER Y# + {120146, "a"}, // MA# ( 𝕒 → a ) MATHEMATICAL DOUBLE-STRUCK SMALL A → LATIN SMALL LETTER A# + {120147, "b"}, // MA# ( 𝕓 → b ) MATHEMATICAL DOUBLE-STRUCK SMALL B → LATIN SMALL LETTER B# + {120148, "c"}, // MA# ( 𝕔 → c ) MATHEMATICAL DOUBLE-STRUCK SMALL C → LATIN SMALL LETTER C# + {120149, "d"}, // MA# ( 𝕕 → d ) MATHEMATICAL DOUBLE-STRUCK SMALL D → LATIN SMALL LETTER D# + {120150, "e"}, // MA# ( 𝕖 → e ) MATHEMATICAL DOUBLE-STRUCK SMALL E → LATIN SMALL LETTER E# + {120151, "f"}, // MA# ( 𝕗 → f ) MATHEMATICAL DOUBLE-STRUCK SMALL F → LATIN SMALL LETTER F# + {120152, "g"}, // MA# ( 𝕘 → g ) MATHEMATICAL DOUBLE-STRUCK SMALL G → LATIN SMALL LETTER G# + {120153, "h"}, // MA# ( 𝕙 → h ) MATHEMATICAL DOUBLE-STRUCK SMALL H → LATIN SMALL LETTER H# + {120154, "i"}, // MA# ( 𝕚 → i ) MATHEMATICAL DOUBLE-STRUCK SMALL I → LATIN SMALL LETTER I# + {120155, "j"}, // MA# ( 𝕛 → j ) MATHEMATICAL DOUBLE-STRUCK SMALL J → LATIN SMALL LETTER J# + {120156, "k"}, // MA# ( 𝕜 → k ) MATHEMATICAL DOUBLE-STRUCK SMALL K → LATIN SMALL LETTER K# + {120157, "l"}, // MA# ( 𝕝 → l ) MATHEMATICAL DOUBLE-STRUCK SMALL L → LATIN SMALL LETTER L# + {120158, "rn"}, // MA# ( 𝕞 → rn ) MATHEMATICAL DOUBLE-STRUCK SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120159, "n"}, // MA# ( 𝕟 → n ) MATHEMATICAL DOUBLE-STRUCK SMALL N → LATIN SMALL LETTER N# + {120160, "o"}, // MA# ( 𝕠 → o ) MATHEMATICAL DOUBLE-STRUCK SMALL O → LATIN SMALL LETTER O# + {120161, "p"}, // MA# ( 𝕡 → p ) MATHEMATICAL DOUBLE-STRUCK SMALL P → LATIN SMALL LETTER P# + {120162, "q"}, // MA# ( 𝕢 → q ) MATHEMATICAL DOUBLE-STRUCK SMALL Q → LATIN SMALL LETTER Q# + {120163, "r"}, // MA# ( 𝕣 → r ) MATHEMATICAL DOUBLE-STRUCK SMALL R → LATIN SMALL LETTER R# + {120164, "s"}, // MA# ( 𝕤 → s ) MATHEMATICAL DOUBLE-STRUCK SMALL S → LATIN SMALL LETTER S# + {120165, "t"}, // MA# ( 𝕥 → t ) MATHEMATICAL DOUBLE-STRUCK SMALL T → LATIN SMALL LETTER T# + {120166, "u"}, // MA# ( 𝕦 → u ) MATHEMATICAL DOUBLE-STRUCK SMALL U → LATIN SMALL LETTER U# + {120167, "v"}, // MA# ( 𝕧 → v ) MATHEMATICAL DOUBLE-STRUCK SMALL V → LATIN SMALL LETTER V# + {120168, "w"}, // MA# ( 𝕨 → w ) MATHEMATICAL DOUBLE-STRUCK SMALL W → LATIN SMALL LETTER W# + {120169, "x"}, // MA# ( 𝕩 → x ) MATHEMATICAL DOUBLE-STRUCK SMALL X → LATIN SMALL LETTER X# + {120170, "y"}, // MA# ( 𝕪 → y ) MATHEMATICAL DOUBLE-STRUCK SMALL Y → LATIN SMALL LETTER Y# + {120171, "z"}, // MA# ( 𝕫 → z ) MATHEMATICAL DOUBLE-STRUCK SMALL Z → LATIN SMALL LETTER Z# + {120172, "A"}, // MA# ( 𝕬 → A ) MATHEMATICAL BOLD FRAKTUR CAPITAL A → LATIN CAPITAL LETTER A# + {120173, "B"}, // MA# ( 𝕭 → B ) MATHEMATICAL BOLD FRAKTUR CAPITAL B → LATIN CAPITAL LETTER B# + {120174, "C"}, // MA# ( 𝕮 → C ) MATHEMATICAL BOLD FRAKTUR CAPITAL C → LATIN CAPITAL LETTER C# + {120175, "D"}, // MA# ( 𝕯 → D ) MATHEMATICAL BOLD FRAKTUR CAPITAL D → LATIN CAPITAL LETTER D# + {120176, "E"}, // MA# ( 𝕰 → E ) MATHEMATICAL BOLD FRAKTUR CAPITAL E → LATIN CAPITAL LETTER E# + {120177, "F"}, // MA# ( 𝕱 → F ) MATHEMATICAL BOLD FRAKTUR CAPITAL F → LATIN CAPITAL LETTER F# + {120178, "G"}, // MA# ( 𝕲 → G ) MATHEMATICAL BOLD FRAKTUR CAPITAL G → LATIN CAPITAL LETTER G# + {120179, "H"}, // MA# ( 𝕳 → H ) MATHEMATICAL BOLD FRAKTUR CAPITAL H → LATIN CAPITAL LETTER H# + {120180, "l"}, // MA# ( 𝕴 → l ) MATHEMATICAL BOLD FRAKTUR CAPITAL I → LATIN SMALL LETTER L# →I→ + {120181, "J"}, // MA# ( 𝕵 → J ) MATHEMATICAL BOLD FRAKTUR CAPITAL J → LATIN CAPITAL LETTER J# + {120182, "K"}, // MA# ( 𝕶 → K ) MATHEMATICAL BOLD FRAKTUR CAPITAL K → LATIN CAPITAL LETTER K# + {120183, "L"}, // MA# ( 𝕷 → L ) MATHEMATICAL BOLD FRAKTUR CAPITAL L → LATIN CAPITAL LETTER L# + {120184, "M"}, // MA# ( 𝕸 → M ) MATHEMATICAL BOLD FRAKTUR CAPITAL M → LATIN CAPITAL LETTER M# + {120185, "N"}, // MA# ( 𝕹 → N ) MATHEMATICAL BOLD FRAKTUR CAPITAL N → LATIN CAPITAL LETTER N# + {120186, "O"}, // MA# ( 𝕺 → O ) MATHEMATICAL BOLD FRAKTUR CAPITAL O → LATIN CAPITAL LETTER O# + {120187, "P"}, // MA# ( 𝕻 → P ) MATHEMATICAL BOLD FRAKTUR CAPITAL P → LATIN CAPITAL LETTER P# + {120188, "Q"}, // MA# ( 𝕼 → Q ) MATHEMATICAL BOLD FRAKTUR CAPITAL Q → LATIN CAPITAL LETTER Q# + {120189, "R"}, // MA# ( 𝕽 → R ) MATHEMATICAL BOLD FRAKTUR CAPITAL R → LATIN CAPITAL LETTER R# + {120190, "S"}, // MA# ( 𝕾 → S ) MATHEMATICAL BOLD FRAKTUR CAPITAL S → LATIN CAPITAL LETTER S# + {120191, "T"}, // MA# ( 𝕿 → T ) MATHEMATICAL BOLD FRAKTUR CAPITAL T → LATIN CAPITAL LETTER T# + {120192, "U"}, // MA# ( 𝖀 → U ) MATHEMATICAL BOLD FRAKTUR CAPITAL U → LATIN CAPITAL LETTER U# + {120193, "V"}, // MA# ( 𝖁 → V ) MATHEMATICAL BOLD FRAKTUR CAPITAL V → LATIN CAPITAL LETTER V# + {120194, "W"}, // MA# ( 𝖂 → W ) MATHEMATICAL BOLD FRAKTUR CAPITAL W → LATIN CAPITAL LETTER W# + {120195, "X"}, // MA# ( 𝖃 → X ) MATHEMATICAL BOLD FRAKTUR CAPITAL X → LATIN CAPITAL LETTER X# + {120196, "Y"}, // MA# ( 𝖄 → Y ) MATHEMATICAL BOLD FRAKTUR CAPITAL Y → LATIN CAPITAL LETTER Y# + {120197, "Z"}, // MA# ( 𝖅 → Z ) MATHEMATICAL BOLD FRAKTUR CAPITAL Z → LATIN CAPITAL LETTER Z# + {120198, "a"}, // MA# ( 𝖆 → a ) MATHEMATICAL BOLD FRAKTUR SMALL A → LATIN SMALL LETTER A# + {120199, "b"}, // MA# ( 𝖇 → b ) MATHEMATICAL BOLD FRAKTUR SMALL B → LATIN SMALL LETTER B# + {120200, "c"}, // MA# ( 𝖈 → c ) MATHEMATICAL BOLD FRAKTUR SMALL C → LATIN SMALL LETTER C# + {120201, "d"}, // MA# ( 𝖉 → d ) MATHEMATICAL BOLD FRAKTUR SMALL D → LATIN SMALL LETTER D# + {120202, "e"}, // MA# ( 𝖊 → e ) MATHEMATICAL BOLD FRAKTUR SMALL E → LATIN SMALL LETTER E# + {120203, "f"}, // MA# ( 𝖋 → f ) MATHEMATICAL BOLD FRAKTUR SMALL F → LATIN SMALL LETTER F# + {120204, "g"}, // MA# ( 𝖌 → g ) MATHEMATICAL BOLD FRAKTUR SMALL G → LATIN SMALL LETTER G# + {120205, "h"}, // MA# ( 𝖍 → h ) MATHEMATICAL BOLD FRAKTUR SMALL H → LATIN SMALL LETTER H# + {120206, "i"}, // MA# ( 𝖎 → i ) MATHEMATICAL BOLD FRAKTUR SMALL I → LATIN SMALL LETTER I# + {120207, "j"}, // MA# ( 𝖏 → j ) MATHEMATICAL BOLD FRAKTUR SMALL J → LATIN SMALL LETTER J# + {120208, "k"}, // MA# ( 𝖐 → k ) MATHEMATICAL BOLD FRAKTUR SMALL K → LATIN SMALL LETTER K# + {120209, "l"}, // MA# ( 𝖑 → l ) MATHEMATICAL BOLD FRAKTUR SMALL L → LATIN SMALL LETTER L# + {120210, "rn"}, // MA# ( 𝖒 → rn ) MATHEMATICAL BOLD FRAKTUR SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120211, "n"}, // MA# ( 𝖓 → n ) MATHEMATICAL BOLD FRAKTUR SMALL N → LATIN SMALL LETTER N# + {120212, "o"}, // MA# ( 𝖔 → o ) MATHEMATICAL BOLD FRAKTUR SMALL O → LATIN SMALL LETTER O# + {120213, "p"}, // MA# ( 𝖕 → p ) MATHEMATICAL BOLD FRAKTUR SMALL P → LATIN SMALL LETTER P# + {120214, "q"}, // MA# ( 𝖖 → q ) MATHEMATICAL BOLD FRAKTUR SMALL Q → LATIN SMALL LETTER Q# + {120215, "r"}, // MA# ( 𝖗 → r ) MATHEMATICAL BOLD FRAKTUR SMALL R → LATIN SMALL LETTER R# + {120216, "s"}, // MA# ( 𝖘 → s ) MATHEMATICAL BOLD FRAKTUR SMALL S → LATIN SMALL LETTER S# + {120217, "t"}, // MA# ( 𝖙 → t ) MATHEMATICAL BOLD FRAKTUR SMALL T → LATIN SMALL LETTER T# + {120218, "u"}, // MA# ( 𝖚 → u ) MATHEMATICAL BOLD FRAKTUR SMALL U → LATIN SMALL LETTER U# + {120219, "v"}, // MA# ( 𝖛 → v ) MATHEMATICAL BOLD FRAKTUR SMALL V → LATIN SMALL LETTER V# + {120220, "w"}, // MA# ( 𝖜 → w ) MATHEMATICAL BOLD FRAKTUR SMALL W → LATIN SMALL LETTER W# + {120221, "x"}, // MA# ( 𝖝 → x ) MATHEMATICAL BOLD FRAKTUR SMALL X → LATIN SMALL LETTER X# + {120222, "y"}, // MA# ( 𝖞 → y ) MATHEMATICAL BOLD FRAKTUR SMALL Y → LATIN SMALL LETTER Y# + {120223, "z"}, // MA# ( 𝖟 → z ) MATHEMATICAL BOLD FRAKTUR SMALL Z → LATIN SMALL LETTER Z# + {120224, "A"}, // MA# ( 𝖠 → A ) MATHEMATICAL SANS-SERIF CAPITAL A → LATIN CAPITAL LETTER A# + {120225, "B"}, // MA# ( 𝖡 → B ) MATHEMATICAL SANS-SERIF CAPITAL B → LATIN CAPITAL LETTER B# + {120226, "C"}, // MA# ( 𝖢 → C ) MATHEMATICAL SANS-SERIF CAPITAL C → LATIN CAPITAL LETTER C# + {120227, "D"}, // MA# ( 𝖣 → D ) MATHEMATICAL SANS-SERIF CAPITAL D → LATIN CAPITAL LETTER D# + {120228, "E"}, // MA# ( 𝖤 → E ) MATHEMATICAL SANS-SERIF CAPITAL E → LATIN CAPITAL LETTER E# + {120229, "F"}, // MA# ( 𝖥 → F ) MATHEMATICAL SANS-SERIF CAPITAL F → LATIN CAPITAL LETTER F# + {120230, "G"}, // MA# ( 𝖦 → G ) MATHEMATICAL SANS-SERIF CAPITAL G → LATIN CAPITAL LETTER G# + {120231, "H"}, // MA# ( 𝖧 → H ) MATHEMATICAL SANS-SERIF CAPITAL H → LATIN CAPITAL LETTER H# + {120232, "l"}, // MA# ( 𝖨 → l ) MATHEMATICAL SANS-SERIF CAPITAL I → LATIN SMALL LETTER L# →I→ + {120233, "J"}, // MA# ( 𝖩 → J ) MATHEMATICAL SANS-SERIF CAPITAL J → LATIN CAPITAL LETTER J# + {120234, "K"}, // MA# ( 𝖪 → K ) MATHEMATICAL SANS-SERIF CAPITAL K → LATIN CAPITAL LETTER K# + {120235, "L"}, // MA# ( 𝖫 → L ) MATHEMATICAL SANS-SERIF CAPITAL L → LATIN CAPITAL LETTER L# + {120236, "M"}, // MA# ( 𝖬 → M ) MATHEMATICAL SANS-SERIF CAPITAL M → LATIN CAPITAL LETTER M# + {120237, "N"}, // MA# ( 𝖭 → N ) MATHEMATICAL SANS-SERIF CAPITAL N → LATIN CAPITAL LETTER N# + {120238, "O"}, // MA# ( 𝖮 → O ) MATHEMATICAL SANS-SERIF CAPITAL O → LATIN CAPITAL LETTER O# + {120239, "P"}, // MA# ( 𝖯 → P ) MATHEMATICAL SANS-SERIF CAPITAL P → LATIN CAPITAL LETTER P# + {120240, "Q"}, // MA# ( 𝖰 → Q ) MATHEMATICAL SANS-SERIF CAPITAL Q → LATIN CAPITAL LETTER Q# + {120241, "R"}, // MA# ( 𝖱 → R ) MATHEMATICAL SANS-SERIF CAPITAL R → LATIN CAPITAL LETTER R# + {120242, "S"}, // MA# ( 𝖲 → S ) MATHEMATICAL SANS-SERIF CAPITAL S → LATIN CAPITAL LETTER S# + {120243, "T"}, // MA# ( 𝖳 → T ) MATHEMATICAL SANS-SERIF CAPITAL T → LATIN CAPITAL LETTER T# + {120244, "U"}, // MA# ( 𝖴 → U ) MATHEMATICAL SANS-SERIF CAPITAL U → LATIN CAPITAL LETTER U# + {120245, "V"}, // MA# ( 𝖵 → V ) MATHEMATICAL SANS-SERIF CAPITAL V → LATIN CAPITAL LETTER V# + {120246, "W"}, // MA# ( 𝖶 → W ) MATHEMATICAL SANS-SERIF CAPITAL W → LATIN CAPITAL LETTER W# + {120247, "X"}, // MA# ( 𝖷 → X ) MATHEMATICAL SANS-SERIF CAPITAL X → LATIN CAPITAL LETTER X# + {120248, "Y"}, // MA# ( 𝖸 → Y ) MATHEMATICAL SANS-SERIF CAPITAL Y → LATIN CAPITAL LETTER Y# + {120249, "Z"}, // MA# ( 𝖹 → Z ) MATHEMATICAL SANS-SERIF CAPITAL Z → LATIN CAPITAL LETTER Z# + {120250, "a"}, // MA# ( 𝖺 → a ) MATHEMATICAL SANS-SERIF SMALL A → LATIN SMALL LETTER A# + {120251, "b"}, // MA# ( 𝖻 → b ) MATHEMATICAL SANS-SERIF SMALL B → LATIN SMALL LETTER B# + {120252, "c"}, // MA# ( 𝖼 → c ) MATHEMATICAL SANS-SERIF SMALL C → LATIN SMALL LETTER C# + {120253, "d"}, // MA# ( 𝖽 → d ) MATHEMATICAL SANS-SERIF SMALL D → LATIN SMALL LETTER D# + {120254, "e"}, // MA# ( 𝖾 → e ) MATHEMATICAL SANS-SERIF SMALL E → LATIN SMALL LETTER E# + {120255, "f"}, // MA# ( 𝖿 → f ) MATHEMATICAL SANS-SERIF SMALL F → LATIN SMALL LETTER F# + {120256, "g"}, // MA# ( 𝗀 → g ) MATHEMATICAL SANS-SERIF SMALL G → LATIN SMALL LETTER G# + {120257, "h"}, // MA# ( 𝗁 → h ) MATHEMATICAL SANS-SERIF SMALL H → LATIN SMALL LETTER H# + {120258, "i"}, // MA# ( 𝗂 → i ) MATHEMATICAL SANS-SERIF SMALL I → LATIN SMALL LETTER I# + {120259, "j"}, // MA# ( 𝗃 → j ) MATHEMATICAL SANS-SERIF SMALL J → LATIN SMALL LETTER J# + {120260, "k"}, // MA# ( 𝗄 → k ) MATHEMATICAL SANS-SERIF SMALL K → LATIN SMALL LETTER K# + {120261, "l"}, // MA# ( 𝗅 → l ) MATHEMATICAL SANS-SERIF SMALL L → LATIN SMALL LETTER L# + {120262, "rn"}, // MA# ( 𝗆 → rn ) MATHEMATICAL SANS-SERIF SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120263, "n"}, // MA# ( 𝗇 → n ) MATHEMATICAL SANS-SERIF SMALL N → LATIN SMALL LETTER N# + {120264, "o"}, // MA# ( 𝗈 → o ) MATHEMATICAL SANS-SERIF SMALL O → LATIN SMALL LETTER O# + {120265, "p"}, // MA# ( 𝗉 → p ) MATHEMATICAL SANS-SERIF SMALL P → LATIN SMALL LETTER P# + {120266, "q"}, // MA# ( 𝗊 → q ) MATHEMATICAL SANS-SERIF SMALL Q → LATIN SMALL LETTER Q# + {120267, "r"}, // MA# ( 𝗋 → r ) MATHEMATICAL SANS-SERIF SMALL R → LATIN SMALL LETTER R# + {120268, "s"}, // MA# ( 𝗌 → s ) MATHEMATICAL SANS-SERIF SMALL S → LATIN SMALL LETTER S# + {120269, "t"}, // MA# ( 𝗍 → t ) MATHEMATICAL SANS-SERIF SMALL T → LATIN SMALL LETTER T# + {120270, "u"}, // MA# ( 𝗎 → u ) MATHEMATICAL SANS-SERIF SMALL U → LATIN SMALL LETTER U# + {120271, "v"}, // MA# ( 𝗏 → v ) MATHEMATICAL SANS-SERIF SMALL V → LATIN SMALL LETTER V# + {120272, "w"}, // MA# ( 𝗐 → w ) MATHEMATICAL SANS-SERIF SMALL W → LATIN SMALL LETTER W# + {120273, "x"}, // MA# ( 𝗑 → x ) MATHEMATICAL SANS-SERIF SMALL X → LATIN SMALL LETTER X# + {120274, "y"}, // MA# ( 𝗒 → y ) MATHEMATICAL SANS-SERIF SMALL Y → LATIN SMALL LETTER Y# + {120275, "z"}, // MA# ( 𝗓 → z ) MATHEMATICAL SANS-SERIF SMALL Z → LATIN SMALL LETTER Z# + {120276, "A"}, // MA# ( 𝗔 → A ) MATHEMATICAL SANS-SERIF BOLD CAPITAL A → LATIN CAPITAL LETTER A# + {120277, "B"}, // MA# ( 𝗕 → B ) MATHEMATICAL SANS-SERIF BOLD CAPITAL B → LATIN CAPITAL LETTER B# + {120278, "C"}, // MA# ( 𝗖 → C ) MATHEMATICAL SANS-SERIF BOLD CAPITAL C → LATIN CAPITAL LETTER C# + {120279, "D"}, // MA# ( 𝗗 → D ) MATHEMATICAL SANS-SERIF BOLD CAPITAL D → LATIN CAPITAL LETTER D# + {120280, "E"}, // MA# ( 𝗘 → E ) MATHEMATICAL SANS-SERIF BOLD CAPITAL E → LATIN CAPITAL LETTER E# + {120281, "F"}, // MA# ( 𝗙 → F ) MATHEMATICAL SANS-SERIF BOLD CAPITAL F → LATIN CAPITAL LETTER F# + {120282, "G"}, // MA# ( 𝗚 → G ) MATHEMATICAL SANS-SERIF BOLD CAPITAL G → LATIN CAPITAL LETTER G# + {120283, "H"}, // MA# ( 𝗛 → H ) MATHEMATICAL SANS-SERIF BOLD CAPITAL H → LATIN CAPITAL LETTER H# + {120284, "l"}, // MA# ( 𝗜 → l ) MATHEMATICAL SANS-SERIF BOLD CAPITAL I → LATIN SMALL LETTER L# →I→ + {120285, "J"}, // MA# ( 𝗝 → J ) MATHEMATICAL SANS-SERIF BOLD CAPITAL J → LATIN CAPITAL LETTER J# + {120286, "K"}, // MA# ( 𝗞 → K ) MATHEMATICAL SANS-SERIF BOLD CAPITAL K → LATIN CAPITAL LETTER K# + {120287, "L"}, // MA# ( 𝗟 → L ) MATHEMATICAL SANS-SERIF BOLD CAPITAL L → LATIN CAPITAL LETTER L# + {120288, "M"}, // MA# ( 𝗠 → M ) MATHEMATICAL SANS-SERIF BOLD CAPITAL M → LATIN CAPITAL LETTER M# + {120289, "N"}, // MA# ( 𝗡 → N ) MATHEMATICAL SANS-SERIF BOLD CAPITAL N → LATIN CAPITAL LETTER N# + {120290, "O"}, // MA# ( 𝗢 → O ) MATHEMATICAL SANS-SERIF BOLD CAPITAL O → LATIN CAPITAL LETTER O# + {120291, "P"}, // MA# ( 𝗣 → P ) MATHEMATICAL SANS-SERIF BOLD CAPITAL P → LATIN CAPITAL LETTER P# + {120292, "Q"}, // MA# ( 𝗤 → Q ) MATHEMATICAL SANS-SERIF BOLD CAPITAL Q → LATIN CAPITAL LETTER Q# + {120293, "R"}, // MA# ( 𝗥 → R ) MATHEMATICAL SANS-SERIF BOLD CAPITAL R → LATIN CAPITAL LETTER R# + {120294, "S"}, // MA# ( 𝗦 → S ) MATHEMATICAL SANS-SERIF BOLD CAPITAL S → LATIN CAPITAL LETTER S# + {120295, "T"}, // MA# ( 𝗧 → T ) MATHEMATICAL SANS-SERIF BOLD CAPITAL T → LATIN CAPITAL LETTER T# + {120296, "U"}, // MA# ( 𝗨 → U ) MATHEMATICAL SANS-SERIF BOLD CAPITAL U → LATIN CAPITAL LETTER U# + {120297, "V"}, // MA# ( 𝗩 → V ) MATHEMATICAL SANS-SERIF BOLD CAPITAL V → LATIN CAPITAL LETTER V# + {120298, "W"}, // MA# ( 𝗪 → W ) MATHEMATICAL SANS-SERIF BOLD CAPITAL W → LATIN CAPITAL LETTER W# + {120299, "X"}, // MA# ( 𝗫 → X ) MATHEMATICAL SANS-SERIF BOLD CAPITAL X → LATIN CAPITAL LETTER X# + {120300, "Y"}, // MA# ( 𝗬 → Y ) MATHEMATICAL SANS-SERIF BOLD CAPITAL Y → LATIN CAPITAL LETTER Y# + {120301, "Z"}, // MA# ( 𝗭 → Z ) MATHEMATICAL SANS-SERIF BOLD CAPITAL Z → LATIN CAPITAL LETTER Z# + {120302, "a"}, // MA# ( 𝗮 → a ) MATHEMATICAL SANS-SERIF BOLD SMALL A → LATIN SMALL LETTER A# + {120303, "b"}, // MA# ( 𝗯 → b ) MATHEMATICAL SANS-SERIF BOLD SMALL B → LATIN SMALL LETTER B# + {120304, "c"}, // MA# ( 𝗰 → c ) MATHEMATICAL SANS-SERIF BOLD SMALL C → LATIN SMALL LETTER C# + {120305, "d"}, // MA# ( 𝗱 → d ) MATHEMATICAL SANS-SERIF BOLD SMALL D → LATIN SMALL LETTER D# + {120306, "e"}, // MA# ( 𝗲 → e ) MATHEMATICAL SANS-SERIF BOLD SMALL E → LATIN SMALL LETTER E# + {120307, "f"}, // MA# ( 𝗳 → f ) MATHEMATICAL SANS-SERIF BOLD SMALL F → LATIN SMALL LETTER F# + {120308, "g"}, // MA# ( 𝗴 → g ) MATHEMATICAL SANS-SERIF BOLD SMALL G → LATIN SMALL LETTER G# + {120309, "h"}, // MA# ( 𝗵 → h ) MATHEMATICAL SANS-SERIF BOLD SMALL H → LATIN SMALL LETTER H# + {120310, "i"}, // MA# ( 𝗶 → i ) MATHEMATICAL SANS-SERIF BOLD SMALL I → LATIN SMALL LETTER I# + {120311, "j"}, // MA# ( 𝗷 → j ) MATHEMATICAL SANS-SERIF BOLD SMALL J → LATIN SMALL LETTER J# + {120312, "k"}, // MA# ( 𝗸 → k ) MATHEMATICAL SANS-SERIF BOLD SMALL K → LATIN SMALL LETTER K# + {120313, "l"}, // MA# ( 𝗹 → l ) MATHEMATICAL SANS-SERIF BOLD SMALL L → LATIN SMALL LETTER L# + {120314, "rn"}, // MA# ( 𝗺 → rn ) MATHEMATICAL SANS-SERIF BOLD SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120315, "n"}, // MA# ( 𝗻 → n ) MATHEMATICAL SANS-SERIF BOLD SMALL N → LATIN SMALL LETTER N# + {120316, "o"}, // MA# ( 𝗼 → o ) MATHEMATICAL SANS-SERIF BOLD SMALL O → LATIN SMALL LETTER O# + {120317, "p"}, // MA# ( 𝗽 → p ) MATHEMATICAL SANS-SERIF BOLD SMALL P → LATIN SMALL LETTER P# + {120318, "q"}, // MA# ( 𝗾 → q ) MATHEMATICAL SANS-SERIF BOLD SMALL Q → LATIN SMALL LETTER Q# + {120319, "r"}, // MA# ( 𝗿 → r ) MATHEMATICAL SANS-SERIF BOLD SMALL R → LATIN SMALL LETTER R# + {120320, "s"}, // MA# ( 𝘀 → s ) MATHEMATICAL SANS-SERIF BOLD SMALL S → LATIN SMALL LETTER S# + {120321, "t"}, // MA# ( 𝘁 → t ) MATHEMATICAL SANS-SERIF BOLD SMALL T → LATIN SMALL LETTER T# + {120322, "u"}, // MA# ( 𝘂 → u ) MATHEMATICAL SANS-SERIF BOLD SMALL U → LATIN SMALL LETTER U# + {120323, "v"}, // MA# ( 𝘃 → v ) MATHEMATICAL SANS-SERIF BOLD SMALL V → LATIN SMALL LETTER V# + {120324, "w"}, // MA# ( 𝘄 → w ) MATHEMATICAL SANS-SERIF BOLD SMALL W → LATIN SMALL LETTER W# + {120325, "x"}, // MA# ( 𝘅 → x ) MATHEMATICAL SANS-SERIF BOLD SMALL X → LATIN SMALL LETTER X# + {120326, "y"}, // MA# ( 𝘆 → y ) MATHEMATICAL SANS-SERIF BOLD SMALL Y → LATIN SMALL LETTER Y# + {120327, "z"}, // MA# ( 𝘇 → z ) MATHEMATICAL SANS-SERIF BOLD SMALL Z → LATIN SMALL LETTER Z# + {120328, "A"}, // MA# ( 𝘈 → A ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL A → LATIN CAPITAL LETTER A# + {120329, "B"}, // MA# ( 𝘉 → B ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL B → LATIN CAPITAL LETTER B# + {120330, "C"}, // MA# ( 𝘊 → C ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL C → LATIN CAPITAL LETTER C# + {120331, "D"}, // MA# ( 𝘋 → D ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL D → LATIN CAPITAL LETTER D# + {120332, "E"}, // MA# ( 𝘌 → E ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL E → LATIN CAPITAL LETTER E# + {120333, "F"}, // MA# ( 𝘍 → F ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL F → LATIN CAPITAL LETTER F# + {120334, "G"}, // MA# ( 𝘎 → G ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL G → LATIN CAPITAL LETTER G# + {120335, "H"}, // MA# ( 𝘏 → H ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL H → LATIN CAPITAL LETTER H# + {120336, "l"}, // MA# ( 𝘐 → l ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL I → LATIN SMALL LETTER L# →I→ + {120337, "J"}, // MA# ( 𝘑 → J ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL J → LATIN CAPITAL LETTER J# + {120338, "K"}, // MA# ( 𝘒 → K ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL K → LATIN CAPITAL LETTER K# + {120339, "L"}, // MA# ( 𝘓 → L ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL L → LATIN CAPITAL LETTER L# + {120340, "M"}, // MA# ( 𝘔 → M ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL M → LATIN CAPITAL LETTER M# + {120341, "N"}, // MA# ( 𝘕 → N ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL N → LATIN CAPITAL LETTER N# + {120342, "O"}, // MA# ( 𝘖 → O ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL O → LATIN CAPITAL LETTER O# + {120343, "P"}, // MA# ( 𝘗 → P ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL P → LATIN CAPITAL LETTER P# + {120344, "Q"}, // MA# ( 𝘘 → Q ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q → LATIN CAPITAL LETTER Q# + {120345, "R"}, // MA# ( 𝘙 → R ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL R → LATIN CAPITAL LETTER R# + {120346, "S"}, // MA# ( 𝘚 → S ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL S → LATIN CAPITAL LETTER S# + {120347, "T"}, // MA# ( 𝘛 → T ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL T → LATIN CAPITAL LETTER T# + {120348, "U"}, // MA# ( 𝘜 → U ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL U → LATIN CAPITAL LETTER U# + {120349, "V"}, // MA# ( 𝘝 → V ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL V → LATIN CAPITAL LETTER V# + {120350, "W"}, // MA# ( 𝘞 → W ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL W → LATIN CAPITAL LETTER W# + {120351, "X"}, // MA# ( 𝘟 → X ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL X → LATIN CAPITAL LETTER X# + {120352, "Y"}, // MA# ( 𝘠 → Y ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y → LATIN CAPITAL LETTER Y# + {120353, "Z"}, // MA# ( 𝘡 → Z ) MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z → LATIN CAPITAL LETTER Z# + {120354, "a"}, // MA# ( 𝘢 → a ) MATHEMATICAL SANS-SERIF ITALIC SMALL A → LATIN SMALL LETTER A# + {120355, "b"}, // MA# ( 𝘣 → b ) MATHEMATICAL SANS-SERIF ITALIC SMALL B → LATIN SMALL LETTER B# + {120356, "c"}, // MA# ( 𝘤 → c ) MATHEMATICAL SANS-SERIF ITALIC SMALL C → LATIN SMALL LETTER C# + {120357, "d"}, // MA# ( 𝘥 → d ) MATHEMATICAL SANS-SERIF ITALIC SMALL D → LATIN SMALL LETTER D# + {120358, "e"}, // MA# ( 𝘦 → e ) MATHEMATICAL SANS-SERIF ITALIC SMALL E → LATIN SMALL LETTER E# + {120359, "f"}, // MA# ( 𝘧 → f ) MATHEMATICAL SANS-SERIF ITALIC SMALL F → LATIN SMALL LETTER F# + {120360, "g"}, // MA# ( 𝘨 → g ) MATHEMATICAL SANS-SERIF ITALIC SMALL G → LATIN SMALL LETTER G# + {120361, "h"}, // MA# ( 𝘩 → h ) MATHEMATICAL SANS-SERIF ITALIC SMALL H → LATIN SMALL LETTER H# + {120362, "i"}, // MA# ( 𝘪 → i ) MATHEMATICAL SANS-SERIF ITALIC SMALL I → LATIN SMALL LETTER I# + {120363, "j"}, // MA# ( 𝘫 → j ) MATHEMATICAL SANS-SERIF ITALIC SMALL J → LATIN SMALL LETTER J# + {120364, "k"}, // MA# ( 𝘬 → k ) MATHEMATICAL SANS-SERIF ITALIC SMALL K → LATIN SMALL LETTER K# + {120365, "l"}, // MA# ( 𝘭 → l ) MATHEMATICAL SANS-SERIF ITALIC SMALL L → LATIN SMALL LETTER L# + {120366, "rn"}, // MA# ( 𝘮 → rn ) MATHEMATICAL SANS-SERIF ITALIC SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120367, "n"}, // MA# ( 𝘯 → n ) MATHEMATICAL SANS-SERIF ITALIC SMALL N → LATIN SMALL LETTER N# + {120368, "o"}, // MA# ( 𝘰 → o ) MATHEMATICAL SANS-SERIF ITALIC SMALL O → LATIN SMALL LETTER O# + {120369, "p"}, // MA# ( 𝘱 → p ) MATHEMATICAL SANS-SERIF ITALIC SMALL P → LATIN SMALL LETTER P# + {120370, "q"}, // MA# ( 𝘲 → q ) MATHEMATICAL SANS-SERIF ITALIC SMALL Q → LATIN SMALL LETTER Q# + {120371, "r"}, // MA# ( 𝘳 → r ) MATHEMATICAL SANS-SERIF ITALIC SMALL R → LATIN SMALL LETTER R# + {120372, "s"}, // MA# ( 𝘴 → s ) MATHEMATICAL SANS-SERIF ITALIC SMALL S → LATIN SMALL LETTER S# + {120373, "t"}, // MA# ( 𝘵 → t ) MATHEMATICAL SANS-SERIF ITALIC SMALL T → LATIN SMALL LETTER T# + {120374, "u"}, // MA# ( 𝘶 → u ) MATHEMATICAL SANS-SERIF ITALIC SMALL U → LATIN SMALL LETTER U# + {120375, "v"}, // MA# ( 𝘷 → v ) MATHEMATICAL SANS-SERIF ITALIC SMALL V → LATIN SMALL LETTER V# + {120376, "w"}, // MA# ( 𝘸 → w ) MATHEMATICAL SANS-SERIF ITALIC SMALL W → LATIN SMALL LETTER W# + {120377, "x"}, // MA# ( 𝘹 → x ) MATHEMATICAL SANS-SERIF ITALIC SMALL X → LATIN SMALL LETTER X# + {120378, "y"}, // MA# ( 𝘺 → y ) MATHEMATICAL SANS-SERIF ITALIC SMALL Y → LATIN SMALL LETTER Y# + {120379, "z"}, // MA# ( 𝘻 → z ) MATHEMATICAL SANS-SERIF ITALIC SMALL Z → LATIN SMALL LETTER Z# + {120380, "A"}, // MA# ( 𝘼 → A ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A → LATIN CAPITAL LETTER A# + {120381, "B"}, // MA# ( 𝘽 → B ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B → LATIN CAPITAL LETTER B# + {120382, "C"}, // MA# ( 𝘾 → C ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C → LATIN CAPITAL LETTER C# + {120383, "D"}, // MA# ( 𝘿 → D ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D → LATIN CAPITAL LETTER D# + {120384, "E"}, // MA# ( 𝙀 → E ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E → LATIN CAPITAL LETTER E# + {120385, "F"}, // MA# ( 𝙁 → F ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F → LATIN CAPITAL LETTER F# + {120386, "G"}, // MA# ( 𝙂 → G ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G → LATIN CAPITAL LETTER G# + {120387, "H"}, // MA# ( 𝙃 → H ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H → LATIN CAPITAL LETTER H# + {120388, "l"}, // MA# ( 𝙄 → l ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I → LATIN SMALL LETTER L# →I→ + {120389, "J"}, // MA# ( 𝙅 → J ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J → LATIN CAPITAL LETTER J# + {120390, "K"}, // MA# ( 𝙆 → K ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K → LATIN CAPITAL LETTER K# + {120391, "L"}, // MA# ( 𝙇 → L ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L → LATIN CAPITAL LETTER L# + {120392, "M"}, // MA# ( 𝙈 → M ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M → LATIN CAPITAL LETTER M# + {120393, "N"}, // MA# ( 𝙉 → N ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N → LATIN CAPITAL LETTER N# + {120394, "O"}, // MA# ( 𝙊 → O ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O → LATIN CAPITAL LETTER O# + {120395, "P"}, // MA# ( 𝙋 → P ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P → LATIN CAPITAL LETTER P# + {120396, "Q"}, // MA# ( 𝙌 → Q ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q → LATIN CAPITAL LETTER Q# + {120397, "R"}, // MA# ( 𝙍 → R ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R → LATIN CAPITAL LETTER R# + {120398, "S"}, // MA# ( 𝙎 → S ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S → LATIN CAPITAL LETTER S# + {120399, "T"}, // MA# ( 𝙏 → T ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T → LATIN CAPITAL LETTER T# + {120400, "U"}, // MA# ( 𝙐 → U ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U → LATIN CAPITAL LETTER U# + {120401, "V"}, // MA# ( 𝙑 → V ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V → LATIN CAPITAL LETTER V# + {120402, "W"}, // MA# ( 𝙒 → W ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W → LATIN CAPITAL LETTER W# + {120403, "X"}, // MA# ( 𝙓 → X ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X → LATIN CAPITAL LETTER X# + {120404, "Y"}, // MA# ( 𝙔 → Y ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y → LATIN CAPITAL LETTER Y# + {120405, "Z"}, // MA# ( 𝙕 → Z ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z → LATIN CAPITAL LETTER Z# + {120406, "a"}, // MA# ( 𝙖 → a ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A → LATIN SMALL LETTER A# + {120407, "b"}, // MA# ( 𝙗 → b ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B → LATIN SMALL LETTER B# + {120408, "c"}, // MA# ( 𝙘 → c ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C → LATIN SMALL LETTER C# + {120409, "d"}, // MA# ( 𝙙 → d ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D → LATIN SMALL LETTER D# + {120410, "e"}, // MA# ( 𝙚 → e ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E → LATIN SMALL LETTER E# + {120411, "f"}, // MA# ( 𝙛 → f ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F → LATIN SMALL LETTER F# + {120412, "g"}, // MA# ( 𝙜 → g ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G → LATIN SMALL LETTER G# + {120413, "h"}, // MA# ( 𝙝 → h ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H → LATIN SMALL LETTER H# + {120414, "i"}, // MA# ( 𝙞 → i ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I → LATIN SMALL LETTER I# + {120415, "j"}, // MA# ( 𝙟 → j ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J → LATIN SMALL LETTER J# + {120416, "k"}, // MA# ( 𝙠 → k ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K → LATIN SMALL LETTER K# + {120417, "l"}, // MA# ( 𝙡 → l ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L → LATIN SMALL LETTER L# + {120418, "rn"}, // MA# ( 𝙢 → rn ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120419, "n"}, // MA# ( 𝙣 → n ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N → LATIN SMALL LETTER N# + {120420, "o"}, // MA# ( 𝙤 → o ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O → LATIN SMALL LETTER O# + {120421, "p"}, // MA# ( 𝙥 → p ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P → LATIN SMALL LETTER P# + {120422, "q"}, // MA# ( 𝙦 → q ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q → LATIN SMALL LETTER Q# + {120423, "r"}, // MA# ( 𝙧 → r ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R → LATIN SMALL LETTER R# + {120424, "s"}, // MA# ( 𝙨 → s ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S → LATIN SMALL LETTER S# + {120425, "t"}, // MA# ( 𝙩 → t ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T → LATIN SMALL LETTER T# + {120426, "u"}, // MA# ( 𝙪 → u ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U → LATIN SMALL LETTER U# + {120427, "v"}, // MA# ( 𝙫 → v ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V → LATIN SMALL LETTER V# + {120428, "w"}, // MA# ( 𝙬 → w ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W → LATIN SMALL LETTER W# + {120429, "x"}, // MA# ( 𝙭 → x ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X → LATIN SMALL LETTER X# + {120430, "y"}, // MA# ( 𝙮 → y ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y → LATIN SMALL LETTER Y# + {120431, "z"}, // MA# ( 𝙯 → z ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z → LATIN SMALL LETTER Z# + {120432, "A"}, // MA# ( 𝙰 → A ) MATHEMATICAL MONOSPACE CAPITAL A → LATIN CAPITAL LETTER A# + {120433, "B"}, // MA# ( 𝙱 → B ) MATHEMATICAL MONOSPACE CAPITAL B → LATIN CAPITAL LETTER B# + {120434, "C"}, // MA# ( 𝙲 → C ) MATHEMATICAL MONOSPACE CAPITAL C → LATIN CAPITAL LETTER C# + {120435, "D"}, // MA# ( 𝙳 → D ) MATHEMATICAL MONOSPACE CAPITAL D → LATIN CAPITAL LETTER D# + {120436, "E"}, // MA# ( 𝙴 → E ) MATHEMATICAL MONOSPACE CAPITAL E → LATIN CAPITAL LETTER E# + {120437, "F"}, // MA# ( 𝙵 → F ) MATHEMATICAL MONOSPACE CAPITAL F → LATIN CAPITAL LETTER F# + {120438, "G"}, // MA# ( 𝙶 → G ) MATHEMATICAL MONOSPACE CAPITAL G → LATIN CAPITAL LETTER G# + {120439, "H"}, // MA# ( 𝙷 → H ) MATHEMATICAL MONOSPACE CAPITAL H → LATIN CAPITAL LETTER H# + {120440, "l"}, // MA# ( 𝙸 → l ) MATHEMATICAL MONOSPACE CAPITAL I → LATIN SMALL LETTER L# →I→ + {120441, "J"}, // MA# ( 𝙹 → J ) MATHEMATICAL MONOSPACE CAPITAL J → LATIN CAPITAL LETTER J# + {120442, "K"}, // MA# ( 𝙺 → K ) MATHEMATICAL MONOSPACE CAPITAL K → LATIN CAPITAL LETTER K# + {120443, "L"}, // MA# ( 𝙻 → L ) MATHEMATICAL MONOSPACE CAPITAL L → LATIN CAPITAL LETTER L# + {120444, "M"}, // MA# ( 𝙼 → M ) MATHEMATICAL MONOSPACE CAPITAL M → LATIN CAPITAL LETTER M# + {120445, "N"}, // MA# ( 𝙽 → N ) MATHEMATICAL MONOSPACE CAPITAL N → LATIN CAPITAL LETTER N# + {120446, "O"}, // MA# ( 𝙾 → O ) MATHEMATICAL MONOSPACE CAPITAL O → LATIN CAPITAL LETTER O# + {120447, "P"}, // MA# ( 𝙿 → P ) MATHEMATICAL MONOSPACE CAPITAL P → LATIN CAPITAL LETTER P# + {120448, "Q"}, // MA# ( 𝚀 → Q ) MATHEMATICAL MONOSPACE CAPITAL Q → LATIN CAPITAL LETTER Q# + {120449, "R"}, // MA# ( 𝚁 → R ) MATHEMATICAL MONOSPACE CAPITAL R → LATIN CAPITAL LETTER R# + {120450, "S"}, // MA# ( 𝚂 → S ) MATHEMATICAL MONOSPACE CAPITAL S → LATIN CAPITAL LETTER S# + {120451, "T"}, // MA# ( 𝚃 → T ) MATHEMATICAL MONOSPACE CAPITAL T → LATIN CAPITAL LETTER T# + {120452, "U"}, // MA# ( 𝚄 → U ) MATHEMATICAL MONOSPACE CAPITAL U → LATIN CAPITAL LETTER U# + {120453, "V"}, // MA# ( 𝚅 → V ) MATHEMATICAL MONOSPACE CAPITAL V → LATIN CAPITAL LETTER V# + {120454, "W"}, // MA# ( 𝚆 → W ) MATHEMATICAL MONOSPACE CAPITAL W → LATIN CAPITAL LETTER W# + {120455, "X"}, // MA# ( 𝚇 → X ) MATHEMATICAL MONOSPACE CAPITAL X → LATIN CAPITAL LETTER X# + {120456, "Y"}, // MA# ( 𝚈 → Y ) MATHEMATICAL MONOSPACE CAPITAL Y → LATIN CAPITAL LETTER Y# + {120457, "Z"}, // MA# ( 𝚉 → Z ) MATHEMATICAL MONOSPACE CAPITAL Z → LATIN CAPITAL LETTER Z# + {120458, "a"}, // MA# ( 𝚊 → a ) MATHEMATICAL MONOSPACE SMALL A → LATIN SMALL LETTER A# + {120459, "b"}, // MA# ( 𝚋 → b ) MATHEMATICAL MONOSPACE SMALL B → LATIN SMALL LETTER B# + {120460, "c"}, // MA# ( 𝚌 → c ) MATHEMATICAL MONOSPACE SMALL C → LATIN SMALL LETTER C# + {120461, "d"}, // MA# ( 𝚍 → d ) MATHEMATICAL MONOSPACE SMALL D → LATIN SMALL LETTER D# + {120462, "e"}, // MA# ( 𝚎 → e ) MATHEMATICAL MONOSPACE SMALL E → LATIN SMALL LETTER E# + {120463, "f"}, // MA# ( 𝚏 → f ) MATHEMATICAL MONOSPACE SMALL F → LATIN SMALL LETTER F# + {120464, "g"}, // MA# ( 𝚐 → g ) MATHEMATICAL MONOSPACE SMALL G → LATIN SMALL LETTER G# + {120465, "h"}, // MA# ( 𝚑 → h ) MATHEMATICAL MONOSPACE SMALL H → LATIN SMALL LETTER H# + {120466, "i"}, // MA# ( 𝚒 → i ) MATHEMATICAL MONOSPACE SMALL I → LATIN SMALL LETTER I# + {120467, "j"}, // MA# ( 𝚓 → j ) MATHEMATICAL MONOSPACE SMALL J → LATIN SMALL LETTER J# + {120468, "k"}, // MA# ( 𝚔 → k ) MATHEMATICAL MONOSPACE SMALL K → LATIN SMALL LETTER K# + {120469, "l"}, // MA# ( 𝚕 → l ) MATHEMATICAL MONOSPACE SMALL L → LATIN SMALL LETTER L# + {120470, "rn"}, // MA# ( 𝚖 → rn ) MATHEMATICAL MONOSPACE SMALL M → LATIN SMALL LETTER R, LATIN SMALL LETTER N# →m→ + {120471, "n"}, // MA# ( 𝚗 → n ) MATHEMATICAL MONOSPACE SMALL N → LATIN SMALL LETTER N# + {120472, "o"}, // MA# ( 𝚘 → o ) MATHEMATICAL MONOSPACE SMALL O → LATIN SMALL LETTER O# + {120473, "p"}, // MA# ( 𝚙 → p ) MATHEMATICAL MONOSPACE SMALL P → LATIN SMALL LETTER P# + {120474, "q"}, // MA# ( 𝚚 → q ) MATHEMATICAL MONOSPACE SMALL Q → LATIN SMALL LETTER Q# + {120475, "r"}, // MA# ( 𝚛 → r ) MATHEMATICAL MONOSPACE SMALL R → LATIN SMALL LETTER R# + {120476, "s"}, // MA# ( 𝚜 → s ) MATHEMATICAL MONOSPACE SMALL S → LATIN SMALL LETTER S# + {120477, "t"}, // MA# ( 𝚝 → t ) MATHEMATICAL MONOSPACE SMALL T → LATIN SMALL LETTER T# + {120478, "u"}, // MA# ( 𝚞 → u ) MATHEMATICAL MONOSPACE SMALL U → LATIN SMALL LETTER U# + {120479, "v"}, // MA# ( 𝚟 → v ) MATHEMATICAL MONOSPACE SMALL V → LATIN SMALL LETTER V# + {120480, "w"}, // MA# ( 𝚠 → w ) MATHEMATICAL MONOSPACE SMALL W → LATIN SMALL LETTER W# + {120481, "x"}, // MA# ( 𝚡 → x ) MATHEMATICAL MONOSPACE SMALL X → LATIN SMALL LETTER X# + {120482, "y"}, // MA# ( 𝚢 → y ) MATHEMATICAL MONOSPACE SMALL Y → LATIN SMALL LETTER Y# + {120483, "z"}, // MA# ( 𝚣 → z ) MATHEMATICAL MONOSPACE SMALL Z → LATIN SMALL LETTER Z# + {120484, "i"}, // MA# ( 𝚤 → i ) MATHEMATICAL ITALIC SMALL DOTLESS I → LATIN SMALL LETTER I# →ı→ + {120488, "A"}, // MA# ( 𝚨 → A ) MATHEMATICAL BOLD CAPITAL ALPHA → LATIN CAPITAL LETTER A# →𝐀→ + {120489, "B"}, // MA# ( 𝚩 → B ) MATHEMATICAL BOLD CAPITAL BETA → LATIN CAPITAL LETTER B# →Β→ + {120492, "E"}, // MA# ( 𝚬 → E ) MATHEMATICAL BOLD CAPITAL EPSILON → LATIN CAPITAL LETTER E# →𝐄→ + {120493, "Z"}, // MA# ( 𝚭 → Z ) MATHEMATICAL BOLD CAPITAL ZETA → LATIN CAPITAL LETTER Z# →Ζ→ + {120494, "H"}, // MA# ( 𝚮 → H ) MATHEMATICAL BOLD CAPITAL ETA → LATIN CAPITAL LETTER H# →Η→ + {120496, "l"}, // MA# ( 𝚰 → l ) MATHEMATICAL BOLD CAPITAL IOTA → LATIN SMALL LETTER L# →Ι→ + {120497, "K"}, // MA# ( 𝚱 → K ) MATHEMATICAL BOLD CAPITAL KAPPA → LATIN CAPITAL LETTER K# →Κ→ + {120499, "M"}, // MA# ( 𝚳 → M ) MATHEMATICAL BOLD CAPITAL MU → LATIN CAPITAL LETTER M# →𝐌→ + {120500, "N"}, // MA# ( 𝚴 → N ) MATHEMATICAL BOLD CAPITAL NU → LATIN CAPITAL LETTER N# →𝐍→ + {120502, "O"}, // MA# ( 𝚶 → O ) MATHEMATICAL BOLD CAPITAL OMICRON → LATIN CAPITAL LETTER O# →𝐎→ + {120504, "P"}, // MA# ( 𝚸 → P ) MATHEMATICAL BOLD CAPITAL RHO → LATIN CAPITAL LETTER P# →𝐏→ + {120507, "T"}, // MA# ( 𝚻 → T ) MATHEMATICAL BOLD CAPITAL TAU → LATIN CAPITAL LETTER T# →Τ→ + {120508, "Y"}, // MA# ( 𝚼 → Y ) MATHEMATICAL BOLD CAPITAL UPSILON → LATIN CAPITAL LETTER Y# →Υ→ + {120510, "X"}, // MA# ( 𝚾 → X ) MATHEMATICAL BOLD CAPITAL CHI → LATIN CAPITAL LETTER X# →Χ→ + {120514, "a"}, // MA# ( 𝛂 → a ) MATHEMATICAL BOLD SMALL ALPHA → LATIN SMALL LETTER A# →α→ + {120516, "y"}, // MA# ( 𝛄 → y ) MATHEMATICAL BOLD SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {120522, "i"}, // MA# ( 𝛊 → i ) MATHEMATICAL BOLD SMALL IOTA → LATIN SMALL LETTER I# →ι→ + {120526, "v"}, // MA# ( 𝛎 → v ) MATHEMATICAL BOLD SMALL NU → LATIN SMALL LETTER V# →ν→ + {120528, "o"}, // MA# ( 𝛐 → o ) MATHEMATICAL BOLD SMALL OMICRON → LATIN SMALL LETTER O# →𝐨→ + {120530, "p"}, // MA# ( 𝛒 → p ) MATHEMATICAL BOLD SMALL RHO → LATIN SMALL LETTER P# →ρ→ + {120532, "o"}, // MA# ( 𝛔 → o ) MATHEMATICAL BOLD SMALL SIGMA → LATIN SMALL LETTER O# →σ→ + {120534, "u"}, // MA# ( 𝛖 → u ) MATHEMATICAL BOLD SMALL UPSILON → LATIN SMALL LETTER U# →υ→→ʋ→ + {120544, "p"}, // MA# ( 𝛠 → p ) MATHEMATICAL BOLD RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {120546, "A"}, // MA# ( 𝛢 → A ) MATHEMATICAL ITALIC CAPITAL ALPHA → LATIN CAPITAL LETTER A# →Α→ + {120547, "B"}, // MA# ( 𝛣 → B ) MATHEMATICAL ITALIC CAPITAL BETA → LATIN CAPITAL LETTER B# →Β→ + {120550, "E"}, // MA# ( 𝛦 → E ) MATHEMATICAL ITALIC CAPITAL EPSILON → LATIN CAPITAL LETTER E# →Ε→ + {120551, "Z"}, // MA# ( 𝛧 → Z ) MATHEMATICAL ITALIC CAPITAL ZETA → LATIN CAPITAL LETTER Z# →𝑍→ + {120552, "H"}, // MA# ( 𝛨 → H ) MATHEMATICAL ITALIC CAPITAL ETA → LATIN CAPITAL LETTER H# →Η→ + {120554, "l"}, // MA# ( 𝛪 → l ) MATHEMATICAL ITALIC CAPITAL IOTA → LATIN SMALL LETTER L# →Ι→ + {120555, "K"}, // MA# ( 𝛫 → K ) MATHEMATICAL ITALIC CAPITAL KAPPA → LATIN CAPITAL LETTER K# →𝐾→ + {120557, "M"}, // MA# ( 𝛭 → M ) MATHEMATICAL ITALIC CAPITAL MU → LATIN CAPITAL LETTER M# →𝑀→ + {120558, "N"}, // MA# ( 𝛮 → N ) MATHEMATICAL ITALIC CAPITAL NU → LATIN CAPITAL LETTER N# →𝑁→ + {120560, "O"}, // MA# ( 𝛰 → O ) MATHEMATICAL ITALIC CAPITAL OMICRON → LATIN CAPITAL LETTER O# →𝑂→ + {120562, "P"}, // MA# ( 𝛲 → P ) MATHEMATICAL ITALIC CAPITAL RHO → LATIN CAPITAL LETTER P# →Ρ→ + {120565, "T"}, // MA# ( 𝛵 → T ) MATHEMATICAL ITALIC CAPITAL TAU → LATIN CAPITAL LETTER T# →Τ→ + {120566, "Y"}, // MA# ( 𝛶 → Y ) MATHEMATICAL ITALIC CAPITAL UPSILON → LATIN CAPITAL LETTER Y# →Υ→ + {120568, "X"}, // MA# ( 𝛸 → X ) MATHEMATICAL ITALIC CAPITAL CHI → LATIN CAPITAL LETTER X# →Χ→ + {120572, "a"}, // MA# ( 𝛼 → a ) MATHEMATICAL ITALIC SMALL ALPHA → LATIN SMALL LETTER A# →α→ + {120574, "y"}, // MA# ( 𝛾 → y ) MATHEMATICAL ITALIC SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {120580, "i"}, // MA# ( 𝜄 → i ) MATHEMATICAL ITALIC SMALL IOTA → LATIN SMALL LETTER I# →ι→ + {120584, "v"}, // MA# ( 𝜈 → v ) MATHEMATICAL ITALIC SMALL NU → LATIN SMALL LETTER V# →ν→ + {120586, "o"}, // MA# ( 𝜊 → o ) MATHEMATICAL ITALIC SMALL OMICRON → LATIN SMALL LETTER O# →𝑜→ + {120588, "p"}, // MA# ( 𝜌 → p ) MATHEMATICAL ITALIC SMALL RHO → LATIN SMALL LETTER P# →ρ→ + {120590, "o"}, // MA# ( 𝜎 → o ) MATHEMATICAL ITALIC SMALL SIGMA → LATIN SMALL LETTER O# →σ→ + {120592, "u"}, // MA# ( 𝜐 → u ) MATHEMATICAL ITALIC SMALL UPSILON → LATIN SMALL LETTER U# →υ→→ʋ→ + {120602, "p"}, // MA# ( 𝜚 → p ) MATHEMATICAL ITALIC RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {120604, "A"}, // MA# ( 𝜜 → A ) MATHEMATICAL BOLD ITALIC CAPITAL ALPHA → LATIN CAPITAL LETTER A# →Α→ + {120605, "B"}, // MA# ( 𝜝 → B ) MATHEMATICAL BOLD ITALIC CAPITAL BETA → LATIN CAPITAL LETTER B# →Β→ + {120608, "E"}, // MA# ( 𝜠 → E ) MATHEMATICAL BOLD ITALIC CAPITAL EPSILON → LATIN CAPITAL LETTER E# →Ε→ + {120609, "Z"}, // MA# ( 𝜡 → Z ) MATHEMATICAL BOLD ITALIC CAPITAL ZETA → LATIN CAPITAL LETTER Z# →Ζ→ + {120610, "H"}, // MA# ( 𝜢 → H ) MATHEMATICAL BOLD ITALIC CAPITAL ETA → LATIN CAPITAL LETTER H# →𝑯→ + {120612, "l"}, // MA# ( 𝜤 → l ) MATHEMATICAL BOLD ITALIC CAPITAL IOTA → LATIN SMALL LETTER L# →Ι→ + {120613, "K"}, // MA# ( 𝜥 → K ) MATHEMATICAL BOLD ITALIC CAPITAL KAPPA → LATIN CAPITAL LETTER K# →𝑲→ + {120615, "M"}, // MA# ( 𝜧 → M ) MATHEMATICAL BOLD ITALIC CAPITAL MU → LATIN CAPITAL LETTER M# →𝑴→ + {120616, "N"}, // MA# ( 𝜨 → N ) MATHEMATICAL BOLD ITALIC CAPITAL NU → LATIN CAPITAL LETTER N# →𝑵→ + {120618, "O"}, // MA# ( 𝜪 → O ) MATHEMATICAL BOLD ITALIC CAPITAL OMICRON → LATIN CAPITAL LETTER O# →𝑶→ + {120620, "P"}, // MA# ( 𝜬 → P ) MATHEMATICAL BOLD ITALIC CAPITAL RHO → LATIN CAPITAL LETTER P# →Ρ→ + {120623, "T"}, // MA# ( 𝜯 → T ) MATHEMATICAL BOLD ITALIC CAPITAL TAU → LATIN CAPITAL LETTER T# →Τ→ + {120624, "Y"}, // MA# ( 𝜰 → Y ) MATHEMATICAL BOLD ITALIC CAPITAL UPSILON → LATIN CAPITAL LETTER Y# →Υ→ + {120626, "X"}, // MA# ( 𝜲 → X ) MATHEMATICAL BOLD ITALIC CAPITAL CHI → LATIN CAPITAL LETTER X# →𝑿→ + {120630, "a"}, // MA# ( 𝜶 → a ) MATHEMATICAL BOLD ITALIC SMALL ALPHA → LATIN SMALL LETTER A# →α→ + {120632, "y"}, // MA# ( 𝜸 → y ) MATHEMATICAL BOLD ITALIC SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {120638, "i"}, // MA# ( 𝜾 → i ) MATHEMATICAL BOLD ITALIC SMALL IOTA → LATIN SMALL LETTER I# →ι→ + {120642, "v"}, // MA# ( 𝝂 → v ) MATHEMATICAL BOLD ITALIC SMALL NU → LATIN SMALL LETTER V# →ν→ + {120644, "o"}, // MA# ( 𝝄 → o ) MATHEMATICAL BOLD ITALIC SMALL OMICRON → LATIN SMALL LETTER O# →𝒐→ + {120646, "p"}, // MA# ( 𝝆 → p ) MATHEMATICAL BOLD ITALIC SMALL RHO → LATIN SMALL LETTER P# →ρ→ + {120648, "o"}, // MA# ( 𝝈 → o ) MATHEMATICAL BOLD ITALIC SMALL SIGMA → LATIN SMALL LETTER O# →σ→ + {120650, "u"}, // MA# ( 𝝊 → u ) MATHEMATICAL BOLD ITALIC SMALL UPSILON → LATIN SMALL LETTER U# →υ→→ʋ→ + {120660, "p"}, // MA# ( 𝝔 → p ) MATHEMATICAL BOLD ITALIC RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {120662, "A"}, // MA# ( 𝝖 → A ) MATHEMATICAL SANS-SERIF BOLD CAPITAL ALPHA → LATIN CAPITAL LETTER A# →Α→ + {120663, "B"}, // MA# ( 𝝗 → B ) MATHEMATICAL SANS-SERIF BOLD CAPITAL BETA → LATIN CAPITAL LETTER B# →Β→ + {120666, "E"}, // MA# ( 𝝚 → E ) MATHEMATICAL SANS-SERIF BOLD CAPITAL EPSILON → LATIN CAPITAL LETTER E# →Ε→ + {120667, "Z"}, // MA# ( 𝝛 → Z ) MATHEMATICAL SANS-SERIF BOLD CAPITAL ZETA → LATIN CAPITAL LETTER Z# →Ζ→ + {120668, "H"}, // MA# ( 𝝜 → H ) MATHEMATICAL SANS-SERIF BOLD CAPITAL ETA → LATIN CAPITAL LETTER H# →Η→ + {120670, "l"}, // MA# ( 𝝞 → l ) MATHEMATICAL SANS-SERIF BOLD CAPITAL IOTA → LATIN SMALL LETTER L# →Ι→ + {120671, "K"}, // MA# ( 𝝟 → K ) MATHEMATICAL SANS-SERIF BOLD CAPITAL KAPPA → LATIN CAPITAL LETTER K# →Κ→ + {120673, "M"}, // MA# ( 𝝡 → M ) MATHEMATICAL SANS-SERIF BOLD CAPITAL MU → LATIN CAPITAL LETTER M# →Μ→ + {120674, "N"}, // MA# ( 𝝢 → N ) MATHEMATICAL SANS-SERIF BOLD CAPITAL NU → LATIN CAPITAL LETTER N# →Ν→ + {120676, "O"}, // MA# ( 𝝤 → O ) MATHEMATICAL SANS-SERIF BOLD CAPITAL OMICRON → LATIN CAPITAL LETTER O# →Ο→ + {120678, "P"}, // MA# ( 𝝦 → P ) MATHEMATICAL SANS-SERIF BOLD CAPITAL RHO → LATIN CAPITAL LETTER P# →Ρ→ + {120681, "T"}, // MA# ( 𝝩 → T ) MATHEMATICAL SANS-SERIF BOLD CAPITAL TAU → LATIN CAPITAL LETTER T# →Τ→ + {120682, "Y"}, // MA# ( 𝝪 → Y ) MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON → LATIN CAPITAL LETTER Y# →Υ→ + {120684, "X"}, // MA# ( 𝝬 → X ) MATHEMATICAL SANS-SERIF BOLD CAPITAL CHI → LATIN CAPITAL LETTER X# →Χ→ + {120688, "a"}, // MA# ( 𝝰 → a ) MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA → LATIN SMALL LETTER A# →α→ + {120690, "y"}, // MA# ( 𝝲 → y ) MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {120696, "i"}, // MA# ( 𝝸 → i ) MATHEMATICAL SANS-SERIF BOLD SMALL IOTA → LATIN SMALL LETTER I# →ι→ + {120700, "v"}, // MA# ( 𝝼 → v ) MATHEMATICAL SANS-SERIF BOLD SMALL NU → LATIN SMALL LETTER V# →ν→ + {120702, "o"}, // MA# ( 𝝾 → o ) MATHEMATICAL SANS-SERIF BOLD SMALL OMICRON → LATIN SMALL LETTER O# →ο→ + {120704, "p"}, // MA# ( 𝞀 → p ) MATHEMATICAL SANS-SERIF BOLD SMALL RHO → LATIN SMALL LETTER P# →ρ→ + {120706, "o"}, // MA# ( 𝞂 → o ) MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA → LATIN SMALL LETTER O# →σ→ + {120708, "u"}, // MA# ( 𝞄 → u ) MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON → LATIN SMALL LETTER U# →υ→→ʋ→ + {120718, "p"}, // MA# ( 𝞎 → p ) MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {120720, "A"}, // MA# ( 𝞐 → A ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ALPHA → LATIN CAPITAL LETTER A# →Α→ + {120721, "B"}, // MA# ( 𝞑 → B ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL BETA → LATIN CAPITAL LETTER B# →Β→ + {120724, "E"}, // MA# ( 𝞔 → E ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL EPSILON → LATIN CAPITAL LETTER E# →Ε→ + {120725, "Z"}, // MA# ( 𝞕 → Z ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ZETA → LATIN CAPITAL LETTER Z# →Ζ→ + {120726, "H"}, // MA# ( 𝞖 → H ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ETA → LATIN CAPITAL LETTER H# →Η→ + {120728, "l"}, // MA# ( 𝞘 → l ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL IOTA → LATIN SMALL LETTER L# →Ι→ + {120729, "K"}, // MA# ( 𝞙 → K ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL KAPPA → LATIN CAPITAL LETTER K# →Κ→ + {120731, "M"}, // MA# ( 𝞛 → M ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL MU → LATIN CAPITAL LETTER M# →Μ→ + {120732, "N"}, // MA# ( 𝞜 → N ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL NU → LATIN CAPITAL LETTER N# →Ν→ + {120734, "O"}, // MA# ( 𝞞 → O ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMICRON → LATIN CAPITAL LETTER O# →Ο→ + {120736, "P"}, // MA# ( 𝞠 → P ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL RHO → LATIN CAPITAL LETTER P# →Ρ→ + {120739, "T"}, // MA# ( 𝞣 → T ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL TAU → LATIN CAPITAL LETTER T# →Τ→ + {120740, "Y"}, // MA# ( 𝞤 → Y ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON → LATIN CAPITAL LETTER Y# →Υ→ + {120742, "X"}, // MA# ( 𝞦 → X ) MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL CHI → LATIN CAPITAL LETTER X# →Χ→ + {120746, "a"}, // MA# ( 𝞪 → a ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA → LATIN SMALL LETTER A# →α→ + {120748, "y"}, // MA# ( 𝞬 → y ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA → LATIN SMALL LETTER Y# →γ→ + {120754, "i"}, // MA# ( 𝞲 → i ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA → LATIN SMALL LETTER I# →ι→ + {120758, "v"}, // MA# ( 𝞶 → v ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU → LATIN SMALL LETTER V# →ν→ + {120760, "o"}, // MA# ( 𝞸 → o ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMICRON → LATIN SMALL LETTER O# →ο→ + {120762, "p"}, // MA# ( 𝞺 → p ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO → LATIN SMALL LETTER P# →ρ→ + {120764, "o"}, // MA# ( 𝞼 → o ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA → LATIN SMALL LETTER O# →σ→ + {120766, "u"}, // MA# ( 𝞾 → u ) MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON → LATIN SMALL LETTER U# →υ→→ʋ→ + {120776, "p"}, // MA# ( 𝟈 → p ) MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL → LATIN SMALL LETTER P# →ρ→ + {120778, "F"}, // MA# ( 𝟊 → F ) MATHEMATICAL BOLD CAPITAL DIGAMMA → LATIN CAPITAL LETTER F# →Ϝ→ + {120782, "O"}, // MA# ( 𝟎 → O ) MATHEMATICAL BOLD DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {120783, "l"}, // MA# ( 𝟏 → l ) MATHEMATICAL BOLD DIGIT ONE → LATIN SMALL LETTER L# →1→ + {120784, "2"}, // MA# ( 𝟐 → 2 ) MATHEMATICAL BOLD DIGIT TWO → DIGIT TWO# + {120785, "3"}, // MA# ( 𝟑 → 3 ) MATHEMATICAL BOLD DIGIT THREE → DIGIT THREE# + {120786, "4"}, // MA# ( 𝟒 → 4 ) MATHEMATICAL BOLD DIGIT FOUR → DIGIT FOUR# + {120787, "5"}, // MA# ( 𝟓 → 5 ) MATHEMATICAL BOLD DIGIT FIVE → DIGIT FIVE# + {120788, "6"}, // MA# ( 𝟔 → 6 ) MATHEMATICAL BOLD DIGIT SIX → DIGIT SIX# + {120789, "7"}, // MA# ( 𝟕 → 7 ) MATHEMATICAL BOLD DIGIT SEVEN → DIGIT SEVEN# + {120790, "8"}, // MA# ( 𝟖 → 8 ) MATHEMATICAL BOLD DIGIT EIGHT → DIGIT EIGHT# + {120791, "9"}, // MA# ( 𝟗 → 9 ) MATHEMATICAL BOLD DIGIT NINE → DIGIT NINE# + {120792, "O"}, // MA# ( 𝟘 → O ) MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {120793, "l"}, // MA# ( 𝟙 → l ) MATHEMATICAL DOUBLE-STRUCK DIGIT ONE → LATIN SMALL LETTER L# →1→ + {120794, "2"}, // MA# ( 𝟚 → 2 ) MATHEMATICAL DOUBLE-STRUCK DIGIT TWO → DIGIT TWO# + {120795, "3"}, // MA# ( 𝟛 → 3 ) MATHEMATICAL DOUBLE-STRUCK DIGIT THREE → DIGIT THREE# + {120796, "4"}, // MA# ( 𝟜 → 4 ) MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR → DIGIT FOUR# + {120797, "5"}, // MA# ( 𝟝 → 5 ) MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE → DIGIT FIVE# + {120798, "6"}, // MA# ( 𝟞 → 6 ) MATHEMATICAL DOUBLE-STRUCK DIGIT SIX → DIGIT SIX# + {120799, "7"}, // MA# ( 𝟟 → 7 ) MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN → DIGIT SEVEN# + {120800, "8"}, // MA# ( 𝟠 → 8 ) MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT → DIGIT EIGHT# + {120801, "9"}, // MA# ( 𝟡 → 9 ) MATHEMATICAL DOUBLE-STRUCK DIGIT NINE → DIGIT NINE# + {120802, "O"}, // MA# ( 𝟢 → O ) MATHEMATICAL SANS-SERIF DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {120803, "l"}, // MA# ( 𝟣 → l ) MATHEMATICAL SANS-SERIF DIGIT ONE → LATIN SMALL LETTER L# →1→ + {120804, "2"}, // MA# ( 𝟤 → 2 ) MATHEMATICAL SANS-SERIF DIGIT TWO → DIGIT TWO# + {120805, "3"}, // MA# ( 𝟥 → 3 ) MATHEMATICAL SANS-SERIF DIGIT THREE → DIGIT THREE# + {120806, "4"}, // MA# ( 𝟦 → 4 ) MATHEMATICAL SANS-SERIF DIGIT FOUR → DIGIT FOUR# + {120807, "5"}, // MA# ( 𝟧 → 5 ) MATHEMATICAL SANS-SERIF DIGIT FIVE → DIGIT FIVE# + {120808, "6"}, // MA# ( 𝟨 → 6 ) MATHEMATICAL SANS-SERIF DIGIT SIX → DIGIT SIX# + {120809, "7"}, // MA# ( 𝟩 → 7 ) MATHEMATICAL SANS-SERIF DIGIT SEVEN → DIGIT SEVEN# + {120810, "8"}, // MA# ( 𝟪 → 8 ) MATHEMATICAL SANS-SERIF DIGIT EIGHT → DIGIT EIGHT# + {120811, "9"}, // MA# ( 𝟫 → 9 ) MATHEMATICAL SANS-SERIF DIGIT NINE → DIGIT NINE# + {120812, "O"}, // MA# ( 𝟬 → O ) MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {120813, "l"}, // MA# ( 𝟭 → l ) MATHEMATICAL SANS-SERIF BOLD DIGIT ONE → LATIN SMALL LETTER L# →1→ + {120814, "2"}, // MA# ( 𝟮 → 2 ) MATHEMATICAL SANS-SERIF BOLD DIGIT TWO → DIGIT TWO# + {120815, "3"}, // MA# ( 𝟯 → 3 ) MATHEMATICAL SANS-SERIF BOLD DIGIT THREE → DIGIT THREE# + {120816, "4"}, // MA# ( 𝟰 → 4 ) MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR → DIGIT FOUR# + {120817, "5"}, // MA# ( 𝟱 → 5 ) MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE → DIGIT FIVE# + {120818, "6"}, // MA# ( 𝟲 → 6 ) MATHEMATICAL SANS-SERIF BOLD DIGIT SIX → DIGIT SIX# + {120819, "7"}, // MA# ( 𝟳 → 7 ) MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN → DIGIT SEVEN# + {120820, "8"}, // MA# ( 𝟴 → 8 ) MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT → DIGIT EIGHT# + {120821, "9"}, // MA# ( 𝟵 → 9 ) MATHEMATICAL SANS-SERIF BOLD DIGIT NINE → DIGIT NINE# + {120822, "O"}, // MA# ( 𝟶 → O ) MATHEMATICAL MONOSPACE DIGIT ZERO → LATIN CAPITAL LETTER O# →0→ + {120823, "l"}, // MA# ( 𝟷 → l ) MATHEMATICAL MONOSPACE DIGIT ONE → LATIN SMALL LETTER L# →1→ + {120824, "2"}, // MA# ( 𝟸 → 2 ) MATHEMATICAL MONOSPACE DIGIT TWO → DIGIT TWO# + {120825, "3"}, // MA# ( 𝟹 → 3 ) MATHEMATICAL MONOSPACE DIGIT THREE → DIGIT THREE# + {120826, "4"}, // MA# ( 𝟺 → 4 ) MATHEMATICAL MONOSPACE DIGIT FOUR → DIGIT FOUR# + {120827, "5"}, // MA# ( 𝟻 → 5 ) MATHEMATICAL MONOSPACE DIGIT FIVE → DIGIT FIVE# + {120828, "6"}, // MA# ( 𝟼 → 6 ) MATHEMATICAL MONOSPACE DIGIT SIX → DIGIT SIX# + {120829, "7"}, // MA# ( 𝟽 → 7 ) MATHEMATICAL MONOSPACE DIGIT SEVEN → DIGIT SEVEN# + {120830, "8"}, // MA# ( 𝟾 → 8 ) MATHEMATICAL MONOSPACE DIGIT EIGHT → DIGIT EIGHT# + {120831, "9"}, // MA# ( 𝟿 → 9 ) MATHEMATICAL MONOSPACE DIGIT NINE → DIGIT NINE# + {125127, "l"}, // MA#* ( ‎𞣇‎ → l ) MENDE KIKAKUI DIGIT ONE → LATIN SMALL LETTER L# + {125131, "8"}, // MA#* ( ‎𞣋‎ → 8 ) MENDE KIKAKUI DIGIT FIVE → DIGIT EIGHT# + {126464, "l"}, // MA# ( ‎𞸀‎ → l ) ARABIC MATHEMATICAL ALEF → LATIN SMALL LETTER L# →‎ا‎→→1→ + {126500, "o"}, // MA# ( ‎𞸤‎ → o ) ARABIC MATHEMATICAL INITIAL HEH → LATIN SMALL LETTER O# →‎ه‎→ + {126564, "o"}, // MA# ( ‎𞹤‎ → o ) ARABIC MATHEMATICAL STRETCHED HEH → LATIN SMALL LETTER O# →‎ه‎→ + {126592, "l"}, // MA# ( ‎𞺀‎ → l ) ARABIC MATHEMATICAL LOOPED ALEF → LATIN SMALL LETTER L# →‎ا‎→→1→ + {126596, "o"}, // MA# ( ‎𞺄‎ → o ) ARABIC MATHEMATICAL LOOPED HEH → LATIN SMALL LETTER O# →‎ه‎→ + {127232, "O."}, // MA#* ( 🄀 → O. ) DIGIT ZERO FULL STOP → LATIN CAPITAL LETTER O, FULL STOP# →0.→ + {127233, "O,"}, // MA#* ( 🄁 → O, ) DIGIT ZERO COMMA → LATIN CAPITAL LETTER O, COMMA# →0,→ + {127234, "l,"}, // MA#* ( 🄂 → l, ) DIGIT ONE COMMA → LATIN SMALL LETTER L, COMMA# →1,→ + {127235, "2,"}, // MA#* ( 🄃 → 2, ) DIGIT TWO COMMA → DIGIT TWO, COMMA# + {127236, "3,"}, // MA#* ( 🄄 → 3, ) DIGIT THREE COMMA → DIGIT THREE, COMMA# + {127237, "4,"}, // MA#* ( 🄅 → 4, ) DIGIT FOUR COMMA → DIGIT FOUR, COMMA# + {127238, "5,"}, // MA#* ( 🄆 → 5, ) DIGIT FIVE COMMA → DIGIT FIVE, COMMA# + {127239, "6,"}, // MA#* ( 🄇 → 6, ) DIGIT SIX COMMA → DIGIT SIX, COMMA# + {127240, "7,"}, // MA#* ( 🄈 → 7, ) DIGIT SEVEN COMMA → DIGIT SEVEN, COMMA# + {127241, "8,"}, // MA#* ( 🄉 → 8, ) DIGIT EIGHT COMMA → DIGIT EIGHT, COMMA# + {127242, "9,"}, // MA#* ( 🄊 → 9, ) DIGIT NINE COMMA → DIGIT NINE, COMMA# + {127248, "(A)"}, // MA#* ( 🄐 → (A) ) PARENTHESIZED LATIN CAPITAL LETTER A → LEFT PARENTHESIS, LATIN CAPITAL LETTER A, RIGHT PARENTHESIS# + {127249, "(B)"}, // MA#* ( 🄑 → (B) ) PARENTHESIZED LATIN CAPITAL LETTER B → LEFT PARENTHESIS, LATIN CAPITAL LETTER B, RIGHT PARENTHESIS# + {127250, "(C)"}, // MA#* ( 🄒 → (C) ) PARENTHESIZED LATIN CAPITAL LETTER C → LEFT PARENTHESIS, LATIN CAPITAL LETTER C, RIGHT PARENTHESIS# + {127251, "(D)"}, // MA#* ( 🄓 → (D) ) PARENTHESIZED LATIN CAPITAL LETTER D → LEFT PARENTHESIS, LATIN CAPITAL LETTER D, RIGHT PARENTHESIS# + {127252, "(E)"}, // MA#* ( 🄔 → (E) ) PARENTHESIZED LATIN CAPITAL LETTER E → LEFT PARENTHESIS, LATIN CAPITAL LETTER E, RIGHT PARENTHESIS# + {127253, "(F)"}, // MA#* ( 🄕 → (F) ) PARENTHESIZED LATIN CAPITAL LETTER F → LEFT PARENTHESIS, LATIN CAPITAL LETTER F, RIGHT PARENTHESIS# + {127254, "(G)"}, // MA#* ( 🄖 → (G) ) PARENTHESIZED LATIN CAPITAL LETTER G → LEFT PARENTHESIS, LATIN CAPITAL LETTER G, RIGHT PARENTHESIS# + {127255, "(H)"}, // MA#* ( 🄗 → (H) ) PARENTHESIZED LATIN CAPITAL LETTER H → LEFT PARENTHESIS, LATIN CAPITAL LETTER H, RIGHT PARENTHESIS# + {127256, "(l)"}, // MA#* ( 🄘 → (l) ) PARENTHESIZED LATIN CAPITAL LETTER I → LEFT PARENTHESIS, LATIN SMALL LETTER L, RIGHT PARENTHESIS# →(I)→ + {127257, "(J)"}, // MA#* ( 🄙 → (J) ) PARENTHESIZED LATIN CAPITAL LETTER J → LEFT PARENTHESIS, LATIN CAPITAL LETTER J, RIGHT PARENTHESIS# + {127258, "(K)"}, // MA#* ( 🄚 → (K) ) PARENTHESIZED LATIN CAPITAL LETTER K → LEFT PARENTHESIS, LATIN CAPITAL LETTER K, RIGHT PARENTHESIS# + {127259, "(L)"}, // MA#* ( 🄛 → (L) ) PARENTHESIZED LATIN CAPITAL LETTER L → LEFT PARENTHESIS, LATIN CAPITAL LETTER L, RIGHT PARENTHESIS# + {127260, "(M)"}, // MA#* ( 🄜 → (M) ) PARENTHESIZED LATIN CAPITAL LETTER M → LEFT PARENTHESIS, LATIN CAPITAL LETTER M, RIGHT PARENTHESIS# + {127261, "(N)"}, // MA#* ( 🄝 → (N) ) PARENTHESIZED LATIN CAPITAL LETTER N → LEFT PARENTHESIS, LATIN CAPITAL LETTER N, RIGHT PARENTHESIS# + {127262, "(O)"}, // MA#* ( 🄞 → (O) ) PARENTHESIZED LATIN CAPITAL LETTER O → LEFT PARENTHESIS, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS# + {127263, "(P)"}, // MA#* ( 🄟 → (P) ) PARENTHESIZED LATIN CAPITAL LETTER P → LEFT PARENTHESIS, LATIN CAPITAL LETTER P, RIGHT PARENTHESIS# + {127264, "(Q)"}, // MA#* ( 🄠 → (Q) ) PARENTHESIZED LATIN CAPITAL LETTER Q → LEFT PARENTHESIS, LATIN CAPITAL LETTER Q, RIGHT PARENTHESIS# + {127265, "(R)"}, // MA#* ( 🄡 → (R) ) PARENTHESIZED LATIN CAPITAL LETTER R → LEFT PARENTHESIS, LATIN CAPITAL LETTER R, RIGHT PARENTHESIS# + {127266, "(S)"}, // MA#* ( 🄢 → (S) ) PARENTHESIZED LATIN CAPITAL LETTER S → LEFT PARENTHESIS, LATIN CAPITAL LETTER S, RIGHT PARENTHESIS# + {127267, "(T)"}, // MA#* ( 🄣 → (T) ) PARENTHESIZED LATIN CAPITAL LETTER T → LEFT PARENTHESIS, LATIN CAPITAL LETTER T, RIGHT PARENTHESIS# + {127268, "(U)"}, // MA#* ( 🄤 → (U) ) PARENTHESIZED LATIN CAPITAL LETTER U → LEFT PARENTHESIS, LATIN CAPITAL LETTER U, RIGHT PARENTHESIS# + {127269, "(V)"}, // MA#* ( 🄥 → (V) ) PARENTHESIZED LATIN CAPITAL LETTER V → LEFT PARENTHESIS, LATIN CAPITAL LETTER V, RIGHT PARENTHESIS# + {127270, "(W)"}, // MA#* ( 🄦 → (W) ) PARENTHESIZED LATIN CAPITAL LETTER W → LEFT PARENTHESIS, LATIN CAPITAL LETTER W, RIGHT PARENTHESIS# + {127271, "(X)"}, // MA#* ( 🄧 → (X) ) PARENTHESIZED LATIN CAPITAL LETTER X → LEFT PARENTHESIS, LATIN CAPITAL LETTER X, RIGHT PARENTHESIS# + {127272, "(Y)"}, // MA#* ( 🄨 → (Y) ) PARENTHESIZED LATIN CAPITAL LETTER Y → LEFT PARENTHESIS, LATIN CAPITAL LETTER Y, RIGHT PARENTHESIS# + {127273, "(Z)"}, // MA#* ( 🄩 → (Z) ) PARENTHESIZED LATIN CAPITAL LETTER Z → LEFT PARENTHESIS, LATIN CAPITAL LETTER Z, RIGHT PARENTHESIS# + {127274, "(S)"}, // MA#* ( 🄪 → (S) ) TORTOISE SHELL BRACKETED LATIN CAPITAL LETTER S → LEFT PARENTHESIS, LATIN CAPITAL LETTER S, RIGHT PARENTHESIS# →〔S〕→ + {128768, "QE"}, // MA#* ( 🜀 → QE ) ALCHEMICAL SYMBOL FOR QUINTESSENCE → LATIN CAPITAL LETTER Q, LATIN CAPITAL LETTER E# + {128775, "AR"}, // MA#* ( 🜇 → AR ) ALCHEMICAL SYMBOL FOR AQUA REGIA-2 → LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER R# + {128844, "C"}, // MA#* ( 🝌 → C ) ALCHEMICAL SYMBOL FOR CALX → LATIN CAPITAL LETTER C# + {128860, "sss"}, // MA#* ( 🝜 → sss ) ALCHEMICAL SYMBOL FOR STRATUM SUPER STRATUM → LATIN SMALL LETTER S, LATIN SMALL LETTER S, LATIN SMALL LETTER S# + {128872, "T"}, // MA#* ( 🝨 → T ) ALCHEMICAL SYMBOL FOR CRUCIBLE-4 → LATIN CAPITAL LETTER T# + {128875, "MB"}, // MA#* ( 🝫 → MB ) ALCHEMICAL SYMBOL FOR BATH OF MARY → LATIN CAPITAL LETTER M, LATIN CAPITAL LETTER B# + {128876, "VB"}, // MA#* ( 🝬 → VB ) ALCHEMICAL SYMBOL FOR BATH OF VAPOURS → LATIN CAPITAL LETTER V, LATIN CAPITAL LETTER B# +}; +// clang-format on + +const char* findConfusable(uint32_t codepoint) +{ + auto it = std::lower_bound( + std::begin(kConfusables), + std::end(kConfusables), + codepoint, + [](const Confusable& lhs, uint32_t rhs) + { + return lhs.codepoint < rhs; + } + ); + + return (it != std::end(kConfusables) && it->codepoint == codepoint) ? it->text : nullptr; +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Cst.cpp b/External/luau/Ast/src/Cst.cpp new file mode 100644 index 00000000..16bf1bfc --- /dev/null +++ b/External/luau/Ast/src/Cst.cpp @@ -0,0 +1,358 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Ast.h" +#include "Luau/Cst.h" +#include "Luau/Common.h" + +namespace Luau +{ + +int gCstRttiIndex = 0; + +CstAttr::CstAttr(bool hasAt) + : CstNode(CstClassIndex()) + , hasAt(hasAt) +{ +} + +CstParametrizedAttr::CstParametrizedAttr(Position openParenPosition, Position closeParenPosition, AstArray argsCommaPositions) + : CstNode(CstClassIndex()) + , openParenPosition(openParenPosition) + , closeParenPosition(closeParenPosition) + , argsCommaPositions(argsCommaPositions) +{ +} + +CstAttrList::CstAttrList(Position atBracketPosition, Position closeBracketPosition, AstArray commaPositions) + : atBracketPosition(atBracketPosition) + , closeBracketPosition(closeBracketPosition) + , commaPositions(commaPositions) +{ +} + +CstExprGroup::CstExprGroup(Position closePosition) + : CstNode(CstClassIndex()) + , closePosition(closePosition) +{ +} + +CstExprConstantNumber::CstExprConstantNumber(const AstArray& value) + : CstNode(CstClassIndex()) + , value(value) +{ +} + +CstExprConstantString::CstExprConstantString(AstArray sourceString, QuoteStyle quoteStyle, unsigned int blockDepth) + : CstNode(CstClassIndex()) + , sourceString(sourceString) + , quoteStyle(quoteStyle) + , blockDepth(blockDepth) +{ + LUAU_ASSERT(blockDepth == 0 || quoteStyle == QuoteStyle::QuotedRaw); +} + +CstExprConstantInteger::CstExprConstantInteger(const AstArray& value) + : CstNode(CstClassIndex()) + , value(value) +{ +} + +CstExprCall::CstExprCall(Position openParens, Position closeParens, AstArray commaPositions) + : CstNode(CstClassIndex()) + , openParens(openParens) + , closeParens(closeParens) + , commaPositions(commaPositions) +{ +} + +CstExprIndexExpr::CstExprIndexExpr(Position openBracketPosition, Position closeBracketPosition) + : CstNode(CstClassIndex()) + , openBracketPosition(openBracketPosition) + , closeBracketPosition(closeBracketPosition) +{ +} + +CstExprFunction::CstExprFunction() + : CstNode(CstClassIndex()) +{ +} + +CstExprTable::CstExprTable(const AstArray& items) + : CstNode(CstClassIndex()) + , items(items) +{ +} + +CstExprOp::CstExprOp(Position opPosition) + : CstNode(CstClassIndex()) + , opPosition(opPosition) +{ +} + +CstExprTypeAssertion::CstExprTypeAssertion(Position opPosition) + : CstNode(CstClassIndex()) + , opPosition(opPosition) +{ +} + +CstExprIfElse::CstExprIfElse(Position thenPosition, Position elsePosition, bool isElseIf, Position annotationColonPosition) + : CstNode(CstClassIndex()) + , thenPosition(thenPosition) + , elsePosition(elsePosition) + , isElseIf(isElseIf) + , annotationColonPosition(annotationColonPosition) +{ +} + +CstExprInterpString::CstExprInterpString(AstArray> sourceStrings, AstArray stringPositions) + : CstNode(CstClassIndex()) + , sourceStrings(sourceStrings) + , stringPositions(stringPositions) +{ +} + +CstExprExplicitTypeInstantiation::CstExprExplicitTypeInstantiation(CstTypeInstantiation instantiation) + : CstNode(CstClassIndex()) + , instantiation(instantiation) +{ +} + +CstStatDo::CstStatDo(Position statsStartPosition, Position endPosition) + : CstNode(CstClassIndex()) + , statsStartPosition(statsStartPosition) + , endPosition(endPosition) +{ +} + +CstStatRepeat::CstStatRepeat(Position untilPosition) + : CstNode(CstClassIndex()) + , untilPosition(untilPosition) +{ +} + +CstStatReturn::CstStatReturn(AstArray commaPositions) + : CstNode(CstClassIndex()) + , commaPositions(commaPositions) +{ +} + +CstStatLocal::CstStatLocal( + AstArray varsAnnotationColonPositions, + AstArray varsCommaPositions, + AstArray valuesCommaPositions +) + : CstNode(CstClassIndex()) + , varsAnnotationColonPositions(varsAnnotationColonPositions) + , varsCommaPositions(varsCommaPositions) + , valuesCommaPositions(valuesCommaPositions) +{ +} + +CstStatFor::CstStatFor(Position annotationColonPosition, Position equalsPosition, Position endCommaPosition, Position stepCommaPosition) + : CstNode(CstClassIndex()) + , annotationColonPosition(annotationColonPosition) + , equalsPosition(equalsPosition) + , endCommaPosition(endCommaPosition) + , stepCommaPosition(stepCommaPosition) +{ +} + +CstStatForIn::CstStatForIn( + AstArray varsAnnotationColonPositions, + AstArray varsCommaPositions, + AstArray valuesCommaPositions +) + : CstNode(CstClassIndex()) + , varsAnnotationColonPositions(varsAnnotationColonPositions) + , varsCommaPositions(varsCommaPositions) + , valuesCommaPositions(valuesCommaPositions) +{ +} + +CstStatIf::CstStatIf(Position annotationColonPosition) + : CstNode(CstClassIndex()) + , annotationColonPosition(annotationColonPosition) +{ +} + +CstStatAssign::CstStatAssign(AstArray varsCommaPositions, Position equalsPosition, AstArray valuesCommaPositions) + : CstNode(CstClassIndex()) + , varsCommaPositions(varsCommaPositions) + , equalsPosition(equalsPosition) + , valuesCommaPositions(valuesCommaPositions) +{ +} + +CstStatCompoundAssign::CstStatCompoundAssign(Position opPosition) + : CstNode(CstClassIndex()) + , opPosition(opPosition) +{ +} + +CstStatFunction::CstStatFunction(Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists({}) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstStatFunction::CstStatFunction(AstArray attrLists, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists(attrLists) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstStatLocalFunction::CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists({}) + , localKeywordPosition(localKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstStatLocalFunction::CstStatLocalFunction(AstArray attrLists, Position localKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , attrLists(attrLists) + , localKeywordPosition(localKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstGenericType::CstGenericType(Position defaultEqualsPosition) + : CstNode(CstClassIndex()) + , defaultEqualsPosition(defaultEqualsPosition) +{ +} + +CstGenericTypePack::CstGenericTypePack(Position ellipsisPosition, Position defaultEqualsPosition) + : CstNode(CstClassIndex()) + , ellipsisPosition(ellipsisPosition) + , defaultEqualsPosition(defaultEqualsPosition) +{ +} + +CstStatTypeAlias::CstStatTypeAlias( + Position typeKeywordPosition, + Position genericsOpenPosition, + AstArray genericsCommaPositions, + Position genericsClosePosition, + Position equalsPosition +) + : CstNode(CstClassIndex()) + , typeKeywordPosition(typeKeywordPosition) + , genericsOpenPosition(genericsOpenPosition) + , genericsCommaPositions(genericsCommaPositions) + , genericsClosePosition(genericsClosePosition) + , equalsPosition(equalsPosition) +{ +} + +CstStatTypeFunction::CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , typeKeywordPosition(typeKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + +CstTypeReference::CstTypeReference( + Position prefixPointPosition, + Position openParametersPosition, + AstArray parametersCommaPositions, + Position closeParametersPosition +) + : CstNode(CstClassIndex()) + , prefixPointPosition(prefixPointPosition) + , openParametersPosition(openParametersPosition) + , parametersCommaPositions(parametersCommaPositions) + , closeParametersPosition(closeParametersPosition) +{ +} + +CstTypeTable::CstTypeTable(AstArray items, bool isArray) + : CstNode(CstClassIndex()) + , items(items) + , isArray(isArray) +{ +} + +CstTypeFunction::CstTypeFunction( + Position openGenericsPosition, + AstArray genericsCommaPositions, + Position closeGenericsPosition, + Position openArgsPosition, + AstArray argumentNameColonPositions, + AstArray argumentsCommaPositions, + Position closeArgsPosition, + Position returnArrowPosition +) + : CstNode(CstClassIndex()) + , openGenericsPosition(openGenericsPosition) + , genericsCommaPositions(genericsCommaPositions) + , closeGenericsPosition(closeGenericsPosition) + , openArgsPosition(openArgsPosition) + , argumentNameColonPositions(argumentNameColonPositions) + , argumentsCommaPositions(argumentsCommaPositions) + , closeArgsPosition(closeArgsPosition) + , returnArrowPosition(returnArrowPosition) +{ +} + +CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition) + : CstNode(CstClassIndex()) + , openPosition(openPosition) + , closePosition(closePosition) +{ +} + +CstTypeUnion::CstTypeUnion(Position leadingPosition, AstArray separatorPositions) + : CstNode(CstClassIndex()) + , leadingPosition(leadingPosition) + , separatorPositions(separatorPositions) +{ +} + +CstTypeIntersection::CstTypeIntersection(Position leadingPosition, AstArray separatorPositions) + : CstNode(CstClassIndex()) + , leadingPosition(leadingPosition) + , separatorPositions(separatorPositions) +{ +} + +CstTypeSingletonString::CstTypeSingletonString(AstArray sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth) + : CstNode(CstClassIndex()) + , sourceString(sourceString) + , quoteStyle(quoteStyle) + , blockDepth(blockDepth) +{ + LUAU_ASSERT(quoteStyle != CstExprConstantString::QuoteStyle::QuotedInterp); +} + +CstTypeGroup::CstTypeGroup(Position closePosition) + : CstNode(CstClassIndex()) + , closePosition(closePosition) +{ +} + +CstTypePackExplicit::CstTypePackExplicit() + : CstNode(CstClassIndex()) + , openParenthesesPosition(Position::missing()) + , closeParenthesesPosition(Position::missing()) + , commaPositions({}) +{ +} + +CstTypePackExplicit::CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions) + : CstNode(CstClassIndex()) + , openParenthesesPosition(openParenthesesPosition) + , closeParenthesesPosition(closeParenthesesPosition) + , commaPositions(commaPositions) +{ +} + +CstTypePackGeneric::CstTypePackGeneric(Position ellipsisPosition) + : CstNode(CstClassIndex()) + , ellipsisPosition(ellipsisPosition) +{ +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Lexer.cpp b/External/luau/Ast/src/Lexer.cpp new file mode 100644 index 00000000..4c019c16 --- /dev/null +++ b/External/luau/Ast/src/Lexer.cpp @@ -0,0 +1,1331 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Lexer.h" + +#include "Luau/Allocator.h" +#include "Luau/Common.h" +#include "Luau/Confusables.h" +#include "Luau/StringUtils.h" + +#include + +namespace Luau +{ + +Lexeme::Lexeme(const Location& location, Type type) + : type(type) + , location(location) + , length(0) + , data(nullptr) +{ +} + +Lexeme::Lexeme(const Location& location, char character) + : type(static_cast(static_cast(character))) + , location(location) + , length(0) + , data(nullptr) +{ +} + +Lexeme::Lexeme(const Location& location, Type type, const char* data, size_t size) + : type(type) + , location(location) + , length(unsigned(size)) + , data(data) +{ + LUAU_ASSERT( + type == RawString || type == QuotedString || type == InterpStringBegin || type == InterpStringMid || type == InterpStringEnd || + type == InterpStringSimple || type == BrokenInterpDoubleBrace || type == Number || type == Comment || type == BlockComment + ); +} + +Lexeme::Lexeme(const Location& location, Type type, const char* name) + : type(type) + , location(location) + , length(0) + , name(name) +{ + LUAU_ASSERT(type == Name || type == Attribute || (type >= Reserved_BEGIN && type < Lexeme::Reserved_END)); +} + +unsigned int Lexeme::getLength() const +{ + LUAU_ASSERT( + type == RawString || type == QuotedString || type == InterpStringBegin || type == InterpStringMid || type == InterpStringEnd || + type == InterpStringSimple || type == BrokenInterpDoubleBrace || type == Number || type == Comment || type == BlockComment + ); + + return length; +} + +static const char* kReserved[] = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", + "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}; + +std::string Lexeme::toString() const +{ + switch (type) + { + case Eof: + return ""; + + case Equal: + return "'=='"; + + case LessEqual: + return "'<='"; + + case GreaterEqual: + return "'>='"; + + case NotEqual: + return "'~='"; + + case Dot2: + return "'..'"; + + case Dot3: + return "'...'"; + + case SkinnyArrow: + return "'->'"; + + case DoubleColon: + return "'::'"; + + case FloorDiv: + return "'//'"; + + case AddAssign: + return "'+='"; + + case SubAssign: + return "'-='"; + + case MulAssign: + return "'*='"; + + case DivAssign: + return "'/='"; + + case FloorDivAssign: + return "'//='"; + + case ModAssign: + return "'%='"; + + case PowAssign: + return "'^='"; + + case ConcatAssign: + return "'..='"; + + case RawString: + case QuotedString: + return data ? format("\"%.*s\"", length, data) : "string"; + + case InterpStringBegin: + return data ? format("`%.*s{", length, data) : "the beginning of an interpolated string"; + + case InterpStringMid: + return data ? format("}%.*s{", length, data) : "the middle of an interpolated string"; + + case InterpStringEnd: + return data ? format("}%.*s`", length, data) : "the end of an interpolated string"; + + case InterpStringSimple: + return data ? format("`%.*s`", length, data) : "interpolated string"; + + case Number: + return data ? format("'%.*s'", length, data) : "number"; + + case Name: + return name ? format("'%s'", name) : "identifier"; + + case Comment: + return "comment"; + + case Attribute: + return name ? format("'%s'", name) : "attribute"; + + case AttributeOpen: + return "'@['"; + + case BrokenString: + return "malformed string"; + + case BrokenComment: + return "unfinished comment"; + + case BrokenInterpDoubleBrace: + return "'{{', which is invalid (did you mean '\\{'?)"; + + case BrokenUnicode: + if (codepoint) + { + if (const char* confusable = findConfusable(codepoint)) + return format("Unicode character U+%x (did you mean '%s'?)", codepoint, confusable); + + return format("Unicode character U+%x", codepoint); + } + else + { + return "invalid UTF-8 sequence"; + } + + default: + if (type < Char_END) + return format("'%c'", type); + else if (type >= Reserved_BEGIN && type < Reserved_END) + return format("'%s'", kReserved[type - Reserved_BEGIN]); + else + return ""; + } +} + +bool AstNameTable::Entry::operator==(const Entry& other) const +{ + return length == other.length && memcmp(value.value, other.value.value, length) == 0; +} + +size_t AstNameTable::EntryHash::operator()(const Entry& e) const +{ + // FNV1a + uint32_t hash = 2166136261; + + for (size_t i = 0; i < e.length; ++i) + { + hash ^= uint8_t(e.value.value[i]); + hash *= 16777619; + } + + return hash; +} + +AstNameTable::AstNameTable(Allocator& allocator) + : data(128) + , allocator(allocator) +{ + static_assert(sizeof(kReserved) / sizeof(kReserved[0]) == Lexeme::Reserved_END - Lexeme::Reserved_BEGIN); + + for (int i = Lexeme::Reserved_BEGIN; i < Lexeme::Reserved_END; ++i) + addStatic(kReserved[i - Lexeme::Reserved_BEGIN], static_cast(i)); +} + +AstName AstNameTable::addStatic(const char* name, Lexeme::Type type) +{ + AstNameTable::Entry entry = {AstName(name), uint32_t(strlen(name)), type}; + + LUAU_ASSERT(!data.contains(entry)); + data.insert(entry); + + return entry.value; +} + +std::pair AstNameTable::getOrAddWithType(const char* name, size_t length) +{ + AstNameTable::Entry key = {AstName(name), uint32_t(length), Lexeme::Eof}; + const Entry& entry = data.insert(key); + + // entry already was inserted + if (entry.type != Lexeme::Eof) + return std::make_pair(entry.value, entry.type); + + // we just inserted an entry with a non-owned pointer into the map + // we need to correct it, *but* we need to be careful about not disturbing the hash value + char* nameData = static_cast(allocator.allocate(length + 1)); + memcpy(nameData, name, length); + nameData[length] = 0; + + const_cast(entry).value = AstName(nameData); + const_cast(entry).type = (name[0] == '@' ? Lexeme::Attribute : Lexeme::Name); + + return std::make_pair(entry.value, entry.type); +} + +std::pair AstNameTable::getWithType(const char* name, size_t length) const +{ + if (const Entry* entry = data.find({AstName(name), uint32_t(length), Lexeme::Eof})) + { + return std::make_pair(entry->value, entry->type); + } + return std::make_pair(AstName(), Lexeme::Name); +} + +AstName AstNameTable::getOrAdd(const char* name, size_t len) +{ + return getOrAddWithType(name, len).first; +} + +AstName AstNameTable::getOrAdd(const char* name) +{ + return getOrAddWithType(name, strlen(name)).first; +} + +AstName AstNameTable::get(const char* name) const +{ + return getWithType(name, strlen(name)).first; +} + +inline bool isAlpha(char ch) +{ + // use or trick to convert to lower case and unsigned comparison to do range check + return unsigned((ch | ' ') - 'a') < 26; +} + +inline bool isDigit(char ch) +{ + return unsigned(ch - '0') < 10; +} + +inline bool isHexDigit(char ch) +{ + // use or trick to convert to lower case and unsigned comparison to do range check + return unsigned(ch - '0') < 10 || unsigned((ch | ' ') - 'a') < 6; +} + +inline bool isNewline(char ch) +{ + return ch == '\n'; +} + +static char unescape(char ch) +{ + switch (ch) + { + case 'a': + return '\a'; + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'v': + return '\v'; + default: + return ch; + } +} + +unsigned int Lexeme::getBlockDepth() const +{ + LUAU_ASSERT(type == Lexeme::RawString || type == Lexeme::BlockComment); + + // If we have a well-formed string, we are guaranteed to see 2 `]` characters after the end of the string contents + LUAU_ASSERT(*(data + length) == ']'); + unsigned int depth = 0; + do + { + depth++; + } while (*(data + length + depth) != ']'); + + return depth - 1; +} + +Lexeme::QuoteStyle Lexeme::getQuoteStyle() const +{ + LUAU_ASSERT(type == Lexeme::QuotedString); + + // If we have a well-formed string, we are guaranteed to see a closing delimiter after the string + LUAU_ASSERT(data); + + char quote = *(data + length); + if (quote == '\'') + return Lexeme::QuoteStyle::Single; + else if (quote == '"') + return Lexeme::QuoteStyle::Double; + + LUAU_ASSERT(!"Unknown quote style"); + return Lexeme::QuoteStyle::Double; // unreachable, but required due to compiler warning +} + +Lexer::Lexer(const char* buffer, size_t bufferSize, AstNameTable& names, Position startPosition) + : buffer(buffer) + , bufferSize(bufferSize) + , offset(0) + , line(startPosition.line) + , lineOffset(0u - startPosition.column) + , lexeme((Location(Position(startPosition.line, startPosition.column), 0)), Lexeme::Eof) + , names(names) + , skipComments(false) + , readNames(true) +{ +} + +void Lexer::setSkipComments(bool skip) +{ + skipComments = skip; +} + +void Lexer::setReadNames(bool read) +{ + readNames = read; +} + +const Lexeme& Lexer::next() +{ + return next(this->skipComments, true); +} + +const Lexeme& Lexer::next(bool skipComments, bool updatePrevLocation) +{ + // in skipComments mode we reject valid comments + do + { + // consume whitespace before the token + while (isSpace(peekch())) + consumeAny(); + + if (updatePrevLocation) + prevLocation = lexeme.location; + + lexeme = readNext(); + updatePrevLocation = false; + } while (skipComments && (lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment)); + + return lexeme; +} + +void Lexer::nextline() +{ + while (peekch() != 0 && peekch() != '\r' && !isNewline(peekch())) + consume(); + + next(); +} + +Lexeme Lexer::lookahead() +{ + unsigned int currentOffset = offset; + unsigned int currentLine = line; + unsigned int currentLineOffset = lineOffset; + Lexeme currentLexeme = lexeme; + Location currentPrevLocation = prevLocation; + size_t currentBraceStackSize = braceStack.size(); + BraceType currentBraceType = braceStack.empty() ? BraceType::Normal : braceStack.back(); + + Lexeme result = next(); + + offset = currentOffset; + line = currentLine; + lineOffset = currentLineOffset; + lexeme = currentLexeme; + prevLocation = currentPrevLocation; + + if (braceStack.size() < currentBraceStackSize) + braceStack.push_back(currentBraceType); + else if (braceStack.size() > currentBraceStackSize) + braceStack.pop_back(); + + return result; +} + +bool Lexer::isReserved(const std::string& word) +{ + for (int i = Lexeme::Reserved_BEGIN; i < Lexeme::Reserved_END; ++i) + if (word == kReserved[i - Lexeme::Reserved_BEGIN]) + return true; + + return false; +} + +LUAU_FORCEINLINE +char Lexer::peekch() const +{ + return (offset < bufferSize) ? buffer[offset] : 0; +} + +LUAU_FORCEINLINE +char Lexer::peekch(unsigned int lookahead) const +{ + return (offset + lookahead < bufferSize) ? buffer[offset + lookahead] : 0; +} + +LUAU_FORCEINLINE +Position Lexer::position() const +{ + return Position(line, offset - lineOffset); +} + +LUAU_FORCEINLINE +void Lexer::consume() +{ + // consume() assumes current character is known to not be a newline; use consumeAny if this is not guaranteed + LUAU_ASSERT(!isNewline(buffer[offset])); + + offset++; +} + +LUAU_FORCEINLINE +void Lexer::consumeAny() +{ + if (isNewline(buffer[offset])) + { + line++; + lineOffset = offset + 1; + } + + offset++; +} + +Lexeme Lexer::readCommentBody() +{ + Position start = position(); + + LUAU_ASSERT(peekch(0) == '-' && peekch(1) == '-'); + consume(); + consume(); + + size_t startOffset = offset; + + if (peekch() == '[') + { + int sep = skipLongSeparator(); + + if (sep >= 0) + { + return readLongString(start, sep, Lexeme::BlockComment, Lexeme::BrokenComment); + } + } + + // fall back to single-line comment + while (peekch() != 0 && peekch() != '\r' && !isNewline(peekch())) + consume(); + + return Lexeme(Location(start, position()), Lexeme::Comment, &buffer[startOffset], offset - startOffset); +} + +// Given a sequence [===[ or ]===], returns: +// 1. number of equal signs (or 0 if none present) between the brackets +// 2. -1 if this is not a long comment/string separator +// 3. -N if this is a malformed separator +// Does *not* consume the closing brace. +int Lexer::skipLongSeparator() +{ + char start = peekch(); + + LUAU_ASSERT(start == '[' || start == ']'); + consume(); + + int count = 0; + + while (peekch() == '=') + { + consume(); + count++; + } + + return (start == peekch()) ? count : (-count) - 1; +} + +Lexeme Lexer::readLongString(const Position& start, int sep, Lexeme::Type ok, Lexeme::Type broken) +{ + // skip (second) [ + LUAU_ASSERT(peekch() == '['); + consume(); + + unsigned int startOffset = offset; + + while (peekch()) + { + if (peekch() == ']') + { + if (skipLongSeparator() == sep) + { + LUAU_ASSERT(peekch() == ']'); + consume(); // skip (second) ] + + unsigned int endOffset = offset - sep - 2; + LUAU_ASSERT(endOffset >= startOffset); + + return Lexeme(Location(start, position()), ok, &buffer[startOffset], endOffset - startOffset); + } + } + else + { + consumeAny(); + } + } + + return Lexeme(Location(start, position()), broken); +} + +void Lexer::readBackslashInString() +{ + LUAU_ASSERT(peekch() == '\\'); + consume(); + switch (peekch()) + { + case '\r': + consume(); + if (peekch() == '\n') + consumeAny(); + break; + + case 0: + break; + + case 'z': + consume(); + while (isSpace(peekch())) + consumeAny(); + break; + + default: + consumeAny(); + } +} + +Lexeme Lexer::readQuotedString() +{ + Position start = position(); + + char delimiter = peekch(); + LUAU_ASSERT(delimiter == '\'' || delimiter == '"'); + consume(); + + unsigned int startOffset = offset; + + while (peekch() != delimiter) + { + switch (peekch()) + { + case 0: + case '\r': + case '\n': + return Lexeme(Location(start, position()), Lexeme::BrokenString); + + case '\\': + readBackslashInString(); + break; + + default: + consume(); + } + } + + consume(); + + return Lexeme(Location(start, position()), Lexeme::QuotedString, &buffer[startOffset], offset - startOffset - 1); +} + +Lexeme Lexer::readInterpolatedStringBegin() +{ + LUAU_ASSERT(peekch() == '`'); + + Position start = position(); + consume(); + + return readInterpolatedStringSection(start, Lexeme::InterpStringBegin, Lexeme::InterpStringSimple); +} + +Lexeme Lexer::readInterpolatedStringSection(Position start, Lexeme::Type formatType, Lexeme::Type endType) +{ + unsigned int startOffset = offset; + + while (peekch() != '`') + { + switch (peekch()) + { + case 0: + case '\r': + case '\n': + return Lexeme(Location(start, position()), Lexeme::BrokenString); + + case '\\': + // Allow for \u{}, which would otherwise be consumed by looking for { + if (peekch(1) == 'u' && peekch(2) == '{') + { + consume(); // backslash + consume(); // u + consume(); // { + break; + } + + readBackslashInString(); + break; + + case '{': + { + braceStack.push_back(BraceType::InterpolatedString); + + if (peekch(1) == '{') + { + Lexeme brokenDoubleBrace = + Lexeme(Location(start, position()), Lexeme::BrokenInterpDoubleBrace, &buffer[startOffset], offset - startOffset); + consume(); + consume(); + return brokenDoubleBrace; + } + + consume(); + return Lexeme(Location(start, position()), formatType, &buffer[startOffset], offset - startOffset - 1); + } + + default: + consume(); + } + } + + consume(); + + return Lexeme(Location(start, position()), endType, &buffer[startOffset], offset - startOffset - 1); +} + +Lexeme Lexer::readNumber(const Position& start, unsigned int startOffset) +{ + LUAU_ASSERT(isDigit(peekch())); + + // This function does not do the number parsing - it only skips a number-like pattern. + // It uses the same logic as Lua stock lexer; the resulting string is later converted + // to a number with proper verification. + do + { + consume(); + } while (isDigit(peekch()) || peekch() == '.' || peekch() == '_'); + + if (peekch() == 'e' || peekch() == 'E') + { + consume(); + + if (peekch() == '+' || peekch() == '-') + consume(); + } + + while (isAlpha(peekch()) || isDigit(peekch()) || peekch() == '_') + consume(); + + return Lexeme(Location(start, position()), Lexeme::Number, &buffer[startOffset], offset - startOffset); +} + +std::pair Lexer::readName() +{ + LUAU_ASSERT(isAlpha(peekch()) || peekch() == '_' || peekch() == '@'); + + unsigned int startOffset = offset; + + do + consume(); + while (isAlpha(peekch()) || isDigit(peekch()) || peekch() == '_'); + + return readNames ? names.getOrAddWithType(&buffer[startOffset], offset - startOffset) + : names.getWithType(&buffer[startOffset], offset - startOffset); +} + +Lexeme Lexer::readNext() +{ + Position start = position(); + + switch (peekch()) + { + case 0: + return Lexeme(Location(start, 0), Lexeme::Eof); + + case '-': + { + if (peekch(1) == '>') + { + consume(); + consume(); + return Lexeme(Location(start, 2), Lexeme::SkinnyArrow); + } + else if (peekch(1) == '=') + { + consume(); + consume(); + return Lexeme(Location(start, 2), Lexeme::SubAssign); + } + else if (peekch(1) == '-') + { + return readCommentBody(); + } + else + { + consume(); + return Lexeme(Location(start, 1), '-'); + } + } + + case '[': + { + int sep = skipLongSeparator(); + + if (sep >= 0) + { + return readLongString(start, sep, Lexeme::RawString, Lexeme::BrokenString); + } + else if (sep == -1) + { + return Lexeme(Location(start, 1), '['); + } + else + { + return Lexeme(Location(start, position()), Lexeme::BrokenString); + } + } + + case '{': + { + consume(); + + if (!braceStack.empty()) + braceStack.push_back(BraceType::Normal); + + return Lexeme(Location(start, 1), '{'); + } + + case '}': + { + consume(); + + if (braceStack.empty()) + { + return Lexeme(Location(start, 1), '}'); + } + + const BraceType braceStackTop = braceStack.back(); + braceStack.pop_back(); + + if (braceStackTop != BraceType::InterpolatedString) + { + return Lexeme(Location(start, 1), '}'); + } + + return readInterpolatedStringSection(start, Lexeme::InterpStringMid, Lexeme::InterpStringEnd); + } + + case '=': + { + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::Equal); + } + else + return Lexeme(Location(start, 1), '='); + } + + case '<': + { + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::LessEqual); + } + else + return Lexeme(Location(start, 1), '<'); + } + + case '>': + { + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::GreaterEqual); + } + else + return Lexeme(Location(start, 1), '>'); + } + + case '~': + { + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::NotEqual); + } + else + return Lexeme(Location(start, 1), '~'); + } + + case '"': + case '\'': + return readQuotedString(); + + case '`': + return readInterpolatedStringBegin(); + + case '.': + consume(); + + if (peekch() == '.') + { + consume(); + + if (peekch() == '.') + { + consume(); + + return Lexeme(Location(start, 3), Lexeme::Dot3); + } + else if (peekch() == '=') + { + consume(); + + return Lexeme(Location(start, 3), Lexeme::ConcatAssign); + } + else + return Lexeme(Location(start, 2), Lexeme::Dot2); + } + else + { + if (isDigit(peekch())) + { + return readNumber(start, offset - 1); + } + else + return Lexeme(Location(start, 1), '.'); + } + + case '+': + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::AddAssign); + } + else + return Lexeme(Location(start, 1), '+'); + + case '/': + { + consume(); + + char ch = peekch(); + + if (ch == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::DivAssign); + } + else if (ch == '/') + { + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); + } + else + return Lexeme(Location(start, 2), Lexeme::FloorDiv); + } + else + return Lexeme(Location(start, 1), '/'); + } + + case '*': + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::MulAssign); + } + else + return Lexeme(Location(start, 1), '*'); + + case '%': + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::ModAssign); + } + else + return Lexeme(Location(start, 1), '%'); + + case '^': + consume(); + + if (peekch() == '=') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::PowAssign); + } + else + return Lexeme(Location(start, 1), '^'); + + case ':': + { + consume(); + if (peekch() == ':') + { + consume(); + return Lexeme(Location(start, 2), Lexeme::DoubleColon); + } + else + return Lexeme(Location(start, 1), ':'); + } + + case '(': + case ')': + case ']': + case ';': + case ',': + case '#': + case '?': + case '&': + case '|': + { + char ch = peekch(); + consume(); + + return Lexeme(Location(start, 1), ch); + } + case '@': + { + if (peekch(1) == '[') + { + consume(); + consume(); + + return Lexeme(Location(start, 2), Lexeme::AttributeOpen); + } + else + { + // consume @ first + consume(); + + if (isAlpha(peekch()) || peekch() == '_') + { + std::pair attribute = readName(); + return Lexeme(Location(start, position()), Lexeme::Attribute, attribute.first.value); + } + else + { + return Lexeme(Location(start, position()), Lexeme::Attribute, ""); + } + } + } + default: + if (isDigit(peekch())) + { + return readNumber(start, offset); + } + else if (isAlpha(peekch()) || peekch() == '_') + { + std::pair name = readName(); + + return Lexeme(Location(start, position()), name.second, name.first.value); + } + else if (peekch() & 0x80) + { + return readUtf8Error(); + } + else + { + char ch = peekch(); + consume(); + + return Lexeme(Location(start, 1), ch); + } + } +} + +std::optional Lexer::peekBraceStackTop() +{ + if (braceStack.empty()) + return std::nullopt; + else + return {braceStack.back()}; +} + +LUAU_NOINLINE Lexeme Lexer::readUtf8Error() +{ + Position start = position(); + uint32_t codepoint = 0; + int size = 0; + + if ((peekch() & 0b10000000) == 0b00000000) + { + size = 1; + codepoint = peekch() & 0x7F; + } + else if ((peekch() & 0b11100000) == 0b11000000) + { + size = 2; + codepoint = peekch() & 0b11111; + } + else if ((peekch() & 0b11110000) == 0b11100000) + { + size = 3; + codepoint = peekch() & 0b1111; + } + else if ((peekch() & 0b11111000) == 0b11110000) + { + size = 4; + codepoint = peekch() & 0b111; + } + else + { + consume(); + return Lexeme(Location(start, position()), Lexeme::BrokenUnicode); + } + + consume(); + + for (int i = 1; i < size; ++i) + { + if ((peekch() & 0b11000000) != 0b10000000) + return Lexeme(Location(start, position()), Lexeme::BrokenUnicode); + + codepoint = codepoint << 6; + codepoint |= (peekch() & 0b00111111); + consume(); + } + + Lexeme result(Location(start, position()), Lexeme::BrokenUnicode); + result.codepoint = codepoint; + return result; +} + +static size_t toUtf8(char* data, unsigned int code) +{ + // U+0000..U+007F + if (code < 0x80) + { + data[0] = char(code); + return 1; + } + // U+0080..U+07FF + else if (code < 0x800) + { + data[0] = char(0xC0 | (code >> 6)); + data[1] = char(0x80 | (code & 0x3F)); + return 2; + } + // U+0800..U+FFFF + else if (code < 0x10000) + { + data[0] = char(0xE0 | (code >> 12)); + data[1] = char(0x80 | ((code >> 6) & 0x3F)); + data[2] = char(0x80 | (code & 0x3F)); + return 3; + } + // U+10000..U+10FFFF + else if (code < 0x110000) + { + data[0] = char(0xF0 | (code >> 18)); + data[1] = char(0x80 | ((code >> 12) & 0x3F)); + data[2] = char(0x80 | ((code >> 6) & 0x3F)); + data[3] = char(0x80 | (code & 0x3F)); + return 4; + } + else + { + return 0; + } +} + +bool Lexer::fixupQuotedString(std::string& data) +{ + if (data.empty() || data.find('\\') == std::string::npos) + return true; + + size_t size = data.size(); + size_t write = 0; + + for (size_t i = 0; i < size;) + { + if (data[i] != '\\') + { + data[write++] = data[i]; + i++; + continue; + } + + if (i + 1 == size) + return false; + + char escape = data[i + 1]; + i += 2; // skip \e + + switch (escape) + { + case '\n': + data[write++] = '\n'; + break; + + case '\r': + data[write++] = '\n'; + if (i < size && data[i] == '\n') + i++; + break; + + case 0: + return false; + + case 'x': + { + // hex escape codes are exactly 2 hex digits long + if (i + 2 > size) + return false; + + unsigned int code = 0; + + for (int j = 0; j < 2; ++j) + { + char ch = data[i + j]; + if (!isHexDigit(ch)) + return false; + + // use or trick to convert to lower case + code = 16 * code + (isDigit(ch) ? ch - '0' : (ch | ' ') - 'a' + 10); + } + + data[write++] = char(code); + i += 2; + break; + } + + case 'z': + { + while (i < size && isSpace(data[i])) + i++; + break; + } + + case 'u': + { + // unicode escape codes are at least 3 characters including braces + if (i + 3 > size) + return false; + + if (data[i] != '{') + return false; + i++; + + if (data[i] == '}') + return false; + + unsigned int code = 0; + + for (int j = 0; j < 16; ++j) + { + if (i == size) + return false; + + char ch = data[i]; + + if (ch == '}') + break; + + if (!isHexDigit(ch)) + return false; + + // use or trick to convert to lower case + code = 16 * code + (isDigit(ch) ? ch - '0' : (ch | ' ') - 'a' + 10); + i++; + } + + if (i == size || data[i] != '}') + return false; + i++; + + size_t utf8 = toUtf8(&data[write], code); + if (utf8 == 0) + return false; + + write += utf8; + break; + } + + default: + { + if (isDigit(escape)) + { + unsigned int code = escape - '0'; + + for (int j = 0; j < 2; ++j) + { + if (i == size || !isDigit(data[i])) + break; + + code = 10 * code + (data[i] - '0'); + i++; + } + + if (code > UCHAR_MAX) + return false; + + data[write++] = char(code); + } + else + { + data[write++] = unescape(escape); + } + } + } + } + + LUAU_ASSERT(write <= size); + data.resize(write); + + return true; +} + +void Lexer::fixupMultilineString(std::string& data) +{ + if (data.empty()) + return; + + // Lua rules for multiline strings are as follows: + // - standalone \r, \r\n, \n\r and \n are all considered newlines + // - first newline in the multiline string is skipped + // - all other newlines are normalized to \n + + // Since our lexer just treats \n as newlines, we apply a simplified set of rules that is sufficient to get normalized newlines for Windows/Unix: + // - \r\n and \n are considered newlines + // - first newline is skipped + // - newlines are normalized to \n + + // This makes the string parsing behavior consistent with general lexing behavior - a standalone \r isn't considered a new line from the line + // tracking perspective + + const char* src = data.c_str(); + char* dst = &data[0]; + + // skip leading newline + if (src[0] == '\r' && src[1] == '\n') + { + src += 2; + } + else if (src[0] == '\n') + { + src += 1; + } + + // parse the rest of the string, converting newlines as we go + while (*src) + { + if (src[0] == '\r' && src[1] == '\n') + { + *dst++ = '\n'; + src += 2; + } + else // note, this handles \n by just writing it without changes + { + *dst++ = *src; + src += 1; + } + } + + data.resize(dst - &data[0]); +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Location.cpp b/External/luau/Ast/src/Location.cpp new file mode 100644 index 00000000..825c60e8 --- /dev/null +++ b/External/luau/Ast/src/Location.cpp @@ -0,0 +1,55 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Location.h" + +namespace Luau +{ + +void Position::shift(const Position& start, const Position& oldEnd, const Position& newEnd) +{ + if (*this >= start) + { + if (this->line > oldEnd.line) + this->line += (newEnd.line - oldEnd.line); + else + { + this->line = newEnd.line; + this->column += (newEnd.column - oldEnd.column); + } + } +} + +bool Location::encloses(const Location& l) const +{ + return begin <= l.begin && end >= l.end; +} + +bool Location::overlaps(const Location& l) const +{ + return (begin <= l.begin && end >= l.begin) || (begin <= l.end && end >= l.end) || (begin >= l.begin && end <= l.end); +} + +bool Location::contains(const Position& p) const +{ + return begin <= p && p < end; +} + +bool Location::containsClosed(const Position& p) const +{ + return begin <= p && p <= end; +} + +void Location::extend(const Location& other) +{ + if (other.begin < begin) + begin = other.begin; + if (other.end > end) + end = other.end; +} + +void Location::shift(const Position& start, const Position& oldEnd, const Position& newEnd) +{ + begin.shift(start, oldEnd, newEnd); + end.shift(start, oldEnd, newEnd); +} + +} // namespace Luau diff --git a/External/luau/Ast/src/Parser.cpp b/External/luau/Ast/src/Parser.cpp new file mode 100644 index 00000000..44b4acd9 --- /dev/null +++ b/External/luau/Ast/src/Parser.cpp @@ -0,0 +1,5539 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Parser.h" + +#include "Luau/Ast.h" +#include "Luau/Common.h" +#include "Luau/TimeTrace.h" + +#include +#include + +#include +#include +#include +#include + +LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) +LUAU_FASTINTVARIABLE(LuauTypeLengthLimit, 1000) +LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) + +// Warning: If you are introducing new syntax, ensure that it is behind a separate +// flag so that we don't break production games by reverting syntax changes. +// See docs/SyntaxChanges.md for an explanation. +LUAU_FASTFLAGVARIABLE(LuauSolverV2) +LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) +LUAU_FASTFLAGVARIABLE(LuauIntegerType2) +LUAU_FASTFLAGVARIABLE(LuauExportValueSyntax) +LUAU_FLAGVERSION(LuauExportValueSyntax, 5) + +LUAU_FASTFLAGVARIABLE(DebugLuauNoInline) +LUAU_FASTFLAGVARIABLE(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauAllowGlobalDeclarationToBeCalledClass) +LUAU_FASTFLAGVARIABLE(LuauSingleTypeOptionalPackReturnsAttributeParens) +LUAU_FASTFLAGVARIABLE(DebugLuauParseExactTables) +LUAU_FASTFLAGVARIABLE(LuauExperimentalIfLocalSyntax) +LUAU_FLAGVERSION(LuauExperimentalIfLocalSyntax, 2) + +// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix +bool luau_telemetry_parsed_return_type_variadic_with_type_suffix = false; + +namespace Luau +{ + +using AttributeArgumentsValidator = std::function>(Location, const AstArray&)>; + +struct AttributeEntry +{ + const char* name; + AstAttr::Type type; + std::optional argsValidator; +}; + +std::vector> deprecatedArgsValidator(Location attrLoc, const AstArray& args) +{ + + if (args.size == 0) + return {}; + if (args.size > 1) + return {{attrLoc, "@deprecated can be parametrized only by 1 argument"}}; + + if (!args.data[0]->is()) + return {{args.data[0]->location, "Unknown argument type for @deprecated"}}; + + std::vector> errors; + for (const AstExprTable::Item& item : args.data[0]->as()->items) + { + if (item.kind == AstExprTable::Item::Kind::Record) + { + AstArray keyString = item.key->as()->value; + std::string key(keyString.data, keyString.size); + if (key != "use" && key != "reason") + { + errors.emplace_back( + item.key->location, + format("Unknown argument '%s' for @deprecated. Only string constants for 'use' and 'reason' are allowed", key.c_str()) + ); + } + else if (!item.value->is()) + { + errors.emplace_back(item.value->location, format("Only constant string allowed as value for '%s'", key.c_str())); + } + } + else + { + errors.emplace_back(item.value->location, "Only constants keys 'use' and 'reason' are allowed for @deprecated attribute"); + } + } + return errors; +} + +AttributeEntry kAttributeEntries_DEPRECATED[] = { + {"@checked", AstAttr::Type::Checked, {}}, + {"@native", AstAttr::Type::Native, {}}, + {"@deprecated", AstAttr::Type::Deprecated, {}}, + {nullptr, AstAttr::Type::Checked, {}} +}; + +AttributeEntry kAttributeEntries[] = { + {"checked", AstAttr::Type::Checked, {}}, + {"native", AstAttr::Type::Native, {}}, + {"deprecated", AstAttr::Type::Deprecated, deprecatedArgsValidator}, + {nullptr, AstAttr::Type::Checked, {}} +}; + +std::pair&> kDebugAttributeEntries[] = { + {{"debugnoinline", AstAttr::Type::DebugNoinline, {}}, FFlag::DebugLuauNoInline}, +}; + +ParseError::ParseError(const Location& location, std::string message) + : location(location) + , message(std::move(message)) +{ +} + +const char* ParseError::what() const throw() +{ + return message.c_str(); +} + +const Location& ParseError::getLocation() const +{ + return location; +} + +const std::string& ParseError::getMessage() const +{ + return message; +} + +// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string object / exception plumbing +LUAU_NOINLINE void ParseError::raise(const Location& location, const char* format, ...) +{ + va_list args; + va_start(args, format); + std::string message = vformat(format, args); + va_end(args); + + throw ParseError(location, message); +} + +ParseErrors::ParseErrors(std::vector errors) + : errors(std::move(errors)) +{ + LUAU_ASSERT(!this->errors.empty()); + + if (this->errors.size() == 1) + message = this->errors.front().what(); + else + message = format("%d parse errors", int(this->errors.size())); +} + +const char* ParseErrors::what() const throw() +{ + return message.c_str(); +} + +const std::vector& ParseErrors::getErrors() const +{ + return errors; +} + +template +TempVector::TempVector(std::vector& storage) + : storage(storage) + , offset(storage.size()) + , size_(0) +{ +} + +template +TempVector::~TempVector() +{ + LUAU_ASSERT(storage.size() == offset + size_); + storage.erase(storage.begin() + offset, storage.end()); +} + +template +const T& TempVector::operator[](size_t index) const +{ + LUAU_ASSERT(index < size_); + return storage[offset + index]; +} + +template +const T& TempVector::front() const +{ + LUAU_ASSERT(size_ > 0); + return storage[offset]; +} + +template +const T& TempVector::back() const +{ + LUAU_ASSERT(size_ > 0); + return storage.back(); +} + +template +bool TempVector::empty() const +{ + return size_ == 0; +} + +template +size_t TempVector::size() const +{ + return size_; +} + +template +void TempVector::push_back(const T& item) +{ + LUAU_ASSERT(storage.size() == offset + size_); + storage.push_back(item); + size_++; +} + +static bool shouldParseTypePack(Lexer& lexer) +{ + if (lexer.current().type == Lexeme::Dot3) + return true; + else if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == Lexeme::Dot3) + return true; + + return false; +} + +ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options) +{ + LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser"); + + Parser p(buffer, bufferSize, names, allocator, options); + + try + { + AstStatBlock* root = p.parseChunk(); + size_t lines = p.lexer.current().location.end.line + (bufferSize > 0 && buffer[bufferSize - 1] != '\n'); + + return ParseResult{root, lines, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations), std::move(p.cstNodeMap)}; + } + catch (ParseError& err) + { + // when catching a fatal error, append it to the list of non-fatal errors and return + p.parseErrors.push_back(err); + + return ParseResult{nullptr, 0, {}, p.parseErrors, {}, std::move(p.cstNodeMap)}; + } +} + +template +ParseNodeResult Parser::runParse(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options, F f) +{ + LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser"); + + Parser p(buffer, bufferSize, names, allocator, options); + + try + { + Node* expr = f(p); + size_t lines = p.lexer.current().location.end.line + (bufferSize > 0 && buffer[bufferSize - 1] != '\n'); + + Lexeme eof = p.lexer.next(); + if (eof.type != Lexeme::Eof) + { + expr = nullptr; + p.parseErrors.emplace_back(eof.location, "Expected end of file"); + } + + return ParseNodeResult{ + expr, lines, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations), std::move(p.cstNodeMap) + }; + } + catch (ParseError& err) + { + // when catching a fatal error, append it to the list of non-fatal errors and return + p.parseErrors.push_back(err); + + return ParseNodeResult{nullptr, 0, {}, p.parseErrors, {}, std::move(p.cstNodeMap)}; + } +} + +ParseNodeResult Parser::parseExpr(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options) +{ + return Parser::runParse( + buffer, + bufferSize, + names, + allocator, + std::move(options), + [](auto&& parser) + { + return parser.parseExpr(); + } + ); +} + +ParseNodeResult Parser::parseType(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options) +{ + return Parser::runParse( + buffer, + bufferSize, + names, + allocator, + std::move(options), + [](auto&& parser) + { + return parser.parseType(); + } + ); +} + +Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, const ParseOptions& options) + : options(options) + , lexer(buffer, bufferSize, names, options.parseFragment ? options.parseFragment->resumePosition : Position(0, 0)) + , allocator(allocator) + , recursionCounter(0) + , endMismatchSuspect(Lexeme(Location(), Lexeme::Eof)) + , localMap{} + , declaredExportBindings{} + , cstNodeMap{} +{ + Function top; + top.vararg = true; + + functionStack.reserve(8); + functionStack.push_back(top); + + nameSelf = names.getOrAdd("self"); + nameNumber = names.getOrAdd("number"); + nameError = names.getOrAdd(kParseNameError); + nameNil = names.getOrAdd("nil"); // nil is a reserved keyword + + matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0); + matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1; + + // required for lookahead() to work across a comment boundary and for nextLexeme() to work when captureComments is false + lexer.setSkipComments(true); + + // read first lexeme (any hot comments get .header = true) + LUAU_ASSERT(hotcommentHeader); + nextLexeme(); + + // all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode + hotcommentHeader = false; + + // preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays + localStack.reserve(16); + scratchStat.reserve(16); + scratchExpr.reserve(16); + scratchLocal.reserve(16); + scratchBinding.reserve(16); + + if (options.parseFragment) + { + localMap = options.parseFragment->localMap; + localStack = options.parseFragment->localStack; + } +} + +bool Parser::blockFollow(const Lexeme& l) +{ + return l.type == Lexeme::Eof || l.type == Lexeme::ReservedElse || l.type == Lexeme::ReservedElseif || l.type == Lexeme::ReservedEnd || + l.type == Lexeme::ReservedUntil; +} + +AstStatBlock* Parser::parseChunk() +{ + AstStatBlock* result = parseBlock(); + + if (lexer.current().type != Lexeme::Eof) + expectAndConsumeFail(Lexeme::Eof, nullptr); + + return result; +} + +// chunk ::= {stat [`;']} [laststat [`;']] +// block ::= chunk +AstStatBlock* Parser::parseBlock() +{ + unsigned int localsBegin = saveLocals(); + + AstStatBlock* result = parseBlockNoScope(); + + restoreLocals(localsBegin); + + return result; +} + +static bool isStatLast(AstStat* stat) +{ + return stat->is() || stat->is() || stat->is(); +} + +AstStatBlock* Parser::parseBlockNoScope() +{ + TempVector body(scratchStat); + + const Position prevPosition = lexer.previousLocation().end; + + while (!blockFollow(lexer.current())) + { + unsigned int oldRecursionCount = recursionCounter; + + incrementRecursionCounter("block"); + + AstStat* stat = parseStat(); + + recursionCounter = oldRecursionCount; + + if (lexer.current().type == ';') + { + nextLexeme(); + stat->hasSemicolon = true; + stat->location.end = lexer.previousLocation().end; + } + + body.push_back(stat); + + if (isStatLast(stat)) + break; + } + + const Location location = Location(prevPosition, lexer.current().location.begin); + + return allocator.alloc(location, copy(body)); +} + +// stat ::= +// varlist `=' explist | +// 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 namelist in explist do block end | +// function funcname funcbody | +// attributes function funcname funcbody | +// local function Name funcbody | +// local attributes function Name funcbody | +// local namelist [`=' explist] +// laststat ::= return [explist] | break +AstStat* Parser::parseStat() +{ + // guess the type from the token type + switch (lexer.current().type) + { + case Lexeme::ReservedIf: + return parseIf(); + case Lexeme::ReservedWhile: + return parseWhile(); + case Lexeme::ReservedDo: + return parseDo(); + case Lexeme::ReservedFor: + return parseFor(); + case Lexeme::ReservedRepeat: + return parseRepeat(); + case Lexeme::ReservedFunction: + return parseFunctionStat(AstArray({nullptr, 0})); + case Lexeme::ReservedLocal: + { + Location start = lexer.current().location; + return parseLocal(start, start.begin, {nullptr, 0}, false); + } + case Lexeme::ReservedReturn: + return parseReturn(); + case Lexeme::ReservedBreak: + return parseBreak(); + case Lexeme::Attribute: + case Lexeme::AttributeOpen: + return parseAttributeStat(); + default:; + } + + Location start = lexer.current().location; + + // we need to disambiguate a few cases, primarily assignment (lvalue = ...) vs statements-that-are calls + AstExpr* expr = parsePrimaryExpr(/* asStatement= */ true); + + if (expr->is()) + return allocator.alloc(expr->location, expr); + + // if the next token is , or =, it's an assignment (, means it's an assignment with multiple variables) + if (lexer.current().type == ',' || lexer.current().type == '=') + return parseAssignment(expr); + + // if the next token is a compound assignment operator, it's a compound assignment (these don't support multiple variables) + if (std::optional op = parseCompoundOp(lexer.current())) + return parseCompoundAssignment(expr, *op); + + // we know this isn't a call or an assignment; therefore it must be a context-sensitive keyword such as `type` or `continue` + AstName ident = getIdentifier(expr); + + if (ident == "type") + return parseTypeAlias(expr->location, /* exported= */ false, expr->location.begin); + + if (FFlag::DebugLuauUserDefinedClasses && ident == "class") + return parseClassStat(start, /* exported */ false, /* open */ false); + else if (FFlag::DebugLuauUserDefinedClasses && ident == "open") + { + if (lexer.current().type != Lexeme::Name || AstName(lexer.current().name) != "class") + return reportStatError(expr->location, copy({expr}), {}, "Incomplete statement: expected a class definition after 'open'"); + + nextLexeme(); // consume `class` + return parseClassStat(start, /* exported */ false, /* open */ true); + } + + if (ident == "export") + { + if (FFlag::LuauExportValueSyntax) + { + Lexeme current = lexer.current(); + + bool isExportValue = current.type == Lexeme::ReservedLocal || current.type == Lexeme::ReservedFunction || + (current.type == Lexeme::Name && AstName(current.name) == "const"); + + if (FFlag::DebugLuauUserDefinedClasses) + isExportValue |= current.type == Lexeme::Name && (AstName(current.name) == "class" || AstName(current.name) == "open"); + + if (isExportValue) + { + return parseExportValue(expr->location, expr->location.begin, AstArray({nullptr, 0})); + } + else if (current.type == Lexeme::Name && AstName(current.name) == "type") + { + Position typeKeywordPosition = current.location.begin; + nextLexeme(); + return parseTypeAlias(expr->location, /* exported= */ true, typeKeywordPosition); + } + } + // TODO: remove with LuauExportValueSyntax + else if (FFlag::DebugLuauUserDefinedClasses && AstName(lexer.current().name) == "class") + { + nextLexeme(); + return parseClassStat(start, /* exported */ true, /* open */ false); + } + else if (FFlag::DebugLuauUserDefinedClasses && AstName(lexer.current().name) == "open") + { + nextLexeme(); // consume `open` + + if (lexer.current().type != Lexeme::Name || AstName(lexer.current().name) != "class") + return reportStatError(expr->location, copy({expr}), {}, "Incomplete statement: expected a class definition after 'open'"); + + nextLexeme(); // consume `class` + return parseClassStat(start, /* exported */ true, /* open */ true); + } + else + { + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "type") + { + Position typeKeywordPosition = lexer.current().location.begin; + nextLexeme(); + return parseTypeAlias(expr->location, /* exported= */ true, typeKeywordPosition); + } + } + } + + + if (ident == "continue") + return parseContinue(expr->location); + + if (ident == "const") + return parseLocal(expr->location, expr->location.begin, AstArray({nullptr, 0}), true); + + if (options.allowDeclarationSyntax) + { + if (ident == "declare") + return parseDeclaration(expr->location, AstArray({nullptr, 0})); + } + + // skip unexpected symbol if lexer couldn't advance at all (statements are parsed in a loop) + if (start == lexer.current().location) + nextLexeme(); + + return reportStatError(expr->location, copy({expr}), {}, "Incomplete statement: expected assignment or a function call"); +} + +// if exp then block {elseif exp then block} [else block] end +AstStat* Parser::parseIf() +{ + Location start = lexer.current().location; + + nextLexeme(); // if / elseif + + if (FFlag::LuauExperimentalIfLocalSyntax) + { + if (lexer.current().type == Lexeme::ReservedLocal) + return parseIfLocalCondition(start); + + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const" && lexer.lookahead().type == Lexeme::Name) + return parseIfLocalCondition(start); + } + + AstExpr* cond = parseExpr(); + + Lexeme matchThen = lexer.current(); + std::optional thenLocation; + if (expectAndConsume(Lexeme::ReservedThen, "if statement")) + thenLocation = matchThen.location; + + AstStatBlock* thenbody = parseBlock(); + + Location end = start; + std::optional elseLocation; + AstStat* elsebody = parseElseBody(start, matchThen, thenbody, end, elseLocation); + + return allocator.alloc(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation); +} + +// (`if' | `elseif') (`local' | `const') binding `=' exp then block {elseif exp then block} [else block] end +// +// LUAU_NOINLINE keeps the `if local`/`if const` locals off parseIf's frame: parseIf recurses through +// long if/elseif chains and this variant is rarely taken. `start` is the location of the already- +// consumed `if`/`elseif` keyword; the current lexeme is the `local`/`const` keyword. +LUAU_NOINLINE AstStat* Parser::parseIfLocalCondition(const Location& start) +{ + LUAU_ASSERT(FFlag::LuauExperimentalIfLocalSyntax); + + bool condIsConst = (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const"); + std::optional condKeywordLocation = lexer.current().location; + nextLexeme(); // consume 'local' or 'const' + + Binding binding = parseBinding(condIsConst); + + if (lexer.current().type == ',') + report(lexer.current().location, "Expected '=' after variable name in 'if local', got ','; only a single binding is allowed"); + + std::optional equalsPosition; + if (lexer.current().type == '=') + equalsPosition = lexer.current().location; + expectAndConsume('=', "if local declaration"); + + AstExpr* cond = parseExpr(); + + unsigned int localsBegin = saveLocals(); + AstLocal* condLocal = pushLocal(binding); + + Lexeme matchThen = lexer.current(); + std::optional thenLocation; + if (expectAndConsume(Lexeme::ReservedThen, "if statement")) + thenLocation = matchThen.location; + + AstStatBlock* thenbody = parseBlock(); + + // Restore locals after then-block so condLocal is not visible in else/elseif + restoreLocals(localsBegin); + + Location end = start; + std::optional elseLocation; + AstStat* elsebody = parseElseBody(start, matchThen, thenbody, end, elseLocation); + + AstStatIf* node = allocator.alloc( + Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, condLocal, condIsConst, condKeywordLocation, equalsPosition + ); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(binding.annotation ? binding.colonPosition : Position::missing()); + return node; +} + +AstStat* Parser::parseElseBody( + const Location& start, + const Lexeme& matchThen, + AstStatBlock* thenbody, + Location& end, + std::optional& elseLocation +) +{ + AstStat* elsebody = nullptr; + end = start; + elseLocation = std::nullopt; + + if (lexer.current().type == Lexeme::ReservedElseif) + { + thenbody->hasEnd = true; + unsigned int oldRecursionCount = recursionCounter; + incrementRecursionCounter("elseif"); + elseLocation = lexer.current().location; + elsebody = parseIf(); + end = elsebody->location; + recursionCounter = oldRecursionCount; + } + else + { + Lexeme matchThenElse = matchThen; + + if (lexer.current().type == Lexeme::ReservedElse) + { + thenbody->hasEnd = true; + elseLocation = lexer.current().location; + matchThenElse = lexer.current(); + nextLexeme(); + + elsebody = parseBlock(); + elsebody->location.begin = matchThenElse.location.end; + } + + end = lexer.current().location; + + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse); + + if (elsebody) + { + if (AstStatBlock* elseBlock = elsebody->as()) + elseBlock->hasEnd = hasEnd; + } + else + thenbody->hasEnd = hasEnd; + } + + return elsebody; +} + +// while exp do block end +AstStat* Parser::parseWhile() +{ + Location start = lexer.current().location; + + nextLexeme(); // while + + AstExpr* cond = parseExpr(); + + Lexeme matchDo = lexer.current(); + bool hasDo = expectAndConsume(Lexeme::ReservedDo, "while loop"); + + functionStack.back().loopDepth++; + + AstStatBlock* body = parseBlock(); + + functionStack.back().loopDepth--; + + Location end = lexer.current().location; + + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + body->hasEnd = hasEnd; + + return allocator.alloc(Location(start, end), cond, body, hasDo, matchDo.location); +} + +// repeat block until exp +AstStat* Parser::parseRepeat() +{ + Location start = lexer.current().location; + + Lexeme matchRepeat = lexer.current(); + nextLexeme(); // repeat + + unsigned int localsBegin = saveLocals(); + + functionStack.back().loopDepth++; + + AstStatBlock* body = parseBlockNoScope(); + + functionStack.back().loopDepth--; + + bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat); + body->hasEnd = hasUntil; + Position untilPosition = hasUntil ? lexer.previousLocation().begin : Position::missing(); + + AstExpr* cond = parseExpr(); + + restoreLocals(localsBegin); + + AstStatRepeat* node = allocator.alloc(Location(start, cond->location), cond, body, hasUntil); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(untilPosition); + return node; +} + +// do block end +AstStat* Parser::parseDo() +{ + Location start = lexer.current().location; + + Lexeme matchDo = lexer.current(); + nextLexeme(); // do + + Position statsStart = lexer.current().location.begin; + + AstStatBlock* body = parseBlock(); + + body->location.begin = start.begin; + + Location endLocation = lexer.current().location; + body->hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + if (body->hasEnd) + body->location.end = endLocation.end; + + if (options.storeCstData) + cstNodeMap[body] = allocator.alloc(statsStart, body->hasEnd ? endLocation.begin : Position::missing()); + + return body; +} + +// break +AstStat* Parser::parseBreak() +{ + Location start = lexer.current().location; + + nextLexeme(); // break + + if (functionStack.back().loopDepth == 0) + return reportStatError(start, {}, copy({allocator.alloc(start)}), "break statement must be inside a loop"); + + return allocator.alloc(start); +} + +// continue +AstStat* Parser::parseContinue(const Location& start) +{ + if (functionStack.back().loopDepth == 0) + return reportStatError(start, {}, copy({allocator.alloc(start)}), "continue statement must be inside a loop"); + + // note: the token is already parsed for us! + + return allocator.alloc(start); +} + +// for binding `=' exp `,' exp [`,' exp] do block end | +// for bindinglist in explist do block end | +AstStat* Parser::parseFor() +{ + Location start = lexer.current().location; + + nextLexeme(); // for + + Binding varname = parseBinding(); + + if (lexer.current().type == '=') + { + Position equalsPosition = lexer.current().location.begin; + nextLexeme(); + + AstExpr* from = parseExpr(); + + bool hasEndComma = expectAndConsume(',', "index range"); + Position endCommaPosition = hasEndComma ? lexer.previousLocation().begin : Position::missing(); + + AstExpr* to = parseExpr(); + + Position stepCommaPosition = Position::missing(); + AstExpr* step = nullptr; + + if (lexer.current().type == ',') + { + stepCommaPosition = lexer.current().location.begin; + nextLexeme(); + + step = parseExpr(); + } + + Lexeme matchDo = lexer.current(); + bool hasDo = expectAndConsume(Lexeme::ReservedDo, "for loop"); + + unsigned int localsBegin = saveLocals(); + + functionStack.back().loopDepth++; + + AstLocal* var = pushLocal(varname); + + AstStatBlock* body = parseBlock(); + + functionStack.back().loopDepth--; + + restoreLocals(localsBegin); + + Location end = lexer.current().location; + + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + body->hasEnd = hasEnd; + + AstStatFor* node = allocator.alloc(Location(start, end), var, from, to, step, body, hasDo, matchDo.location); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(varname.colonPosition, equalsPosition, endCommaPosition, stepCommaPosition); + + return node; + } + else + { + TempVector names(scratchBinding); + AstArray varsCommaPosition; + names.push_back(varname); + + if (lexer.current().type == ',') + { + if (options.storeCstData) + { + Position initialCommaPosition = lexer.current().location.begin; + nextLexeme(); + parseBindingList(names, false, &varsCommaPosition, &initialCommaPosition); + } + else + { + nextLexeme(); + + parseBindingList(names); + } + } + + Location inLocation = lexer.current().location; + bool hasIn = expectAndConsume(Lexeme::ReservedIn, "for loop"); + + TempVector values(scratchExpr); + TempVector valuesCommaPositions(scratchPosition); + parseExprList(values, options.storeCstData ? &valuesCommaPositions : nullptr); + + Lexeme matchDo = lexer.current(); + bool hasDo = expectAndConsume(Lexeme::ReservedDo, "for loop"); + + unsigned int localsBegin = saveLocals(); + + functionStack.back().loopDepth++; + + TempVector vars(scratchLocal); + + for (size_t i = 0; i < names.size(); ++i) + vars.push_back(pushLocal(names[i])); + + AstStatBlock* body = parseBlock(); + + functionStack.back().loopDepth--; + + restoreLocals(localsBegin); + + Location end = lexer.current().location; + + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); + body->hasEnd = hasEnd; + + AstStatForIn* node = + allocator.alloc(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location); + if (options.storeCstData) + { + cstNodeMap[node] = allocator.alloc(extractAnnotationColonPositions(names), varsCommaPosition, copy(valuesCommaPositions)); + } + return node; + } +} + +// funcname ::= Name {`.' Name} [`:' Name] +AstExpr* Parser::parseFunctionName(bool& hasself, AstName& debugname) +{ + if (lexer.current().type == Lexeme::Name) + debugname = AstName(lexer.current().name); + + // parse funcname into a chain of indexing operators + AstExpr* expr = parseNameExpr("function name"); + + unsigned int oldRecursionCount = recursionCounter; + + while (lexer.current().type == '.') + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + Name name = parseName("field name"); + + // while we could concatenate the name chain, for now let's just write the short name + debugname = name.name; + + expr = allocator.alloc(Location(expr->location, name.location), expr, name.name, name.location, opPosition, '.'); + + // note: while the parser isn't recursive here, we're generating recursive structures of unbounded depth + incrementRecursionCounter("function name"); + } + + recursionCounter = oldRecursionCount; + + // finish with : + if (lexer.current().type == ':') + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + Name name = parseName("method name"); + + // while we could concatenate the name chain, for now let's just write the short name + debugname = name.name; + + expr = allocator.alloc(Location(expr->location, name.location), expr, name.name, name.location, opPosition, ':'); + + hasself = true; + } + + return expr; +} + +AstStatClass* Parser::getMatchingClass(AstExpr* expr) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + if (AstExprGlobal* g = expr->as()) + { + if (AstStatClass** classDecl = classesWithinModule.find(g->name)) + return *classDecl; + } + return nullptr; +} + +bool Parser::isExprLValue(AstExpr* expr) +{ + return (expr->is() && !expr->as()->local->isConst) || + (expr->is() && !(FFlag::DebugLuauUserDefinedClasses && getMatchingClass(expr) != nullptr)) || + expr->is() || expr->is(); +} + +// function funcname funcbody +AstStatFunction* Parser::parseFunctionStat(const AstArray& attributes, TempVector* cstAttrLists) +{ + Location start = getAttributeStartLocation(attributes, cstAttrLists, lexer.current().location); + + Lexeme matchFunction = lexer.current(); + nextLexeme(); + + bool hasself = false; + AstName debugname; + AstExpr* expr = parseFunctionName(hasself, debugname); + + if (!isExprLValue(expr)) + { + expr = FFlag::LuauExportValueSyntax ? reportLValueError(expr) + : reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); + } + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; + + AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, nullptr, attributes).first; + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; + + AstStatFunction* node = allocator.alloc(Location(start, body->location), expr, body); + if (options.storeCstData) + cstNodeMap[node] = cstAttrLists ? allocator.alloc(copy(*cstAttrLists), matchFunction.location.begin) + : allocator.alloc(matchFunction.location.begin); + + return node; +} + +std::optional Parser::validateAttribute( + Location loc, + const char* attributeName, + const TempVector& attributes, + const AstArray& args +) +{ + // check if the attribute name is valid + std::optional type; + std::optional argsValidator; + + for (int i = 0; kAttributeEntries[i].name; ++i) + { + if (strcmp(attributeName, kAttributeEntries[i].name) == 0) + { + type = kAttributeEntries[i].type; + argsValidator = kAttributeEntries[i].argsValidator; + break; + } + } + + for (const auto& [attributeEntry, fflagBool] : kDebugAttributeEntries) + { + if (fflagBool && strcmp(attributeName, attributeEntry.name) == 0) + { + type = attributeEntry.type; + argsValidator = attributeEntry.argsValidator; + break; + } + } + + if (!type) + { + if (strlen(attributeName) == 0) + report(loc, "Attribute name is missing"); + else + report(loc, "Invalid attribute '@%s'", attributeName); + } + else + { + // check that attribute is not duplicated + for (const AstAttr* attr : attributes) + { + if (attr->type == *type) + report(loc, "Cannot duplicate attribute '@%s'", attributeName); + } + if (argsValidator) + { + auto errorsToReport = (*argsValidator)(loc, args); + for (const auto& [errorLoc, msg] : errorsToReport) + { + report(errorLoc, "%s", msg.c_str()); + } + } + } + + return type; +} + +// attrlist = '@[' parattr {',' parattr} ']' +void Parser::parseAttrList(TempVector& attributes, TempVector* cstAttrLists) +{ + Lexeme open = lexer.current(); + + LUAU_ASSERT(open.type == Lexeme::Type::AttributeOpen); + + nextLexeme(); + + AstArray empty; + TempVector commaPositions(scratchPosition); + + if (lexer.current().type != ']') + { + while (true) + { + Name name = parseName("attribute name"); + + Location nameLoc = name.location; + const char* attrName = name.name.value; + + Lexeme argOpen = lexer.current(); + Lexeme::Type argOpenType = argOpen.type; + + if (argOpenType == Lexeme::RawString || argOpenType == Lexeme::QuotedString || argOpenType == '{' || argOpenType == '(') + { + Position openParenPosition = argOpenType == '(' ? argOpen.location.begin : Position::missing(); + TempVector argCommaPositions(scratchPosition2); + Position closeParenPosition = Position::missing(); + + auto [args, argsLocation, _exprLocation] = + options.storeCstData ? parseCallList(&argCommaPositions, &closeParenPosition) : parseCallList(nullptr, nullptr); + + for (const AstExpr* arg : args) + { + if (!isConstantLiteral(arg) && !isLiteralTable(arg)) + report(argsLocation, "Only literals can be passed as arguments for attributes"); + } + + std::optional type = validateAttribute(nameLoc, attrName, attributes, args); + + AstAttr* node = + allocator.alloc(Location(nameLoc, argsLocation), type.value_or(AstAttr::Type::Unknown), args, AstName(attrName)); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(openParenPosition, closeParenPosition, copy(argCommaPositions)); + + attributes.push_back(node); + } + else + { + std::optional type = validateAttribute(nameLoc, attrName, attributes, empty); + + AstAttr* node = allocator.alloc(nameLoc, type.value_or(AstAttr::Type::Unknown), empty, AstName(attrName)); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ false); + + attributes.push_back(node); + } + + const Lexeme& current = lexer.current(); + if (current.type == ',') + { + if (options.storeCstData) + commaPositions.push_back(current.location.begin); + + nextLexeme(); + } + else + { + break; + } + } + } + else + { + report(Location(open.location, lexer.current().location), "Attribute list cannot be empty"); + + // autocomplete expects at least one unknown attribute. + AstAttr* node = allocator.alloc(Location(open.location, lexer.current().location), AstAttr::Type::Unknown, empty, nameError); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ false); + + attributes.push_back(node); + } + + bool closingBracketFound = expectMatchAndConsume(']', open); + + if (options.storeCstData) + { + LUAU_ASSERT(cstAttrLists); + cstAttrLists->push_back(allocator.alloc( + open.location.begin, closingBracketFound ? lexer.previousLocation().begin : Position::missing(), copy(commaPositions) + )); + } +} + +// attribute ::= '@' NAME +void Parser::parseAttribute(TempVector& attributes) +{ + AstArray empty; + + LUAU_ASSERT(lexer.current().type == Lexeme::Type::Attribute); + + Location loc = lexer.current().location; + + const char* name = lexer.current().name; + std::optional type = validateAttribute(loc, name, attributes, empty); + + nextLexeme(); + + AstAttr* node = allocator.alloc(loc, type.value_or(AstAttr::Type::Unknown), empty, AstName(name)); + attributes.push_back(node); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(/* hasAt */ true); +} + +// attributes ::= {attribute} +AstArray Parser::parseAttributes(TempVector* cstAttrLists) +{ + Lexeme::Type type = lexer.current().type; + + LUAU_ASSERT(type == Lexeme::Attribute || type == Lexeme::AttributeOpen); + + TempVector attributes(scratchAttr); + + while (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) + { + if (lexer.current().type == Lexeme::Type::Attribute) + parseAttribute(attributes); + else + parseAttrList(attributes, cstAttrLists); + } + + return copy(attributes); +} + +Location Parser::getAttributeStartLocation( + const AstArray& attributes, + const TempVector* cstAttrLists, + const Location& defaultLocation +) +{ + if (attributes.size > 0) + { + if (cstAttrLists && cstAttrLists->size() > 0) + { + Location firstAttrLocation = attributes.data[0]->location; + const Position atBracketPosition = (*cstAttrLists)[0]->atBracketPosition; + + if (firstAttrLocation.begin < atBracketPosition) + return firstAttrLocation; + else + return Location(atBracketPosition, atBracketPosition); + } + else + return attributes.data[0]->location; + } + else if (cstAttrLists && cstAttrLists->size() > 0) + { + const Position atBracketPosition = (*cstAttrLists)[0]->atBracketPosition; + return Location(atBracketPosition, atBracketPosition); + } + else + return defaultLocation; +} + +// attributes local function Name funcbody +// attributes function funcname funcbody +// attributes `declare function' Name`(' [parlist] `)' [`:` Type] +// declare Name '{' Name ':' attributes `(' [parlist] `)' [`:` Type] '}' +AstStat* Parser::parseAttributeStat() +{ + const Location startLocation = lexer.current().location; + + AstArray attributes; + TempVector cstAttrLists(scratchCstAttrList); + attributes = parseAttributes(&cstAttrLists); + + Lexeme::Type type = lexer.current().type; + + switch (type) + { + case Lexeme::Type::ReservedFunction: + return parseFunctionStat(attributes, &cstAttrLists); + case Lexeme::Type::ReservedLocal: + return parseLocal( + getAttributeStartLocation(attributes, &cstAttrLists, startLocation), lexer.current().location.begin, attributes, false, &cstAttrLists + ); + case Lexeme::Type::Name: + { + if (FFlag::LuauExportValueSyntax && AstName(lexer.current().name) == "export") + { + Location keywordLoc = lexer.current().location; + nextLexeme(); + return parseExportValue(getAttributeStartLocation(attributes, &cstAttrLists, startLocation), keywordLoc.begin, attributes, &cstAttrLists); + } + + if (strcmp("const", lexer.current().data) == 0) + { + Location keywordLoc = lexer.current().location; + nextLexeme(); + return parseLocal(getAttributeStartLocation(attributes, &cstAttrLists, startLocation), keywordLoc.begin, attributes, true, &cstAttrLists); + } + if (options.allowDeclarationSyntax && !strcmp("declare", lexer.current().data)) + { + AstExpr* expr = parsePrimaryExpr(/* asStatement= */ true); + return parseDeclaration(expr->location, attributes); + } + } + [[fallthrough]]; + default: + return reportStatError( + lexer.current().location, + {}, + {}, + "Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got " + "%s instead", + lexer.current().toString().c_str() + ); + } +} + +bool isEnoughValues(TempVector& values, size_t expected) +{ + if (values.size() > 0) + { + AstExpr* last = values.back(); + if (last->is() || last->is()) + return true; + } + return values.size() == expected; +} + +AstStat* Parser::parseLocal( + const Location start, + const Position keywordPosition, + const AstArray& attributes, + bool isConst, + TempVector* cstAttrLists +) +{ + if (!isConst) + nextLexeme(); // local + + if (lexer.current().type == Lexeme::ReservedFunction) + { + Lexeme matchFunction = lexer.current(); + nextLexeme(); + + Position functionKeywordPosition = matchFunction.location.begin; + // matchFunction is only used for diagnostics; to make it suitable for detecting missed indentation between + // `local function` and `end`, we patch the token to begin at the column where `local` starts + if (matchFunction.location.begin.line == start.begin.line) + matchFunction.location.begin.column = start.begin.column; + + Name name = parseName("variable name"); + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; + + auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name, attributes, isConst); + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; + + Location location{start.begin, body->location.end}; + + AstStatLocalFunction* node = + allocator.alloc(location, var, body, isConst, isConst ? keywordPosition : Position::missing()); + if (options.storeCstData) + { + cstNodeMap[node] = cstAttrLists != nullptr + ? allocator.alloc(copy(*cstAttrLists), keywordPosition, functionKeywordPosition) + : allocator.alloc(keywordPosition, functionKeywordPosition); + } + return node; + } + else + { + if (attributes.size != 0) + { + return reportStatError( + lexer.current().location, + {}, + {}, + "Expected 'function' after local declaration with attribute, but got %s instead", + lexer.current().toString().c_str() + ); + } + + matchRecoveryStopOnToken['=']++; + + TempVector names(scratchBinding); + AstArray varsCommaPositions; + if (options.storeCstData) + parseBindingList(names, false, &varsCommaPositions, nullptr, nullptr, isConst); + else + parseBindingList(names, false, nullptr, nullptr, nullptr, isConst); + + matchRecoveryStopOnToken['=']--; + + TempVector vars(scratchLocal); + + TempVector values(scratchExpr); + TempVector valuesCommaPositions(scratchPosition); + + std::optional equalsSignLocation; + + if (lexer.current().type == '=') + { + equalsSignLocation = lexer.current().location; + + nextLexeme(); + + parseExprList(values, options.storeCstData ? &valuesCommaPositions : nullptr); + } + + for (size_t i = 0; i < names.size(); ++i) + vars.push_back(pushLocal(names[i])); + + Location end = values.empty() ? lexer.previousLocation() : values.back()->location; + + AstStatLocal* node = allocator.alloc(Location(start, end), copy(vars), copy(values), equalsSignLocation, isConst); + if (options.storeCstData) + { + cstNodeMap[node] = allocator.alloc(extractAnnotationColonPositions(names), varsCommaPositions, copy(valuesCommaPositions)); + } + + // It is a syntax error when a const declaration *definitely* does + // not have enough values, for example: + // + // const foo + // const bar, baz = 42 + // + // Both error as there's probably user error (`foo` and `baz` can + // only ever be `nil`). We report an error but return the + // declaration as-is, as it's still reasonable syntactically. + if (isConst && !isEnoughValues(values, vars.size())) + report(node->location, "Missing initializer in const declaration"); + + return node; + } +} + +// return [explist] +AstStat* Parser::parseReturn() +{ + Location start = lexer.current().location; + + nextLexeme(); + + TempVector list(scratchExpr); + TempVector commaPositions(scratchPosition); + + if (!blockFollow(lexer.current()) && lexer.current().type != ';') + parseExprList(list, options.storeCstData ? &commaPositions : nullptr); + + Location end = list.empty() ? start : list.back()->location; + + AstStatReturn* node = allocator.alloc(Location(start, end), copy(list)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(commaPositions)); + + if (FFlag::LuauExportValueSyntax && functionStack.size() == 1) + { + if (!declaredExportBindings.empty()) + report(node->location, "Exporting values is not compatible with top-level return (export/return conflict)"); + + hasModuleReturn = true; + } + + return node; +} + +// type Name [`<' varlist `>'] `=' Type +AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position typeKeywordPosition) +{ + // parsing a type function + if (lexer.current().type == Lexeme::ReservedFunction) + return parseTypeFunction(start, exported, typeKeywordPosition); + + // parsing a type alias + + // note: `type` token is already parsed for us, so we just need to parse the rest + + std::optional name = parseNameOpt("type name"); + + // Use error name if the name is missing + if (!name) + name = Name(nameError, lexer.current().location); + + Position genericsOpenPosition = Position::missing(); + AstArray genericsCommaPositions; + Position genericsClosePosition = Position::missing(); + auto [generics, genericPacks] = options.storeCstData + ? parseGenericTypeList( + /* withDefaultValues= */ true, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition + ) + : parseGenericTypeList(/* withDefaultValues= */ true); + + bool equalsFound = expectAndConsume('=', "type alias"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); + + AstType* type = parseType(); + + AstStatTypeAlias* node = + allocator.alloc(Location(start, type->location), name->name, name->location, generics, genericPacks, type, exported); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc( + typeKeywordPosition, genericsOpenPosition, genericsCommaPositions, genericsClosePosition, equalsPosition + ); + return node; +} + +namespace +{ + +const std::unordered_set kAllowedMetamethods{ + "__init", + "__call", + "__concat", + "__unm", + "__add", + "__sub", + "__mul", + "__div", + "__mod", + "__pow", + "__tostring", + "__eq", + "__lt", + "__le", + "__iter", + "__len", + "__idiv", +}; + +const std::unordered_set kExplicitlyDisallowedMetamethods{ + "__index", + "__newindex", + "__mode", + "__metatable", + "__type", +}; + +} // namespace + +// classStatement ::= [`open`] `class` Name classProps `end` +// classProps ::= classProp [classProps] +// classProp ::= name [: classQualifier* type] +LUAU_NOINLINE AstStat* Parser::parseClassStat(const Location& start, bool exported, bool open) +{ + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + std::optional name = parseNameOpt("type name"); + + // Use error name if the name is missing + if (!name) + name = Name(nameError, lexer.current().location); + + AstLocal* nameLocal = + allocator.alloc(name->name, name->location, nullptr, functionStack.size() - 1, functionStack.back().loopDepth, nullptr, true); + + AstExpr* super = nullptr; + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "extends") + { + nextLexeme(); + + super = parseClassRefExpr(); + } + + TempVector declarations(scratchClassDeclarations); + + // TODO: This does not seem particularly performant, but we need to + // establish the invariant that properties and methods share a + // namespace, so writing something like: + // + // class Foo + // public x + // function x() end + // end + // + // ... must fail. This gets the job done but maybe we can do something + // slightly more performant here (e.g.: a "scratch" set). + DenseHashSet classMemberNamespace; + + while (lexer.current().type != Lexeme::ReservedEnd && lexer.current().type != Lexeme::Eof) + { + std::optional qualifierLocation; + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "public") + { + qualifierLocation = lexer.current().location; + nextLexeme(); + } + + // If we saw a qualifier _and_ the current token is not `function`, + // assume this is a property. + if (qualifierLocation && lexer.current().type != Lexeme::ReservedFunction) + { + std::optional propName = parseNameOpt("class property name"); + if (!propName) + continue; + + AstType* propType = nullptr; + std::optional typeColonLocation; + + if (lexer.current().type == ':') + { + typeColonLocation = lexer.current().location; + nextLexeme(); + propType = parseType(); + } + + if (strncmp(propName->name.value, "__", 2) == 0) + report(propName->location, "Class properties cannot start with '__'"); + else if (classMemberNamespace.contains(propName->name)) + report(propName->location, "Duplicate class member '%s'", propName->name.value); + else + { + classMemberNamespace.insert(propName->name); + + // Either both of these are present or neither are. + LUAU_ASSERT((bool)propType == (bool)typeColonLocation); + declarations.push_back( + AstClassProperty{ + *qualifierLocation, + propName->name, + propName->location, + typeColonLocation, + propType, + } + ); + } + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + auto matchFunction = lexer.current(); + nextLexeme(); + + Name name = parseName("method name"); + // This is a little funky as we pass in a debug name but not a + // local name. The reason is that // in the declaration: + // + // class Student + // public name + // function print(self) + // print(`Hello, I'm a student and my name is {self.name}`) + // end + // end + // + // ... `print` inside `Student.print` is the _global_ print, and not + // the class' print. That would be `self.print`. + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; + + auto [body, _] = parseFunctionBody(false, matchFunction, name.name, nullptr, {}); + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; + + if (body->args.size > 0 && body->args.data[0]->name == "self" && body->args.data[0]->annotation != nullptr) + report(body->args.data[0]->annotation->location, "The 'self' parameter cannot have a type annotation"); + + if (strncmp(name.name.value, "__", 2) == 0) + { + if (kExplicitlyDisallowedMetamethods.count(name.name.value) > 0) + report(name.location, "Classes cannot define '%s' as a metamethod", name.name.value); + else if (kAllowedMetamethods.count(name.name.value) == 0) + report(name.location, "Cannot use '%s' as a method name: names starting with '__' are reserved", name.name.value); + } + + // TODO CLI-200853: We should support attributes, we do not need + // to support them prior to the full launch. + if (classMemberNamespace.contains(name.name)) + { + report(name.location, "Duplicate class member '%s'", name.name.value); + } + else + { + classMemberNamespace.insert(name.name); + + declarations.push_back( + AstClassMethod{ + qualifierLocation, + matchFunction.location, + name.name, + name.location, + body, + } + ); + } + } + else + { + report(lexer.current().location, "Only class properties and functions can be declared within a class"); + nextLexeme(); // skip the unexpected token to avoid an infinite loop + } + } + + // TODO: We should use `expectMatchEndAndConsume`. It is difficult as we + // are treating "class" as a contextual keyword (and we must as we also) + // plan to add a `class` library. + Location end = lexer.current().location; + expectAndConsume(Lexeme::ReservedEnd, "class"); + Location location{start, end}; + + // We only allow classes at the top level: we can make use of the + // recursion counter to check this, though it's a little clowny. + if (recursionCounter > 1) + report(nameLocal->location, "Cannot declare class '%s' inside another statement or expression", nameLocal->name.value); + + AstStatClass* cls = allocator.alloc(location, nameLocal, super, copy(declarations), exported, open); + if (classesWithinModule.contains(nameLocal->name)) + { + return reportStatError( + nameLocal->location, + {}, + copy({static_cast(cls)}), + "A class named '%s' has already been declared in this module", + nameLocal->name.value + ); + } + classesWithinModule[nameLocal->name] = cls; + return cls; +} + +// type function Name `(' arglist `)' `=' funcbody `end' +AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition) +{ + Lexeme matchFn = lexer.current(); + nextLexeme(); + + size_t errorsAtStart = parseErrors.size(); + + // parse the name of the type function + std::optional fnName = parseNameOpt("type function name"); + if (!fnName) + fnName = Name(nameError, lexer.current().location); + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; + + size_t oldTypeFunctionDepth = typeFunctionDepth; + typeFunctionDepth = functionStack.size(); + + AstExprFunction* body = parseFunctionBody(/* hasself */ false, matchFn, fnName->name, nullptr, AstArray({nullptr, 0})).first; + + typeFunctionDepth = oldTypeFunctionDepth; + + matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; + + bool hasErrors = parseErrors.size() > errorsAtStart; + + AstStatTypeFunction* node = + allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported, hasErrors); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(typeKeywordPosition, matchFn.location.begin); + return node; +} + +AstDeclaredExternTypeProperty Parser::parseDeclaredExternTypeMethod(const AstArray& attributes) +{ + Location start = lexer.current().location; + + nextLexeme(); + + Name fnName = parseName("function name"); + + // TODO: generic method declarations CLI-39909 + AstArray generics; + AstArray genericPacks; + generics.size = 0; + generics.data = nullptr; + genericPacks.size = 0; + genericPacks.data = nullptr; + + MatchLexeme matchParen = lexer.current(); + expectAndConsume('(', "function parameter list start"); + + TempVector args(scratchBinding); + + bool vararg = false; + Location varargLocation; + AstTypePack* varargAnnotation = nullptr; + if (lexer.current().type != ')') + std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3 */ true); + + expectMatchAndConsume(')', matchParen); + + AstTypePack* retTypes = parseOptionalReturnType(); + if (!retTypes) + retTypes = allocator.alloc(lexer.current().location, AstTypeList{copy(nullptr, 0), nullptr}); + Location end = lexer.previousLocation(); + + TempVector vars(scratchType); + TempVector> varNames(scratchOptArgName); + + if (args.size() == 0 || args[0].name.name != "self" || args[0].annotation != nullptr) + { + return AstDeclaredExternTypeProperty{ + fnName.name, fnName.location, reportTypeError(Location(start, end), {}, "'self' must be present as the unannotated first parameter"), true + }; + } + + // Skip the first index. + for (size_t i = 1; i < args.size(); ++i) + { + varNames.push_back(AstArgumentName{args[i].name.name, args[i].name.location}); + + if (args[i].annotation) + vars.push_back(args[i].annotation); + else + vars.push_back(reportTypeError(Location(start, end), {}, "All declaration parameters aside from 'self' must be annotated")); + } + + if (vararg && !varargAnnotation) + report(start, "All declaration parameters aside from 'self' must be annotated"); + + AstType* fnType = allocator.alloc( + Location(start, end), attributes, generics, genericPacks, AstTypeList{copy(vars), varargAnnotation}, copy(varNames), retTypes + ); + + return AstDeclaredExternTypeProperty{fnName.name, fnName.location, fnType, true, Location(start, end)}; +} + +AstStat* Parser::parseDeclaration(const Location& start, const AstArray& attributes) +{ + // `declare` token is already parsed at this point + + if ((attributes.size != 0) && (lexer.current().type != Lexeme::ReservedFunction)) + return reportStatError( + lexer.current().location, + {}, + {}, + "Expected a function type declaration after attribute, but got %s instead", + lexer.current().toString().c_str() + ); + + if (lexer.current().type == Lexeme::ReservedFunction) + { + nextLexeme(); + + Name globalName = parseName("global function name"); + auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); + + MatchLexeme matchParen = lexer.current(); + + expectAndConsume('(', "global function declaration"); + + TempVector args(scratchBinding); + + bool vararg = false; + Location varargLocation; + AstTypePack* varargAnnotation = nullptr; + + if (lexer.current().type != ')') + std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true); + + expectMatchAndConsume(')', matchParen); + + AstTypePack* retTypes; + retTypes = parseOptionalReturnType(); + if (!retTypes) + retTypes = allocator.alloc(lexer.current().location, AstTypeList{copy(nullptr, 0), nullptr}); + Location end = lexer.current().location; + + TempVector vars(scratchType); + TempVector varNames(scratchArgName); + + for (size_t i = 0; i < args.size(); ++i) + { + if (!args[i].annotation) + return reportStatError(Location(start, end), {}, {}, "All declaration parameters must be annotated"); + + vars.push_back(args[i].annotation); + varNames.push_back({args[i].name.name, args[i].name.location}); + } + + if (vararg && !varargAnnotation) + return reportStatError(Location(start, end), {}, {}, "All declaration parameters must be annotated"); + + return allocator.alloc( + Location(start, end), + attributes, + globalName.name, + globalName.location, + generics, + genericPacks, + AstTypeList{copy(vars), varargAnnotation}, + copy(varNames), + vararg, + varargLocation, + retTypes + ); + } + // When FFlag::LuauAllowGlobalDeclarationToBeCalledClass is set, `declare class : T` is parsed as a + // global variable declaration whose name is `class`, not as a malformed class declaration. This allows + // us to support a global table like string/math/bit32 called `class`. CLI-203833 tracks the work to actually + // remove support for `declare class X [extends Y]` syntax. + else if (AstName(lexer.current().name) == "extern") + { + if (AstName(lexer.current().name) == "extern") + { + nextLexeme(); + if (AstName(lexer.current().name) != "type") + return reportStatError( + lexer.current().location, {}, {}, "Expected `type` keyword after `extern`, but got %s instead", lexer.current().name + ); + } + + nextLexeme(); + + Location classStart = lexer.current().location; + Name className = parseName("type name"); + std::optional superName = std::nullopt; + + if (AstName(lexer.current().name) == "extends") + { + nextLexeme(); + superName = parseName("supertype name").name; + } + + if (AstName(lexer.current().name) != "with") + report( + lexer.current().location, + "Expected `with` keyword before listing properties of the external type, but got %s instead", + lexer.current().name + ); + else + nextLexeme(); + + TempVector props(scratchDeclaredClassProps); + AstTableIndexer* indexer = nullptr; + + while (lexer.current().type != Lexeme::ReservedEnd) + { + AstArray attributes{nullptr, 0}; + + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) + { + attributes = Parser::parseAttributes(); + + if (lexer.current().type != Lexeme::ReservedFunction) + return reportStatError( + lexer.current().location, + {}, + {}, + "Expected a method type declaration after attribute, but got %s instead", + lexer.current().toString().c_str() + ); + } + + // There are two possibilities: Either it's a property or a function. + if (lexer.current().type == Lexeme::ReservedFunction) + { + props.push_back(parseDeclaredExternTypeMethod(attributes)); + } + else if (lexer.current().type == '[') + { + const Lexeme begin = lexer.current(); + nextLexeme(); // [ + + if ((lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) && lexer.lookahead().type == ']') + { + const Location nameBegin = lexer.current().location; + std::optional> chars = parseCharArray(); + + const Location nameEnd = lexer.previousLocation(); + + expectMatchAndConsume(']', begin); + expectAndConsume(':', "property type annotation"); + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back( + AstDeclaredExternTypeProperty{ + AstName(chars->data), Location(nameBegin, nameEnd), type, false, Location(begin.location, lexer.previousLocation()) + } + ); + } + else + { + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + } + else if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, begin).node; + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one indexer on an extern type"); + } + else + { + indexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, begin).node; + } + } + else + { + AstTableAccess access = AstTableAccess::ReadWrite; + + if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':') + { + if (AstName(lexer.current().name) == "read") + { + access = AstTableAccess::Read; + lexer.next(); + } + else if (AstName(lexer.current().name) == "write") + { + access = AstTableAccess::Write; + lexer.next(); + } + else + { + report(lexer.current().location, "Expected blank or 'read' or 'write' attribute, got '%s'", lexer.current().name); + lexer.next(); + } + } + + Location propStart = lexer.current().location; + std::optional propName = parseNameOpt("property name"); + + if (!propName) + break; + + expectAndConsume(':', "property type annotation"); + AstType* propType = parseType(); + props.push_back( + AstDeclaredExternTypeProperty{ + propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation()), access + } + ); + } + } + + Location classEnd = lexer.current().location; + nextLexeme(); // skip past `end` + + return allocator.alloc(Location(classStart, classEnd), className.name, superName, copy(props), indexer); + } + else if (std::optional globalName = parseNameOpt("global variable name")) + { + expectAndConsume(':', "global variable declaration"); + + AstType* type = parseType(/* in declaration context */ true); + return allocator.alloc(Location(start, type->location), globalName->name, globalName->location, type); + } + else + { + return reportStatError(start, {}, {}, "declare must be followed by an identifier, 'function', or 'extern type'"); + } +} + +AstExprError* Parser::reportLValueError(AstExpr* expr) +{ + if (expr->is() && expr->as()->local->isConst) + { + AstExprLocal* local = expr->as(); + return reportExprError(expr->location, copy({expr}), "Variable '%s' is constant and may not be reassigned", local->local->name.value); + } + if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstStatClass* classStat = getMatchingClass(expr)) + { + return reportExprError( + expr->location, + copy({expr}), + "'%s' refers to a class and cannot be used as a variable name (defined on line %d)", + classStat->name->name.value, + classStat->location.begin.line + 1 + ); + } + } + + return reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); +} + +// varlist `=' explist +AstStat* Parser::parseAssignment(AstExpr* initial) +{ + if (!isExprLValue(initial)) + initial = FFlag::LuauExportValueSyntax + ? reportLValueError(initial) + : reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); + + TempVector vars(scratchExpr); + TempVector varsCommaPositions(scratchPosition); + vars.push_back(initial); + + while (lexer.current().type == ',') + { + if (options.storeCstData) + varsCommaPositions.push_back(lexer.current().location.begin); + nextLexeme(); + + AstExpr* expr = parsePrimaryExpr(/* asStatement= */ true); + + if (!isExprLValue(expr)) + expr = FFlag::LuauExportValueSyntax ? reportLValueError(expr) + : reportExprError(expr->location, copy({expr}), "Assigned expression must be a variable or a field"); + + vars.push_back(expr); + } + + bool equalsFound = expectAndConsume('=', "assignment"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); + + TempVector values(scratchExprAux); + TempVector valuesCommaPositions(scratchPosition); + parseExprList(values, options.storeCstData ? &valuesCommaPositions : nullptr); + + AstStatAssign* node = allocator.alloc(Location(initial->location, values.back()->location), copy(vars), copy(values)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(varsCommaPositions), equalsPosition, copy(valuesCommaPositions)); + return node; +} + +AstStat* Parser::parseExportValue( + const Location& start, + const Position keywordPosition, + const AstArray& attributes, + TempVector* cstAttrLists +) +{ + if (functionStack.size() != 1 || recursionCounter != 1) + report(start, "'export' may only be applied to top-level statements"); + + if (hasModuleReturn) + report(start, "Exporting values is not compatible with top-level return (export/return conflict)"); + + auto checkDuplicateExport = [&](AstName name, const Location& location) -> bool + { + if (declaredExportBindings.find(name)) + return false; + + declaredExportBindings[name] = location; + return true; + }; + + auto exportLocalStat = [&](AstStat* stat, const Location& keywordLocation) -> AstStat* + { + if (AstStatLocal* localStat = stat->as()) + { + localStat->isExported = true; + + for (AstLocal* local : localStat->vars) + { + if (!checkDuplicateExport(local->name, local->location)) + { + report(local->location, "Duplicate exported identifier '%s'", local->name.value); + continue; + } + + local->isExported = true; + } + + localStat->keywordLocation = keywordLocation; + } + else + LUAU_ASSERT(!"Expected export local/const to parse as AstStatLocal"); + + return stat; + }; + + if (attributes.size != 0 && lexer.current().type != Lexeme::ReservedFunction) + { + report( + lexer.current().location, + "Expected 'function' after export declaration with attribute, but got %s instead", + lexer.current().toString().c_str() + ); + } + + if (lexer.current().type == Lexeme::ReservedLocal) + { + Location localKeywordLocation = lexer.current().location; + + if (lexer.lookahead().type == Lexeme::ReservedFunction) + { + report(start, "'export' must be followed by an identifier or 'function'; try removing 'local'"); + // still parse the function for error recovery + return parseLocal(start, localKeywordLocation.begin, {nullptr, 0}, true); + } + + return exportLocalStat(parseLocal(start, keywordPosition, {nullptr, 0}, false), localKeywordLocation); + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + auto funcStat = parseLocal(start, keywordPosition, attributes, true, cstAttrLists); + if (!funcStat->is()) + // parseLocal returned a parse error + return funcStat; + + auto func = funcStat->as(); + + if (!checkDuplicateExport(func->name->name, func->name->location)) + report(func->name->location, "Duplicate exported identifier '%s'", func->name->name.value); + + func->name->isExported = true; + func->name->isConst = true; + return func; + } + else if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const") + { + Location constKeywordLocation = lexer.current().location; + nextLexeme(); + + if (lexer.current().type == Lexeme::ReservedFunction) + { + report(start, "'export' must be followed by an identifier or 'function'"); + // still parse the function for error recovery + return parseLocal(start, constKeywordLocation.begin, {nullptr, 0}, true); + } + + return exportLocalStat(parseLocal(start, constKeywordLocation.begin, {nullptr, 0}, true), constKeywordLocation); + } + else if (FFlag::DebugLuauUserDefinedClasses && lexer.current().type == Lexeme::Name && + (AstName(lexer.current().name) == "class" || AstName(lexer.current().name) == "open")) + { + bool open = AstName(lexer.current().name) == "open"; + + nextLexeme(); + if (open) + { + if (lexer.current().type != Lexeme::Name || AstName(lexer.current().name) != "class") + return reportStatError(start, {}, {}, "Incomplete statement: expected a class definition after 'open'"); + + nextLexeme(); // consume 'class' after 'open' + } + + auto stat = parseClassStat(start, /* exported */ true, open); + if (auto classStat = stat->as()) + { + if (!checkDuplicateExport(classStat->name->name, classStat->name->location)) + report(classStat->name->location, "Duplicate exported class '%s'", classStat->name->name.value); + + classStat->name->isExported = true; + } + return stat; + } + + return reportStatError(start, {}, {}, "'export' must be followed by an identifier or 'function'"); +} + +// var [`+=' | `-=' | `*=' | `/=' | `%=' | `^=' | `..='] exp +AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op) +{ + if (!isExprLValue(initial)) + { + initial = FFlag::LuauExportValueSyntax + ? reportLValueError(initial) + : reportExprError(initial->location, copy({initial}), "Assigned expression must be a variable or a field"); + } + + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + AstExpr* value = parseExpr(); + + AstStatCompoundAssign* node = allocator.alloc(Location(initial->location, value->location), op, initial, value); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(opPosition); + return node; +} + +std::pair> Parser::prepareFunctionArguments(const Location& start, bool hasself, const TempVector& args) +{ + AstLocal* self = nullptr; + + if (hasself) + self = pushLocal(Binding(Name(nameSelf, start), nullptr)); + + TempVector vars(scratchLocal); + + for (size_t i = 0; i < args.size(); ++i) + vars.push_back(pushLocal(args[i])); + + return {self, copy(vars)}; +} + +// funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end +// parlist ::= bindinglist [`,' `...'] | `...' +std::pair Parser::parseFunctionBody( + bool hasself, + const Lexeme& matchFunction, + const AstName& debugname, + const Name* localName, + const AstArray& attributes, + const bool isConst, + TempVector* cstAttrLists +) +{ + Location start = matchFunction.location; + + if (attributes.size > 0) + start = attributes.data[0]->location; + + auto* cstNode = options.storeCstData ? allocator.alloc() : nullptr; + + if (cstNode && cstAttrLists) + cstNode->attrLists = copy(*cstAttrLists); + + auto [generics, genericPacks] = + cstNode + ? parseGenericTypeList( + /* withDefaultValues= */ false, &cstNode->openGenericsPosition, &cstNode->genericsCommaPositions, &cstNode->closeGenericsPosition + ) + : parseGenericTypeList(/* withDefaultValues= */ false); + + MatchLexeme matchParen = lexer.current(); + expectAndConsume('(', "function"); + + // NOTE: This was added in conjunction with passing `searchForMissing` to + // `expectMatchAndConsume` inside `parseTableType` so that the behavior of + // parsing code like below (note the missing `}`): + // + // function (t: { a: number ) end + // + // ... will still parse as (roughly): + // + // function (t: { a: number }) end + // + matchRecoveryStopOnToken[')']++; + + TempVector args(scratchBinding); + + bool vararg = false; + Location varargLocation; + AstTypePack* varargAnnotation = nullptr; + + if (lexer.current().type != ')') + { + if (cstNode) + std::tie(vararg, varargLocation, varargAnnotation) = + parseBindingList(args, /* allowDot3= */ true, &cstNode->argsCommaPositions, nullptr, &cstNode->varargAnnotationColonPosition); + else + std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true); + } + + std::optional argLocation; + + if (matchParen.type == Lexeme::Type('(') && lexer.current().type == Lexeme::Type(')')) + argLocation = Location(matchParen.position, lexer.current().location.end); + + expectMatchAndConsume(')', matchParen, true); + + matchRecoveryStopOnToken[')']--; + + AstTypePack* typelist = parseOptionalReturnType(cstNode ? &cstNode->returnSpecifierPosition : nullptr); + + AstLocal* funLocal = nullptr; + + if (localName) + { + funLocal = pushLocal(Binding(*localName, nullptr, {0, 0}, isConst)); + } + + unsigned int localsBegin = saveLocals(); + + Function fun; + fun.vararg = vararg; + + functionStack.emplace_back(fun); + + auto [self, vars] = prepareFunctionArguments(start, hasself, args); + + AstStatBlock* body = parseBlock(); + + functionStack.pop_back(); + + restoreLocals(localsBegin); + + Location end = lexer.current().location; + + bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); + body->hasEnd = hasEnd; + + AstExprFunction* node = allocator.alloc( + Location(start, end), + attributes, + generics, + genericPacks, + self, + vars, + vararg, + varargLocation, + body, + functionStack.size(), + debugname, + typelist, + varargAnnotation, + argLocation + ); + if (options.storeCstData) + { + cstNode->functionKeywordPosition = matchFunction.location.begin; + cstNode->argsAnnotationColonPositions = extractAnnotationColonPositions(args); + cstNodeMap[node] = cstNode; + } + + return {node, funLocal}; +} + +// explist ::= {exp `,'} exp +void Parser::parseExprList(TempVector& result, TempVector* commaPositions) +{ + result.push_back(parseExpr()); + + while (lexer.current().type == ',') + { + if (commaPositions) + commaPositions->push_back(lexer.current().location.begin); + nextLexeme(); + + if (lexer.current().type == ')') + { + report(lexer.current().location, "Expected expression after ',' but got ')' instead"); + break; + } + + result.push_back(parseExpr()); + } +} + +Parser::Binding Parser::parseBinding(bool isConst) +{ + std::optional name = parseNameOpt("variable name"); + + // Use placeholder if the name is missing + if (!name) + name = Name(nameError, lexer.current().location); + + Position colonPosition = lexer.current().type == ':' ? lexer.current().location.begin : Position::missing(); + AstType* annotation = parseOptionalType(); + + if (options.storeCstData) + return Binding(*name, annotation, colonPosition, isConst); + else + return Binding(*name, annotation, Position::missing(), isConst); +} + +AstArray Parser::extractAnnotationColonPositions(const TempVector& bindings) +{ + TempVector annotationColonPositions(scratchPosition); + for (size_t i = 0; i < bindings.size(); ++i) + annotationColonPositions.push_back(bindings[i].colonPosition); + return copy(annotationColonPositions); +} + +// bindinglist ::= (binding | `...') [`,' bindinglist] +LUAU_NOINLINE std::tuple Parser::parseBindingList( + TempVector& result, + bool allowDot3, + AstArray* commaPositions, + Position* initialCommaPosition, + Position* varargAnnotationColonPosition, + bool isConst +) +{ + TempVector localCommaPositions(scratchPosition); + + if (commaPositions && initialCommaPosition) + localCommaPositions.push_back(*initialCommaPosition); + + while (true) + { + if (lexer.current().type == Lexeme::Dot3 && allowDot3) + { + Location varargLocation = lexer.current().location; + nextLexeme(); + + AstTypePack* tailAnnotation = nullptr; + if (lexer.current().type == ':') + { + if (varargAnnotationColonPosition) + *varargAnnotationColonPosition = lexer.current().location.begin; + + nextLexeme(); + tailAnnotation = parseVariadicArgumentTypePack(); + } + + if (commaPositions) + *commaPositions = copy(localCommaPositions); + + return {true, varargLocation, tailAnnotation}; + } + + result.push_back(parseBinding(isConst)); + + if (lexer.current().type != ',') + break; + if (commaPositions) + localCommaPositions.push_back(lexer.current().location.begin); + nextLexeme(); + } + + if (commaPositions) + *commaPositions = copy(localCommaPositions); + + return {false, Location(), nullptr}; +} + +AstType* Parser::parseOptionalType() +{ + if (lexer.current().type == ':') + { + nextLexeme(); + return parseType(); + } + else + return nullptr; +} + +// TypeList ::= Type [`,' TypeList] | ...Type +AstTypePack* Parser::parseTypeList( + TempVector& result, + TempVector>& resultNames, + TempVector* commaPositions, + TempVector* nameColonPositions +) +{ + while (true) + { + if (shouldParseTypePack(lexer)) + return parseTypePack(); + + if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == ':') + { + // Fill in previous argument names with empty slots + while (resultNames.size() < result.size()) + resultNames.push_back({}); + if (nameColonPositions) + { + while (nameColonPositions->size() < result.size()) + nameColonPositions->push_back(Position::missing()); + } + + resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location}); + nextLexeme(); + + if (nameColonPositions) + nameColonPositions->push_back(lexer.current().location.begin); + expectAndConsume(':'); + } + else if (!resultNames.empty()) + { + // If we have a type with named arguments, provide elements for all types + resultNames.push_back({}); + if (nameColonPositions) + nameColonPositions->push_back(Position::missing()); + } + + result.push_back(parseType()); + if (lexer.current().type != ',') + break; + + if (commaPositions) + commaPositions->push_back(lexer.current().location.begin); + nextLexeme(); + + if (lexer.current().type == ')') + { + report(lexer.current().location, "Expected type after ',' but got ')' instead"); + break; + } + } + + return nullptr; +} + +AstTypePack* Parser::parseOptionalReturnType(Position* returnSpecifierPosition) +{ + if (lexer.current().type == ':' || lexer.current().type == Lexeme::SkinnyArrow) + { + if (lexer.current().type == Lexeme::SkinnyArrow) + report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'"); + + if (returnSpecifierPosition) + *returnSpecifierPosition = lexer.current().location.begin; + nextLexeme(); + + unsigned int oldRecursionCount = recursionCounter; + + auto result = parseReturnType(); + LUAU_ASSERT(result); + + // At this point, if we find a , character, it indicates that there are multiple return types + // in this type annotation, but the list wasn't wrapped in parentheses. + if (lexer.current().type == ',') + { + report(lexer.current().location, "Expected a statement, got ','; did you forget to wrap the list of return types in parentheses?"); + + nextLexeme(); + } + + recursionCounter = oldRecursionCount; + + return result; + } + + return nullptr; +} + +static bool isTypeFollow(Lexeme::Type c) +{ + return c == '|' || c == '?' || c == '&'; +} + +// ReturnType ::= Type | `(' TypeList `)' +AstTypePack* Parser::parseReturnType() +{ + incrementRecursionCounter("type annotation"); + + Lexeme begin = lexer.current(); + + if (lexer.current().type != '(') + { + if (shouldParseTypePack(lexer)) + { + return parseTypePack(); + } + else + { + AstType* type = parseType(); + AstTypePackExplicit* node = allocator.alloc(type->location, AstTypeList{copy(&type, 1), nullptr}); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(); + return node; + } + } + + nextLexeme(); // Consume '(' + + matchRecoveryStopOnToken[Lexeme::SkinnyArrow]++; + + TempVector result(scratchType); + TempVector> resultNames(scratchOptArgName); + TempVector commaPositions(scratchPosition); + TempVector nameColonPositions(scratchPosition2); + AstTypePack* varargAnnotation = nullptr; + + // possibly () -> ReturnType + if (lexer.current().type != ')') + { + if (options.storeCstData) + varargAnnotation = parseTypeList(result, resultNames, &commaPositions, &nameColonPositions); + else + varargAnnotation = parseTypeList(result, resultNames); + } + + const Location location{begin.location, lexer.current().location}; + bool closeParenFound = expectMatchAndConsume(')', begin, true); + Position closeParenthesesPosition = closeParenFound ? lexer.previousLocation().begin : Position::missing(); + + matchRecoveryStopOnToken[Lexeme::SkinnyArrow]--; + + if (lexer.current().type != Lexeme::SkinnyArrow && resultNames.empty()) + { + // If it turns out that it's just '(A)', it's possible that there are unions/intersections to follow, so fold over it. + if (result.size() == 1) + { + // TODO(CLI-140667): stop parsing type suffix when varargAnnotation != nullptr - this should be a parse error + AstType* inner = nullptr; + + bool parensBelongToInnerGroup = false; + if (FFlag::LuauSingleTypeOptionalPackReturnsAttributeParens) + { + if (varargAnnotation == nullptr && isTypeFollow(lexer.current().type)) + { + inner = allocator.alloc(location, result[0]); + parensBelongToInnerGroup = true; + if (options.storeCstData) + cstNodeMap[inner] = allocator.alloc(closeParenFound ? closeParenthesesPosition : Position::missing()); + } + else + inner = result[0]; + } + else + { + if (varargAnnotation == nullptr) + { + inner = allocator.alloc(location, result[0]); + + if (options.storeCstData) + cstNodeMap[inner] = allocator.alloc(closeParenFound ? closeParenthesesPosition : Position::missing()); + } + else + inner = result[0]; + } + + + AstType* returnType = parseTypeSuffix(inner, begin.location); + + if (DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix && varargAnnotation != nullptr && + (returnType->is() || returnType->is())) + luau_telemetry_parsed_return_type_variadic_with_type_suffix = true; + + // If parseType parses nothing, then returnType->location.end only points at the last non-type-pack + // type to successfully parse. We need the span of the whole annotation. + Position endPos = result.size() == 1 ? location.end : returnType->location.end; + + AstTypePackExplicit* node = + allocator.alloc(Location{location.begin, endPos}, AstTypeList{copy(&returnType, 1), varargAnnotation}); + if (FFlag::LuauSingleTypeOptionalPackReturnsAttributeParens && options.storeCstData) + { + cstNodeMap[node] = parensBelongToInnerGroup + ? allocator.alloc() + : allocator.alloc(location.begin, closeParenthesesPosition, copy(commaPositions)); + return node; + } + else + { + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(); + return node; + } + } + + AstTypePackExplicit* node = allocator.alloc(location, AstTypeList{copy(result), varargAnnotation}); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(location.begin, closeParenthesesPosition, copy(commaPositions)); + return node; + } + + Position returnArrowPosition = lexer.current().location.begin; + AstType* tail = parseFunctionTypeTail(begin, {nullptr, 0}, {}, {}, copy(result), copy(resultNames), varargAnnotation); + + if (options.storeCstData && tail->is()) + { + cstNodeMap[tail] = allocator.alloc( + Position::missing(), + AstArray{}, + Position::missing(), + location.begin, + copy(nameColonPositions), + copy(commaPositions), + closeParenthesesPosition, + returnArrowPosition + ); + } + + AstTypePackExplicit* node = allocator.alloc(Location{location, tail->location}, AstTypeList{copy(&tail, 1), nullptr}); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(); + return node; +} + +std::pair Parser::extractStringDetails() +{ + CstExprConstantString::QuoteStyle style; + unsigned int blockDepth = 0; + + switch (lexer.current().type) + { + case Lexeme::QuotedString: + style = lexer.current().getQuoteStyle() == Lexeme::QuoteStyle::Double ? CstExprConstantString::QuoteStyle::QuotedDouble + : CstExprConstantString::QuoteStyle::QuotedSingle; + break; + case Lexeme::InterpStringSimple: + style = CstExprConstantString::QuoteStyle::QuotedInterp; + break; + case Lexeme::RawString: + { + style = CstExprConstantString::QuoteStyle::QuotedRaw; + blockDepth = lexer.current().getBlockDepth(); + break; + } + default: + LUAU_ASSERT(false && "Invalid string type"); + } + + return {style, blockDepth}; +} + +// TableIndexer ::= `[' Type `]' `:' Type +Parser::TableIndexerResult Parser::parseTableIndexer(AstTableAccess access, std::optional accessLocation, Lexeme begin) +{ + AstType* index = parseType(); + + bool indexerCloseFound = expectMatchAndConsume(']', begin); + Position indexerClosePosition = indexerCloseFound ? lexer.previousLocation().begin : Position::missing(); + + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); + + AstType* result = parseType(); + + return { + allocator.alloc(AstTableIndexer{index, result, Location(begin.location, result->location), access, accessLocation}), + begin.location.begin, + indexerClosePosition, + colonPosition, + }; +} + +// TableProp ::= Name `:' Type +// TablePropOrIndexer ::= TableProp | TableIndexer +// PropList ::= TablePropOrIndexer {fieldsep TablePropOrIndexer} [fieldsep] +// TableType ::= `{' PropList `}' +AstType* Parser::parseTableType(bool inDeclarationContext) +{ + incrementRecursionCounter("type annotation"); + + TempVector props(scratchTableTypeProps); + TempVector cstItems(scratchCstTableTypeProps); + AstTableIndexer* indexer = nullptr; + + Location start = lexer.current().location; + + MatchLexeme matchBrace = lexer.current(); + expectAndConsume('{', "table type"); + + bool isArray = false; + // When the flag is set, tables are exact by default unless ... is present at the end. + bool isExact = FFlag::DebugLuauParseExactTables; + + while (lexer.current().type != '}') + { + AstTableAccess access = AstTableAccess::ReadWrite; + std::optional accessLocation; + + if (FFlag::DebugLuauParseExactTables && lexer.current().type == Lexeme::Dot3) + { + nextLexeme(); + isExact = false; + // ... is always last. + break; + } + + if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':') + { + if (AstName(lexer.current().name) == "read") + { + accessLocation = lexer.current().location; + access = AstTableAccess::Read; + lexer.next(); + } + else if (AstName(lexer.current().name) == "write") + { + accessLocation = lexer.current().location; + access = AstTableAccess::Write; + lexer.next(); + } + } + + if (lexer.current().type == '[') + { + const Lexeme begin = lexer.current(); + nextLexeme(); // [ + + if ((lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) && lexer.lookahead().type == ']') + { + CstExprConstantString::QuoteStyle style; + unsigned int blockDepth = 0; + if (options.storeCstData) + std::tie(style, blockDepth) = extractStringDetails(); + + Position stringPosition = lexer.current().location.begin; + AstArray sourceString; + std::optional> chars = parseCharArray(options.storeCstData ? &sourceString : nullptr); + + bool closingBracketFound = expectMatchAndConsume(']', begin); + Position indexerClosePosition = closingBracketFound ? lexer.previousLocation().begin : Position::missing(); + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); + + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back(AstTableProp{AstName(chars->data), begin.location, type, access, accessLocation}); + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + CstTypeTable::Item{ + CstTypeTable::Item::Kind::StringProperty, + begin.location.begin, + indexerClosePosition, + colonPosition, + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), + allocator.alloc(sourceString, style, blockDepth), + stringPosition + } + ); + } + } + else + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + else + { + if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer = parseTableIndexer(access, accessLocation, begin).node; + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one table indexer"); + } + else + { + auto tableIndexerResult = parseTableIndexer(access, accessLocation, begin); + indexer = tableIndexerResult.node; + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + CstTypeTable::Item{ + CstTypeTable::Item::Kind::Indexer, + tableIndexerResult.indexerOpenPosition, + tableIndexerResult.indexerClosePosition, + tableIndexerResult.colonPosition, + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), + } + ); + } + } + } + } + else if (props.empty() && !indexer && !(lexer.current().type == Lexeme::Name && lexer.lookahead().type == ':')) + { + AstType* type = parseType(); + + // array-like table type: {T} desugars into {[number]: T} + isArray = true; + Location nullTypeLocation = Location(start.begin, 0); + AstType* index = allocator.alloc(nullTypeLocation, std::nullopt, nameNumber, std::nullopt, nullTypeLocation); + indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); + + if (FFlag::DebugLuauParseExactTables) + { + if (lexer.current().type == ',' && lexer.lookahead().type == Lexeme::Dot3) + { + nextLexeme(); + nextLexeme(); + isExact = false; + } + } + + break; + } + else + { + std::optional name = parseNameOpt("table field"); + + if (!name) + break; + + bool colonFound = expectAndConsume(':', "table field"); + Position colonPosition = colonFound ? lexer.previousLocation().begin : Position::missing(); + + AstType* type = parseType(inDeclarationContext); + + props.push_back(AstTableProp{name->name, name->location, type, access, accessLocation}); + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + CstTypeTable::Item{ + CstTypeTable::Item::Kind::Property, + Position::missing(), + Position::missing(), + colonPosition, + separator, + separator != CstExprTable::Separator::Missing ? lexer.current().location.begin : Position::missing(), + } + ); + } + } + + if (lexer.current().type == ',' || lexer.current().type == ';') + { + nextLexeme(); + } + else + { + if (lexer.current().type != '}') + break; + } + } + + Location end = lexer.current().location; + + if (!expectMatchAndConsume('}', matchBrace, /* searchForMissing = */ true)) + end = lexer.previousLocation(); + + AstTypeTable* node = allocator.alloc(Location(start, end), copy(props), indexer, isExact); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(cstItems), isArray); + return node; +} + +// ReturnType ::= Type | `(' TypeList `)' +// FunctionType ::= [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType +AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray& attributes) +{ + incrementRecursionCounter("type annotation"); + + bool forceFunctionType = lexer.current().type == '<'; + + Lexeme begin = lexer.current(); + + Position genericsOpenPosition = Position::missing(); + AstArray genericsCommaPositions; + Position genericsClosePosition = Position::missing(); + auto [generics, genericPacks] = options.storeCstData + ? parseGenericTypeList( + /* withDefaultValues= */ false, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition + ) + : parseGenericTypeList(/* withDefaultValues= */ false); + + Lexeme parameterStart = lexer.current(); + + bool openArgsFound = expectAndConsume('(', "function parameters"); + + matchRecoveryStopOnToken[Lexeme::SkinnyArrow]++; + + TempVector params(scratchType); + TempVector> names(scratchOptArgName); + TempVector nameColonPositions(scratchPosition); + TempVector argCommaPositions(scratchPosition2); + AstTypePack* varargAnnotation = nullptr; + + if (lexer.current().type != ')') + { + if (options.storeCstData) + varargAnnotation = parseTypeList(params, names, &argCommaPositions, &nameColonPositions); + else + varargAnnotation = parseTypeList(params, names); + } + + Location closeArgsLocation = lexer.current().location; + bool closeArgsFound = expectMatchAndConsume(')', parameterStart, true); + + matchRecoveryStopOnToken[Lexeme::SkinnyArrow]--; + + AstArray paramTypes = copy(params); + + if (!names.empty()) + forceFunctionType = true; + + bool returnTypeIntroducer = lexer.current().type == Lexeme::SkinnyArrow || lexer.current().type == ':'; + + // Not a function at all. Just a parenthesized type. Or maybe a type pack with a single element + if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer) + { + if (allowPack) + { + AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr}); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc( + openArgsFound ? parameterStart.location.begin : Position::missing(), + closeArgsFound ? closeArgsLocation.begin : Position::missing(), + copy(argCommaPositions) + ); + return {{}, node}; + } + else + { + AstTypeGroup* node = allocator.alloc(Location(parameterStart.location, closeArgsLocation), params[0]); + + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(closeArgsFound ? closeArgsLocation.begin : Position::missing()); + + return {node, {}}; + } + } + + if (!forceFunctionType && !returnTypeIntroducer && allowPack) + { + AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation}); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc( + openArgsFound ? parameterStart.location.begin : Position::missing(), + closeArgsFound ? closeArgsLocation.begin : Position::missing(), + copy(argCommaPositions) + ); + return {{}, node}; + } + + AstArray> paramNames = copy(names); + + Position returnArrowPosition = lexer.current().location.begin; + AstType* node = parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation); + if (options.storeCstData && node->is()) + { + cstNodeMap[node] = allocator.alloc( + genericsOpenPosition, + genericsCommaPositions, + genericsClosePosition, + openArgsFound ? parameterStart.location.begin : Position::missing(), + copy(nameColonPositions), + copy(argCommaPositions), + closeArgsFound ? closeArgsLocation.begin : Position::missing(), + returnArrowPosition + ); + } + return {node, {}}; +} + +AstType* Parser::parseFunctionTypeTail( + const Lexeme& begin, + const AstArray& attributes, + AstArray generics, + AstArray genericPacks, + AstArray params, + AstArray> paramNames, + AstTypePack* varargAnnotation +) +{ + incrementRecursionCounter("type annotation"); + + if (lexer.current().type == ':') + { + report(lexer.current().location, "Return types in function type annotations are written after '->' instead of ':'"); + lexer.next(); + } + // Users occasionally write '()' as the 'unit' type when they actually want to use 'nil', here we'll try to give a more specific error + else if (lexer.current().type != Lexeme::SkinnyArrow && generics.size == 0 && genericPacks.size == 0 && params.size == 0) + { + report(Location(begin.location, lexer.previousLocation()), "Expected '->' after '()' when parsing function type; did you mean 'nil'?"); + + return allocator.alloc(begin.location, std::nullopt, nameNil, std::nullopt, begin.location); + } + else + { + expectAndConsume(Lexeme::SkinnyArrow, "function type"); + } + + auto returnType = parseReturnType(); + LUAU_ASSERT(returnType); + + AstTypeList paramTypes = AstTypeList{params, varargAnnotation}; + return allocator.alloc( + Location(begin.location, returnType->location), attributes, generics, genericPacks, paramTypes, paramNames, returnType + ); +} + +// Type ::= +// nil | +// Name[`.' Name] [`<' namelist `>'] | +// `{' [PropList] `}' | +// `(' [TypeList] `)' `->` ReturnType +// `typeof` Type +AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) +{ + TempVector parts(scratchType); + TempVector separatorPositions(scratchPosition); + Position leadingPosition = Position::missing(); + + if (type != nullptr) + parts.push_back(type); + + incrementRecursionCounter("type annotation"); + + bool isUnion = false; + bool isIntersection = false; + unsigned int optionalCount = 0; + + Location location = begin; + + while (true) + { + Lexeme::Type c = lexer.current().type; + Position separatorPosition = lexer.current().location.begin; + if (c == '|') + { + nextLexeme(); + + unsigned int oldRecursionCount = recursionCounter; + parts.push_back(parseSimpleType(/* allowPack= */ false).type); + recursionCounter = oldRecursionCount; + + isUnion = true; + + if (options.storeCstData) + { + if (type == nullptr && !leadingPosition.hasValue()) + leadingPosition = separatorPosition; + else + separatorPositions.push_back(separatorPosition); + } + } + else if (c == '?') + { + LUAU_ASSERT(parts.size() >= 1); + + Location loc = lexer.current().location; + nextLexeme(); + + parts.push_back(allocator.alloc(Location(loc))); + optionalCount++; + + isUnion = true; + } + else if (c == '&') + { + nextLexeme(); + + unsigned int oldRecursionCount = recursionCounter; + parts.push_back(parseSimpleType(/* allowPack= */ false).type); + recursionCounter = oldRecursionCount; + + isIntersection = true; + + if (options.storeCstData) + { + if (type == nullptr && !leadingPosition.hasValue()) + leadingPosition = separatorPosition; + else + separatorPositions.push_back(separatorPosition); + } + } + else if (c == Lexeme::Dot3) + { + report(lexer.current().location, "Unexpected '...' after type annotation"); + nextLexeme(); + } + else + break; + + if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + optionalCount) + ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); + } + + if (parts.size() == 1 && !isUnion && !isIntersection) + return parts[0]; + if (isUnion && isIntersection) + { + return reportTypeError( + Location(begin, parts.back()->location), + copy(parts), + "Mixing union and intersection types is not allowed; consider wrapping in parentheses." + ); + } + + location.end = parts.back()->location.end; + + if (isUnion) + { + AstTypeUnion* node = allocator.alloc(location, copy(parts)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(leadingPosition, copy(separatorPositions)); + return node; + } + + if (isIntersection) + { + AstTypeIntersection* node = allocator.alloc(location, copy(parts)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(leadingPosition, copy(separatorPositions)); + return node; + } + + LUAU_ASSERT(false); + ParseError::raise(begin, "Composite type was not an intersection or union."); +} + +AstTypeOrPack Parser::parseSimpleTypeOrPack() +{ + unsigned int oldRecursionCount = recursionCounter; + // recursion counter is incremented in parseSimpleType + + Location begin = lexer.current().location; + + auto [type, typePack] = parseSimpleType(/* allowPack= */ true); + + if (typePack) + { + LUAU_ASSERT(!type); + return {{}, typePack}; + } + + recursionCounter = oldRecursionCount; + + return {parseTypeSuffix(type, begin), {}}; +} + +AstType* Parser::parseType(bool inDeclarationContext) +{ + unsigned int oldRecursionCount = recursionCounter; + // recursion counter is incremented in parseSimpleType and/or parseTypeSuffix + + Location begin = lexer.current().location; + + AstType* type = nullptr; + + Lexeme::Type c = lexer.current().type; + if (c != '|' && c != '&') + { + type = parseSimpleType(/* allowPack= */ false, /* in declaration context */ inDeclarationContext).type; + recursionCounter = oldRecursionCount; + } + + AstType* typeWithSuffix = parseTypeSuffix(type, begin); + recursionCounter = oldRecursionCount; + + return typeWithSuffix; +} + +// Type ::= nil | Name[`.' Name] [ `<' Type [`,' ...] `>' ] | `typeof' `(' expr `)' | `{' [PropList] `}' +// | [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType +AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) +{ + incrementRecursionCounter("type annotation"); + + Location start = lexer.current().location; + + AstArray attributes{nullptr, 0}; + + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) + { + if (!inDeclarationContext) + { + return {reportTypeError(start, {}, "attributes are not allowed in declaration context")}; + } + else + { + attributes = Parser::parseAttributes(); + return parseFunctionType(allowPack, attributes); + } + } + else if (lexer.current().type == Lexeme::ReservedNil) + { + nextLexeme(); + return {allocator.alloc(start, std::nullopt, nameNil, std::nullopt, start), {}}; + } + else if (lexer.current().type == Lexeme::ReservedTrue) + { + nextLexeme(); + return {allocator.alloc(start, true)}; + } + else if (lexer.current().type == Lexeme::ReservedFalse) + { + nextLexeme(); + return {allocator.alloc(start, false)}; + } + else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) + { + CstExprConstantString::QuoteStyle style; + unsigned int blockDepth = 0; + if (options.storeCstData) + std::tie(style, blockDepth) = extractStringDetails(); + + AstArray originalString; + if (std::optional> value = parseCharArray(options.storeCstData ? &originalString : nullptr)) + { + AstArray svalue = *value; + auto node = allocator.alloc(start, svalue); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(originalString, style, blockDepth); + return {node}; + } + else + return {reportTypeError(start, {}, "String literal contains malformed escape sequence")}; + } + else if (lexer.current().type == Lexeme::InterpStringBegin || lexer.current().type == Lexeme::InterpStringSimple) + { + parseInterpString(); + + return {reportTypeError(start, {}, "Interpolated string literals cannot be used as types")}; + } + else if (lexer.current().type == Lexeme::BrokenString) + { + nextLexeme(); + return {reportTypeError(start, {}, "Malformed string; did you forget to finish it?")}; + } + else if (lexer.current().type == Lexeme::Name) + { + std::optional prefix; + Position prefixPointPosition = Position::missing(); + std::optional prefixLocation; + AstLocal* prefixLocal = nullptr; + Name name = parseName("type name"); + + if (lexer.current().type == '.') + { + prefixPointPosition = lexer.current().location.begin; + nextLexeme(); + + prefix = name.name; + prefixLocation = name.location; + + AstLocal* const* prefixLocalValue = localMap.find(name.name); + prefixLocal = (prefixLocalValue && *prefixLocalValue) ? *prefixLocalValue : nullptr; + + name = parseIndexName("field name", prefixPointPosition); + } + else if (lexer.current().type == Lexeme::Dot3) + { + report(lexer.current().location, "Unexpected '...' after type name; type pack is not allowed in this context"); + nextLexeme(); + } + else if (name.name == "typeof") + { + Lexeme typeofBegin = lexer.current(); + bool openParenFound = expectAndConsume('(', "typeof type"); + + AstExpr* expr = parseExpr(); + + Location end = lexer.current().location; + + bool closeParenFound = expectMatchAndConsume(')', typeofBegin); + + AstTypeTypeof* node = allocator.alloc(Location(start, end), expr); + if (options.storeCstData) + { + cstNodeMap[node] = allocator.alloc( + openParenFound ? typeofBegin.location.begin : Position::missing(), closeParenFound ? end.begin : Position::missing() + ); + } + return {node, {}}; + } + + bool hasParameters = false; + AstArray parameters{}; + Position parametersOpeningPosition = Position::missing(); + TempVector parametersCommaPositions(scratchPosition); + Position parametersClosingPosition = Position::missing(); + + if (lexer.current().type == '<') + { + hasParameters = true; + if (options.storeCstData) + parameters = parseTypeParams(¶metersOpeningPosition, ¶metersCommaPositions, ¶metersClosingPosition); + else + parameters = parseTypeParams(); + } + + Location end = lexer.previousLocation(); + + AstTypeReference* node = allocator.alloc( + Location(start, end), prefix, name.name, prefixLocation, name.location, hasParameters, parameters, prefixLocal + ); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc( + prefixPointPosition, parametersOpeningPosition, copy(parametersCommaPositions), parametersClosingPosition + ); + return {node, {}}; + } + else if (lexer.current().type == '{') + { + return {parseTableType(/* inDeclarationContext */ inDeclarationContext), {}}; + } + else if (lexer.current().type == '(' || lexer.current().type == '<') + { + return parseFunctionType(allowPack, AstArray({nullptr, 0})); + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + nextLexeme(); + + return { + reportTypeError( + start, + {}, + "Using 'function' as a type annotation is not supported, consider replacing with a function type annotation e.g. '(...any) -> " + "...any'" + ), + {} + }; + } + else + { + // For a missing type annotation, capture 'space' between last token and the next one + Location astErrorlocation(lexer.previousLocation().end, start.begin); + // The parse error includes the next lexeme to make it easier to display where the error is (e.g. in an IDE or a CLI error message). + // Including the current lexeme also makes the parse error consistent with other parse errors returned by Luau. + Location parseErrorLocation(lexer.previousLocation().end, start.end); + return {reportMissingTypeError(parseErrorLocation, astErrorlocation, "Expected type, got %s", lexer.current().toString().c_str()), {}}; + } +} + +AstTypePack* Parser::parseVariadicArgumentTypePack() +{ + // Generic: a... + if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == Lexeme::Dot3) + { + Name name = parseName("generic name"); + Location end = lexer.current().location; + + // This will not fail because of the lookahead guard. + expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); + AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(end.begin); + return node; + } + // Variadic: T + else + { + AstType* variadicAnnotation = parseType(); + return allocator.alloc(variadicAnnotation->location, variadicAnnotation); + } +} + +AstTypePack* Parser::parseTypePack() +{ + // Variadic: ...T + if (lexer.current().type == Lexeme::Dot3) + { + Location start = lexer.current().location; + nextLexeme(); + AstType* varargTy = parseType(); + return allocator.alloc(Location(start, varargTy->location), varargTy); + } + // Generic: a... + else if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == Lexeme::Dot3) + { + Name name = parseName("generic name"); + Location end = lexer.current().location; + + // This will not fail because of the lookahead guard. + expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); + AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(end.begin); + return node; + } + + // TODO: shouldParseTypePack can be removed and parseTypePack can be called unconditionally instead + LUAU_ASSERT(!"parseTypePack can't be called if shouldParseTypePack() returned false"); + return nullptr; +} + +std::optional Parser::parseUnaryOp(const Lexeme& l) +{ + if (l.type == Lexeme::ReservedNot) + return AstExprUnary::Op::Not; + else if (l.type == '-') + return AstExprUnary::Op::Minus; + else if (l.type == '#') + return AstExprUnary::Op::Len; + else + return std::nullopt; +} + +std::optional Parser::parseBinaryOp(const Lexeme& l) +{ + if (l.type == '+') + return AstExprBinary::Add; + else if (l.type == '-') + return AstExprBinary::Sub; + else if (l.type == '*') + return AstExprBinary::Mul; + else if (l.type == '/') + return AstExprBinary::Div; + else if (l.type == Lexeme::FloorDiv) + return AstExprBinary::FloorDiv; + else if (l.type == '%') + return AstExprBinary::Mod; + else if (l.type == '^') + return AstExprBinary::Pow; + else if (l.type == Lexeme::Dot2) + return AstExprBinary::Concat; + else if (l.type == Lexeme::NotEqual) + return AstExprBinary::CompareNe; + else if (l.type == Lexeme::Equal) + return AstExprBinary::CompareEq; + else if (l.type == '<') + return AstExprBinary::CompareLt; + else if (l.type == Lexeme::LessEqual) + return AstExprBinary::CompareLe; + else if (l.type == '>') + return AstExprBinary::CompareGt; + else if (l.type == Lexeme::GreaterEqual) + return AstExprBinary::CompareGe; + else if (l.type == Lexeme::ReservedAnd) + return AstExprBinary::And; + else if (l.type == Lexeme::ReservedOr) + return AstExprBinary::Or; + else + return std::nullopt; +} + +std::optional Parser::parseCompoundOp(const Lexeme& l) +{ + if (l.type == Lexeme::AddAssign) + return AstExprBinary::Add; + else if (l.type == Lexeme::SubAssign) + return AstExprBinary::Sub; + else if (l.type == Lexeme::MulAssign) + return AstExprBinary::Mul; + else if (l.type == Lexeme::DivAssign) + return AstExprBinary::Div; + else if (l.type == Lexeme::FloorDivAssign) + return AstExprBinary::FloorDiv; + else if (l.type == Lexeme::ModAssign) + return AstExprBinary::Mod; + else if (l.type == Lexeme::PowAssign) + return AstExprBinary::Pow; + else if (l.type == Lexeme::ConcatAssign) + return AstExprBinary::Concat; + else + return std::nullopt; +} + +std::optional Parser::checkUnaryConfusables() +{ + const Lexeme& curr = lexer.current(); + + // early-out: need to check if this is a possible confusable quickly + if (curr.type != '!') + return {}; + + // slow path: possible confusable + Location start = curr.location; + + if (curr.type == '!') + { + report(start, "Unexpected '!'; did you mean 'not'?"); + return AstExprUnary::Op::Not; + } + + return {}; +} + +std::optional Parser::checkBinaryConfusables(const BinaryOpPriority binaryPriority[], unsigned int limit) +{ + const Lexeme& curr = lexer.current(); + + // early-out: need to check if this is a possible confusable quickly + if (curr.type != '&' && curr.type != '|' && curr.type != '!') + return {}; + + // slow path: possible confusable + Location start = curr.location; + Lexeme next = lexer.lookahead(); + + if (curr.type == '&' && next.type == '&' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::And].left > limit) + { + nextLexeme(); + report(Location(start, next.location), "Unexpected '&&'; did you mean 'and'?"); + return AstExprBinary::And; + } + else if (curr.type == '|' && next.type == '|' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::Or].left > limit) + { + nextLexeme(); + report(Location(start, next.location), "Unexpected '||'; did you mean 'or'?"); + return AstExprBinary::Or; + } + else if (curr.type == '!' && next.type == '=' && curr.location.end == next.location.begin && + binaryPriority[AstExprBinary::CompareNe].left > limit) + { + nextLexeme(); + report(Location(start, next.location), "Unexpected '!='; did you mean '~='?"); + return AstExprBinary::CompareNe; + } + + return std::nullopt; +} + +// subexpr -> (asexp | unop subexpr) { binop subexpr } +// where `binop' is any binary operator with a priority higher than `limit' +AstExpr* Parser::parseExpr(unsigned int limit) +{ + static const BinaryOpPriority binaryPriority[] = { + {6, 6}, // '+' + {6, 6}, // '-' + {7, 7}, // '*' + {7, 7}, // '/' + {7, 7}, // '//' + {7, 7}, // `%' + {10, 9}, // power (right associative) + {5, 4}, // concat (right associative) + {3, 3}, // inequality + {3, 3}, // equality + {3, 3}, // '<' + {3, 3}, // '<=' + {3, 3}, // '>' + {3, 3}, // '>=' + {2, 2}, // logical and + {1, 1} // logical or + }; + + static_assert(sizeof(binaryPriority) / sizeof(binaryPriority[0]) == size_t(AstExprBinary::Op__Count), "binaryPriority needs an entry per op"); + + unsigned int oldRecursionCount = recursionCounter; + + // this handles recursive calls to parseSubExpr/parseExpr + incrementRecursionCounter("expression"); + + const unsigned int unaryPriority = 8; + + Location start = lexer.current().location; + + AstExpr* expr; + + std::optional uop = parseUnaryOp(lexer.current()); + + if (!uop) + uop = checkUnaryConfusables(); + + if (uop) + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + AstExpr* subexpr = parseExpr(unaryPriority); + + expr = allocator.alloc(Location(start, subexpr->location), *uop, subexpr); + if (options.storeCstData) + cstNodeMap[expr] = allocator.alloc(opPosition); + } + else + { + expr = parseAssertionExpr(); + } + + // expand while operators have priorities higher than `limit' + std::optional op = parseBinaryOp(lexer.current()); + + if (!op) + op = checkBinaryConfusables(binaryPriority, limit); + + while (op && binaryPriority[*op].left > limit) + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + // read sub-expression with higher priority + AstExpr* next = parseExpr(binaryPriority[*op].right); + + expr = allocator.alloc(Location(start, next->location), *op, expr, next); + if (options.storeCstData) + cstNodeMap[expr] = allocator.alloc(opPosition); + op = parseBinaryOp(lexer.current()); + + if (!op) + op = checkBinaryConfusables(binaryPriority, limit); + + // note: while the parser isn't recursive here, we're generating recursive structures of unbounded depth + incrementRecursionCounter("expression"); + } + + recursionCounter = oldRecursionCount; + + return expr; +} + +// NAME +AstExpr* Parser::parseNameExpr(const char* context) +{ + std::optional name = parseNameOpt(context); + + if (!name) + return allocator.alloc(lexer.current().location, copy({}), unsigned(parseErrors.size() - 1)); + + AstLocal* const* value = localMap.find(name->name); + + if (value && *value) + { + AstLocal* local = *value; + + if (local->functionDepth < typeFunctionDepth) + return reportExprError(lexer.current().location, {}, "Type function cannot reference outer local '%s'", local->name.value); + + return allocator.alloc(name->location, local, local->functionDepth != functionStack.size() - 1); + } + + return allocator.alloc(name->location, name->name); +} + +// prefixexp -> NAME | '(' expr ')' +AstExpr* Parser::parsePrefixExpr() +{ + if (lexer.current().type == '(') + { + Position start = lexer.current().location.begin; + + MatchLexeme matchParen = lexer.current(); + nextLexeme(); + + AstExpr* expr = parseExpr(); + + Position end = lexer.current().location.end; + + bool closeParenFound = false; + + if (lexer.current().type != ')') + { + const char* suggestion = (lexer.current().type == '=') ? "; did you mean to use '{' when defining a table?" : nullptr; + + expectMatchAndConsumeFail(static_cast(')'), matchParen, suggestion); + + end = lexer.previousLocation().end; + } + else + { + closeParenFound = true; + + nextLexeme(); + } + + AstExpr* exprGroup = allocator.alloc(Location(start, end), expr); + + if (options.storeCstData) + cstNodeMap[exprGroup] = allocator.alloc(closeParenFound ? lexer.previousLocation().begin : Position::missing()); + + return exprGroup; + } + else + { + return parseNameExpr("expression"); + } +} + +// primaryexp -> prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } +AstExpr* Parser::parsePrimaryExpr(bool asStatement) +{ + Position start = lexer.current().location.begin; + + AstExpr* expr = parsePrefixExpr(); + + unsigned int oldRecursionCount = recursionCounter; + + while (true) + { + if (lexer.current().type == '.') + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + Name index = parseIndexName(nullptr, opPosition); + + expr = allocator.alloc(Location(start, index.location.end), expr, index.name, index.location, opPosition, '.'); + } + else if (lexer.current().type == '[') + { + expr = parseIndexExpr(start, expr); + } + else if (lexer.current().type == ':') + { + expr = parseMethodCall(start, expr); + } + else if (lexer.current().type == '(') + { + // This error is handled inside 'parseFunctionArgs' as well, but for better error recovery we need to break out the current loop here + if (!asStatement && expr->location.end.line != lexer.current().location.begin.line) + { + reportAmbiguousCallError(); + break; + } + + expr = parseFunctionArgs(expr, false); + } + else if (lexer.current().type == '{' || lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) + { + expr = parseFunctionArgs(expr, false); + } + else if (lexer.current().type == '<' && lexer.lookahead().type == '<') + { + expr = parseExplicitTypeInstantiationExpr(start, *expr); + } + else + { + break; + } + + // note: while the parser isn't recursive here, we're generating recursive structures of unbounded depth + incrementRecursionCounter("expression"); + } + + recursionCounter = oldRecursionCount; + + return expr; +} + +LUAU_NOINLINE AstExpr* Parser::parseIndexExpr(Position start, AstExpr* expr) +{ + MatchLexeme matchBracket = lexer.current(); + nextLexeme(); + + AstExpr* index = parseExpr(); + + Position closeBracketPosition = lexer.current().location.begin; + Position end = lexer.current().location.end; + + bool closingBracketFound = expectMatchAndConsume(']', matchBracket); + + expr = allocator.alloc(Location(start, end), expr, index); + if (options.storeCstData) + cstNodeMap[expr] = allocator.alloc(matchBracket.position, closingBracketFound ? closeBracketPosition : Position::missing()); + + return expr; +} + +AstExpr* Parser::parseMethodCall(Position start, AstExpr* expr) +{ + Position opPosition = lexer.current().location.begin; + nextLexeme(); + + Name index = parseIndexName("method name", opPosition); + AstExpr* func = allocator.alloc(Location(start, index.location.end), expr, index.name, index.location, opPosition, ':'); + + AstArray typeArguments; + CstTypeInstantiation* cstTypeArguments = options.storeCstData ? allocator.alloc() : nullptr; + + if (lexer.current().type == '<' && lexer.lookahead().type == '<') + { + typeArguments = parseTypeInstantiationExpr(cstTypeArguments); + } + + expr = parseFunctionArgs(func, true); + + if (options.storeCstData) + { + CstNode** cstNode = cstNodeMap.find(expr); + if (cstNode) + { + CstExprCall* exprCall = (*cstNode)->as(); + LUAU_ASSERT(exprCall); + exprCall->explicitTypes = cstTypeArguments; + } + } + + // If we have an AstExprCall, fill in the type arguments + if (auto call = expr->as(); call && typeArguments.size > 0) + call->typeArguments = typeArguments; + + return expr; +} + +// asexp -> simpleexp [`::' Type] +AstExpr* Parser::parseAssertionExpr() +{ + Location start = lexer.current().location; + AstExpr* expr = parseSimpleExpr(); + + if (lexer.current().type == Lexeme::DoubleColon) + { + Position opPosition = lexer.current().location.begin; + nextLexeme(); + AstType* annotation = parseType(); + AstExprTypeAssertion* node = allocator.alloc(Location(start, annotation->location), expr, annotation); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(opPosition); + return node; + } + else + return expr; +} + +static ConstantNumberParseResult parseInteger(double& result, const char* data, int base) +{ + LUAU_ASSERT(base == 2 || base == 16); + + // Some libc implementations accept an optional 0b prefix for base-2 parsing. + // Binary literals have already had their leading 0b stripped by us. + if (base == 2 && data[0] == '0' && (data[1] == 'b' || data[1] == 'B')) + return ConstantNumberParseResult::Malformed; + + char* end = nullptr; + unsigned long long value = strtoull(data, &end, base); + + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = double(value); + + if (value == ULLONG_MAX && errno == ERANGE) + { + // 'errno' might have been set before we called 'strtoull', but we don't want the overhead of resetting a TLS variable on each call + // so we only reset it when we get a result that might be an out-of-range error and parse again to make sure + errno = 0; + value = strtoull(data, &end, base); + + if (errno == ERANGE) + return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; + } + + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; + + return ConstantNumberParseResult::Ok; +} + +static ConstantNumberParseResult parseInteger64(int64_t& result, const char* data, int base) +{ + LUAU_ASSERT(base == 2 || base == 10 || base == 16); + + char* end = nullptr; + + if (base == 10) + { + result = strtoll(data, &end, 10); + + if (end == data || *end != 'i' || end[1] != '\0') + return ConstantNumberParseResult::Malformed; + + if (((result == LLONG_MIN) || (result == LLONG_MAX)) && (errno == ERANGE)) + { + // 'errno' might have been set before we called 'strtoll', but we don't want the overhead of resetting a TLS variable on each call + // so we only reset it when we get a result that might be an out-of-range error and parse again to make sure + errno = 0; + result = strtoll(data, &end, 10); + + if (errno == ERANGE) + return ConstantNumberParseResult::IntOverflow; + } + } + else + { + if (base == 2 && data[0] == '0' && (data[1] == 'b' || data[1] == 'B')) + return ConstantNumberParseResult::Malformed; + + // hex and binary literals represent bit patterns covering the full uint64 range + unsigned long long u = strtoull(data, &end, base); + + if (end == data || *end != 'i' || end[1] != '\0') + return ConstantNumberParseResult::Malformed; + + if ((u == ULLONG_MAX) && (errno == ERANGE)) + { + // 'errno' might have been set before we called 'strtoull', but we don't want the overhead of resetting a TLS variable on each call + // so we only reset it when we get a result that might be an out-of-range error and parse again to make sure + errno = 0; + u = strtoull(data, &end, base); + + if (errno == ERANGE) + return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; + } + + result = (int64_t)u; + } + + return ConstantNumberParseResult::Ok; +} + +static ConstantNumberParseResult parseDouble(double& result, const char* data) +{ + // binary literal + if (data[0] == '0' && (data[1] == 'b' || data[1] == 'B') && data[2]) + return parseInteger(result, data + 2, 2); + + // hexadecimal literal + if (data[0] == '0' && (data[1] == 'x' || data[1] == 'X') && data[2]) + return parseInteger(result, data, 16); // pass in '0x' prefix, it's handled by 'strtoull' + + char* end = nullptr; + double value = strtod(data, &end); + + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = value; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) + { + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); + + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; + } + + return ConstantNumberParseResult::Ok; +} + +// LUAU_NOINLINE is used to limit the stack cost of parseSimpleExpr which is on the recursive expression-parsing path +LUAU_NOINLINE AstExpr* Parser::parseAttributedFunction(const Location& start) +{ + AstArray attributes{nullptr, 0}; + TempVector cstAttrLists(scratchCstAttrList); + + attributes = parseAttributes(&cstAttrLists); + + if (lexer.current().type != Lexeme::ReservedFunction) + { + return reportExprError(start, {}, "Expected 'function' declaration after attribute, but got %s instead", lexer.current().toString().c_str()); + } + + Lexeme matchFunction = lexer.current(); + nextLexeme(); + + return parseFunctionBody(false, matchFunction, AstName(), nullptr, attributes, false, &cstAttrLists).first; +} + +// simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | [attributes] FUNCTION body | primaryexp +AstExpr* Parser::parseSimpleExpr() +{ + Location start = lexer.current().location; + + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) + { + return parseAttributedFunction(start); + } + + if (lexer.current().type == Lexeme::ReservedNil) + { + nextLexeme(); + + return allocator.alloc(start); + } + else if (lexer.current().type == Lexeme::ReservedTrue) + { + nextLexeme(); + + return allocator.alloc(start, true); + } + else if (lexer.current().type == Lexeme::ReservedFalse) + { + nextLexeme(); + + return allocator.alloc(start, false); + } + else if (lexer.current().type == Lexeme::ReservedFunction) + { + Lexeme matchFunction = lexer.current(); + nextLexeme(); + + return parseFunctionBody(false, matchFunction, AstName(), nullptr, AstArray{nullptr, 0}, false, nullptr).first; + } + else if (lexer.current().type == Lexeme::Number) + { + return parseNumber(); + } + else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || + lexer.current().type == Lexeme::InterpStringSimple) + { + return parseString(); + } + else if (lexer.current().type == Lexeme::InterpStringBegin) + { + return parseInterpString(); + } + else if (lexer.current().type == Lexeme::BrokenString) + { + nextLexeme(); + return reportExprError(start, {}, "Malformed string; did you forget to finish it?"); + } + else if (lexer.current().type == Lexeme::BrokenInterpDoubleBrace) + { + nextLexeme(); + return reportExprError(start, {}, "Double braces are not permitted within interpolated strings; did you mean '\\{'?"); + } + else if (lexer.current().type == Lexeme::Dot3) + { + if (functionStack.back().vararg) + { + nextLexeme(); + + return allocator.alloc(start); + } + else + { + nextLexeme(); + + return reportExprError(start, {}, "Cannot use '...' outside of a vararg function"); + } + } + else if (lexer.current().type == '{') + { + return parseTableConstructor(); + } + else if (lexer.current().type == Lexeme::ReservedIf) + { + return parseIfElseExpr(); + } + else + { + return parsePrimaryExpr(/* asStatement= */ false); + } +} + +std::tuple, Location, Location> Parser::parseCallList(TempVector* commaPositions, Position* closeParenPosition) +{ + LUAU_ASSERT( + lexer.current().type == '(' || lexer.current().type == '{' || lexer.current().type == Lexeme::RawString || + lexer.current().type == Lexeme::QuotedString + ); + if (lexer.current().type == '(') + { + Position argStart = lexer.current().location.end; + + MatchLexeme matchParen = lexer.current(); + nextLexeme(); + + TempVector args(scratchExpr); + + if (lexer.current().type != ')') + parseExprList(args, commaPositions); + + Location end = lexer.current().location; + Position argEnd = end.end; + + bool closeParenFound = expectMatchAndConsume(')', matchParen); + if (closeParenPosition && closeParenFound) + *closeParenPosition = end.begin; + + return {copy(args), Location(argStart, argEnd), Location(matchParen.position, lexer.previousLocation().begin)}; + } + else if (lexer.current().type == '{') + { + Position argStart = lexer.current().location.end; + AstExpr* expr = parseTableConstructor(); + Position argEnd = lexer.previousLocation().end; + + return {copy(&expr, 1), Location(argStart, argEnd), expr->location}; + } + else + { + Location argLocation = lexer.current().location; + AstExpr* expr = parseString(); + return {copy(&expr, 1), argLocation, expr->location}; + } +} + +// args ::= `(' [explist] `)' | tableconstructor | String +AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) +{ + if (lexer.current().type == '(') + { + Position argStart = lexer.current().location.end; + if (func->location.end.line != lexer.current().location.begin.line) + reportAmbiguousCallError(); + + MatchLexeme matchParen = lexer.current(); + nextLexeme(); + + TempVector args(scratchExpr); + TempVector commaPositions(scratchPosition); + + if (lexer.current().type != ')') + parseExprList(args, options.storeCstData ? &commaPositions : nullptr); + + Location end = lexer.current().location; + Position argEnd = end.end; + + bool closingParenFound = expectMatchAndConsume(')', matchParen); + + AstExprCall* node = allocator.alloc( + Location(func->location, end), func, copy(args), self, AstArray{}, Location(argStart, argEnd) + ); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc( + matchParen.position, closingParenFound ? lexer.previousLocation().begin : Position::missing(), copy(commaPositions) + ); + return node; + } + else if (lexer.current().type == '{') + { + Position argStart = lexer.current().location.end; + AstExpr* expr = parseTableConstructor(); + Position argEnd = lexer.previousLocation().end; + + AstExprCall* node = allocator.alloc( + Location(func->location, expr->location), func, copy(&expr, 1), self, AstArray{}, Location(argStart, argEnd) + ); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(Position::missing(), Position::missing(), AstArray{nullptr, 0}); + return node; + } + else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) + { + Location argLocation = lexer.current().location; + AstExpr* expr = parseString(); + + AstExprCall* node = allocator.alloc( + Location(func->location, expr->location), func, copy(&expr, 1), self, AstArray{}, argLocation + ); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(Position::missing(), Position::missing(), AstArray{nullptr, 0}); + return node; + } + else + { + return reportFunctionArgsError(func, self); + } +} + +LUAU_NOINLINE AstExpr* Parser::reportFunctionArgsError(AstExpr* func, bool self) +{ + if (self && lexer.current().location.begin.line != func->location.end.line) + { + return reportExprError(func->location, copy({func}), "Expected function call arguments after '('"); + } + else + { + return reportExprError( + Location(func->location.begin, lexer.current().location.begin), + copy({func}), + "Expected '(', '{' or when parsing function call, got %s", + lexer.current().toString().c_str() + ); + } +} + +LUAU_NOINLINE void Parser::reportAmbiguousCallError() +{ + report( + lexer.current().location, + "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of " + "new statement; use ';' to separate statements" + ); +} + +CstExprTable::Separator Parser::tableSeparator() +{ + if (lexer.current().type == ',') + return CstExprTable::Separator::Comma; + else if (lexer.current().type == ';') + return CstExprTable::Separator::Semicolon; + else + return CstExprTable::Separator::Missing; +} + +// tableconstructor ::= `{' [fieldlist] `}' +// fieldlist ::= field {fieldsep field} [fieldsep] +// field ::= `[' exp `]' `=' exp | Name `=' exp | exp +// fieldsep ::= `,' | `;' +AstExpr* Parser::parseTableConstructor() +{ + TempVector items(scratchItem); + TempVector cstItems(scratchCstItem); + + Location start = lexer.current().location; + + MatchLexeme matchBrace = lexer.current(); + expectAndConsume('{', "table literal"); + + while (lexer.current().type != '}') + { + if (lexer.current().type == '[') + { + Position indexerOpenPosition = lexer.current().location.begin; + MatchLexeme matchLocationBracket = lexer.current(); + nextLexeme(); + + AstExpr* key = parseExpr(); + + bool closingBracketFound = expectMatchAndConsume(']', matchLocationBracket); + Position indexerClosePosition = closingBracketFound ? lexer.previousLocation().begin : Position::missing(); + + bool equalsFound = expectAndConsume('=', "table field"); + Position equalsPosition = equalsFound ? lexer.previousLocation().begin : Position::missing(); + + AstExpr* value = parseExpr(); + + items.push_back({AstExprTable::Item::Kind::General, key, value}); + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {indexerOpenPosition, + indexerClosePosition, + equalsPosition, + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } + } + else if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == '=') + { + Name name = parseName("table field"); + + Position equalsPosition = lexer.current().location.begin; + expectAndConsume('=', "table field"); + + AstArray nameString; + nameString.data = const_cast(name.name.value); + nameString.size = strlen(name.name.value); + + AstExpr* key = allocator.alloc(name.location, nameString, AstExprConstantString::QuoteStyle::Unquoted); + AstExpr* value = parseExpr(); + + if (AstExprFunction* func = value->as()) + func->debugname = name.name; + + items.push_back({AstExprTable::Item::Kind::Record, key, value}); + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {Position::missing(), + Position::missing(), + equalsPosition, + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } + } + else + { + AstExpr* expr = parseExpr(); + + items.push_back({AstExprTable::Item::Kind::List, nullptr, expr}); + if (options.storeCstData) + { + CstExprTable::Separator separator = tableSeparator(); + cstItems.push_back( + {Position::missing(), + Position::missing(), + Position::missing(), + separator, + separator == CstExprTable::Separator::Missing ? Position::missing() : lexer.current().location.begin} + ); + } + } + + if (lexer.current().type == ',' || lexer.current().type == ';') + { + nextLexeme(); + } + else if (lexer.current().type == '[' || lexer.current().type == Lexeme::Name) + { + report(lexer.current().location, "Expected ',' after table constructor element"); + } + else if (lexer.current().type != '}') + { + break; + } + } + + Location end = lexer.current().location; + + if (!expectMatchAndConsume('}', matchBrace)) + end = lexer.previousLocation(); + + AstExprTable* node = allocator.alloc(Location(start, end), copy(items)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(cstItems)); + return node; +} + +AstExpr* Parser::parseIfElseExpr() +{ + Location start = lexer.current().location; + + nextLexeme(); // skip if / elseif + + if (FFlag::LuauExperimentalIfLocalSyntax) + { + if (lexer.current().type == Lexeme::ReservedLocal) + return parseIfElseExprLocalCondition(start); + + if (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const" && lexer.lookahead().type == Lexeme::Name) + return parseIfElseExprLocalCondition(start); + } + + AstExpr* condition = parseExpr(); + + bool hasThen = expectAndConsume(Lexeme::ReservedThen, "if then else expression"); + Position thenPosition = hasThen ? lexer.previousLocation().begin : Position::missing(); + + AstExpr* trueExpr = parseExpr(); + + bool hasElse = false; + Position elsePosition = lexer.current().location.begin; + bool isElseIf = false; + AstExpr* falseExpr = parseIfElseExprTail(hasElse, isElseIf); + + Location end = falseExpr->location; + + AstExprIfElse* node = allocator.alloc(Location(start, end), condition, hasThen, trueExpr, hasElse, falseExpr); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(thenPosition, elsePosition, isElseIf); + return node; +} + +// (`if' | `elseif') (`local' | `const') binding `=' exp then exp {elseif exp then exp} else exp +// +// LUAU_NOINLINE keeps the `if local`/`if const` locals off parseIfElseExpr's frame: parseIfElseExpr recurses +// through long elseif chains and this variant is rarely taken. `start` is the location of the already +// consumed `if`/`elseif` keyword; the current lexeme is the `local`/`const` keyword. +LUAU_NOINLINE AstExpr* Parser::parseIfElseExprLocalCondition(const Location& start) +{ + LUAU_ASSERT(FFlag::LuauExperimentalIfLocalSyntax); + + bool condIsConst = (lexer.current().type == Lexeme::Name && AstName(lexer.current().name) == "const"); + std::optional condKeywordLocation = lexer.current().location; + nextLexeme(); // consume 'local' or 'const' + + Binding binding = parseBinding(condIsConst); + + if (lexer.current().type == ',') + report(lexer.current().location, "Expected '=' after variable name in 'if local', got ','; only a single binding is allowed"); + + std::optional equalsPosition; + if (expectAndConsume('=', "if local declaration")) + equalsPosition = lexer.previousLocation(); + + AstExpr* condition = parseExpr(); + + bool hasThen = expectAndConsume(Lexeme::ReservedThen, "if then else expression"); + Position thenPosition = hasThen ? lexer.previousLocation().begin : Position::missing(); + + // Push the binding after the condition so the condition cannot reference it, and restore after the + // true expression so it is not visible in the else/elseif branch. + unsigned int localsBegin = saveLocals(); + AstLocal* condLocal = pushLocal(binding); + + AstExpr* trueExpr = parseExpr(); + + restoreLocals(localsBegin); + + bool hasElse = false; + Position elsePosition = lexer.current().location.begin; + bool isElseIf = false; + AstExpr* falseExpr = parseIfElseExprTail(hasElse, isElseIf); + + Location end = falseExpr->location; + + AstExprIfElse* node = allocator.alloc( + Location(start, end), condition, hasThen, trueExpr, hasElse, falseExpr, condLocal, condIsConst, condKeywordLocation, equalsPosition + ); + if (options.storeCstData) + cstNodeMap[node] = + allocator.alloc(thenPosition, elsePosition, isElseIf, binding.annotation ? binding.colonPosition : Position::missing()); + return node; +} + +AstExpr* Parser::parseIfElseExprTail(bool& hasElse, bool& isElseIf) +{ + AstExpr* falseExpr = nullptr; + if (lexer.current().type == Lexeme::ReservedElseif) + { + unsigned int oldRecursionCount = recursionCounter; + incrementRecursionCounter("expression"); + hasElse = true; + falseExpr = parseIfElseExpr(); + recursionCounter = oldRecursionCount; + isElseIf = true; + } + else + { + hasElse = expectAndConsume(Lexeme::ReservedElse, "if then else expression"); + falseExpr = parseExpr(); + } + + return falseExpr; +} + +// Name +std::optional Parser::parseNameOpt(const char* context) +{ + if (lexer.current().type != Lexeme::Name) + { + reportNameError(context); + + return {}; + } + + Name result(AstName(lexer.current().name), lexer.current().location); + + nextLexeme(); + + return result; +} + +Parser::Name Parser::parseName(const char* context) +{ + if (std::optional name = parseNameOpt(context)) + return *name; + + Location location = lexer.current().location; + location.end = location.begin; + + return Name(nameError, location); +} + +Parser::Name Parser::parseIndexName(const char* context, const Position& previous) +{ + if (std::optional name = parseNameOpt(context)) + return *name; + + // If we have a reserved keyword next at the same line, assume it's an incomplete name + if (lexer.current().type >= Lexeme::Reserved_BEGIN && lexer.current().type < Lexeme::Reserved_END && + lexer.current().location.begin.line == previous.line) + { + Name result(AstName(lexer.current().name), lexer.current().location); + + nextLexeme(); + + return result; + } + + Location location = lexer.current().location; + location.end = location.begin; + + return Name(nameError, location); +} + +std::pair, AstArray> Parser::parseGenericTypeList( + bool withDefaultValues, + Position* openPosition, + AstArray* commaPositions, + Position* closePosition +) +{ + TempVector names{scratchGenericTypes}; + TempVector namePacks{scratchGenericTypePacks}; + TempVector localCommaPositions{scratchPosition}; + + if (lexer.current().type == '<') + { + Lexeme begin = lexer.current(); + if (openPosition) + *openPosition = begin.location.begin; + nextLexeme(); + + bool seenPack = false; + bool seenDefault = false; + + while (true) + { + Location nameLocation = lexer.current().location; + AstName name = parseName().name; + if (lexer.current().type == Lexeme::Dot3 || seenPack) + { + seenPack = true; + + Position ellipsisPosition = Position::missing(); + if (lexer.current().type != Lexeme::Dot3) + report(lexer.current().location, "Generic types come before generic type packs"); + else + { + ellipsisPosition = lexer.current().location.begin; + nextLexeme(); + } + + if (withDefaultValues && lexer.current().type == '=') + { + seenDefault = true; + Position equalsPosition = lexer.current().location.begin; + nextLexeme(); + + if (shouldParseTypePack(lexer)) + { + AstTypePack* typePack = parseTypePack(); + + AstGenericTypePack* node = allocator.alloc(nameLocation, name, typePack); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(ellipsisPosition, equalsPosition); + namePacks.push_back(node); + } + else + { + auto [type, typePack] = parseSimpleTypeOrPack(); + + if (type) + report(type->location, "Expected type pack after '=', got type"); + + AstGenericTypePack* node = allocator.alloc(nameLocation, name, typePack); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(ellipsisPosition, equalsPosition); + namePacks.push_back(node); + } + } + else + { + if (seenDefault) + report(lexer.current().location, "Expected default type pack after type pack name"); + + AstGenericTypePack* node = allocator.alloc(nameLocation, name, nullptr); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(ellipsisPosition, Position::missing()); + namePacks.push_back(node); + } + } + else + { + if (withDefaultValues && lexer.current().type == '=') + { + seenDefault = true; + Position equalsPosition = lexer.current().location.begin; + nextLexeme(); + + AstType* defaultType = parseType(); + + AstGenericType* node = allocator.alloc(nameLocation, name, defaultType); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(equalsPosition); + names.push_back(node); + } + else + { + if (seenDefault) + report(lexer.current().location, "Expected default type after type name"); + + AstGenericType* node = allocator.alloc(nameLocation, name, nullptr); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(Position::missing()); + names.push_back(node); + } + } + + if (lexer.current().type == ',') + { + if (commaPositions) + localCommaPositions.push_back(lexer.current().location.begin); + nextLexeme(); + + if (lexer.current().type == '>') + { + report(lexer.current().location, "Expected type after ',' but got '>' instead"); + break; + } + } + else + break; + } + + bool closingBracketFound = expectMatchAndConsume('>', begin); + if (closePosition && closingBracketFound) + *closePosition = lexer.previousLocation().begin; + } + + if (commaPositions) + *commaPositions = copy(localCommaPositions); + + AstArray generics = copy(names); + AstArray genericPacks = copy(namePacks); + return {generics, genericPacks}; +} + +AstArray Parser::parseTypeParams(Position* openingPosition, TempVector* commaPositions, Position* closingPosition) +{ + TempVector parameters{scratchTypeOrPack}; + + if (lexer.current().type == '<') + { + Lexeme begin = lexer.current(); + if (openingPosition) + *openingPosition = begin.location.begin; + nextLexeme(); + + while (true) + { + if (shouldParseTypePack(lexer)) + { + AstTypePack* typePack = parseTypePack(); + parameters.push_back({{}, typePack}); + } + else if (lexer.current().type == '(') + { + Location begin = lexer.current().location; + AstType* type = nullptr; + AstTypePack* typePack = nullptr; + Lexeme::Type c = lexer.current().type; + + if (c != '|' && c != '&') + { + auto typeOrTypePack = parseSimpleType(/* allowPack */ true, /* inDeclarationContext */ false); + type = typeOrTypePack.type; + typePack = typeOrTypePack.typePack; + } + + // Consider the following type: + // + // X<(T)> + // + // Is this a type pack or a parenthesized type? The + // assumption will be a type pack, as that's what allows one + // to express either a singular type pack or a potential + // complex type. + + if (typePack) + { + auto explicitTypePack = typePack->as(); + if (explicitTypePack && explicitTypePack->typeList.tailType == nullptr && explicitTypePack->typeList.types.size == 1 && + isTypeFollow(lexer.current().type)) + { + // If we parsed an explicit type pack with a single + // type in it (something of the form `(T)`), and + // the next lexeme is one that follows a type + // (&, |, ?), then assume that this was actually a + // parenthesized type. + auto parenthesizedType = explicitTypePack->typeList.types.data[0]; + + AstTypeGroup* typeGroup = allocator.alloc(parenthesizedType->location, parenthesizedType); + + if (options.storeCstData) + { + CstNode** cstNode = cstNodeMap.find(explicitTypePack); + + LUAU_ASSERT(cstNode && *cstNode); + if (cstNode && *cstNode) + { + CstTypePackExplicit* cstExplicitTypePack = (*cstNode)->as(); + LUAU_ASSERT(cstExplicitTypePack); + + if (cstExplicitTypePack) + cstNodeMap[typeGroup] = allocator.alloc(cstExplicitTypePack->closeParenthesesPosition); + } + } + + parameters.push_back({parseTypeSuffix(typeGroup, begin), {}}); + } + else + { + // Otherwise, it's a type pack. + parameters.push_back({{}, typePack}); + } + } + else + { + // There's two cases in which `typePack` will be null: + // - We try to parse a simple type or a type pack, and + // we get a simple type: there's no ambiguity and + // we attempt to parse a complex type. + // - The next lexeme was a `|` or `&` indicating a + // union or intersection type with a leading + // separator. We just fall right into + // `parseTypeSuffix`, which allows its first + // argument to be `nullptr` + parameters.push_back({parseTypeSuffix(type, begin), {}}); + } + } + else if (lexer.current().type == '>' && parameters.empty()) + { + break; + } + else + { + parameters.push_back({parseType(), {}}); + } + + if (lexer.current().type == ',') + { + if (commaPositions) + commaPositions->push_back(lexer.current().location.begin); + nextLexeme(); + } + else + break; + } + + bool closingBracketFound = expectMatchAndConsume('>', begin); + if (closingPosition && closingBracketFound) + *closingPosition = lexer.previousLocation().begin; + } + + return copy(parameters); +} + +std::optional> Parser::parseCharArray(AstArray* originalString) +{ + LUAU_ASSERT( + lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::RawString || + lexer.current().type == Lexeme::InterpStringSimple + ); + + scratchData.assign(lexer.current().data, lexer.current().getLength()); + if (originalString) + *originalString = copy(scratchData); + + if (lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::InterpStringSimple) + { + if (!Lexer::fixupQuotedString(scratchData)) + { + nextLexeme(); + return std::nullopt; + } + } + else + { + Lexer::fixupMultilineString(scratchData); + } + + AstArray value = copy(scratchData); + nextLexeme(); + return value; +} + +AstExpr* Parser::parseString() +{ + Location location = lexer.current().location; + + AstExprConstantString::QuoteStyle style; + switch (lexer.current().type) + { + case Lexeme::QuotedString: + case Lexeme::InterpStringSimple: + style = AstExprConstantString::QuoteStyle::QuotedSimple; + break; + case Lexeme::RawString: + style = AstExprConstantString::QuoteStyle::QuotedRaw; + break; + default: + LUAU_ASSERT(false && "Invalid string type"); + } + + CstExprConstantString::QuoteStyle fullStyle; + unsigned int blockDepth; + if (options.storeCstData) + std::tie(fullStyle, blockDepth) = extractStringDetails(); + + AstArray originalString; + if (std::optional> value = parseCharArray(options.storeCstData ? &originalString : nullptr)) + { + AstExprConstantString* node = allocator.alloc(location, *value, style); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(originalString, fullStyle, blockDepth); + return node; + } + else + return reportExprError(location, {}, "String literal contains malformed escape sequence"); +} + +AstExpr* Parser::parseInterpString() +{ + TempVector> strings(scratchString); + TempVector> sourceStrings(scratchString2); + TempVector stringPositions(scratchPosition); + TempVector expressions(scratchExpr); + + Location startLocation = lexer.current().location; + Location endLocation; + + do + { + Lexeme currentLexeme = lexer.current(); + LUAU_ASSERT( + currentLexeme.type == Lexeme::InterpStringBegin || currentLexeme.type == Lexeme::InterpStringMid || + currentLexeme.type == Lexeme::InterpStringEnd || currentLexeme.type == Lexeme::InterpStringSimple + ); + + endLocation = currentLexeme.location; + + scratchData.assign(currentLexeme.data, currentLexeme.getLength()); + + if (options.storeCstData) + { + sourceStrings.push_back(copy(scratchData)); + stringPositions.push_back(currentLexeme.location.begin); + } + + if (!Lexer::fixupQuotedString(scratchData)) + { + nextLexeme(); + return reportExprError(Location{startLocation, endLocation}, {}, "Interpolated string literal contains malformed escape sequence"); + } + + AstArray chars = copy(scratchData); + + nextLexeme(); + + strings.push_back(chars); + + if (currentLexeme.type == Lexeme::InterpStringEnd || currentLexeme.type == Lexeme::InterpStringSimple) + { + break; + } + + bool errorWhileChecking = false; + + switch (lexer.current().type) + { + case Lexeme::InterpStringMid: + case Lexeme::InterpStringEnd: + { + errorWhileChecking = true; + nextLexeme(); + expressions.push_back(reportExprError(endLocation, {}, "Malformed interpolated string, expected expression inside '{}'")); + break; + } + case Lexeme::BrokenString: + { + errorWhileChecking = true; + nextLexeme(); + expressions.push_back(reportExprError(endLocation, {}, "Malformed interpolated string; did you forget to add a '`'?")); + break; + } + default: + expressions.push_back(parseExpr()); + } + + if (errorWhileChecking) + { + break; + } + + switch (lexer.current().type) + { + case Lexeme::InterpStringBegin: + case Lexeme::InterpStringMid: + case Lexeme::InterpStringEnd: + break; + case Lexeme::BrokenInterpDoubleBrace: + nextLexeme(); + return reportExprError(endLocation, {}, "Double braces are not permitted within interpolated strings; did you mean '\\{'?"); + case Lexeme::BrokenString: + nextLexeme(); + LUAU_FALLTHROUGH; + case Lexeme::Eof: + { + AstArray> stringsArray = copy(strings); + AstArray exprs = copy(expressions); + AstExprInterpString* node = allocator.alloc(Location{startLocation, lexer.previousLocation()}, stringsArray, exprs); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(sourceStrings), copy(stringPositions)); + if (auto top = lexer.peekBraceStackTop()) + { + // We are in a broken interpolated string, the top of the stack is non empty, we are missing '}' + if (*top == Lexer::BraceType::InterpolatedString) + report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '}'?"); + } + else + { + // We are in a broken interpolated string, the top of the stack is empty, we are missing '`'. + report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '`'?"); + } + return node; + } + default: + return reportExprError(endLocation, {}, "Malformed interpolated string, got %s", lexer.current().toString().c_str()); + } + } while (true); + + AstArray> stringsArray = copy(strings); + AstArray expressionsArray = copy(expressions); + AstExprInterpString* node = allocator.alloc(Location{startLocation, endLocation}, stringsArray, expressionsArray); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(sourceStrings), copy(stringPositions)); + return node; +} + +LUAU_NOINLINE AstExpr* Parser::parseExplicitTypeInstantiationExpr(Position start, AstExpr& basedOnExpr) +{ + CstExprExplicitTypeInstantiation* cstNode = nullptr; + if (options.storeCstData) + { + cstNode = allocator.alloc(CstTypeInstantiation{}); + } + + Location endLocation; + AstArray typesOrPacks = parseTypeInstantiationExpr(cstNode ? &cstNode->instantiation : nullptr, &endLocation); + + AstExpr* expr = allocator.alloc(Location(start, endLocation.end), &basedOnExpr, typesOrPacks); + + if (options.storeCstData) + { + cstNodeMap[expr] = cstNode; + } + + return expr; +} + +// classrefexp -> NAME { `.' NAME | `[' exp `]' } +AstExpr* Parser::parseClassRefExpr() +{ + Position start = lexer.current().location.begin; + + AstExpr* name = parseNameExpr("class reference expression"); + + const Lexeme& dotOrBracket = lexer.current(); + if (dotOrBracket.type == '.') + { + nextLexeme(); + const Position& dotPosition = dotOrBracket.location.begin; + Parser::Name index = parseIndexName("class reference expression", dotPosition); + + return allocator.alloc(Location(start, index.location.end), name, index.name, index.location, dotPosition, '.'); + } + else if (dotOrBracket.type == '[') + { + nextLexeme(); + AstExpr* key = parseExpr(); + expectAndConsume(']', "class reference expression"); + return allocator.alloc(Location(start, lexer.previousLocation().end), name, key); + } + else + return name; +} + +AstArray Parser::parseTypeInstantiationExpr(CstTypeInstantiation* cstNodeOut, Location* endLocationOut) +{ + LUAU_ASSERT(lexer.current().type == '<' && lexer.lookahead().type == '<'); + + if (cstNodeOut) + { + cstNodeOut->leftArrow1Position = lexer.current().location.begin; + } + + Lexeme begin = lexer.current(); + lexer.next(); + + TempVector commaPositions = TempVector{scratchPosition}; + + AstArray typeOrPacks = parseTypeParams( + cstNodeOut ? &cstNodeOut->leftArrow2Position : nullptr, + cstNodeOut ? &commaPositions : nullptr, + cstNodeOut ? &cstNodeOut->rightArrow1Position : nullptr + ); + + if (cstNodeOut) + { + cstNodeOut->commaPositions = copy(commaPositions); + + if (lexer.current().type == '>') + { + cstNodeOut->rightArrow2Position = lexer.current().location.begin; + } + } + + if (endLocationOut) + { + *endLocationOut = lexer.current().location; + } + + expectMatchAndConsume('>', begin); + return typeOrPacks; +} + + +AstExpr* Parser::parseNumber() +{ + Location start = lexer.current().location; + + scratchData.assign(lexer.current().data, lexer.current().getLength()); + AstArray sourceData; + if (options.storeCstData) + sourceData = copy(scratchData); + + // Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al + if (scratchData.find('_') != std::string::npos) + { + scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end()); + } + + if (FFlag::LuauIntegerType2 && (scratchData.back() == 'i')) + { + int64_t value = 0; + ConstantNumberParseResult result; + if ((strncmp(scratchData.c_str(), "0x", 2) == 0) || (strncmp(scratchData.c_str(), "0X", 2) == 0)) + result = parseInteger64(value, scratchData.c_str(), 16); // pass in '0x' prefix, it's handled by strtoll + else if ((strncmp(scratchData.c_str(), "0b", 2) == 0) || (strncmp(scratchData.c_str(), "0B", 2) == 0)) + result = parseInteger64(value, scratchData.c_str() + 2, 2); + else + result = parseInteger64(value, scratchData.c_str(), 10); + + nextLexeme(); + + if (result == ConstantNumberParseResult::Malformed) + return reportExprError(start, {}, "Malformed integer"); + + if (result != ConstantNumberParseResult::Ok) + return reportExprError(start, {}, "Integer overflow"); + + AstExprConstantInteger* node = allocator.alloc(start, value, result); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(sourceData); + return node; + } + else + { + double value = 0; + ConstantNumberParseResult result = parseDouble(value, scratchData.c_str()); + nextLexeme(); + + if (result == ConstantNumberParseResult::Malformed) + return reportExprError(start, {}, "Malformed number"); + + AstExprConstantNumber* node = allocator.alloc(start, value, result); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(sourceData); + return node; + } +} + +AstLocal* Parser::pushLocal(const Binding& binding) +{ + const Name& name = binding.name; + AstLocal*& local = localMap[name.name]; + + local = allocator.alloc( + name.name, name.location, /* shadow= */ local, functionStack.size() - 1, functionStack.back().loopDepth, binding.annotation, binding.isConst + ); + + localStack.push_back(local); + + return local; +} + +unsigned int Parser::saveLocals() +{ + return unsigned(localStack.size()); +} + +void Parser::restoreLocals(unsigned int offset) +{ + for (size_t i = localStack.size(); i > offset; --i) + { + AstLocal* l = localStack[i - 1]; + + localMap[l->name] = l->shadow; + } + + localStack.resize(offset); +} + +bool Parser::expectAndConsume(char value, const char* context) +{ + return expectAndConsume(static_cast(static_cast(value)), context); +} + +bool Parser::expectAndConsume(Lexeme::Type type, const char* context) +{ + if (lexer.current().type != type) + return expectAndConsumeFailWithLookahead(type, context); + + nextLexeme(); + return true; +} + +// LUAU_NOINLINE is used to limit the stack cost due to std::string objects, and to increase caller performance since this code is cold +LUAU_NOINLINE bool Parser::expectAndConsumeFailWithLookahead(Lexeme::Type type, const char* context) +{ + expectAndConsumeFail(type, context); + + // check if this is an extra token and the expected token is next + if (lexer.lookahead().type == type) + { + // skip invalid and consume expected + nextLexeme(); + nextLexeme(); + } + + return false; +} + +// LUAU_NOINLINE is used to limit the stack cost due to std::string objects, and to increase caller performance since this code is cold +LUAU_NOINLINE void Parser::expectAndConsumeFail(Lexeme::Type type, const char* context) +{ + std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString(); + std::string currLexemeString = lexer.current().toString(); + + if (context) + report(lexer.current().location, "Expected %s when parsing %s, got %s", typeString.c_str(), context, currLexemeString.c_str()); + else + report(lexer.current().location, "Expected %s, got %s", typeString.c_str(), currLexemeString.c_str()); +} + +bool Parser::expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing) +{ + Lexeme::Type type = static_cast(static_cast(value)); + + if (lexer.current().type != type) + { + expectMatchAndConsumeFail(type, begin); + + return expectMatchAndConsumeRecover(value, begin, searchForMissing); + } + else + { + nextLexeme(); + + return true; + } +} + +// LUAU_NOINLINE is used to limit the stack cost due to std::string objects, and to increase caller performance since this code is cold +LUAU_NOINLINE bool Parser::expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing) +{ + Lexeme::Type type = static_cast(static_cast(value)); + + if (searchForMissing) + { + // previous location is taken because 'current' lexeme is already the next token + unsigned currentLine = lexer.previousLocation().end.line; + + // search to the end of the line for expected token + // we will also stop if we hit a token that can be handled by parsing function above the current one + Lexeme::Type lexemeType = lexer.current().type; + + while (currentLine == lexer.current().location.begin.line && lexemeType != type && matchRecoveryStopOnToken[lexemeType] == 0) + { + nextLexeme(); + lexemeType = lexer.current().type; + } + + if (lexemeType == type) + { + nextLexeme(); + + return true; + } + } + else + { + // check if this is an extra token and the expected token is next + if (lexer.lookahead().type == type) + { + // skip invalid and consume expected + nextLexeme(); + nextLexeme(); + + return true; + } + } + + return false; +} + +// LUAU_NOINLINE is used to limit the stack cost due to std::string objects, and to increase caller performance since this code is cold +LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra) +{ + std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString(); + std::string matchString = Lexeme(Location(Position(0, 0), 0), begin.type).toString(); + + if (lexer.current().location.begin.line == begin.position.line) + report( + lexer.current().location, + "Expected %s (to close %s at column %d), got %s%s", + typeString.c_str(), + matchString.c_str(), + begin.position.column + 1, + lexer.current().toString().c_str(), + extra ? extra : "" + ); + else + report( + lexer.current().location, + "Expected %s (to close %s at line %d), got %s%s", + typeString.c_str(), + matchString.c_str(), + begin.position.line + 1, + lexer.current().toString().c_str(), + extra ? extra : "" + ); +} + +// LUAU_NOINLINE is used to limit the stack cost of callers inlining it +LUAU_NOINLINE bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin) +{ + if (lexer.current().type != type) + return expectMatchEndAndConsumeFailWithLookahead(type, begin); + + // If the token matches on a different line and a different column, it suggests misleading indentation + // This can be used to pinpoint the problem location for a possible future *actual* mismatch + if (lexer.current().location.begin.line != begin.position.line && lexer.current().location.begin.column != begin.position.column && + endMismatchSuspect.position.line < begin.position.line) // Only replace the previous suspect with more recent suspects + { + endMismatchSuspect = begin; + } + + nextLexeme(); + + return true; +} + +// LUAU_NOINLINE is used to limit the stack cost due to std::string objects, and to increase caller performance since this code is cold +LUAU_NOINLINE bool Parser::expectMatchEndAndConsumeFailWithLookahead(Lexeme::Type type, const MatchLexeme& begin) +{ + if (endMismatchSuspect.type != Lexeme::Eof && endMismatchSuspect.position.line > begin.position.line) + { + std::string matchString = Lexeme(Location(Position(0, 0), 0), endMismatchSuspect.type).toString(); + std::string suggestion = format("; did you forget to close %s at line %d?", matchString.c_str(), endMismatchSuspect.position.line + 1); + + expectMatchAndConsumeFail(type, begin, suggestion.c_str()); + } + else + { + expectMatchAndConsumeFail(type, begin); + } + + // check if this is an extra token and the expected token is next + if (lexer.lookahead().type == type) + { + // skip invalid and consume expected + nextLexeme(); + nextLexeme(); + + return true; + } + + return false; +} + +template +AstArray Parser::copy(const T* data, size_t size) +{ + AstArray result; + + result.data = size ? static_cast(allocator.allocate(sizeof(T) * size)) : nullptr; + result.size = size; + + // This is equivalent to std::uninitialized_copy, but without the exception guarantee + // since our types don't have destructors + for (size_t i = 0; i < size; ++i) + new (result.data + i) T(data[i]); + + return result; +} + +template +AstArray Parser::copy(const TempVector& data) +{ + return copy(data.empty() ? nullptr : &data[0], data.size()); +} + +template +AstArray Parser::copy(std::initializer_list data) +{ + return copy(data.size() == 0 ? nullptr : data.begin(), data.size()); +} + +AstArray Parser::copy(const std::string& data) +{ + AstArray result = copy(data.c_str(), data.size() + 1); + + result.size = data.size(); + + return result; +} + +void Parser::incrementRecursionCounter(const char* context) +{ + recursionCounter++; + + if (recursionCounter > unsigned(FInt::LuauRecursionLimit)) + { + ParseError::raise(lexer.current().location, "Exceeded allowed recursion depth; simplify your %s to make the code compile", context); + } +} + +void Parser::report(const Location& location, const char* format, va_list args) +{ + // To reduce number of errors reported to user for incomplete statements, we skip multiple errors at the same location + // For example, consider 'local a = (((b + ' where multiple tokens haven't been written yet + if (!parseErrors.empty() && location == parseErrors.back().getLocation()) + return; + + std::string message = vformat(format, args); + + // when limited to a single error, behave as if the error recovery is disabled + if (FInt::LuauParseErrorLimit == 1) + throw ParseError(location, message); + + parseErrors.emplace_back(location, message); + + if (parseErrors.size() >= unsigned(FInt::LuauParseErrorLimit) && !options.noErrorLimit) + ParseError::raise(location, "Reached error limit (%d)", int(FInt::LuauParseErrorLimit)); +} + +void Parser::report(const Location& location, const char* format, ...) +{ + va_list args; + va_start(args, format); + report(location, format, args); + va_end(args); +} + +LUAU_NOINLINE void Parser::reportNameError(const char* context) +{ + if (context) + report(lexer.current().location, "Expected identifier when parsing %s, got %s", context, lexer.current().toString().c_str()); + else + report(lexer.current().location, "Expected identifier, got %s", lexer.current().toString().c_str()); +} + +AstStatError* Parser::reportStatError( + const Location& location, + const AstArray& expressions, + const AstArray& statements, + const char* format, + ... +) +{ + va_list args; + va_start(args, format); + report(location, format, args); + va_end(args); + + return allocator.alloc(location, expressions, statements, unsigned(parseErrors.size() - 1)); +} + +AstExprError* Parser::reportExprError(const Location& location, const AstArray& expressions, const char* format, ...) +{ + va_list args; + va_start(args, format); + report(location, format, args); + va_end(args); + + return allocator.alloc(location, expressions, unsigned(parseErrors.size() - 1)); +} + +AstTypeError* Parser::reportTypeError(const Location& location, const AstArray& types, const char* format, ...) +{ + va_list args; + va_start(args, format); + report(location, format, args); + va_end(args); + + return allocator.alloc(location, types, false, unsigned(parseErrors.size() - 1)); +} + +AstTypeError* Parser::reportMissingTypeError(const Location& parseErrorLocation, const Location& astErrorLocation, const char* format, ...) +{ + va_list args; + va_start(args, format); + report(parseErrorLocation, format, args); + va_end(args); + + return allocator.alloc(astErrorLocation, AstArray{}, true, unsigned(parseErrors.size() - 1)); +} + +void Parser::nextLexeme() +{ + Lexeme::Type type = lexer.next(/* skipComments= */ false, true).type; + + while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment) + { + const Lexeme& lexeme = lexer.current(); + + if (options.captureComments) + commentLocations.push_back(Comment{lexeme.type, lexeme.location}); + + // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme. + // The parser will turn this into a proper syntax error. + if (lexeme.type == Lexeme::BrokenComment) + return; + + // Comments starting with ! are called "hot comments" and contain directives for type checking / linting / compiling + if (lexeme.type == Lexeme::Comment && lexeme.getLength() && lexeme.data[0] == '!') + { + const char* text = lexeme.data; + + unsigned int end = lexeme.getLength(); + while (end > 0 && isSpace(text[end - 1])) + --end; + + hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)}); + } + + type = lexer.next(/* skipComments= */ false, /* updatePrevLocation= */ false).type; + } +} + +} // namespace Luau diff --git a/External/luau/Ast/src/PrettyPrinter.cpp b/External/luau/Ast/src/PrettyPrinter.cpp new file mode 100644 index 00000000..63992308 --- /dev/null +++ b/External/luau/Ast/src/PrettyPrinter.cpp @@ -0,0 +1,2241 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/PrettyPrinter.h" + +#include "Luau/Cst.h" +#include "Luau/Parser.h" +#include "Luau/StringUtils.h" +#include "Luau/Common.h" + +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauExportValueSyntax) +LUAU_FASTFLAG(LuauExperimentalIfLocalSyntax) + +namespace +{ +bool isIdentifierStartChar(char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} + +bool isDigit(char c) +{ + return c >= '0' && c <= '9'; +} + +bool isIdentifierChar(char c) +{ + return isIdentifierStartChar(c) || isDigit(c); +} + +const std::vector keywords = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", + "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}; + +} // namespace + +namespace Luau +{ + +struct Writer +{ + virtual ~Writer() {} + + virtual void advance(const Position&) = 0; + virtual void newline() = 0; + virtual void space() = 0; + virtual void maybeSpace(const Position& newPos, int reserve) = 0; + virtual void write(std::string_view) = 0; + virtual void writeMultiline(std::string_view) = 0; + virtual void identifier(std::string_view name) = 0; + virtual void keyword(std::string_view) = 0; + virtual void symbol(std::string_view) = 0; + virtual void literal(std::string_view) = 0; + virtual void string(std::string_view) = 0; + virtual void sourceString(std::string_view, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth) = 0; +}; + +struct StringWriter : Writer +{ + std::string ss; + Position pos{0, 0}; + char lastChar = '\0'; // used to determine whether we need to inject an extra space to preserve grammatical correctness. + + const std::string& str() const + { + return ss; + } + + void advance(const Position& newPos) override + { + while (pos.line < newPos.line) + newline(); + + if (pos.column < newPos.column) + write(std::string(newPos.column - pos.column, ' ')); + } + + void maybeSpace(const Position& newPos, int reserve) override + { + if (pos.column + reserve < newPos.column) + space(); + } + + void newline() override + { + ss += '\n'; + pos.column = 0; + ++pos.line; + lastChar = '\n'; + } + + void space() override + { + ss += ' '; + ++pos.column; + lastChar = ' '; + } + + void writeMultiline(std::string_view s) override + { + if (s.empty()) + return; + + ss.append(s.data(), s.size()); + lastChar = s[s.size() - 1]; + + size_t index = 0; + size_t numLines = 0; + while (true) + { + auto newlinePos = s.find('\n', index); + if (newlinePos == std::string::npos) + break; + numLines++; + index = newlinePos + 1; + } + + pos.line += unsigned(numLines); + if (numLines > 0) + pos.column = unsigned(s.size()) - unsigned(index); + else + pos.column += unsigned(s.size()); + } + + void write(std::string_view s) override + { + if (s.empty()) + return; + + ss.append(s.data(), s.size()); + pos.column += unsigned(s.size()); + lastChar = s[s.size() - 1]; + } + + void write(char c) + { + ss += c; + pos.column += 1; + lastChar = c; + } + + void identifier(std::string_view s) override + { + if (s.empty()) + return; + + if (isIdentifierChar(lastChar)) + space(); + + write(s); + } + + void keyword(std::string_view s) override + { + if (s.empty()) + return; + + if (isIdentifierChar(lastChar)) + space(); + + write(s); + } + + void symbol(std::string_view s) override + { + write(s); + } + + void literal(std::string_view s) override + { + if (s.empty()) + return; + + else if (isIdentifierChar(lastChar) && isDigit(s[0])) + space(); + + write(s); + } + + void string(std::string_view s) override + { + char quote = '\''; + if (std::string::npos != s.find(quote)) + quote = '\"'; + + write(quote); + write(escape(s)); + write(quote); + } + + void sourceString(std::string_view s, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth) override + { + if (quoteStyle == CstExprConstantString::QuoteStyle::QuotedRaw) + { + auto blocks = std::string(blockDepth, '='); + write('['); + write(blocks); + write('['); + writeMultiline(s); + write(']'); + write(blocks); + write(']'); + } + else + { + LUAU_ASSERT(blockDepth == 0); + + char quote = '"'; + switch (quoteStyle) + { + case CstExprConstantString::QuoteStyle::QuotedDouble: + quote = '"'; + break; + case CstExprConstantString::QuoteStyle::QuotedSingle: + quote = '\''; + break; + case CstExprConstantString::QuoteStyle::QuotedInterp: + quote = '`'; + break; + default: + LUAU_ASSERT(!"Unhandled quote type"); + } + + write(quote); + writeMultiline(s); + write(quote); + } + } +}; + +class CommaSeparatorInserter +{ +public: + explicit CommaSeparatorInserter(Writer& w, const Position* commaPosition = nullptr) + : first(true) + , writer(w) + , commaPosition(commaPosition) + { + } + void operator()() + { + if (first) + first = !first; + else + { + if (commaPosition) + { + writer.advance(*commaPosition); + commaPosition++; + } + writer.symbol(","); + } + } + +private: + bool first; + Writer& writer; + const Position* commaPosition; +}; + +class ArgNameInserter +{ +public: + ArgNameInserter(Writer& w, const AstArray>& names, const AstArray& colonPositions) + : writer(w) + , names(names) + , colonPositions(colonPositions) + { + } + + void operator()() + { + if (idx < names.size) + { + const auto name = names.data[idx]; + if (name.has_value()) + { + writer.advance(name->second.begin); + writer.identifier(name->first.value); + if (idx < colonPositions.size) + writer.advance(colonPositions.data[idx]); + writer.symbol(":"); + } + } + idx++; + } + +private: + Writer& writer; + const AstArray>& names; + const AstArray& colonPositions; + size_t idx = 0; +}; + +struct Printer +{ + explicit Printer(Writer& writer, CstNodeMap cstNodeMap) + : writer(writer) + , cstNodeMap(std::move(cstNodeMap)) + { + } + + bool writeTypes = false; + Writer& writer; + CstNodeMap cstNodeMap; + + template + T* lookupCstNode(AstNode* astNode) + { + if (const auto cstNode = cstNodeMap[astNode]) + return cstNode->as(); + return nullptr; + } + + // If pos has a value, advances to it and writes s. Otherwise does nothing. + void maybeAdvanceAndWrite(const Position& pos, std::string_view s, bool alwaysWrite = false) + { + if (pos.hasValue()) + { + advance(pos); + writer.write(s); + } + else if (alwaysWrite) + writer.write(s); + } + + void visualize(const AstLocal& local, Position colonPosition) + { + advance(local.location.begin); + + writer.identifier(local.name.value); + if (writeTypes && local.annotation) + { + maybeAdvanceAndWrite(colonPosition, ":", true); + visualizeTypeAnnotation(*local.annotation); + } + } + + void visualizeTypePackAnnotation(AstTypePack& annotation, bool forVarArg, bool unconditionallyParenthesize = true, bool forFunctionReturn = false) + { + advance(annotation.location.begin); + if (const AstTypePackVariadic* variadicTp = annotation.as()) + { + if (!forVarArg) + writer.symbol("..."); + + visualizeTypeAnnotation(*variadicTp->variadicType); + } + else if (AstTypePackGeneric* genericTp = annotation.as()) + { + writer.symbol(genericTp->genericName.value); + if (const auto cstNode = lookupCstNode(genericTp)) + advance(cstNode->ellipsisPosition); + writer.symbol("..."); + } + else if (AstTypePackExplicit* explicitTp = annotation.as()) + { + LUAU_ASSERT(!forVarArg); + if (const auto cstNode = lookupCstNode(explicitTp)) + visualizeTypeList( + explicitTp->typeList, + /* unconditionallyParenthesize */ false, + cstNode->openParenthesesPosition, + cstNode->closeParenthesesPosition, + cstNode->commaPositions + ); + else if (forFunctionReturn) + { + size_t packSize = explicitTp->typeList.types.size + (explicitTp->typeList.tailType != nullptr ? 1 : 0); + visualizeTypeList( + explicitTp->typeList, /* unconditionallyParenthesize */ forFunctionReturn ? packSize != 1 : unconditionallyParenthesize + ); + } + else + visualizeTypeList(explicitTp->typeList, unconditionallyParenthesize); + } + else + { + LUAU_ASSERT(!"Unknown TypePackAnnotation kind"); + } + } + + void visualizeNamedTypeList( + const AstTypeList& list, + bool unconditionallyParenthesize, + Position openParenthesesPosition, + Position closeParenthesesPosition, + const AstArray& commaPositions, + const AstArray>& argNames, + const AstArray& argNamesColonPositions + ) + { + size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0); + if (typeCount == 0) + { + maybeAdvanceAndWrite(openParenthesesPosition, "(", unconditionallyParenthesize); + + maybeAdvanceAndWrite(closeParenthesesPosition, ")", unconditionallyParenthesize); + } + else if (typeCount == 1) + { + bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is()); + // bool shouldParenthesize = unconditionallyParenthesize && list.types.size != 1; // don't parenthesize singleton type packs + maybeAdvanceAndWrite(openParenthesesPosition, "(", shouldParenthesize); + + ArgNameInserter(writer, argNames, argNamesColonPositions)(); + + // Only variadic tail + if (list.types.size == 0) + { + visualizeTypePackAnnotation(*list.tailType, false); + } + else + { + visualizeTypeAnnotation(*list.types.data[0]); + } + + maybeAdvanceAndWrite(closeParenthesesPosition, ")", shouldParenthesize); + } + else + { + maybeAdvanceAndWrite(openParenthesesPosition, "(", unconditionallyParenthesize); + + CommaSeparatorInserter comma(writer, commaPositions.size > 0 ? commaPositions.begin() : nullptr); + ArgNameInserter argName(writer, argNames, argNamesColonPositions); + for (const auto& el : list.types) + { + comma(); + argName(); + visualizeTypeAnnotation(*el); + } + + if (list.tailType) + { + comma(); + visualizeTypePackAnnotation(*list.tailType, false); + } + + maybeAdvanceAndWrite(closeParenthesesPosition, ")", unconditionallyParenthesize); + } + } + + void visualizeTypeList( + const AstTypeList& list, + bool unconditionallyParenthesize, + Position openParenthesesPosition = Position::missing(), + Position closeParenthesesPosition = Position::missing(), + AstArray commaPositions = {} + ) + { + visualizeNamedTypeList(list, unconditionallyParenthesize, openParenthesesPosition, closeParenthesesPosition, commaPositions, {}, {}); + } + + bool isIntegerish(double d) + { + if (d <= std::numeric_limits::max() && d >= std::numeric_limits::min()) + return double(int(d)) == d && !(d == 0.0 && signbit(d)); + else + return false; + } + + void visualize(AstExpr& expr) + { + advance(expr.location.begin); + + if (const auto& a = expr.as()) + { + writer.symbol("("); + + visualize(*a->expr); + + if (const auto cstNode = lookupCstNode(a)) + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + else + { + advanceBefore(a->location.end, 1); + writer.symbol(")"); + } + } + else if (expr.is()) + { + writer.keyword("nil"); + } + else if (const auto& a = expr.as()) + { + if (a->value) + writer.keyword("true"); + else + writer.keyword("false"); + } + else if (const auto& a = expr.as()) + { + if (const auto cstNode = lookupCstNode(a)) + { + writer.literal(std::string_view(cstNode->value.data, cstNode->value.size)); + } + else + { + if (isinf(a->value)) + { + if (a->value > 0) + writer.literal("1e500"); + else + writer.literal("-1e500"); + } + else if (isnan(a->value)) + writer.literal("0/0"); + else + { + if (isIntegerish(a->value)) + writer.literal(std::to_string(int(a->value))); + else + { + char buffer[100]; + size_t len = snprintf(buffer, sizeof(buffer), "%.17g", a->value); + writer.literal(std::string_view{buffer, len}); + } + } + } + } + else if (const auto& a = expr.as()) + { + if (const auto cstNode = lookupCstNode(a)) + { + writer.literal(std::string_view(cstNode->value.data, cstNode->value.size)); + } + else + { + if (a->value >= 0) + { + char buffer[100]; + size_t len = snprintf(buffer, sizeof(buffer), "%lldi", (long long)a->value); + writer.literal(std::string_view{buffer, len}); + } + else + { + char buffer[100]; + size_t len = snprintf(buffer, sizeof(buffer), "0x%llxi", (unsigned long long)a->value); + writer.literal(std::string_view{buffer, len}); + } + } + } + else if (const auto& a = expr.as()) + { + if (const auto cstNode = lookupCstNode(a)) + { + writer.sourceString( + std::string_view(cstNode->sourceString.data, cstNode->sourceString.size), cstNode->quoteStyle, cstNode->blockDepth + ); + } + else + writer.string(std::string_view(a->value.data, a->value.size)); + } + else if (const auto& a = expr.as()) + { + writer.identifier(a->local->name.value); + } + else if (const auto& a = expr.as()) + { + writer.identifier(a->name.value); + } + else if (expr.is()) + { + writer.symbol("..."); + } + else if (const auto& a = expr.as()) + { + visualize(*a->func); + + const auto cstNode = lookupCstNode(a); + + if (writeTypes && (a->typeArguments.size > 0 || (cstNode && cstNode->explicitTypes))) + { + visualizeExplicitTypeInstantiation(a->typeArguments, cstNode && cstNode->explicitTypes ? cstNode->explicitTypes : nullptr); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->openParens, "("); + else + writer.symbol("("); + + CommaSeparatorInserter comma(writer, cstNode ? cstNode->commaPositions.begin() : nullptr); + for (const auto& arg : a->args) + { + comma(); + visualize(*arg); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->closeParens, ")"); + else + writer.symbol(")"); + } + else if (const auto& a = expr.as()) + { + visualize(*a->expr); + advance(a->opPosition); + writer.symbol(std::string(1, a->op)); + advance(a->indexLocation.begin); + writer.write(a->index.value); + } + else if (const auto& a = expr.as()) + { + const auto cstNode = lookupCstNode(a); + + visualize(*a->expr); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->openBracketPosition, "["); + else + writer.symbol("["); + + visualize(*a->index); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->closeBracketPosition, "]"); + else + writer.symbol("]"); + } + else if (const auto& a = expr.as()) + { + if (const CstExprFunction* cstNode = lookupCstNode(a)) + { + visualizeAttributes(a->attributes, &cstNode->attrLists); + if (cstNode->functionKeywordPosition.hasValue()) + advance(cstNode->functionKeywordPosition); + } + else + { + for (const auto& attribute : a->attributes) + visualizeAttribute(*attribute); + } + + writer.keyword("function"); + visualizeFunctionBody(*a); + } + else if (const auto& a = expr.as()) + { + writer.symbol("{"); + + const CstExprTable::Item* cstItem = nullptr; + if (const auto cstNode = lookupCstNode(a)) + { + LUAU_ASSERT(cstNode->items.size == a->items.size); + cstItem = cstNode->items.begin(); + } + + bool first = true; + + for (const auto& item : a->items) + { + if (!cstItem) + { + if (first) + first = false; + else + writer.symbol(","); + } + + switch (item.kind) + { + case AstExprTable::Item::Kind::List: + break; + + case AstExprTable::Item::Kind::Record: + { + const auto& value = item.key->as()->value; + + advance(item.key->location.begin); + + writer.identifier(std::string_view(value.data, value.size)); + + if (cstItem) + advance(cstItem->equalsPosition); + else + writer.maybeSpace(item.value->location.begin, 1); + writer.symbol("="); + } + break; + + case AstExprTable::Item::Kind::General: + { + if (cstItem) + { + LUAU_ASSERT(cstItem->indexerOpenPosition.hasValue()); + maybeAdvanceAndWrite(cstItem->indexerOpenPosition, "[", true); + + visualize(*item.key); + + maybeAdvanceAndWrite(cstItem->indexerClosePosition, "]"); + + maybeAdvanceAndWrite(cstItem->equalsPosition, "="); + } + else + { + writer.symbol("["); + visualize(*item.key); + writer.symbol("]"); + writer.maybeSpace(item.value->location.begin, 1); + writer.symbol("="); + } + } + break; + + default: + LUAU_ASSERT(!"Unknown table item kind"); + } + + advance(item.value->location.begin); + visualize(*item.value); + + if (cstItem) + { + if (cstItem->separator != CstExprTable::Separator::Missing) + { + LUAU_ASSERT(cstItem->separatorPosition.hasValue()); + maybeAdvanceAndWrite(cstItem->separatorPosition, cstItem->separator == CstExprTable::Separator::Comma ? "," : ";", true); + } + cstItem++; + } + } + + Position endPos = expr.location.end; + if (endPos.column > 0) + --endPos.column; + + advance(endPos); + + writer.symbol("}"); + advance(expr.location.end); + } + else if (const auto& a = expr.as()) + { + if (const auto cstNode = lookupCstNode(a)) + advance(cstNode->opPosition); + + switch (a->op) + { + case AstExprUnary::Op::Not: + writer.keyword("not"); + break; + case AstExprUnary::Op::Minus: + writer.symbol("-"); + break; + case AstExprUnary::Op::Len: + writer.symbol("#"); + break; + } + visualize(*a->expr); + } + else if (const auto& a = expr.as()) + { + visualize(*a->left); + + if (const auto cstNode = lookupCstNode(a)) + advance(cstNode->opPosition); + else + { + switch (a->op) + { + case AstExprBinary::Add: + case AstExprBinary::Sub: + case AstExprBinary::Mul: + case AstExprBinary::Div: + case AstExprBinary::FloorDiv: + case AstExprBinary::Mod: + case AstExprBinary::Pow: + case AstExprBinary::CompareLt: + case AstExprBinary::CompareGt: + writer.maybeSpace(a->right->location.begin, 2); + break; + case AstExprBinary::Concat: + case AstExprBinary::CompareNe: + case AstExprBinary::CompareEq: + case AstExprBinary::CompareLe: + case AstExprBinary::CompareGe: + case AstExprBinary::Or: + writer.maybeSpace(a->right->location.begin, 3); + break; + case AstExprBinary::And: + writer.maybeSpace(a->right->location.begin, 4); + break; + default: + LUAU_ASSERT(!"Unknown Op"); + } + } + + writer.symbol(toString(a->op)); + + visualize(*a->right); + } + else if (const auto& a = expr.as()) + { + visualize(*a->expr); + + if (writeTypes) + { + if (const auto* cstNode = lookupCstNode(a)) + advance(cstNode->opPosition); + else + writer.maybeSpace(a->annotation->location.begin, 2); + writer.symbol("::"); + visualizeTypeAnnotation(*a->annotation); + } + } + else if (const auto& a = expr.as()) + { + writer.keyword("if"); + visualizeElseIfExpr(*a); + } + else if (const auto& a = expr.as()) + { + const auto* cstNode = lookupCstNode(a); + + writer.symbol("`"); + + size_t index = 0; + + for (const auto& string : a->strings) + { + if (cstNode) + { + if (index > 0) + { + advance(cstNode->stringPositions.data[index]); + writer.symbol("}"); + } + const AstArray sourceString = cstNode->sourceStrings.data[index]; + writer.writeMultiline(std::string_view(sourceString.data, sourceString.size)); + } + else + { + writer.write(escape(std::string_view(string.data, string.size), /* escapeForInterpString = */ true)); + } + + if (index < a->expressions.size) + { + writer.symbol("{"); + visualize(*a->expressions.data[index]); + if (!cstNode) + writer.symbol("}"); + } + + index++; + } + + writer.symbol("`"); + } + else if (const auto& a = expr.as()) + { + writer.symbol("(error-expr"); + + for (size_t i = 0; i < a->expressions.size; i++) + { + writer.symbol(i == 0 ? ": " : ", "); + visualize(*a->expressions.data[i]); + } + + writer.symbol(")"); + } + else if (const auto& a = expr.as()) + { + visualize(*a->expr); + + if (writeTypes) + { + const CstExprExplicitTypeInstantiation* cstExprNode = lookupCstNode(a); + + visualizeExplicitTypeInstantiation(a->typeArguments, cstExprNode ? &cstExprNode->instantiation : nullptr); + } + } + else + { + LUAU_ASSERT(!"Unknown AstExpr"); + } + } + + void writeEnd(const Location& loc) + { + Position endPos = loc.end; + if (endPos.column >= 3) + endPos.column -= 3; + advance(endPos); + writer.keyword("end"); + } + + void advance(const Position& newPos) + { + LUAU_ASSERT(newPos.hasValue()); + writer.advance(newPos); + } + + void advanceBefore(const Position& newPos, unsigned int tokenLength) + { + if (newPos.column >= tokenLength) + advance(Position{newPos.line, newPos.column - tokenLength}); + else + advance(newPos); + } + + void visualize(AstStat& program) + { + advance(program.location.begin); + + if (const auto& block = program.as()) + { + if (const auto cstNode = lookupCstNode(block)) + { + writer.keyword("do"); + + advance(cstNode->statsStartPosition); + + for (const auto& s : block->body) + visualize(*s); + + maybeAdvanceAndWrite(cstNode->endPosition, "end"); + } + else + { + for (const auto& s : block->body) + visualize(*s); + + writer.advance(block->location.end); + writeEnd(program.location); + } + } + else if (const auto& a = program.as()) + { + writer.keyword("if"); + visualizeElseIf(*a); + } + else if (const auto& a = program.as()) + { + writer.keyword("while"); + visualize(*a->condition); + // TODO: what if 'hasDo = false'? + advance(a->doLocation.begin); + writer.keyword("do"); + visualizeBlock(*a->body); + advance(a->body->location.end); + writer.keyword("end"); + } + else if (const auto& a = program.as()) + { + writer.keyword("repeat"); + visualizeBlock(*a->body); + if (const auto cstNode = lookupCstNode(a)) + maybeAdvanceAndWrite(cstNode->untilPosition, "until"); + else + { + advanceBefore(a->condition->location.begin, 6); + writer.keyword("until"); + } + visualize(*a->condition); + } + else if (program.is()) + writer.keyword("break"); + else if (program.is()) + writer.keyword("continue"); + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + writer.keyword("return"); + + CommaSeparatorInserter comma(writer, cstNode ? cstNode->commaPositions.begin() : nullptr); + for (const auto& expr : a->list) + { + comma(); + visualize(*expr); + } + } + else if (const auto& a = program.as()) + { + visualize(*a->expr); + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + if (FFlag::LuauExportValueSyntax && a->isExported) + { + writer.keyword("export"); + + if (a->keywordLocation.has_value()) + advance(a->keywordLocation->begin); + + writer.keyword(a->isConst ? "const" : "local"); + } + else if (a->isConst) + { + writer.keyword("const"); + } + else + { + writer.keyword("local"); + } + + CommaSeparatorInserter varComma(writer, cstNode ? cstNode->varsCommaPositions.begin() : nullptr); + for (size_t i = 0; i < a->vars.size; i++) + { + varComma(); + if (cstNode) + { + LUAU_ASSERT(cstNode->varsAnnotationColonPositions.size > i); + visualize(*a->vars.data[i], cstNode->varsAnnotationColonPositions.data[i]); + } + else + visualize(*a->vars.data[i], Position{0, 0}); + } + + if (a->equalsSignLocation) + { + advance(a->equalsSignLocation->begin); + writer.symbol("="); + } + + + CommaSeparatorInserter valueComma(writer, cstNode ? cstNode->valuesCommaPositions.begin() : nullptr); + for (const auto& value : a->values) + { + valueComma(); + visualize(*value); + } + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + writer.keyword("for"); + + visualize(*a->var, cstNode ? cstNode->annotationColonPosition : Position::missing()); + + if (cstNode) + advance(cstNode->equalsPosition); + writer.symbol("="); + + visualize(*a->from); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->endCommaPosition, ","); + else + writer.symbol(","); + + visualize(*a->to); + + if (a->step) + { + if (cstNode) + advance(cstNode->stepCommaPosition); + writer.symbol(","); + + visualize(*a->step); + } + advance(a->doLocation.begin); + writer.keyword("do"); + visualizeBlock(*a->body); + + advance(a->body->location.end); + writer.keyword("end"); + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + writer.keyword("for"); + + CommaSeparatorInserter varComma(writer, cstNode ? cstNode->varsCommaPositions.begin() : nullptr); + for (size_t i = 0; i < a->vars.size; i++) + { + varComma(); + if (cstNode) + { + LUAU_ASSERT(cstNode->varsAnnotationColonPositions.size > i); + visualize(*a->vars.data[i], cstNode->varsAnnotationColonPositions.data[i]); + } + else + visualize(*a->vars.data[i], Position{0, 0}); + } + + advance(a->inLocation.begin); + writer.keyword("in"); + + CommaSeparatorInserter valComma(writer, cstNode ? cstNode->valuesCommaPositions.begin() : nullptr); + + for (const auto& val : a->values) + { + valComma(); + visualize(*val); + } + + advance(a->doLocation.begin); + writer.keyword("do"); + + visualizeBlock(*a->body); + + advance(a->body->location.end); + writer.keyword("end"); + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + CommaSeparatorInserter varComma(writer, cstNode ? cstNode->varsCommaPositions.begin() : nullptr); + for (const auto& var : a->vars) + { + varComma(); + visualize(*var); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->equalsPosition, "="); + else + { + writer.space(); + writer.symbol("="); + } + + CommaSeparatorInserter valueComma(writer, cstNode ? cstNode->valuesCommaPositions.begin() : nullptr); + for (const auto& value : a->values) + { + valueComma(); + visualize(*value); + } + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + visualize(*a->var); + + if (cstNode) + advance(cstNode->opPosition); + + switch (a->op) + { + case AstExprBinary::Add: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("+="); + break; + case AstExprBinary::Sub: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("-="); + break; + case AstExprBinary::Mul: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("*="); + break; + case AstExprBinary::Div: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("/="); + break; + case AstExprBinary::FloorDiv: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 3); + writer.symbol("//="); + break; + case AstExprBinary::Mod: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("%="); + break; + case AstExprBinary::Pow: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 2); + writer.symbol("^="); + break; + case AstExprBinary::Concat: + if (!cstNode) + writer.maybeSpace(a->value->location.begin, 3); + writer.symbol("..="); + break; + default: + LUAU_ASSERT(!"Unexpected compound assignment op"); + } + + visualize(*a->value); + } + else if (const auto& a = program.as()) + { + if (const CstStatFunction* cstNode = lookupCstNode(a)) + { + visualizeAttributes(a->func->attributes, &cstNode->attrLists); + advance(cstNode->functionKeywordPosition); + } + else + visualizeAttributes(a->func->attributes, nullptr); + writer.keyword("function"); + visualize(*a->name); + visualizeFunctionBody(*a->func); + } + else if (const auto& a = program.as()) + { + const auto cstNode = lookupCstNode(a); + + if (cstNode) + visualizeAttributes(a->func->attributes, &cstNode->attrLists); + else + { + for (const auto& attribute : a->func->attributes) + visualizeAttribute(*attribute); + } + + if (cstNode) + advance(cstNode->localKeywordPosition); + + if (FFlag::LuauExportValueSyntax && a->name->isExported) + { + writer.keyword("export"); + } + else if (a->name->isConst) + { + writer.keyword("const"); + } + else + { + writer.keyword("local"); + } + + if (cstNode) + advance(cstNode->functionKeywordPosition); + else + writer.space(); + + writer.keyword("function"); + advance(a->name->location.begin); + writer.identifier(a->name->name.value); + visualizeFunctionBody(*a->func); + } + else if (const auto& a = program.as()) + { + if (writeTypes) + { + const auto* cstNode = lookupCstNode(a); + + if (a->exported) + writer.keyword("export"); + + if (cstNode) + advance(cstNode->typeKeywordPosition); + + writer.keyword("type"); + advance(a->nameLocation.begin); + writer.identifier(a->name.value); + if (a->generics.size > 0 || a->genericPacks.size > 0) + { + if (cstNode) + advance(cstNode->genericsOpenPosition); + writer.symbol("<"); + CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + + for (auto o : a->generics) + { + comma(); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + + if (o->defaultValue) + { + if (const CstGenericType* genericTypeCstNode = lookupCstNode(o)) + advance(genericTypeCstNode->defaultEqualsPosition); + else + writer.maybeSpace(o->defaultValue->location.begin, 2); + + writer.symbol("="); + visualizeTypeAnnotation(*o->defaultValue); + } + } + + for (auto o : a->genericPacks) + { + comma(); + + const auto* genericTypePackCstNode = lookupCstNode(o); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + if (genericTypePackCstNode) + maybeAdvanceAndWrite(genericTypePackCstNode->ellipsisPosition, "..."); + else + writer.symbol("..."); + + if (o->defaultValue) + { + if (cstNode) + advance(genericTypePackCstNode->defaultEqualsPosition); + else + writer.maybeSpace(o->defaultValue->location.begin, 2); + writer.symbol("="); + visualizeTypePackAnnotation(*o->defaultValue, false); + } + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->genericsClosePosition, ">"); + else + writer.symbol(">"); + } + if (cstNode) + maybeAdvanceAndWrite(cstNode->equalsPosition, "="); + else + { + writer.maybeSpace(a->type->location.begin, 2); + writer.symbol("="); + } + + visualizeTypeAnnotation(*a->type); + } + } + else if (const auto& t = program.as()) + { + if (writeTypes) + { + const auto cstNode = lookupCstNode(t); + if (t->exported) + writer.keyword("export"); + if (cstNode) + advance(cstNode->typeKeywordPosition); + else + writer.space(); + writer.keyword("type"); + if (cstNode) + advance(cstNode->functionKeywordPosition); + else + writer.space(); + writer.keyword("function"); + advance(t->nameLocation.begin); + writer.identifier(t->name.value); + visualizeFunctionBody(*t->body); + } + } + else if (const auto& a = program.as()) + { + writer.symbol("(error-stat"); + + for (size_t i = 0; i < a->expressions.size; i++) + { + writer.symbol(i == 0 ? ": " : ", "); + visualize(*a->expressions.data[i]); + } + + for (size_t i = 0; i < a->statements.size; i++) + { + writer.symbol(i == 0 && a->expressions.size == 0 ? ": " : ", "); + visualize(*a->statements.data[i]); + } + + writer.symbol(")"); + } + else if (const auto& a = program.as()) + { + writer.keyword("declare"); + writer.advance(a->nameLocation.begin); + writer.identifier(a->name.value); + + writer.symbol(":"); + visualizeTypeAnnotation(*a->type); + } + else if (const auto& c = program.as(); c && FFlag::DebugLuauUserDefinedClasses) + { + writer.keyword("class"); + writer.advance(c->name->location.begin); + writer.identifier(c->name->name.value); + if (c->super) + { + writer.keyword("extends"); + visualize(*c->super); + } + + for (const auto& member : c->members) + { + visit( + overloaded{ + [&](const AstClassProperty& prop) + { + writer.advance(prop.qualifierLocation.begin); + writer.keyword("public"); + writer.advance(prop.nameLocation.begin); + writer.identifier(prop.name.value); + if (writeTypes && prop.ty) + { + LUAU_ASSERT(prop.typeColonLocation.has_value()); + writer.advance(prop.typeColonLocation->begin); + writer.symbol(":"); + visualizeTypeAnnotation(*prop.ty); + } + }, + [&](const AstClassMethod& method) + { + if (method.qualifierLocation) + { + writer.advance(method.qualifierLocation->begin); + writer.keyword("public"); + } + writer.advance(method.keywordLocation.begin); + writer.keyword("function"); + writer.advance(method.nameLocation.begin); + writer.identifier(method.functionName.value); + visualizeFunctionBody(*method.function); + } + }, + member + ); + } + + writer.newline(); + writer.keyword("end"); + writer.newline(); + } + else + { + LUAU_ASSERT(!"Unknown AstStat"); + } + + if (program.hasSemicolon) + { + advanceBefore(program.location.end, 1); + writer.symbol(";"); + } + } + + void visualizeFunctionBody(AstExprFunction& func) + { + const auto cstNode = lookupCstNode(&func); + + if (func.generics.size > 0 || func.genericPacks.size > 0) + { + CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->openGenericsPosition, "<"); + else + writer.symbol("<"); + + for (const auto& o : func.generics) + { + comma(); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + } + + for (const auto& o : func.genericPacks) + { + comma(); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + if (const auto* genericTypePackCstNode = lookupCstNode(o)) + advance(genericTypePackCstNode->ellipsisPosition); + writer.symbol("..."); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->closeGenericsPosition, ">"); + else + writer.symbol(">"); + } + + if (func.argLocation) + advance(func.argLocation->begin); + writer.symbol("("); + CommaSeparatorInserter comma(writer, cstNode ? cstNode->argsCommaPositions.begin() : nullptr); + + for (size_t i = 0; i < func.args.size; ++i) + { + AstLocal* local = func.args.data[i]; + + comma(); + + advance(local->location.begin); + writer.identifier(local->name.value); + if (writeTypes && local->annotation) + { + if (cstNode) + { + LUAU_ASSERT(cstNode->argsAnnotationColonPositions.size > i); + maybeAdvanceAndWrite(cstNode->argsAnnotationColonPositions.data[i], ":"); + } + else + writer.symbol(":"); + + visualizeTypeAnnotation(*local->annotation); + } + } + + if (func.vararg) + { + comma(); + advance(func.varargLocation.begin); + writer.symbol("..."); + + if (func.varargAnnotation) + { + if (cstNode) + maybeAdvanceAndWrite(cstNode->varargAnnotationColonPosition, ":"); + else + writer.symbol(":"); + + visualizeTypePackAnnotation(*func.varargAnnotation, true); + } + } + + if (func.argLocation) + advanceBefore(func.argLocation->end, 1); + writer.symbol(")"); + + if (writeTypes && func.returnAnnotation != nullptr) + { + if (cstNode) + maybeAdvanceAndWrite(cstNode->returnSpecifierPosition, ":"); + else + writer.symbol(":"); + + if (!cstNode) + writer.space(); + + visualizeTypePackAnnotation(*func.returnAnnotation, false, false, true); + } + + visualizeBlock(*func.body); + advance(func.body->location.end); + writer.keyword("end"); + } + + void visualizeBlock(AstStatBlock& block) + { + for (const auto& s : block.body) + visualize(*s); + writer.advance(block.location.end); + } + + void visualizeBlock(AstStat& stat) + { + if (AstStatBlock* block = stat.as()) + visualizeBlock(*block); + else + LUAU_ASSERT(!"visualizeBlock was expecting an AstStatBlock"); + } + + void visualizeElseIf(AstStatIf& elseif) + { + if (FFlag::LuauExperimentalIfLocalSyntax && elseif.conditionLocal) + { + const auto cstNode = lookupCstNode(&elseif); + + if (elseif.conditionKeywordLocation) + advance(elseif.conditionKeywordLocation->begin); + writer.keyword(elseif.conditionIsConst ? "const" : "local"); + + visualize(*elseif.conditionLocal, cstNode ? cstNode->annotationColonPosition : Position::missing()); + + if (elseif.conditionEqualsLocation) + advance(elseif.conditionEqualsLocation->begin); + writer.symbol("="); + } + + visualize(*elseif.condition); + + if (elseif.thenLocation) + advance(elseif.thenLocation->begin); + writer.keyword("then"); + visualizeBlock(*elseif.thenbody); + + if (elseif.elsebody == nullptr) + { + advance(elseif.thenbody->location.end); + writer.keyword("end"); + } + else if (auto elseifelseif = elseif.elsebody->as()) + { + if (elseif.elseLocation) + advance(elseif.elseLocation->begin); + writer.keyword("elseif"); + visualizeElseIf(*elseifelseif); + } + else + { + if (elseif.elseLocation) + advance(elseif.elseLocation->begin); + writer.keyword("else"); + + visualizeBlock(*elseif.elsebody); + advance(elseif.elsebody->location.end); + writer.keyword("end"); + } + } + + void visualizeElseIfExpr(AstExprIfElse& elseif) + { + const auto cstNode = lookupCstNode(&elseif); + + if (FFlag::LuauExperimentalIfLocalSyntax && elseif.conditionLocal) + { + if (elseif.conditionKeywordLocation) + advance(elseif.conditionKeywordLocation->begin); + writer.keyword(elseif.conditionIsConst ? "const" : "local"); + + visualize(*elseif.conditionLocal, cstNode ? cstNode->annotationColonPosition : Position::missing()); + + if (elseif.conditionEqualsLocation) + advance(elseif.conditionEqualsLocation->begin); + writer.symbol("="); + } + + visualize(*elseif.condition); + if (cstNode) + maybeAdvanceAndWrite(cstNode->thenPosition, "then"); + else + writer.keyword("then"); + + visualize(*elseif.trueExpr); + + if (elseif.falseExpr) + { + if (cstNode) + advance(cstNode->elsePosition); + if (auto elseifelseif = elseif.falseExpr->as(); elseifelseif && (!cstNode || cstNode->isElseIf)) + { + writer.keyword("elseif"); + visualizeElseIfExpr(*elseifelseif); + } + else + { + writer.keyword("else"); + visualize(*elseif.falseExpr); + } + } + } + + void visualizeAttribute(AstAttr& attribute) + { + advance(attribute.location.begin); + if (const CstAttr* cstNode = lookupCstNode(&attribute)) + { + if (cstNode->hasAt) + writer.symbol("@"); + writer.identifier(attribute.name.value); + } + else if (const CstParametrizedAttr* cstParamNode = lookupCstNode(&attribute)) + { + writer.identifier(attribute.name.value); + + maybeAdvanceAndWrite(cstParamNode->openParenPosition, "("); + + const size_t commaPositionSize = cstParamNode->argsCommaPositions.size; + + for (size_t i = 0; i < attribute.args.size; ++i) + { + visualize(*attribute.args.data[i]); + if (i < commaPositionSize) + maybeAdvanceAndWrite(cstParamNode->argsCommaPositions.data[i], ","); + } + + maybeAdvanceAndWrite(cstParamNode->closeParenPosition, ")"); + } + else + { + writer.symbol("@"); + writer.identifier(attribute.name.value); + } + } + + void visualizeAttributes(const AstArray& attributes, const AstArray* attrLists) + { + if (attrLists == nullptr) + { + for (const auto& attribute : attributes) + visualizeAttribute(*attribute); + return; + } + + auto currentAttribute = attributes.begin(); + auto currentAttrList = attrLists->begin(); + + const auto attributesEnd = attributes.end(); + const auto attrListsEnd = attrLists->end(); + + while (currentAttribute != attributesEnd || currentAttrList != attrListsEnd) + { + if (currentAttrList == attrListsEnd || (*currentAttribute)->location.begin < (*currentAttrList)->atBracketPosition) + { + visualizeAttribute(**currentAttribute); + ++currentAttribute; + } + else + { + const CstAttrList* cstAttrList = *currentAttrList; + // Start of attribute list + advance(cstAttrList->atBracketPosition); + writer.symbol("@["); + + for (const Position& commaPosition : cstAttrList->commaPositions) + { + LUAU_ASSERT(currentAttribute != attributesEnd); + LUAU_ASSERT((*currentAttribute)->location.begin < commaPosition); + + visualizeAttribute(**currentAttribute); + ++currentAttribute; + + advance(commaPosition); + writer.symbol(","); + } + + LUAU_ASSERT(currentAttribute != attributesEnd); + visualizeAttribute(**currentAttribute); + ++currentAttribute; + + maybeAdvanceAndWrite(cstAttrList->closeBracketPosition, "]"); + + ++currentAttrList; + } + } + } + + void visualizeTypeAnnotation(AstType& typeAnnotation) + { + advance(typeAnnotation.location.begin); + if (const auto& a = typeAnnotation.as()) + { + const auto cstNode = lookupCstNode(a); + + if (a->prefix) + { + writer.write(a->prefix->value); + if (cstNode) + advance(cstNode->prefixPointPosition); + writer.symbol("."); + } + + advance(a->nameLocation.begin); + writer.write(a->name.value); + if (a->parameters.size > 0 || a->hasParameterList) + { + CommaSeparatorInserter comma(writer, cstNode ? cstNode->parametersCommaPositions.begin() : nullptr); + + if (cstNode) + advance(cstNode->openParametersPosition); + writer.symbol("<"); + + for (auto o : a->parameters) + { + comma(); + + if (o.type) + visualizeTypeAnnotation(*o.type); + else + visualizeTypePackAnnotation(*o.typePack, false); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->closeParametersPosition, ">"); + else + writer.symbol(">"); + } + } + else if (const auto& a = typeAnnotation.as()) + { + const auto cstNode = lookupCstNode(a); + + if (a->generics.size > 0 || a->genericPacks.size > 0) + { + CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + if (cstNode) + advance(cstNode->openGenericsPosition); + writer.symbol("<"); + + for (const auto& o : a->generics) + { + comma(); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + } + + for (const auto& o : a->genericPacks) + { + comma(); + + writer.advance(o->location.begin); + writer.identifier(o->name.value); + if (const auto* genericTypePackCstNode = lookupCstNode(o)) + advance(genericTypePackCstNode->ellipsisPosition); + writer.symbol("..."); + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->closeGenericsPosition, ">"); + else + writer.symbol(">"); + } + + visualizeNamedTypeList( + a->argTypes, + cstNode == nullptr, + cstNode ? cstNode->openArgsPosition : Position::missing(), + cstNode ? cstNode->closeArgsPosition : Position::missing(), + cstNode ? cstNode->argumentsCommaPositions : Luau::AstArray{}, + a->argNames, + cstNode ? cstNode->argumentNameColonPositions : Luau::AstArray{} + ); + + if (cstNode) + advance(cstNode->returnArrowPosition); + writer.symbol("->"); + + visualizeTypePackAnnotation(*a->returnTypes, false, cstNode == nullptr); + } + else if (const auto& a = typeAnnotation.as()) + { + AstTypeReference* indexType = a->indexer ? a->indexer->indexType->as() : nullptr; + + writer.symbol("{"); + + const auto cstNode = lookupCstNode(a); + if (cstNode) + { + if (cstNode->isArray) + { + LUAU_ASSERT(a->props.size == 0 && indexType && indexType->name == "number"); + if (a->indexer->accessLocation) + { + LUAU_ASSERT(a->indexer->access != AstTableAccess::ReadWrite); + advance(a->indexer->accessLocation->begin); + writer.keyword(a->indexer->access == AstTableAccess::Read ? "read" : "write"); + } + visualizeTypeAnnotation(*a->indexer->resultType); + } + else + { + const AstTableProp* prop = a->props.begin(); + + for (const CstTypeTable::Item& item : cstNode->items) + { + // we store indexer as part of items to preserve property ordering + if (item.kind == CstTypeTable::Item::Kind::Indexer) + { + LUAU_ASSERT(a->indexer); + + if (a->indexer->accessLocation) + { + LUAU_ASSERT(a->indexer->access != AstTableAccess::ReadWrite); + advance(a->indexer->accessLocation->begin); + writer.keyword(a->indexer->access == AstTableAccess::Read ? "read" : "write"); + } + + advance(item.indexerOpenPosition); + writer.symbol("["); + + visualizeTypeAnnotation(*a->indexer->indexType); + + maybeAdvanceAndWrite(item.indexerClosePosition, "]"); + + maybeAdvanceAndWrite(item.colonPosition, ":"); + + visualizeTypeAnnotation(*a->indexer->resultType); + + if (item.separator != CstExprTable::Separator::Missing) + { + LUAU_ASSERT(item.separatorPosition.hasValue()); + maybeAdvanceAndWrite(item.separatorPosition, item.separator == CstExprTable::Separator::Comma ? "," : ";", true); + } + } + else + { + if (prop->accessLocation) + { + LUAU_ASSERT(prop->access != AstTableAccess::ReadWrite); + advance(prop->accessLocation->begin); + writer.keyword(prop->access == AstTableAccess::Read ? "read" : "write"); + } + + if (item.kind == CstTypeTable::Item::Kind::StringProperty) + { + LUAU_ASSERT(item.indexerOpenPosition.hasValue()); + maybeAdvanceAndWrite(item.indexerOpenPosition, "["); + + advance(item.stringPosition); + writer.sourceString( + std::string_view(item.stringInfo->sourceString.data, item.stringInfo->sourceString.size), + item.stringInfo->quoteStyle, + item.stringInfo->blockDepth + ); + + maybeAdvanceAndWrite(item.indexerClosePosition, "]"); + } + else + { + advance(prop->location.begin); + writer.identifier(prop->name.value); + } + + maybeAdvanceAndWrite(item.colonPosition, ":"); + + visualizeTypeAnnotation(*prop->type); + + if (item.separator != CstExprTable::Separator::Missing) + { + LUAU_ASSERT(item.separatorPosition.hasValue()); + maybeAdvanceAndWrite(item.separatorPosition, item.separator == CstExprTable::Separator::Comma ? "," : ";", true); + } + + ++prop; + } + } + } + } + else + { + if (a->props.size == 0 && indexType && indexType->name == "number") + { + if (a->indexer->access != AstTableAccess::ReadWrite) + { + if (const std::optional& accessLocation = a->indexer->accessLocation) + advance(accessLocation->begin); + + writer.keyword(a->indexer->access == AstTableAccess::Read ? "read" : "write"); + } + + visualizeTypeAnnotation(*a->indexer->resultType); + } + else + { + CommaSeparatorInserter comma(writer); + + for (size_t i = 0; i < a->props.size; ++i) + { + comma(); + advance(a->props.data[i].location.begin); + writer.identifier(a->props.data[i].name.value); + if (a->props.data[i].type) + { + writer.symbol(":"); + visualizeTypeAnnotation(*a->props.data[i].type); + } + } + if (a->indexer) + { + comma(); + + if (a->indexer->access != AstTableAccess::ReadWrite) + { + if (const std::optional& accessLocation = a->indexer->accessLocation) + advance(accessLocation->begin); + + writer.keyword(a->indexer->access == AstTableAccess::Read ? "read" : "write"); + } + + advance(a->indexer->location.begin); + + writer.symbol("["); + visualizeTypeAnnotation(*a->indexer->indexType); + writer.symbol("]"); + writer.symbol(":"); + visualizeTypeAnnotation(*a->indexer->resultType); + } + } + } + + Position endPos = a->location.end; + if (endPos.column > 0) + --endPos.column; + advance(endPos); + + writer.symbol("}"); + } + else if (auto a = typeAnnotation.as()) + { + writer.keyword("typeof"); + + if (const CstTypeTypeof* cstNode = lookupCstNode(a)) + { + maybeAdvanceAndWrite(cstNode->openPosition, "("); + visualize(*a->expr); + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + } + else + { + writer.symbol("("); + visualize(*a->expr); + writer.symbol(")"); + } + } + else if (const auto& a = typeAnnotation.as()) + { + const auto cstNode = lookupCstNode(a); + + if (!cstNode && a->types.size == 2) + { + AstType* l = a->types.data[0]; + AstType* r = a->types.data[1]; + + auto lta = l->as(); + if (lta && lta->name == "nil" && !r->is()) + std::swap(l, r); + + // it's still possible that we had a (T | U) or (T | nil) and not (nil | T) + auto rta = r->as(); + if (rta && rta->name == "nil") + { + bool wrap = l->as() || l->as(); + + if (wrap) + writer.symbol("("); + + visualizeTypeAnnotation(*l); + + if (wrap) + writer.symbol(")"); + + writer.symbol("?"); + return; + } + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->leadingPosition, "|"); + + size_t separatorIndex = 0; + for (size_t i = 0; i < a->types.size; ++i) + { + if (const auto optional = a->types.data[i]->as()) + { + advance(optional->location.begin); + writer.symbol("?"); + continue; + } + + if (i > 0) + { + if (cstNode) + { + advance(cstNode->separatorPositions.data[separatorIndex]); + separatorIndex++; + } + else + writer.maybeSpace(a->types.data[i]->location.begin, 2); + writer.symbol("|"); + } + + bool wrap = !cstNode && (a->types.data[i]->as() || a->types.data[i]->as()); + + if (wrap) + writer.symbol("("); + + visualizeTypeAnnotation(*a->types.data[i]); + + if (wrap) + writer.symbol(")"); + } + } + else if (const auto& a = typeAnnotation.as()) + { + const auto cstNode = lookupCstNode(a); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->leadingPosition, "&"); + + for (size_t i = 0; i < a->types.size; ++i) + { + if (i > 0) + { + if (cstNode) + advance(cstNode->separatorPositions.data[i - 1]); + else + writer.maybeSpace(a->types.data[i]->location.begin, 2); + writer.symbol("&"); + } + + bool wrap = !cstNode && (a->types.data[i]->as() || a->types.data[i]->as()); + + if (wrap) + writer.symbol("("); + + visualizeTypeAnnotation(*a->types.data[i]); + + if (wrap) + writer.symbol(")"); + } + } + else if (const auto& a = typeAnnotation.as()) + { + writer.symbol("("); + + visualizeTypeAnnotation(*a->type); + + if (const CstTypeGroup* cstNode = lookupCstNode(a)) + maybeAdvanceAndWrite(cstNode->closePosition, ")"); + else + { + advanceBefore(a->location.end, 1); + writer.symbol(")"); + } + } + else if (const auto& a = typeAnnotation.as()) + { + writer.keyword(a->value ? "true" : "false"); + } + else if (const auto& a = typeAnnotation.as()) + { + if (const auto cstNode = lookupCstNode(a)) + { + writer.sourceString( + std::string_view(cstNode->sourceString.data, cstNode->sourceString.size), cstNode->quoteStyle, cstNode->blockDepth + ); + } + else + writer.string(std::string_view(a->value.data, a->value.size)); + } + else if (typeAnnotation.is()) + { + writer.symbol("%error-type%"); + } + else + { + LUAU_ASSERT(!"Unknown AstType"); + } + } + + void visualizeExplicitTypeInstantiation(const AstArray& typeArguments, const CstTypeInstantiation* cstNode) + { + if (cstNode) + maybeAdvanceAndWrite(cstNode->leftArrow1Position, std::string_view("<")); + else + writer.symbol("<"); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->leftArrow2Position, std::string_view("<")); + else + writer.symbol("<"); + + CommaSeparatorInserter comma(writer, cstNode ? cstNode->commaPositions.begin() : nullptr); + for (const auto& typeOrPack : typeArguments) + { + if (typeOrPack.type) + { + comma(); + visualizeTypeAnnotation(*typeOrPack.type); + } + else + { + LUAU_ASSERT(typeOrPack.typePack); + comma(); + visualizeTypePackAnnotation(*typeOrPack.typePack, /* forVarArg = */ false); + } + } + + if (cstNode) + maybeAdvanceAndWrite(cstNode->rightArrow1Position, ">"); + else + writer.symbol(">"); + + if (cstNode) + maybeAdvanceAndWrite(cstNode->rightArrow2Position, ">"); + else + writer.symbol(">"); + } +}; + +std::string toString(AstNode* node) +{ + StringWriter writer; + writer.pos = node->location.begin; + + Printer printer(writer, CstNodeMap{}); + printer.writeTypes = true; + + if (auto statNode = node->asStat()) + printer.visualize(*statNode); + else if (auto exprNode = node->asExpr()) + printer.visualize(*exprNode); + else if (auto typeNode = node->asType()) + printer.visualizeTypeAnnotation(*typeNode); + + return writer.str(); +} + +void dump(AstNode* node) +{ + printf("%s\n", toString(node).c_str()); +} + +std::string prettyPrint(AstStatBlock& block, const CstNodeMap& cstNodeMap) +{ + StringWriter writer; + Printer(writer, cstNodeMap).visualizeBlock(block); + return writer.str(); +} + +std::string prettyPrint(AstStatBlock& block) +{ + return prettyPrint(block, CstNodeMap{}); +} + +std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap) +{ + StringWriter writer; + Printer printer(writer, cstNodeMap); + printer.writeTypes = true; + printer.visualizeBlock(block); + return writer.str(); +} + +std::string prettyPrintWithTypes(AstStatBlock& block) +{ + return prettyPrintWithTypes(block, CstNodeMap{}); +} + +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options, bool withTypes, bool ignoreParseErrors) +{ + options.storeCstData = true; + + auto allocator = Allocator{}; + auto names = AstNameTable{allocator}; + ParseResult parseResult = Parser::parse(source.data(), source.size(), names, allocator, std::move(options)); + + if (!parseResult.errors.empty() && !ignoreParseErrors) + { + // PrettyPrintResult keeps track of only a single error + const ParseError& error = parseResult.errors.front(); + + return PrettyPrintResult{"", error.getLocation(), error.what()}; + } + + LUAU_ASSERT(parseResult.root); + if (!parseResult.root) + return PrettyPrintResult{"", {}, "Internal error: Parser yielded empty parse tree"}; + + if (withTypes) + return PrettyPrintResult{prettyPrintWithTypes(*parseResult.root, parseResult.cstNodeMap)}; + + return PrettyPrintResult{prettyPrint(*parseResult.root, parseResult.cstNodeMap)}; +} + +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeBuilder.h b/External/luau/Bytecode/include/Luau/BytecodeBuilder.h new file mode 100644 index 00000000..8347bd82 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeBuilder.h @@ -0,0 +1,418 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" +#include "Luau/DenseHash.h" +#include "Luau/StringUtils.h" + +#include + +namespace Luau +{ + +static const uint32_t kMaxRegisterCount = 255; +static const uint32_t kMaxUpvalueCount = 200; +static const uint32_t kMaxLocalCount = 200; +static const uint32_t kMaxInstructionCount = 1'000'000'000; + +static const uint8_t kInvalidReg = 255; + +static const uint32_t kMaxConstantCount = 1 << 23; +static const uint32_t kMaxClosureCount = 1 << 15; + +static const int kMaxJumpDistance = 1 << 23; + +class BytecodeEncoder +{ +public: + virtual ~BytecodeEncoder() {} + + virtual void encode(uint32_t* data, size_t count) = 0; +}; + +class BytecodeBuilder +{ +public: + // BytecodeBuilder does *not* copy the data passed via StringRef; instead, it keeps the ref around until finalize() + // Please be careful with the lifetime of the data that's being passed because of this. + // The safe and correct pattern is to only build StringRefs out of pieces of AST (AstName or AstArray<>) that are backed by AstAllocator. + // Note that you must finalize() the builder before the Allocator backing the Ast is destroyed. + struct StringRef + { + // To construct a StringRef, use sref() from Compiler.cpp. + const char* data = nullptr; + size_t length = 0; + + bool operator==(const StringRef& other) const; + }; + + struct TableShape + { + static const unsigned int kMaxLength = 32; + + int32_t keys[kMaxLength]; + // constants are indices that correspond to the proto constant table + // if a key does not have an associated constant to fill in, it has a sentinel value of -1 + int32_t constants[kMaxLength]; + unsigned int length = 0; + bool hasConstants = false; + + bool operator==(const TableShape& other) const; + }; + + struct ClassShape + { + int32_t className; + std::vector propertyNames; + std::vector methodNames; + }; + + BytecodeBuilder(BytecodeEncoder* encoder = 0); + virtual ~BytecodeBuilder() = default; + + uint32_t beginFunction(uint8_t numparams, bool isvararg = false); + void endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags = 0, uint64_t cost = 0); + + void setMainFunction(uint32_t fid); + + int32_t addConstantNil(); + int32_t addConstantBoolean(bool value); + int32_t addConstantNumber(double value); + int32_t addConstantInteger(int64_t value); + int32_t addConstantVectorf(float x, float y, float z, float w); + int32_t addConstantVectord(double x, double y, double z, double w); + int32_t addConstantString(StringRef value); + int32_t addImport(uint32_t iid); + int32_t addConstantTable(const TableShape& shape); + int32_t addConstantClosure(uint32_t fid); + + uint32_t addFbSlot_DEPRECATED(LuauFeedbackType t); + uint32_t addFbSlot_DEPRECATED(LuauFeedbackType t, uint32_t pc); + uint32_t addCallTargetSlot(uint32_t pc); + + int16_t addChildFunction(uint32_t fid); + int32_t addClassShape(ClassShape shape); + + void emitABC(LuauOpcode op, uint8_t a, uint8_t b, uint8_t c); + void emitAD(LuauOpcode op, uint8_t a, int16_t d); + void emitE(LuauOpcode op, int32_t e); + void emitAux(uint32_t aux); + + void undoEmit(LuauOpcode op); + unsigned lastInstruction(); + + size_t emitLabel(); + + [[nodiscard]] bool patchJumpD(size_t jumpLabel, size_t targetLabel); + [[nodiscard]] bool patchSkipC(size_t jumpLabel, size_t targetLabel); + + void patchAux(size_t targetAux, int32_t newValue); + + void foldJumps(); + std::vector expandJumps(bool& hasLongJumpError); + + void setFunctionTypeInfo(std::string value); + void pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc); + void pushUpvalTypeInfo(LuauBytecodeType type); + + uint32_t addUserdataType(const char* name); + void useUserdataType(uint32_t index); + + void setDebugFunctionName(StringRef name); + void setDebugFunctionLineDefined(int line); + void setDebugLine(int line); + void pushDebugLocal(StringRef name, uint8_t reg, uint32_t startpc, uint32_t endpc); + void pushDebugUpval(StringRef name); + + size_t getInstructionCount() const; + size_t getTotalInstructionCount() const; + uint32_t getDebugPC() const; + + void addDebugRemark(const char* format, ...) LUAU_PRINTF_ATTR(2, 3); + + void finalize(); + + enum DumpFlags + { + Dump_Code = 1 << 0, + Dump_Lines = 1 << 1, + Dump_Source = 1 << 2, + Dump_Locals = 1 << 3, + Dump_Remarks = 1 << 4, + Dump_Types = 1 << 5, + Dump_Constants = 1 << 6, + }; + + void setDumpFlags(uint32_t flags) + { + dumpFlags = flags; + dumpFunctionPtr = &BytecodeBuilder::dumpCurrentFunction; + } + + void setDumpSource(const std::string& source); + + bool needsDebugRemarks() const + { + return (dumpFlags & Dump_Remarks) != 0; + } + + const std::string& getBytecode() const + { + LUAU_ASSERT(!bytecode.empty()); // did you forget to call finalize? + return bytecode; + } + + std::string dumpFunction(uint32_t id) const; + std::string dumpEverything() const; + std::string dumpSourceRemarks() const; + std::string dumpTypeInfo() const; + + std::string getFunctionData(uint32_t id) + { + return functions[id].data; + } + + uint32_t getFunctionCount() const + { + return static_cast(functions.size()); + } + + std::vector getStringTable(); + + void annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const; + + void clearStrings() + { + debugStrings.clear(); + stringTable.clear(); + } + + static uint32_t getImportId(int32_t id0); + static uint32_t getImportId(int32_t id0, int32_t id1); + static uint32_t getImportId(int32_t id0, int32_t id1, int32_t id2); + + static int decomposeImportId(uint32_t ids, int32_t& id0, int32_t& id1, int32_t& id2); + + static uint32_t getStringHash(StringRef key); + + static std::string getError(const std::string& message); + + static uint8_t getVersion(); + static uint8_t getTypeEncodingVersion(); + +protected: + struct Constant + { + enum Type + { + Type_Nil, + Type_Boolean, + Type_Number, + Type_Integer, + Type_Vectorf, + Type_Vectord, + Type_String, + Type_Import, + Type_Table, + Type_Closure, + Type_ClassShape, + }; + + Type type; + union + { + bool valueBoolean; + double valueNumber; + int64_t valueInteger64; + float valueVectorf[4]; + double valueVectord[4]; + unsigned int valueString; // index into string table + uint32_t valueImport; // 10-10-10-2 encoded import id + uint32_t valueTable; // index into tableShapes[] + uint32_t valueClosure; // index of function in global list + uint32_t valueClassShape; // index into classShapes[] + }; + }; + + struct ConstantKey + { + Constant::Type type; + // Note: this stores value* from Constant; when type is Type_Number, this stores the same bits as double does but in uint64_t. + // For Type_Vectorf, x and y are stored in 'value' and z and w are stored in 'extra1'. + // For Type_Vectord, x is stored in 'value', y, z and w are stored in 'extra1/2/3' accordingly. + uint64_t value; + uint64_t extra1 = 0; + uint64_t extra2 = 0; + uint64_t extra3 = 0; + + bool operator==(const ConstantKey& key) const + { + return type == key.type && value == key.value && extra1 == key.extra1 && extra2 == key.extra2 && extra3 == key.extra3; + } + }; + + struct Function + { + std::string data; + + uint8_t maxstacksize = 0; + uint8_t numparams = 0; + uint8_t numupvalues = 0; + bool isvararg = false; + + unsigned int debugname = 0; + int debuglinedefined = 0; + + std::string dump; + std::string dumpname; + std::vector dumpinstoffs; + std::string typeinfo; + }; + + struct DebugLocal + { + unsigned int name; + + uint8_t reg; + uint32_t startpc; + uint32_t endpc; + }; + + struct DebugUpval + { + unsigned int name; + }; + + struct TypedLocal + { + LuauBytecodeType type; + uint8_t reg; + uint32_t startpc; + uint32_t endpc; + }; + + struct TypedUpval + { + LuauBytecodeType type; + }; + + struct UserdataType + { + std::string name; + uint32_t nameRef = 0; + bool used = false; + }; + + struct Jump + { + uint32_t source; + uint32_t target; + }; + + struct StringRefHash + { + size_t operator()(const StringRef& v) const; + }; + + struct ConstantKeyHash + { + size_t operator()(const ConstantKey& key) const; + }; + + struct TableShapeHash + { + size_t operator()(const TableShape& v) const; + }; + + std::vector functions; + uint32_t currentFunction = ~0u; + uint32_t mainFunction = ~0u; + + size_t totalInstructionCount = 0; + std::vector insns; + std::vector lines; + std::vector constants; + std::vector protos; + std::vector jumps; + + std::vector tableShapes; + std::vector classShapes; + + struct FeedbackSlot + { + LuauFeedbackType kind; + + union + { + struct + { + uint32_t pc; + } callTarget; + }; + }; + + std::vector fbSlots; + std::vector fbSlots_DEPRECATED; + + bool hasLongJumps = false; + + DenseHashMap constantMap; + DenseHashMap tableShapeMap; + DenseHashMap protoMap; + + int debugLine = 0; + + std::vector debugLocals; + std::vector debugUpvals; + + std::vector typedLocals; + std::vector typedUpvals; + + std::vector userdataTypes; + + DenseHashMap stringTable; + std::vector debugStrings; + + std::vector> debugRemarks; + std::string debugRemarkBuffer; + + BytecodeEncoder* encoder = nullptr; + std::string bytecode; + + uint32_t dumpFlags = 0; + std::vector dumpSource; + std::vector> dumpRemarks; + + std::string tempTypeInfo; + + std::string (BytecodeBuilder::*dumpFunctionPtr)(std::vector&) const = nullptr; + + void validate() const; + void validateInstructions() const; + void validateVariadic() const; + void validateCaptures() const; + virtual void validateConst(int32_t cid) const; + virtual void validateConst(int32_t cid, Constant::Type constType) const; + virtual uint8_t validateProto(int32_t pid) const; + virtual uint8_t validateClosure(int32_t cid) const; + + std::string dumpCurrentFunction(std::vector& dumpinstoffs) const; + virtual void dumpConstant(std::string& result, int k, bool detailed) const; + void dumpInstruction(const uint32_t* opcode, std::string& result, int targetLabel) const; + + int calcLinesSpan() const; + void fillBaselineInfo(int span, int* baseline, size_t baselineSize) const; + + void writeFunction(std::string& ss, uint32_t id, uint8_t flags, uint64_t cost); + void writeLineInfo(std::string& ss) const; + void writeStringTable(std::string& ss) const; + void writeClassShape(std::string& ss, const ClassShape& cs) const; + + int32_t addConstant(const ConstantKey& key, const Constant& value); + unsigned int addStringTableEntry(StringRef value); + + const char* tryGetUserdataTypeName(LuauBytecodeType type) const; + void clearState(); +}; + +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeCallInliner.h b/External/luau/Bytecode/include/Luau/BytecodeCallInliner.h new file mode 100644 index 00000000..85a82b61 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeCallInliner.h @@ -0,0 +1,872 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeOps.h" +#include "Luau/DenseHash.h" + +#include +#include +#include + +namespace Luau +{ +namespace Bytecode +{ + +// Inliner limit is conservatively lower to avoid instructions like namecall that effectively use R(A+1) and CALL/RETURN interpreting 255 as -1 +constexpr uint8_t kMaxInlinerCombinedStackSize = 250; + +template +struct CallInliner +{ + BcFunction& caller; + BcFunction& target; + BcCallFB call; + std::vector callParams; + Reg targetReg; + + uint32_t callerBlocksSizeBeforeInline = 0; + uint32_t callerInstSizeBeforeInline = 0; + uint32_t callerVmConstSizeBeforeInline = 0; + uint32_t callerProtoSizeBeforeInline = 0; + uint32_t callerUpValSizeBeforeInline = 0; + uint32_t callerFeedbackSlotSizeBeforeInline = 0; + + std::vector returnOps; + std::unordered_set callProjections; + std::unordered_map, BcOpHash> varArgMoves; + std::vector> returnSites; + // memoizes target-phi -> caller-phi so a target phi referenced both in a block's phi list and as + // another phi's operand maps to a single caller phi. Without this, the operand reference would get + // its own unanchored duplicate that SCCP never visits (it only visits phis listed in a block) + DenseHashMap mappedPhis; + + CallInliner(BcFunction& caller, BcFunction& target, BcOp callOp) + : caller(caller) + , target(target) + , call(caller.template as>(callOp)) + , callParams(call.params()) + , targetReg(call.getOutReg()) + { + } + + bool hasEdge(const BcEdges& edges, BcBlockEdgeKind kind) + { + for (auto& e : edges) + if (e.kind == kind) + return true; + return false; + } + + void addSuccessor(BcRef from, BcRef to, BcBlockEdgeKind kind) + { + LUAU_ASSERT( + kind != BcBlockEdgeKind::Fallthrough || + (!hasEdge(from->successors, BcBlockEdgeKind::Fallthrough) && !hasEdge(to->predecessors, BcBlockEdgeKind::Fallthrough)) + ); + from->successors.push_back({kind, to.op}); + to->predecessors.push_back({kind, from.op}); + } + + std::pair, BcRef> splitBlockOnOp(BcOp splitOp) + { + // This function splits a block with an instruction in 3 parts: + // prevBlock - retains all ops before splitOp. it is original block to keep all existing + // references to block unaffected. Fallstrhough to insnsBlock. + // insnBlock - freashly created block containing only splitOp. Fallstrhough to nextBlock. + // nextBlock - optionally created if there are any instruction after splitOp. If new block is created + // all successor/predcessor relations are migrated to the new block. + BcRef insn = caller.inst(splitOp); + LUAU_ASSERT(insn->block.kind == BcOpKind::Block); + BcRef prevBlock = caller.block(insn->block); + LUAU_ASSERT(std::find(prevBlock->ops.begin(), prevBlock->ops.end(), splitOp) != prevBlock->ops.end()); + BcRef insnBlock = caller.block(caller.addBlock()); + insnBlock->sortkey = prevBlock->sortkey; + insnBlock->chainkey = prevBlock->chainkey + 1; + + BcRef nextBlock = caller.block(caller.addBlock()); + nextBlock->sortkey = insnBlock->sortkey; + nextBlock->chainkey = insnBlock->chainkey + 1; + while (prevBlock->ops.back() != splitOp) + { + BcRef insn = caller.inst(prevBlock->ops.back()); + prevBlock->ops.pop_back(); + nextBlock->ops.push_front(insn.op); + insn->block = nextBlock.op; + } + // Make all successors to point to next block. + for (BcBlockEdge& e : prevBlock->successors) + { + BcRef succ = caller.block(e.target); + for (BcBlockEdge& pred : succ->predecessors) + if (pred.target == prevBlock.op) + pred.target = nextBlock.op; + } + nextBlock->successors = prevBlock->successors; + prevBlock->successors.clear(); + + addSuccessor(prevBlock, insnBlock, BcBlockEdgeKind::Fallthrough); + addSuccessor(insnBlock, nextBlock, BcBlockEdgeKind::Fallthrough); + + LUAU_ASSERT(prevBlock->ops.back() == splitOp); + prevBlock->ops.pop_back(); + insnBlock->ops.push_back(splitOp); + caller.instOp(splitOp).block = insnBlock.op; + + return {prevBlock, nextBlock}; + } + + BcOp replaceNamecall(BcNamecall namecall, BcRef& prevBlock) + { + // move LOP_NAMECALL to call block + namecall.prependTo(call->block); + prevBlock->ops.pop_back(); + LUAU_ASSERT(targetReg == namecall.getOutReg()); + Reg tableReg = namecall.getOutReg() + 1; + + // and replace it with LOP_MOVE + LOP_GETTABLEKS + BcMove move = BcMove::create(caller); + move.setSrc(namecall.Table()); + move.setOutReg(tableReg); + move.appendTo(prevBlock.op); + + // GETTABLEKS can clobber original source register of NAMECALL and put a function closure there + // But MOVE target already has a table at this point. + namecall.setTable(move.op()); + + BcGetTableKS getTableKS = BcGetTableKS::create(caller); + getTableKS.setSource(move.op()); + getTableKS.setHint(namecall.Hint()); + getTableKS.setKey(namecall.Key().op.index); + getTableKS.setOutReg(targetReg); + getTableKS.appendTo(prevBlock.op); + + return getTableKS.op(); + } + + void appendCmpProto(BcRef& prevBlock, BcOp targetOp, uint32_t targetProtoId) + { + BcCmpProto cmpProto = BcCmpProto::create(caller); + cmpProto.setClosure(targetOp); + cmpProto.setProtoId(targetProtoId); + cmpProto.setFallback(call->block); + cmpProto.appendTo(prevBlock.op); + addSuccessor(prevBlock, caller.block(call->block), BcBlockEdgeKind::Branch); + } + + void allocateBlocks() + { + callerBlocksSizeBeforeInline = uint32_t(caller.blocks.size()); + caller.blocks.resize(callerBlocksSizeBeforeInline + target.blocks.size()); + } + + BcOp mapBlockOp(BcOp targetBlock) + { + LUAU_ASSERT(targetBlock.kind == BcOpKind::Block); + return BcOp{BcOpKind::Block, callerBlocksSizeBeforeInline + targetBlock.index}; + } + + void allocateInstructions() + { + callerInstSizeBeforeInline = uint32_t(caller.instructions.size()); + caller.instructions.resize(callerInstSizeBeforeInline + target.instructions.size()); + } + + uint32_t mapInstIndex(uint32_t index) + { + return callerInstSizeBeforeInline + index; + } + + BcOp mapInstOp(BcOp targetInst) + { + LUAU_ASSERT(targetInst.kind == BcOpKind::Inst); + return BcOp{BcOpKind::Inst, mapInstIndex(targetInst.index)}; + } + + uint32_t mapVmConstIndex(uint32_t index) + { + return callerVmConstSizeBeforeInline + index; + } + + BcOp mapVmConstOp(BcOp targetVmConst) + { + LUAU_ASSERT(targetVmConst.kind == BcOpKind::VmConst); + return BcOp{BcOpKind::VmConst, mapVmConstIndex(targetVmConst.index)}; + } + + void allocateVmConsts() + { + callerVmConstSizeBeforeInline = uint32_t(caller.constants.size()); + caller.constants.reserve(callerVmConstSizeBeforeInline + target.constants.size()); + for (auto& c : target.constants) + caller.constants.push_back(c); + } + + void allocateFeedbackSlots() + { + callerFeedbackSlotSizeBeforeInline = uint32_t(caller.feedbackSlots.size()); + caller.feedbackSlots.reserve(callerFeedbackSlotSizeBeforeInline + target.feedbackSlots.size()); + + for (BcFeedbackSlot slot : target.feedbackSlots) + { + if (slot.kind == LFT_CALLTARGET) + slot.callTarget.inst = mapInstIndex(slot.callTarget.inst); + else + LUAU_ASSERT(!"unknown feedback slot kind"); + + caller.feedbackSlots.push_back(slot); + } + } + + void allocateProtos() + { + callerProtoSizeBeforeInline = uint32_t(caller.protos.size()); + caller.protos.reserve(callerProtoSizeBeforeInline + target.protos.size()); + for (auto p : target.protos) + caller.protos.push_back(p); + } + + BcOp mapProtoOp(BcOp targetProtoOp) + { + LUAU_ASSERT(targetProtoOp.kind == BcOpKind::VmProto); + return BcOp{BcOpKind::VmProto, callerProtoSizeBeforeInline + targetProtoOp.index}; + } + + void allocateUpValues() + { + callerUpValSizeBeforeInline = caller.nups; + caller.nups += target.nups; + } + + BcOp mapUpValueOp(BcOp targetUpval) + { + LUAU_ASSERT(targetUpval.kind == BcOpKind::VmUpvalue); + return BcOp{BcOpKind::VmUpvalue, callerUpValSizeBeforeInline + targetUpval.index}; + } + + void findTargetCallProjections() + { + for (uint32_t i = 0; i < caller.projections.size(); i++) + { + BcProj& proj = caller.projections[i]; + if (proj.op == call.op()) + { + BcOp projOp{BcOpKind::Proj, i}; + if (callProjections.count(projOp) > 0) + continue; + returnOps.resize(proj.index + 1); + BcOp phiOp = caller.addPhi(); + BcRef phi = caller.phi(phiOp); + caller.addUse(phi, projOp); + callProjections.insert(projOp); + returnOps[proj.index] = phiOp; + } + } + } + + void setReturnOp(uint32_t idx, BcOp op) + { + if (idx >= returnOps.size()) + returnOps.resize(idx + 1); + + if (returnOps[idx].kind == BcOpKind::None) + returnOps[idx] = op; + else + { + if (returnOps[idx].kind != BcOpKind::Phi) + { + BcOp phiOp = caller.addPhi(); + BcRef phi = caller.phi(phiOp); + caller.addUse(phi, returnOps[idx]); + returnOps[idx] = phiOp; + } + else + { + BcRef phi = caller.phi(returnOps[idx]); + bool exists = false; + for (auto phiOp : phi->ops) + if (phiOp == op) + { + exists = true; + break; + } + + if (!exists) + caller.addUse(phi, op); + } + } + } + + void replaceReturn(BcRef& nextBlock, BcOp callerBlockOp, BcOp targetReturnOp) + { + BcRef callerBlock = caller.block(callerBlockOp); + BcReturn ret = target.template as>(targetReturnOp); + // multi-value returns are rejected by migrateBlocks before collecting the site + LUAU_ASSERT(ret.ReturnCount() >= 0); + std::vector values = ret.values(); + uint32_t i = 0; + for (; i < values.size(); i++) + { + BcMove move = BcMove::create(caller); + move.setSrc(mapToCallerOp(values[i])); + move.setOutReg(targetReg + i); + move.appendTo(callerBlockOp); + setReturnOp(i, move.op()); + } + int callRes = call.ReturnCount(); + LUAU_ASSERT(callRes >= 0); + for (; i < static_cast(callRes); i++) + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(targetReg + i); + loadNil.appendTo(callerBlockOp); + setReturnOp(i, loadNil.op()); + } + + callerBlock->successors.push_back({BcBlockEdgeKind::Fallthrough, nextBlock.op}); + nextBlock->predecessors.push_back({BcBlockEdgeKind::Fallthrough, callerBlockOp}); + } + + void replaceGetVarArg(BcOp callerBlockOp, BcOp targetGetVarArgsOp) + { + BcGetVarArgs getVarArgs = target.template as>(targetGetVarArgsOp); + int count = getVarArgs.ValuesCount(); + if (count < 0) + count = std::max(0, int(callParams.size() - target.numparams)); + std::vector moves; + for (int i = 0; i < count; i++) + { + if (static_cast(target.numparams + i) < callParams.size()) + { + BcMove move = BcMove::create(caller); + move.setSrc(callParams[target.numparams + i]); + move.setOutReg(mapToCallerReg(getVarArgs.startReg() + i)); + move.appendTo(callerBlockOp); + moves.push_back(move.op()); + } + else + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(mapToCallerReg(getVarArgs.startReg() + i)); + loadNil.appendTo(callerBlockOp); + moves.push_back(loadNil.op()); + } + } + varArgMoves[targetGetVarArgsOp] = moves; + } + + BcOp getVarArgParam(BcGetVarArgs& getVarArgs, uint32_t idx) + { + LUAU_ASSERT(varArgMoves.count(getVarArgs.op()) > 0 && idx < varArgMoves[getVarArgs.op()].size()); + return varArgMoves[getVarArgs.op()][idx]; + } + + bool migrateBlocks(BcRef& nextBlock) + { + BcRef callBlock = caller.block(call->block); + uint32_t insnBlockSortKey = callBlock->sortkey; + uint32_t insnBlockChainKey = callBlock->chainkey; + uint32_t maxChainKey = 0; + for (uint32_t i = 0; i < target.blocks.size(); i++) + { + BcBlock& targetBlock = target.blocks[i]; + BcBlock& callerBlock = caller.blocks[callerBlocksSizeBeforeInline + i]; + BcOp callerBlockOp = BcOp{BcOpKind::Block, callerBlocksSizeBeforeInline + i}; + + if (i == target.exitBlock.index) + { + // it is old exit block + callerBlock.sortkey = kBlockNoStartPc; + callerBlock.flags |= BcBlockFlag::Dead; + continue; + } + callerBlock.sortkey = insnBlockSortKey; + callerBlock.chainkey = insnBlockChainKey + targetBlock.sortkey; + maxChainKey = std::max(callerBlock.chainkey, maxChainKey); + for (auto& e : targetBlock.successors) + if (e.target != target.exitBlock) + callerBlock.successors.push_back({e.kind, mapBlockOp(e.target)}); + + for (auto& e : targetBlock.predecessors) + callerBlock.predecessors.push_back({e.kind, mapBlockOp(e.target)}); + + for (auto op : targetBlock.ops) + { + BcInst& inst = target.instOp(op); + if (inst.op == LOP_GETVARARGS) + { + replaceGetVarArg(callerBlockOp, op); + } + else if (inst.op == LOP_RETURN) + { + if (target.template as>(op).ReturnCount() < 0) + return false; + returnSites.push_back({callerBlockOp, op}); + } + else if (inst.op != LOP_PREPVARARGS) + { + BcOp callerInstOp = mapInstOp(op); + callerBlock.appendInstruction(callerInstOp); + caller.instOp(callerInstOp).block = callerBlockOp; + } + } + } + callBlock->chainkey = maxChainKey + 1; + nextBlock->chainkey = maxChainKey + 2; + return true; + } + + BcOp mapToCallerOp(BcOp targetOp) + { + switch (targetOp.kind) + { + case BcOpKind::Inst: + return mapInstOp(targetOp); + case BcOpKind::Block: + return mapBlockOp(targetOp); + case BcOpKind::Imm: + { + caller.immediates.push_back(target.immOp(targetOp)); + return BcOp{BcOpKind::Imm, static_cast(caller.immediates.size() - 1)}; + } + case BcOpKind::Phi: + { + // memoize before recursing so a phi that (transitively) references itself, as loop-carried + // phis do, resolves to the same caller phi instead of recursing forever + if (auto it = mappedPhis.find(targetOp); it != nullptr) + return *it; + + BcOp callerPhiOp = caller.addPhi(); + mappedPhis[targetOp] = callerPhiOp; + + // loop-carried phis have a register we need to preserve as well + if (auto it = target.regs.find(targetOp); it != target.regs.end()) + { + caller.regs[callerPhiOp] = mapToCallerReg(it->second); + } + + BcRef targetPhi = target.phi(targetOp); + for (uint32_t i = 0; i < targetPhi->ops.size(); i++) + { + BcOp targetPhiOp = targetPhi->ops[i]; + BcOp mapped = mapToCallerOp(targetPhiOp); + BcRef callerPhi = caller.phi(callerPhiOp); + caller.addUse(callerPhi, mapped); + } + + return callerPhiOp; + } + case BcOpKind::Proj: + { + BcRef proj = target.proj(targetOp); + if (target.is_vararg) + { + BcRef inst = target.inst(proj->op); + if (inst->op == LOP_GETVARARGS) + { + BcGetVarArgs getVarArgs = BcGetVarArgs::from(target, inst); + LUAU_ASSERT(getVarArgs.ValuesCount() >= 0); + return getVarArgParam(getVarArgs, proj->index); + } + } + return caller.addProj(mapToCallerOp(proj->op), proj->index); + } + case BcOpKind::VmReg: + if (targetOp.index < target.numparams) + { + // it is an argument for target. we can find it in the call's inputs + LUAU_ASSERT(targetOp.index < callParams.size()); + return callParams[targetOp.index]; + } + else + { + return BcOp{BcOpKind::VmReg, static_cast(mapToCallerReg(targetOp.index))}; + } + case BcOpKind::VmConst: + return mapVmConstOp(targetOp); + case BcOpKind::VmProto: + return mapProtoOp(targetOp); + case BcOpKind::VmUpvalue: + return mapUpValueOp(targetOp); + default: + return targetOp; + } + } + + Reg mapToCallerReg(Reg reg) + { + Reg callerReg = targetReg + 1 + (target.is_vararg ? static_cast(callParams.size()) : 0) + reg; + LUAU_ASSERT(callerReg < caller.maxstacksize); + return callerReg; + } + + bool isMultiConsumer(BcFunction& graph, BcRef& inst) + { + switch (inst->op) + { + case LOP_SETLIST: + return BcSetList::from(graph, inst).Count() < 0; + case LOP_RETURN: + return BcReturn::from(graph, inst).ReturnCount() < 0; + case LOP_CALLFB: + return BcCallFB::from(graph, inst).ParamCount() < 0; + case LOP_CALL: + return BcCall::from(graph, inst).ParamCount() < 0; + default: + return false; + } + } + + void makeFixedConsumer(BcFunction& graph, BcRef& inst) + { + switch (inst->op) + { + case LOP_SETLIST: + { + auto setList = BcSetList::from(graph, inst); + uint32_t count = static_cast(setList.params().size()); + if (count == 0) + setList.detach(); + else + setList.setCount(count); + break; + } + case LOP_RETURN: + { + auto ret = BcReturn::from(graph, inst); + ret.setReturnCount(static_cast(ret.values().size())); + break; + } + case LOP_CALLFB: + { + auto callFb = BcCallFB::from(graph, inst); + callFb.setParamCount(static_cast(callFb.params().size())); + break; + } + case LOP_CALL: + { + auto call = BcCall::from(graph, inst); + call.setParamCount(static_cast(call.params().size())); + break; + } + default: + LUAU_UNREACHABLE(); + } + } + + bool isGetVarArg(BcOp targetOp) + { + if (targetOp.kind != BcOpKind::Inst) + return false; + BcRef inst = target.inst(targetOp); + return inst->op == LOP_GETVARARGS; + } + + void migrateBlockPhis() + { + // This pass cannot be joined with migrateBlocks. + // The reason is phi can refer GETVARARGS instruction's projection. + // mapToCallerOp tries to map it to corresponding MOVE or LOADNIL, + // but they are only materialized during migrateBlocks pass. + for (uint32_t i = 0; i < target.blocks.size(); i++) + { + BcBlock& targetBlock = target.blocks[i]; + BcBlock& callerBlock = caller.blocks[callerBlocksSizeBeforeInline + i]; + + for (auto phiOp : targetBlock.phis) + { + BcOp callerPhiOp = mapToCallerOp(phiOp); + callerBlock.phis.push_back(callerPhiOp); + } + } + } + + void migrateInstructions() + { + for (uint32_t i = 0; i < target.instructions.size(); i++) + { + BcOp targetInsnOp{BcOpKind::Inst, i}; + BcOp callerInsnOp{BcOpKind::Inst, callerInstSizeBeforeInline + i}; + BcRef targetInst = target.inst(targetInsnOp); + BcRef callerInst = caller.inst(callerInsnOp); + + if (targetInst->op == LOP_RETURN || targetInst->op == LOP_GETVARARGS || targetInst->op == LOP_PREPVARARGS) + continue; + + callerInst->op = targetInst->op; + callerInst->block = mapBlockOp(targetInst->block); + callerInst->line = call->line; + + if (target.is_vararg && isMultiConsumer(target, targetInst) && isGetVarArg(targetInst->ops.back())) + { + for (BcOp inp : targetInst->ops) + { + if (inp != targetInst->ops.back()) + caller.addUse(callerInst, mapToCallerOp(inp)); + else + { + LUAU_ASSERT(varArgMoves.count(inp) > 0); + std::vector& moves = varArgMoves[inp]; + for (BcOp move : moves) + caller.addUse(callerInst, move); + } + } + makeFixedConsumer(caller, callerInst); + } + else + { + for (BcOp inp : targetInst->ops) + caller.addUse(callerInst, mapToCallerOp(inp)); + } + if (auto it = target.regs.find(targetInsnOp); it != target.regs.end()) + caller.regs[callerInsnOp] = mapToCallerReg(it->second); + // Instructions with special migration handling. + switch (callerInst->op) + { + case LOP_CALLFB: + { + // Feedback slots are concatenated in optimized version: caller's slots + target's slots. + // So all target's slot should be increased by caller's slots count. + BcCallFB fbcall = BcCallFB::from(caller, callerInst); + + // do not migrate sealed fbcalls + if (fbcall.FbSlot() != -1) + fbcall.setFbSlot(fbcall.FbSlot() + callerFeedbackSlotSizeBeforeInline); + + break; + } + default: + break; + } + } + } + + void replaceCallUsagesInOps(BcOp consumer, BcOps& ops) + { + // It is safe to assume the call instruction is always referred as a projection, + // because inlining of only fixed return size calls are supported and parsers + // always emits projections for fixed return calls. + // If multireturn calls inlining will be supported in the future, it should account + // for bare call replacements as well. + for (BcOp& op : ops) + if (auto it = callProjections.find(op); it != callProjections.end()) + { + BcProj& proj = caller.projOp(*it); + LUAU_ASSERT(proj.index < returnOps.size()); + // the projection operand is rewritten in place, so the `ops` edge already exists; + // only the return def's reverse `uses` edge needs to be recorded + op = returnOps[proj.index]; + caller.recordUse(op, consumer); + } + } + + void replaceCallUsagesWithReturnPhis() + { + for (uint32_t i = 0; i < callerInstSizeBeforeInline; i++) + replaceCallUsagesInOps(BcOp{BcOpKind::Inst, i}, caller.instructions[i].ops); + + for (uint32_t i = 0; i < caller.phis.size(); i++) + if (std::find(returnOps.begin(), returnOps.end(), BcOp{BcOpKind::Phi, i}) == returnOps.end()) + replaceCallUsagesInOps(BcOp{BcOpKind::Phi, i}, caller.phis[i].ops); + } + + void allocateGraphEntitiesForTarget() + { + allocateBlocks(); + allocateInstructions(); + allocateVmConsts(); + allocateFeedbackSlots(); + allocateProtos(); + allocateUpValues(); + } + + void setFallthrough(BcEdges& edges, BcOp entry) + { + for (auto& e : edges) + if (e.kind == BcBlockEdgeKind::Fallthrough) + { + e.target = entry; + return; + } + edges.push_back({BcBlockEdgeKind::Fallthrough, entry}); + } + + void fillUnderCallArguments() + { + if (callParams.size() >= target.numparams) + return; + + BcOp inlineEntryBlock = mapBlockOp(target.entryBlock); + size_t callParamSize = callParams.size(); + callParams.resize(target.numparams); + for (Reg param = target.numparams; param > callParamSize; param--) + { + BcLoadNil loadNil = BcLoadNil::create(caller); + loadNil.setOutReg(targetReg + param); + loadNil.prependTo(inlineEntryBlock); + callParams[param - 1] = loadNil.op(); + } + } + + bool inlineTarget(uint32_t targetProtoId) + { + LUAU_ASSERT(validate()); + uint32_t newMaxStackSize = static_cast(caller.maxstacksize) + static_cast(target.maxstacksize); + + if (target.is_vararg) + // If there are less call arguments than target.numparams it will be filled with nils. + newMaxStackSize += std::max(uint8_t(callParams.size()), target.numparams); + + if (newMaxStackSize >= kMaxInlinerCombinedStackSize) + return false; + + if (call.ParamCount() < 0 || call.ReturnCount() < 0) + return false; + + // inlining of upvalues is not supported yet + if (target.nups > 0) + return false; + + caller.maxstacksize = newMaxStackSize; + + auto [prevBlock, nextBlock] = splitBlockOnOp(call.op()); + + BcOp targetOp = call.Target(); + if (prevBlock->ops.size() > 0) + { + auto lastInst = caller.inst(prevBlock->ops.back()); + if (lastInst->op == LOP_NAMECALL) + targetOp = replaceNamecall(caller.template as>(lastInst.op), prevBlock); + } + + appendCmpProto(prevBlock, targetOp, targetProtoId); + + // Seal FB slot of inlined call. + call.setFbSlot(-1); + + allocateGraphEntitiesForTarget(); + + fillUnderCallArguments(); + + findTargetCallProjections(); + + if (!migrateBlocks(nextBlock)) + return false; + + BcRef callerInlinedEntry = caller.block(mapBlockOp(target.entryBlock)); + + // Remove prevBlock fallthrough to call block from its predecessors + BcEdges& insnPreds = caller.block(call->block)->predecessors; + insnPreds.resize( + unsigned( + std::remove_if( + insnPreds.begin(), + insnPreds.end(), + [prevBlock = prevBlock](BcBlockEdge& p) + { + return p.kind == BcBlockEdgeKind::Fallthrough && p.target == prevBlock.op; + } + ) - + insnPreds.begin() + ) + ); + + setFallthrough(prevBlock->successors, callerInlinedEntry.op); + setFallthrough(callerInlinedEntry->predecessors, prevBlock.op); + + migrateBlockPhis(); + + for (auto& [callerBlockOp, targetReturnOp] : returnSites) + replaceReturn(nextBlock, callerBlockOp, targetReturnOp); + + migrateInstructions(); + + replaceCallUsagesWithReturnPhis(); + + for (BcOp retOp : returnOps) + if (retOp.kind == BcOpKind::Phi) + nextBlock->phis.push_back(retOp); + + LUAU_ASSERT(validate()); + + return true; + } + + bool validate() const + { + if (!validateCfg()) + return false; + if (!validatePhis()) + return false; + return true; + } + + bool validatePhis() const + { + for (BcPhi& phi : caller.phis) + for (BcOp op : phi.ops) + LUAU_ASSERT(op.kind == BcOpKind::Inst || op.kind == BcOpKind::VmReg || op.kind == BcOpKind::Proj || op.kind == BcOpKind::Phi); + return true; + } + + bool validateCfg() const + { + auto validateEdges = [&](uint32_t from, const BcEdges& edges, const BcEdges BcBlock::* mirrorDir) -> bool + { + for (const BcBlockEdge& edge : edges) + { + // In-range + if (edge.target.kind != BcOpKind::Block || edge.target.index >= caller.blocks.size()) + return false; + + const BcBlock& other = caller.blocks[edge.target.index]; + + // Alive + if ((other.flags & BcBlockFlag::Dead) != 0) + return false; + + // Outgoing edge is mirrored in the target + const BcEdges& mirror = other.*mirrorDir; + if (std::find_if( + mirror.begin(), + mirror.end(), + [&](const BcBlockEdge& e) + { + return e.kind == edge.kind && e.target.kind == BcOpKind::Block && e.target.index == from; + } + ) == mirror.end()) + return false; + } + + return true; + }; + + for (uint32_t i = 0; i < caller.blocks.size(); i++) + { + const BcBlock& block = caller.blocks[i]; + + // Skip dead blocks as they might be in inconsistent state + if ((block.flags & BcBlockFlag::Dead) != 0) + continue; + + if (!validateEdges(i, block.successors, &BcBlock::predecessors)) + return false; + + if (!validateEdges(i, block.predecessors, &BcBlock::successors)) + return false; + } + + return true; + } +}; + +template +bool inlineCall(BcFunction& caller, BcFunction& target, BcOp callOp, uint32_t targetProtoId) +{ + CallInliner inliner(caller, target, callOp); + return inliner.inlineTarget(targetProtoId); +} + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeDump.h b/External/luau/Bytecode/include/Luau/BytecodeDump.h new file mode 100644 index 00000000..5e80f433 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeDump.h @@ -0,0 +1,321 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" + +typedef struct lua_TValue TValue; + +namespace Luau +{ +namespace Bytecode +{ + +LUAU_PRINTF_ATTR(2, 3) +void append(std::string& result, const char* fmt, ...); + +void padToDetailColumn(std::string& result, size_t lineStart); +bool isPrintableStringConstant(const char* str, size_t len); +int decomposeImportId(uint32_t ids, int32_t& id0, int32_t& id1, int32_t& id2); + +const char* getBlockEdgeKindName(BcBlockEdgeKind kind); +const char* getLuauOpcodeName(LuauOpcode cmd); + +struct ToStringContext +{ + std::string& result; + bool includeDetailColumn = true; +}; + +void toString(ToStringContext& ctx, const BcImm& imm); +void toString(ToStringContext& ctx, BcFunction& function, const BcVmConst& data, bool detailed); +void toString(ToStringContext& ctx, BcFunction& function, TValue* data, bool detailed); + +template +void toString(ToStringContext& ctx, BcFunction& function, BcOp op); + +template +void toStringUses(ToStringContext& ctx, BcFunction& function, const std::vector& uses) +{ + bool comma = false; + + for (const BcOp& use : uses) + { + if (comma) + append(ctx.result, ", "); + comma = true; + + toString(ctx, function, use); + } +} + +template +void toStringBlockEdges(ToStringContext& ctx, BcFunction& function, const BcEdges& edges) +{ + bool comma = false; + + for (const BcBlockEdge& edge : edges) + { + if (comma) + append(ctx.result, ", "); + comma = true; + + toString(ctx, function, edge.target); + append(ctx.result, " [%s]", getBlockEdgeKindName(edge.kind)); + } +} + +template +void toStringFunctionHeader(ToStringContext& ctx, BcFunction& function) +{ + if (!function.debugname.empty()) + append(ctx.result, "; function %s(", function.debugname.c_str()); + else + append(ctx.result, "; function("); + + for (int i = 0; i < function.numparams; i++) + { + if (i < int(function.locals.size())) + { + const DebugLocal& local = function.locals[i]; + append(ctx.result, "%s%.*s", i == 0 ? "" : ", ", int(local.varname.length()), local.varname.data()); + } + else + { + append(ctx.result, "%s$arg%d", i == 0 ? "" : ", ", i); + } + } + + if (function.numparams != 0 && function.is_vararg) + append(ctx.result, ", ...)"); + else if (function.is_vararg) + append(ctx.result, "...)"); + else + append(ctx.result, ")"); + + if (function.linedefined != ~0u) + append(ctx.result, " line %u", function.linedefined); + + append(ctx.result, " maxstacksize: %d upvalues: %d flags: %d", function.maxstacksize, function.nups, function.flags); +} +template +void toString(ToStringContext& ctx, BcFunction& function, const BcInst& inst, uint32_t index) +{ + size_t start = ctx.result.size(); + + append(ctx.result, " %%%u = %s", index, getLuauOpcodeName(inst.op)); + + bool comma = false; + + for (const BcOp& op : inst.ops) + { + append(ctx.result, comma ? ", " : " "); + comma = true; + + toString(ctx, function, op); + } + + if (ctx.includeDetailColumn && !inst.uses.empty()) + { + padToDetailColumn(ctx.result, start); + + append(ctx.result, " ; uses: "); + toStringUses(ctx, function, inst.uses); + } +} + +template +void toString(ToStringContext& ctx, BcFunction& function, const BcProj& proj) +{ + toString(ctx, function, proj.op); + append(ctx.result, "[%d]", proj.index); +} + +template +void toString(ToStringContext& ctx, BcFunction& function, const BcPhi& phi, uint32_t index) +{ + size_t start = ctx.result.size(); + + append(ctx.result, " phi.%u = ", index); + + bool comma = false; + + for (const BcOp& op : phi.ops) + { + if (comma) + append(ctx.result, ", "); + comma = true; + + toString(ctx, function, op); + + if (op.kind == BcOpKind::Inst) + { + append(ctx.result, " from "); + + toString(ctx, function, function.instOp(op).block); + } + } + + if (ctx.includeDetailColumn && !phi.uses.empty()) + { + padToDetailColumn(ctx.result, start); + + append(ctx.result, " ; uses: "); + toStringUses(ctx, function, phi.uses); + } +} + +template +void toString(ToStringContext& ctx, BcFunction& function, BcOp op) +{ + switch (op.kind) + { + case BcOpKind::None: + break; + case BcOpKind::Imm: + toString(ctx, *function.imm(op)); + break; + case BcOpKind::Inst: + append(ctx.result, "%%%u", op.index); + break; + case BcOpKind::Block: + append(ctx.result, "bb_%u", op.index); + break; + case BcOpKind::Phi: + append(ctx.result, "phi.%u", op.index); + break; + case BcOpKind::Proj: + toString(ctx, function, function.projOp(op)); + break; + case BcOpKind::VmReg: + append(ctx.result, "R%d", op.index); + break; + case BcOpKind::VmConst: + append(ctx.result, "K%d", op.index); + + append(ctx.result, " ("); + toString(ctx, function, function.constOp(op), false); + append(ctx.result, ")"); + break; + case BcOpKind::VmUpvalue: + append(ctx.result, "U%d", op.index); + break; + case BcOpKind::VmProto: + append(ctx.result, "P%d", op.index); + break; + } +} + +template +void toString(ToStringContext& ctx, BcFunction& function, const BcBlock& block, uint32_t blockIdx) +{ + size_t start = ctx.result.size(); + + append(ctx.result, "bb_%d", blockIdx); + + if (function.entryBlock.index == blockIdx) + append(ctx.result, " (entry)"); + else if (function.exitBlock.index == blockIdx) + append(ctx.result, " (exit)"); + + append(ctx.result, ":"); + + if (ctx.includeDetailColumn && block.startpc != kBlockNoStartPc) + { + padToDetailColumn(ctx.result, start); + append(ctx.result, " ; startpc: %u", block.startpc); + } + + append(ctx.result, "\n"); + + if (!block.predecessors.empty()) + { + append(ctx.result, "; predecessors: "); + toStringBlockEdges(ctx, function, block.predecessors); + append(ctx.result, "\n"); + } + + if (!block.successors.empty()) + { + append(ctx.result, "; successors: "); + toStringBlockEdges(ctx, function, block.successors); + append(ctx.result, "\n"); + } + + for (const BcOp& phi : block.phis) + { + toString(ctx, function, function.phiOp(phi), phi.index); + append(ctx.result, "\n"); + } + + for (const BcOp& op : block.ops) + { + toString(ctx, function, *function.inst(op), op.index); + append(ctx.result, "\n"); + } +} + +template +std::string toString(BcFunction& function, bool includeDetailColumn) +{ + std::string result; + ToStringContext ctx{result, includeDetailColumn}; + + toStringFunctionHeader(ctx, function); + append(ctx.result, "\n"); + + for (uint32_t i = 0; i < function.feedbackSlots.size(); ++i) + { + const BcFeedbackSlot& slot = function.feedbackSlots[i]; + + if (slot.kind == LFT_CALLTARGET) + append(ctx.result, "; feedback slot %u: CALLTARGET %%%u\n", i, slot.callTarget.inst); + else + LUAU_ASSERT(!"unknown feedback slot kind"); + } + + std::vector sortedBlocks; + sortedBlocks.reserve(function.blocks.size()); + for (uint32_t i = 0; i < function.blocks.size(); i++) + { + if ((function.blocks[i].flags & BcBlockFlag::Dead) == 0) + sortedBlocks.push_back(BcOp{BcOpKind::Block, i}); + } + + std::sort( + sortedBlocks.begin(), + sortedBlocks.end(), + [&](BcOp opA, BcOp opB) + { + const BcBlock& a = function.blockOp(opA); + const BcBlock& b = function.blockOp(opB); + + if (a.sortkey == b.sortkey) + return a.chainkey < b.chainkey; + + return a.sortkey < b.sortkey; + } + ); + + for (BcOp op : sortedBlocks) + { + const BcBlock& block = function.blocks[op.index]; + + toString(ctx, function, block, uint32_t(op.index)); + + if (op == function.exitBlock) + continue; + + if (block.ops.empty()) + append(ctx.result, " *empty*\n"); + + append(ctx.result, "\n"); + } + + return result; +} + +std::string dump(BcFunction& function); +std::string dump(BcFunction& function); + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeGraph.h b/External/luau/Bytecode/include/Luau/BytecodeGraph.h new file mode 100644 index 00000000..6fb11823 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeGraph.h @@ -0,0 +1,760 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/DenseHash.h" +#include "Luau/SmallVector.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace Luau +{ +namespace Bytecode +{ + +using Instruction = uint32_t; +using Reg = uint8_t; + +enum class BcOpKind : uint32_t +{ + None, + + // To reference a immediate value + Imm, + + // To reference a result of a previous instruction + Inst, + + // To reference a basic block in control flow + Block, + + // Phi operand + Phi, + + // Projection of multireturn call or variadic arguments + Proj, + + // To reference a VM register + VmReg, + + // To reference a VM constant + VmConst, + + // To reference a VM upvalue + VmUpvalue, + + // To reference a VM upvalue + VmProto, +}; + +struct BcOp +{ + BcOpKind kind : 4; + uint32_t index : 28; + + BcOp() + : kind(BcOpKind::None) + , index(0) + { + } + + BcOp(BcOpKind kind, uint32_t index) + : kind(kind) + , index(index) + { + } + + bool operator==(const BcOp& rhs) const + { + return kind == rhs.kind && index == rhs.index; + } + + bool operator!=(const BcOp& rhs) const + { + return !(*this == rhs); + } +}; + +static_assert(sizeof(BcOp) == 4); + +struct BcOpHash +{ + size_t operator()(const BcOp& p) const + { + size_t res = 0; + memcpy(&res, &p, sizeof(p)); + return res; + } +}; + +using RegMap = std::unordered_map; + +enum class BcImmKind : uint8_t +{ + Boolean, + Int, + Import +}; + +struct BcImm +{ + BcImmKind kind; + + union + { + bool valueBoolean; + int32_t valueInt; + uint32_t valueImport; + }; + + bool operator==(const BcImm& rhs) const + { + if (kind == BcImmKind::Boolean && rhs.kind == BcImmKind::Boolean) + return valueBoolean == rhs.valueBoolean; + else if (kind == BcImmKind::Int && rhs.kind == BcImmKind::Int) + return valueInt == rhs.valueInt; + else if (kind == BcImmKind::Import && rhs.kind == BcImmKind::Import) + return valueImport == rhs.valueImport; + else + return false; + } + + bool operator!=(const BcImm& rhs) const + { + return !(*this == rhs); + } +}; + +enum class BcVmConstKind : uint8_t +{ + Nil, + Boolean, + Number, + Vectorf, + Vectord, + String, + Import, + Table, + Closure, + Integer, + ClassShape +}; + +struct BcVmConst +{ + BcVmConstKind kind; + + union + { + bool valueBoolean; + double valueNumber; + float valueVectorf[4]; + double valueVectord[4]; + std::string_view valueString; + uint32_t valueImport; + uint32_t valueTable; + uint32_t valueClosure; + int64_t valueInteger; + uint32_t valueClassShape; + }; + + BcVmConst() + : kind(BcVmConstKind::Nil) + , valueBoolean(0) + { + } + + bool operator==(const BcVmConst& rhs) const + { + if (kind != rhs.kind) + return false; + + switch (kind) + { + case BcVmConstKind::Nil: + return true; + + case BcVmConstKind::Boolean: + return valueBoolean == rhs.valueBoolean; + + case BcVmConstKind::Number: + return valueNumber == rhs.valueNumber; + + case BcVmConstKind::Vectorf: + return valueVectorf[0] == rhs.valueVectorf[0] && valueVectorf[1] == rhs.valueVectorf[1] && valueVectorf[2] == rhs.valueVectorf[2] && + valueVectorf[3] == rhs.valueVectorf[3]; + + case BcVmConstKind::Vectord: + return valueVectord[0] == rhs.valueVectord[0] && valueVectord[1] == rhs.valueVectord[1] && valueVectord[2] == rhs.valueVectord[2] && + valueVectord[3] == rhs.valueVectord[3]; + + case BcVmConstKind::String: + return valueString == rhs.valueString; + + case BcVmConstKind::Import: + return valueImport == rhs.valueImport; + + case BcVmConstKind::Table: + return valueTable == rhs.valueTable; + + case BcVmConstKind::Closure: + return valueClosure == rhs.valueClosure; + + case BcVmConstKind::Integer: + return valueInteger == rhs.valueInteger; + + case BcVmConstKind::ClassShape: + return valueClassShape == rhs.valueClassShape; + + default: + LUAU_ASSERT(!"Unhandled BcVmConstKind"); + return false; + } + return false; + } + + bool operator!=(const BcVmConst& rhs) const + { + return !(*this == rhs); + } +}; + +using BcOps = SmallVector; + +struct BcInst +{ + LuauOpcode op; + BcOp block; + + // Operands + BcOps ops; + std::vector uses; + + uint32_t lastUse = 0; + uint32_t useCount = 0; + uint32_t line = 0; +}; + +// When IrInst operands are used, current instruction index is often required to track lifetime +inline constexpr uint32_t kInvalidInstIdx = ~0u; + +struct BcInstHash +{ + static const uint32_t m = 0x5bd1e995; + static const int r = 24; + + static uint32_t mix(uint32_t h, uint32_t k) + { + // MurmurHash2 step + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + return h; + } + + static uint32_t mix(uint32_t h, BcOp op) + { + static_assert(sizeof(op) == sizeof(uint32_t)); + uint32_t k; + memcpy(&k, &op, sizeof(op)); + + return mix(h, k); + } + + size_t operator()(const BcInst& key) const + { + // MurmurHash2 unrolled + uint32_t h = 25; + + h = mix(h, uint32_t(key.op)); + for (size_t i = 0; i < 7; i++) + h = mix(h, i < uint32_t(key.ops.size()) ? key.ops[i] : BcOp{}); + + // MurmurHash2 tail + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; + } +}; + +struct BcInstEq +{ + bool operator()(const BcInst& a, const BcInst& b) const + { + if (a.op != b.op || a.ops.size() != b.ops.size()) + return false; + for (size_t i = 0; i < a.ops.size(); i++) + if (a.ops[i] != b.ops[i]) + return false; + return true; + } +}; + +inline constexpr uint32_t kBlockNoStartPc = ~0u; + +enum BcBlockEdgeKind +{ + Branch, + Fallthrough, + Loop +}; + +struct BcBlockEdge +{ + BcBlockEdgeKind kind; + BcOp target; +}; + +using BcEdges = SmallVector; + +enum BcBlockFlag +{ + Dead = 1 << 0 +}; + +struct BcBlock +{ + uint8_t flags = 0; + uint32_t useCount = 0; + + std::list phis; + std::list ops; + BcEdges successors; + BcEdges predecessors; + + uint32_t sortkey = ~0u; + uint32_t chainkey = 0; + + // Bytecode PC position at which the block was generated + uint32_t startpc = kBlockNoStartPc; + + void appendInstruction(BcOp inst) + { + LUAU_ASSERT(inst.kind == BcOpKind::Inst); + ops.push_back(inst); + } +}; + +struct BcPhi +{ + BcOps ops; + std::vector uses; +}; + +struct BcProj +{ + BcOp op; + uint32_t index; +}; + +enum class BcCondition : uint8_t +{ + Equal, + LessEqual, + Less, + NotEqual, + NotLessEqual, + NotLess, +}; + +inline BcCondition opcodeToCondition(LuauOpcode op) +{ + switch (op) + { + case LOP_JUMPIFEQ: + return BcCondition::Equal; + case LOP_JUMPIFLE: + return BcCondition::LessEqual; + case LOP_JUMPIFLT: + return BcCondition::Less; + case LOP_JUMPIFNOTEQ: + return BcCondition::NotEqual; + case LOP_JUMPIFNOTLE: + return BcCondition::NotLessEqual; + case LOP_JUMPIFNOTLT: + return BcCondition::NotLess; + default: + LUAU_ASSERT(!"cannot map this opcode to condition"); + return BcCondition::Equal; + } +} + +struct BcFeedbackSlot +{ + LuauFeedbackType kind; + + union + { + struct + { + uint32_t inst; + } callTarget; + }; +}; + +struct TypedLocal +{ + LuauBytecodeType type; + uint8_t reg; + uint32_t startpc; + uint32_t endpc; +}; + +struct DebugLocal +{ + std::string_view varname; + uint8_t reg; + uint32_t startpc; + uint32_t endpc; +}; + +template +struct BcRef +{ + std::vector& vec; + BcOp op; + + T* operator->() + { + LUAU_ASSERT(op.index < vec.size()); + return &vec[op.index]; + } + + T& operator*() + { + LUAU_ASSERT(op.index < vec.size()); + return vec[op.index]; + } +}; + +template +struct BcFunction +{ + uint8_t maxstacksize = 0; + uint8_t numparams = 0; + uint8_t nups = 0; + bool is_vararg = false; + uint8_t flags = 0; + + std::vector blocks; + std::vector instructions; + std::vector constants; + std::vector immediates; + std::vector phis; + std::vector projections; + std::vector tableShapes; + std::vector classShapes; + std::vector feedbackSlots; + + BcOp entryBlock; + BcOp exitBlock; + + std::string typeInfo; + std::vector upvalueTypes; + std::vector localTypes; + std::vector protos; + + std::string debugname; + uint32_t linedefined = ~0u; + std::vector upvalueNames; + std::vector locals; + + RegMap regs; + + BcOp addBlock() + { + blocks.emplace_back(BcBlock{}); + return BcOp{BcOpKind::Block, static_cast(blocks.size() - 1)}; + } + + void addEdge(BcOp from, BcOp to, BcBlockEdgeKind kind) + { + blockOp(from).successors.push_back({kind, to}); + blockOp(to).predecessors.push_back({kind, from}); + } + + BcOp addInst() + { + instructions.emplace_back(BcInst{}); + return BcOp{BcOpKind::Inst, static_cast(instructions.size() - 1)}; + } + + BcOp addInst(LuauOpcode op, BcOp block, std::initializer_list operands = {}, Reg outReg = kInvalidReg) + { + BcOp instOp = addInst(); + BcRef inst = this->inst(instOp); + inst->op = op; + inst->block = block; + blockOp(block).appendInstruction(instOp); + + for (BcOp operand : operands) + addUse(inst, operand); + + if (outReg != kInvalidReg) + regs[instOp] = outReg; + + return instOp; + } + + BcOp addPhi() + { + phis.emplace_back(BcPhi{}); + return BcOp{BcOpKind::Phi, static_cast(phis.size() - 1)}; + } + + BcOp addPhi(BcOp block, std::initializer_list operands, Reg reg) + { + BcOp phiOp = addPhi(); + BcRef phi = this->phi(phiOp); + blockOp(block).phis.push_back(phiOp); + regs[phiOp] = reg; + + for (BcOp operand : operands) + addUse(phi, operand); + + return phiOp; + } + + BcOp addProj(BcOp op, uint32_t index) + { + projections.emplace_back(BcProj{op, index}); + return BcOp{BcOpKind::Proj, static_cast(projections.size() - 1)}; + } + + BcBlock& blockOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Block); + return blocks[op.index]; + } + + BcInst& instOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + return instructions[op.index]; + } + + BcInst* asInstOp(BcOp op) + { + if (op.kind == BcOpKind::Inst) + return &instructions[op.index]; + + return nullptr; + } + + BcImm& immOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Imm); + return immediates[op.index]; + } + + VmConst& constOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return constants[op.index]; + } + + BcPhi& phiOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Phi); + return phis[op.index]; + } + + BcProj& projOp(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Proj); + return projections[op.index]; + } + + uint32_t getBlockIndex(const BcBlock& block) const + { + // Can only be called with blocks from our vector + LUAU_ASSERT(&block >= blocks.data() && &block <= blocks.data() + blocks.size()); + return uint32_t(&block - blocks.data()); + } + + uint32_t getInstIndex(const BcInst& inst) const + { + // Can only be called with instructions from our vector + LUAU_ASSERT(&inst >= instructions.data() && &inst <= instructions.data() + instructions.size()); + return uint32_t(&inst - instructions.data()); + } + + BcOp addImm(BcImmKind kind) + { + BcImm imm{kind}; + imm.valueInt = 0; + immediates.emplace_back(imm); + return BcOp{BcOpKind::Imm, static_cast(immediates.size() - 1)}; + } + + BcOp addImm(const BcImm& imm) + { + immediates.emplace_back(imm); + return BcOp{BcOpKind::Imm, static_cast(immediates.size() - 1)}; + } + + BcOp addImmBool(bool value) + { + BcImm imm{BcImmKind::Boolean}; + imm.valueBoolean = value; + return addImm(imm); + } + + BcOp addImmInt(int32_t value) + { + BcImm imm{BcImmKind::Int}; + imm.valueInt = value; + return addImm(imm); + } + + BcOp addConst(const VmConst& value) + { + constants.emplace_back(value); + return BcOp{BcOpKind::VmConst, static_cast(constants.size() - 1)}; + } + + BcOp addVmReg(Reg reg) + { + return BcOp{BcOpKind::VmReg, reg}; + } + + BcOp addVmUpvalue(uint32_t upvalue) + { + return BcOp{BcOpKind::VmUpvalue, upvalue}; + } + + BcRef block(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Block); + return {blocks, op}; + } + + BcRef inst(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + return {instructions, op}; + } + + template + T as(BcOp op) + { + BcRef insn = inst(op); + LUAU_ASSERT(insn->op == T::opcode); + + return T{*this, insn}; + } + + BcRef imm(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Imm); + return {immediates, op}; + } + + BcRef phi(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Phi); + return {phis, op}; + } + + BcRef proj(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::Proj); + return {projections, op}; + } + + BcRef vmConst(BcOp op) + { + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return {constants, op}; + } + + void recordUse(BcOp usedOp, BcOp user) + { + if (usedOp.kind == BcOpKind::Inst) + this->instOp(usedOp).uses.push_back(user); + else if (usedOp.kind == BcOpKind::Phi) + this->phiOp(usedOp).uses.push_back(user); + } + + void addUse(BcRef instUser, BcOp usedOp) + { + instUser->ops.push_back(usedOp); + recordUse(usedOp, instUser.op); + } + + void addUse(BcRef phiUser, BcOp usedOp) + { + phiUser->ops.push_back(usedOp); + recordUse(usedOp, phiUser.op); + } + + void eraseUse(BcOp userOp, BcOp usedOp) + { + if (usedOp.kind == BcOpKind::Inst) + { + BcRef usedInst = inst(usedOp); + usedInst->uses.erase(std::remove(usedInst->uses.begin(), usedInst->uses.end(), userOp), usedInst->uses.end()); + } + else if (usedOp.kind == BcOpKind::Phi) + { + BcRef usedPhi = phi(usedOp); + usedPhi->uses.erase(std::remove(usedPhi->uses.begin(), usedPhi->uses.end(), userOp), usedPhi->uses.end()); + } + } + + void eraseOp(BcOp op) + { + BcRef instRef = inst(op); + + for (BcOp usedOp : instRef->ops) + eraseUse(op, usedOp); + instRef->ops.clear(); + + BcRef blockRef = block(instRef->block); + blockRef->ops.erase(std::remove(blockRef->ops.begin(), blockRef->ops.end(), op), blockRef->ops.end()); + } + + // replace the instruction's operands while keeping def->use links consistent + void setOps(BcOp op, BcRef inst, std::initializer_list newOps) + { + for (BcOp oldOp : inst->ops) + eraseUse(op, oldOp); + inst->ops.clear(); + for (BcOp newOp : newOps) + { + inst->ops.push_back(newOp); + recordUse(newOp, op); + } + }; +}; + +using CompTimeBcFunction = BcFunction; + +inline BcOp addVmConstNumber(CompTimeBcFunction& func, double value) +{ + BcVmConst constant; + constant.kind = BcVmConstKind::Number; + constant.valueNumber = value; + return func.addConst(constant); +} + +std::optional fromFunctionBytecode(std::string bytecode, std::vector& strings); +std::string toFunctionBytecode(CompTimeBcFunction& fn); +std::string toFunctionBytecode(BytecodeBuilder& bcb, CompTimeBcFunction& fn); + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeOps.h b/External/luau/Bytecode/include/Luau/BytecodeOps.h new file mode 100644 index 00000000..58576cc0 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeOps.h @@ -0,0 +1,346 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" + +#include + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BcInstHelper +{ + BcFunction& graph; + BcRef inst; + + static T create(BcFunction& graph) + { + BcOp op = graph.addInst(); + BcRef inst = graph.inst(op); + inst->op = T::opcode; + return {graph, inst}; + } + + static T from(BcFunction& graph, BcRef& inst) + { + LUAU_ASSERT(inst->op == T::opcode); + return {graph, inst}; + } + + BcInst* operator->() + { + return &inst.vec[inst.op.index]; + } + + BcOp op() + { + return inst.op; + } + + void prependTo(BcOp block) + { + inst->block = block; + graph.blockOp(block).ops.push_front(inst.op); + } + + void appendTo(BcOp block) + { + inst->block = block; + graph.blockOp(block).appendInstruction(inst.op); + } + + void insertBefore(BcRef op) + { + inst->block = op->block; + BcRef block = graph.block(op->block); + auto it = std::find(block->ops.begin(), block->ops.end(), op.op); + LUAU_ASSERT(it != block->ops.end()); + block->ops.insert(it, inst.op); + } + + void detach() + { + if (inst->block.kind != BcOpKind::Block) + return; + BcRef block = graph.block(inst->block); + block->ops.remove(inst.op); + inst->block = BcOp{}; + } + + void setOutReg(Reg out) + { + graph.regs[inst.op] = out; + } + + Reg getOutReg() + { + auto it = graph.regs.find(inst.op); + LUAU_ASSERT(it != graph.regs.end()); + return it->second; + } + +protected: + int intImmInput(uint32_t inputIdx) + { + LUAU_ASSERT(inputIdx < inst->ops.size()); + BcImm& imm = graph.immOp(inst->ops[inputIdx]); + return imm.valueInt; + } + + void setImmInput(uint32_t inputIdx, int value) + { + if (getBcOp(inputIdx).kind == BcOpKind::None) + inst->ops[inputIdx] = graph.addImm(BcImmKind::Int); + BcImm& imm = graph.immOp(inst->ops[inputIdx]); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + imm.valueInt = value; + } + + BcOp getBcOp(uint32_t inputIdx) + { + if (inputIdx >= inst->ops.size()) + inst->ops.resize(inputIdx + 1); + return inst->ops[inputIdx]; + } + + void setBcOp(uint32_t inputIdx, BcOp op) + { + if (inputIdx >= inst->ops.size()) + inst->ops.resize(inputIdx + 1); + inst->ops[inputIdx] = op; + + if (op.kind == BcOpKind::Inst) + { + BcRef opInst = graph.inst(op); + if (std::find(opInst->uses.begin(), opInst->uses.end(), inst.op) == opInst->uses.end()) + { + opInst->uses.push_back(inst.op); + } + } + else if (op.kind == BcOpKind::Phi) + { + BcRef opPhi = graph.phi(op); + if (std::find(opPhi->uses.begin(), opPhi->uses.end(), inst.op) == opPhi->uses.end()) + { + opPhi->uses.push_back(inst.op); + } + } + } + + BcRef getVmConst(uint32_t inputIdx) + { + BcOp constOp = getBcOp(inputIdx); + LUAU_ASSERT(constOp.kind == BcOpKind::VmConst); + return graph.vmConst(constOp); + } + + void setVmConst(uint32_t inputIdx, uint32_t cid) + { + LUAU_ASSERT(cid < graph.constants.size()); + setBcOp(inputIdx, BcOp{BcOpKind::VmConst, cid}); + } + + BcRef getBlock(uint32_t inputIdx) + { + BcOp blockOp = getBcOp(inputIdx); + LUAU_ASSERT(blockOp.kind == BcOpKind::Block); + return graph.block(blockOp); + } + + std::vector sliceInputs(uint32_t startFrom) + { + BcOps& ops = this->inst->ops; + std::vector result; + result.reserve(ops.size() - startFrom); + for (uint32_t i = startFrom; i < ops.size(); i++) + result.push_back(ops[i]); + return result; + } +}; + +#define INT_IMM(name, idx) \ + static const uint32_t k##name = idx; \ + int name() \ + { \ + return this->intImmInput(idx); \ + } \ + void set##name(int value) \ + { \ + this->setImmInput(idx, value); \ + } + +#define BC_OP(name, idx) \ + static const uint32_t k##name = idx; \ + BcOp name() \ + { \ + return this->getBcOp(idx); \ + } \ + void set##name(BcOp value) \ + { \ + this->setBcOp(idx, value); \ + } + +#define VM_CONST(name, idx) \ + static const uint32_t k##name = idx; \ + BcRef name() \ + { \ + return this->getVmConst(idx); \ + } \ + void set##name(uint32_t cid) \ + { \ + this->setVmConst(idx, cid); \ + } + +#define JUMP_TO(name, idx) \ + static const uint32_t k##name = idx; \ + BcRef name() \ + { \ + return this->getBlock(idx); \ + } \ + void set##name(BcOp block) \ + { \ + this->setBcOp(idx, block); \ + } + +template +struct BcMove : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_MOVE; + BC_OP(Src, 0) +}; + +template +struct BcCall : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CALL; + INT_IMM(ParamCount, 0) + INT_IMM(ReturnCount, 1) + BC_OP(Target, 2) + + static const uint32_t kParamStartInput = 3; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcCallFB : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CALLFB; + INT_IMM(ParamCount, 0) + INT_IMM(ReturnCount, 1) + INT_IMM(FbSlot, 2) + BC_OP(Target, 3) + + static const uint32_t kParamStartInput = 4; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcReturn : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_RETURN; + INT_IMM(ReturnCount, 0) + + static const uint32_t kValuesStartInput = 1; + std::vector values() + { + if (ReturnCount() == 0) + return {}; + return this->sliceInputs(kValuesStartInput); + } +}; + +template +struct BcLoadNil : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_LOADNIL; +}; + +template +struct BcNamecall : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_NAMECALL; + BC_OP(Table, 0) + INT_IMM(Hint, 1) + VM_CONST(Key, 2) +}; + +template +struct BcGetTableKS : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETTABLEKS; + BC_OP(Source, 0) + INT_IMM(Hint, 1) + VM_CONST(Key, 2) +}; + +template +struct BcGetVarArgs : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETVARARGS; + static const uint32_t kStartRegInput = 0; + INT_IMM(ValuesCount, 1) + + Reg startReg() + { + return this->inst->ops[kStartRegInput].index; + } +}; + +template +struct BcJump : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_JUMP; + JUMP_TO(Target, 0) +}; + +template +struct BcCmpProto : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_CMPPROTO; + + BC_OP(Closure, 0) + INT_IMM(ProtoId, 1) + JUMP_TO(Fallback, 2) +}; + +template +struct BcSetList : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_SETLIST; + INT_IMM(StartIndex, 0) + INT_IMM(Count, 1) + BC_OP(Target, 2) + static const uint32_t kParamStartInput = 3; + std::vector params() + { + return this->sliceInputs(kParamStartInput); + } +}; + +template +struct BcGetImport : public BcInstHelper> +{ + static const LuauOpcode opcode = LOP_GETIMPORT; + VM_CONST(Import, 0) + INT_IMM(PathLength, 1) + static const uint32_t kPathStartInput = 2; + std::vector importPath() + { + return this->sliceInputs(kPathStartInput); + } +}; + +#undef INT_IMM +#undef BC_OP + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/BytecodeValidation.h b/External/luau/Bytecode/include/Luau/BytecodeValidation.h new file mode 100644 index 00000000..e5031716 --- /dev/null +++ b/External/luau/Bytecode/include/Luau/BytecodeValidation.h @@ -0,0 +1,69 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" + +#include + +namespace Luau +{ + +namespace Bytecode +{ + +template +inline std::vector& usesOf(BcFunction& fn, const BcOp& def) +{ + if (def.kind == BcOpKind::Inst) + return fn.instOp(def).uses; + else + { + LUAU_ASSERT(def.kind == BcOpKind::Phi); + return fn.phiOp(def).uses; + } +} + +template +inline int countUses(BcFunction& fn, const BcOp& def, const BcOp& consumer) +{ + std::vector& uses = usesOf(fn, def); + return static_cast(std::count(uses.begin(), uses.end(), consumer)); +} + +template +inline bool hasUse(BcFunction& fn, const BcOp& def, const BcOp& consumer) +{ + return countUses(fn, def, consumer) > 0; +} + +// for every operand of every inst/phi in a live block, the referenced def must have that consumer in its `uses` +template +bool verifyUseConsistency(BcFunction& fn) +{ + auto checkOperand = [&](const BcOp& consumer, BcOp operand) + { + if (operand.kind != BcOpKind::Inst && operand.kind != BcOpKind::Phi) + return true; + return hasUse(fn, operand, consumer); + }; + + bool result = true; + for (BcBlock& block : fn.blocks) + { + if ((block.flags & BcBlockFlag::Dead) != 0) + continue; + + for (const BcOp& phiOp : block.phis) + for (const BcOp& operand : fn.phiOp(phiOp).ops) + result &= checkOperand(phiOp, operand); + + for (const BcOp& instOp : block.ops) + for (const BcOp& operand : fn.instOp(instOp).ops) + result &= checkOperand(instOp, operand); + } + return result; +} + +} // namespace Bytecode + +} // namespace Luau diff --git a/External/luau/Bytecode/include/Luau/Sccp.h b/External/luau/Bytecode/include/Luau/Sccp.h new file mode 100644 index 00000000..a0cd89cb --- /dev/null +++ b/External/luau/Bytecode/include/Luau/Sccp.h @@ -0,0 +1,993 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/BytecodeValidation.h" +#include "Luau/VecDeque.h" +#include "Luau/DenseHash.h" + +#include +#include +#include + +#include + +namespace Luau +{ +namespace Bytecode +{ + +template +bool bcCompare(T a, T b, BcCondition cond) +{ + switch (cond) + { + case BcCondition::Equal: + return a == b; + case BcCondition::NotEqual: + return a != b; + case BcCondition::Less: + return a < b; + case BcCondition::NotLess: + return !(a < b); + case BcCondition::LessEqual: + return a <= b; + case BcCondition::NotLessEqual: + return !(a <= b); + default: + LUAU_ASSERT(!"Unsupported condition"); + } + + return false; +} + +inline bool bcCompare(double a, double b, BcCondition cond) +{ + // Note: redundant bool() casts work around invalid MSVC optimization that merges cases in this switch, violating IEEE754 comparison semantics + switch (cond) + { + case BcCondition::Equal: + return a == b; + case BcCondition::NotEqual: + return a != b; + case BcCondition::Less: + return a < b; + case BcCondition::NotLess: + return !bool(a < b); + case BcCondition::LessEqual: + return a <= b; + case BcCondition::NotLessEqual: + return !bool(a <= b); + default: + LUAU_ASSERT(!"Unsupported condition"); + } + + return false; +} + +inline std::optional evaluateNumberBinaryOp(double a, double b, LuauOpcode op) +{ + switch (op) + { + case LuauOpcode::LOP_ADD: + return a + b; + case LuauOpcode::LOP_SUB: + return a - b; + case LuauOpcode::LOP_MUL: + return a * b; + case LuauOpcode::LOP_DIV: + return a / b; + case LuauOpcode::LOP_MOD: + return a - floor(a / b) * b; + case LuauOpcode::LOP_POW: + return pow(a, b); + case LuauOpcode::LOP_IDIV: + return floor(a / b); + default: + return std::nullopt; + } +} + +// SCCP is generic over the constant representation +// Each instantiation inherits VmConstOps with the operations the pass needs to evaluate constants +struct VmConstOps +{ + virtual std::optional evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const = 0; + virtual bool falsey(const BcOp& falseyOp) const = 0; + + virtual bool compare(const BcOp& lhsOp, const BcOp& rhsOp, BcCondition cond) const = 0; + + virtual BcOp makeNil() const = 0; + virtual BcOp makeImmBool(bool value) const = 0; + + // true if the VmConst supports ordering comparisons (number, integer, string) + virtual bool isOrderable(const BcOp& vmConstOp) const = 0; + virtual bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + + // true if operands represent the same value exactly (NaN == NaN is true) + virtual bool fullyequal(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + + // returns std::nullopt if the comparison is not supported + virtual std::optional eq(const BcOp& lhsOp, const BcOp& rhsOp) const = 0; + + virtual bool isNil(const BcOp& op) const = 0; + virtual bool isBoolean(const BcOp& op) const = 0; + virtual bool isNumber(const BcOp& op) const = 0; + + virtual bool asBoolean(const BcOp& op) const = 0; + virtual double asNumber(const BcOp& op) const = 0; + + virtual BcRef asImm(BcOp op) const = 0; + + VmConstOps() = default; + virtual ~VmConstOps() = default; + VmConstOps(const VmConstOps&) = default; + VmConstOps(VmConstOps&&) = delete; + VmConstOps& operator=(const VmConstOps&) = default; + VmConstOps& operator=(VmConstOps&&) = delete; +}; + +struct BcVmConstImpl : public VmConstOps +{ + std::optional evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const override; + bool falsey(const BcOp& falseyOp) const override; + + bool compare(const BcOp& lhsOp, const BcOp& rhsOp, BcCondition cond) const override; + + BcOp makeNil() const override; + BcOp makeImmBool(bool value) const override; + + bool isOrderable(const BcOp& op) const override; + bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const override; + + bool fullyequal(const BcOp& lhsOp, const BcOp& rhsOp) const override; + + std::optional eq(const BcOp& lhsOp, const BcOp& rhsOp) const override; + + bool isNil(const BcOp& op) const override; + bool isBoolean(const BcOp& op) const override; + bool isNumber(const BcOp& op) const override; + + bool asBoolean(const BcOp& op) const override; + double asNumber(const BcOp& op) const override; + + BcRef asImm(BcOp op) const override; + + explicit BcVmConstImpl(BcFunction& func) + : VmConstOps() + , func(func) + { + } + + BcFunction& func; +}; + + +enum class Constness +{ + Undetermined, // lattice top + NotAConstant, // lattice bottom + Constant, +}; + +struct ConstnessLattice +{ + Constness kind = Constness::Undetermined; + std::optional constant = std::nullopt; + + ConstnessLattice() = default; + + ConstnessLattice(Constness kind, BcOp bcOp) + : kind(kind) + , constant(bcOp) + { + LUAU_ASSERT(kind == Constness::Constant); + } + + explicit ConstnessLattice(Constness kind) + : kind(kind) + , constant(std::nullopt) + { + } + + ConstnessLattice merge(VmConstOps* impl, const ConstnessLattice& other) const + { + // Undetermined is lattice top: meeting with it yields the other operand + if (kind == Constness::Undetermined) + return other; + if (other.kind == Constness::Undetermined) + return *this; + // two equal constants meet to themselves; anything else falls to bottom + if (equal(impl, other)) + return *this; + return ConstnessLattice(Constness::NotAConstant); + } + + bool equal(VmConstOps* impl, const ConstnessLattice& other) const + { + if (kind != other.kind) + return false; + if (kind == Constness::Constant) + return impl->fullyequal(constant.value(), other.constant.value()); + return true; + } +}; + +enum class ConditionState +{ + AlwaysFalse, + AlwaysTrue, + Unknown, +}; + +struct JumpTarget +{ + bool dead = false; + BcOp blockOp; + ConditionState condition = ConditionState::Unknown; +}; + +using OpConstness = DenseHashMap; + +struct SccpState +{ + OpConstness opConstness; + + ConstnessLattice operandLattice(const BcOp& op) + { + if (op.kind == BcOpKind::Proj || op.kind == BcOpKind::VmReg || op.kind == BcOpKind::VmUpvalue) + return ConstnessLattice(Constness::NotAConstant); + return opConstness[op]; + } + + // an unresolved condition is bottom if any operand is bottom, else top + Constness unknownConditionConstness(std::initializer_list ops) + { + for (const BcOp& op : ops) + { + ConstnessLattice lat = operandLattice(op); + if (lat.kind == Constness::NotAConstant) + return Constness::NotAConstant; + } + return Constness::Undetermined; + } +}; + +class SccpInterpreter +{ +public: + explicit SccpInterpreter(VmConstOps* impl, SccpState* state) + : impl(impl) + , state(state) + { + } + + ConditionState evaluateCondition(const BcOp& op); + ConditionState evaluateComparisonCondition(LuauOpcode op, const BcOp& lhs, const BcOp& rhs); + ConditionState evaluateXeqkCondition(BcRef inst); + + ConstnessLattice evaluateArith(LuauOpcode op, BcRef instRepr); + ConstnessLattice evaluate(LuauOpcode op, BcRef instRepr); + + +private: + VmConstOps* impl; + SccpState* state; +}; + +template +struct Sccp +{ + BcFunction& func; + VmConstOps* impl; + + SccpState state; + SccpInterpreter interpreter; + + // this maps a block (index) to its predecessors that it was reached from + // if a block is not in this map, it is unreachable + DenseHashMap> blockUses; + + VecDeque flowWorklist; + DenseHashSet flowWorklistSet; + + // when a def's lattice value changes, its uses must be re-evaluated + VecDeque ssaWorklist; + + explicit Sccp(BcFunction& func, VmConstOps* impl) + : func(func) + , impl(impl) + , interpreter(impl, &state) + { + } + + std::optional getFallthrough(BcRef block) + { + std::optional fallthrough; + for (auto& succOp : block->successors) + { + if (succOp.kind == BcBlockEdgeKind::Fallthrough) + { + if (fallthrough) + { + LUAU_ASSERT(!"Multiple fallthroughs"); + return std::nullopt; + } + + fallthrough = succOp.target; + } + } + + return fallthrough; + } + + // builds the target/fallthrough pair for a two-way branch + // targetTakenOnTrue says which edge the jump takes when the condition holds; the untaken edge is marked dead when the condition resolves + // a cond of Unknown leaves both edges live, which also covers branches whose condition the pass never folds (loops, CMPPROTO) + std::vector conditionalTargets(BcRef inst, const BcOp& target, ConditionState cond, bool targetTakenOnTrue) + { + LUAU_ASSERT(target.kind == BcOpKind::Block); + std::optional fallthrough = getFallthrough(func.block(inst->block)); + LUAU_ASSERT(fallthrough); + + bool targetDead = false; + bool fallthroughDead = false; + if (cond == ConditionState::AlwaysTrue) + { + targetDead = !targetTakenOnTrue; + fallthroughDead = targetTakenOnTrue; + } + else if (cond == ConditionState::AlwaysFalse) + { + targetDead = targetTakenOnTrue; + fallthroughDead = !targetTakenOnTrue; + } + + return {{targetDead, target, cond}, {fallthroughDead, *fallthrough, cond}}; + } + + // when the condition is a known constant, the untaken path is marked dead and each target carries its resolved condition + std::vector jumpTargets(BcRef inst) + { + switch (inst->op) + { + case LOP_JUMP: + case LOP_JUMPBACK: + { + const BcOp& targetOp = inst->ops[0]; + LUAU_ASSERT(targetOp.kind == BcOpKind::Block); + return {{false, targetOp, ConditionState::AlwaysTrue}}; + } + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + { + ConditionState cond = interpreter.evaluateCondition(inst->ops[0]); + return conditionalTargets(inst, inst->ops[1], cond, inst->op == LOP_JUMPIF); + } + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + { + ConditionState cond = interpreter.evaluateComparisonCondition(inst->op, inst->ops[0], inst->ops[1]); + return conditionalTargets(inst, inst->ops[2], cond, true); + } + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + { + ConditionState cond = interpreter.evaluateXeqkCondition(inst); + bool negated = func.immOp(inst->ops[1]).valueBoolean; + return conditionalTargets(inst, inst->ops[2], cond, !negated); + } + case LOP_FORNPREP: + case LOP_FORNLOOP: + return conditionalTargets(inst, inst->ops[3], ConditionState::Unknown, true); + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + { + const BcOp& targetOp = inst->ops[3]; + LUAU_ASSERT(targetOp.kind == BcOpKind::Block); + return {{false, targetOp, ConditionState::AlwaysTrue}}; + } + case LOP_FORGLOOP: + // FORGLOOP has two leading imm operands FORGPREP* lack, so its target is ops[5] + return conditionalTargets(inst, inst->ops[5], ConditionState::Unknown, true); + case LOP_CMPPROTO: + return conditionalTargets(inst, inst->ops[2], ConditionState::Unknown, true); + case LOP_JUMPX: + LUAU_ASSERT(!"Should have never parsed this"); + [[fallthrough]]; + default: + return {}; + } + } + + // for each operand, evaluate and merge the lattice with the phi lattice + void visitPhi(BcRef phi) + { + LUAU_ASSERT(phi->ops.size() > 0); + + ConstnessLattice fold; + + for (size_t i = 0; i < phi->ops.size(); i++) + { + BcOp op = phi->ops[i]; + + ConstnessLattice lattice = state.operandLattice(op); + fold = lattice.merge(impl, fold); + } + + const ConstnessLattice& prevLattice = state.opConstness[phi.op]; + if (!fold.equal(impl, prevLattice)) + { + for (BcOp use : phi->uses) + ssaWorklist.push_back(use); + } + state.opConstness[phi.op] = fold; + } + + // evaluate an instruction, comparing against the previous lattice value, and inserting all uses into the SSA worklist if it changed + void visitInst(BcRef inst) + { + // CAPTURE REF can be mutated externally via SETUPVAL + // the SSA graph doesn't model that alias, so mark the source non-constant to avoid folding a stale value + if (inst->op == LOP_CAPTURE && inst->ops.size() >= 2) + { + const BcOp& captureTypeOp = inst->ops[0]; + LUAU_ASSERT(captureTypeOp.kind == BcOpKind::Imm); + const BcImm& captureImm = func.immOp(captureTypeOp); + if (captureImm.kind == BcImmKind::Int && captureImm.valueInt == LCT_REF) + { + const BcOp& srcOp = inst->ops[1]; + ConstnessLattice prev = state.opConstness[srcOp]; + if (prev.kind != Constness::NotAConstant && (srcOp.kind == BcOpKind::Inst || srcOp.kind == BcOpKind::Phi)) + { + state.opConstness[srcOp] = ConstnessLattice(Constness::NotAConstant); + + + for (BcOp use : usesOf(func, srcOp)) + ssaWorklist.push_back(use); + } + } + } + + ConstnessLattice lattice = interpreter.evaluate(inst->op, inst); + const ConstnessLattice& prevLattice = state.opConstness[inst.op]; + + ConstnessLattice newVal = lattice.merge(impl, prevLattice); + if (!newVal.equal(impl, prevLattice)) + { + for (BcOp use : inst->uses) + ssaWorklist.push_back(use); + } + + for (const JumpTarget& target : jumpTargets(inst)) + { + uint32_t blockIdx = func.getBlockIndex(func.blockOp(target.blockOp)); + if (!target.dead) + { + blockUses[blockIdx].insert(inst->block); + if (!flowWorklistSet.find(target.blockOp)) + flowWorklist.push_back(target.blockOp); + } + } + + state.opConstness[inst.op] = newVal; + } + + void propagate() + { + BcRef entryBlock = func.block(func.entryBlock); + BcRef exitBlock = func.block(func.exitBlock); + // the entry block is always live + blockUses[func.getBlockIndex(*entryBlock)].insert(entryBlock.op); + // the exit block is always live per serialization requirements + blockUses[func.getBlockIndex(*exitBlock)].insert(exitBlock.op); + + flowWorklist.push_back(func.entryBlock); + while (!flowWorklist.empty() || !ssaWorklist.empty()) + { + while (!flowWorklist.empty()) + { + BcOp blockOp = flowWorklist.front(); + BcRef block = func.block(blockOp); + + flowWorklist.pop_front(); + if (flowWorklistSet.contains(block.op)) + continue; + + for (BcOp& phiOp : block->phis) + { + LUAU_ASSERT(phiOp.kind == BcOpKind::Phi); + visitPhi(func.phi(phiOp)); + } + + for (BcOp& op : block->ops) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + visitInst(func.inst(op)); + } + + bool blockEndsWithBranch = false; + if (!block->ops.empty() && block->ops.back().kind == BcOpKind::Inst) + blockEndsWithBranch = !jumpTargets(func.inst(block->ops.back())).empty(); + + for (BcBlockEdge succEdge : block->successors) + { + if (succEdge.kind == BcBlockEdgeKind::Fallthrough) + { + uint32_t succIdx = func.getBlockIndex(func.blockOp(succEdge.target)); + // If this block ends with a branch and the fallthrough wasn't already added by visitInst, skip it + if (blockEndsWithBranch && !blockUses[succIdx].contains(block.op)) + continue; + + blockUses[succIdx].insert(block.op); + if (!flowWorklistSet.contains(succEdge.target)) + flowWorklist.push_back(succEdge.target); + } + } + flowWorklistSet.insert(block.op); + } + + while (!ssaWorklist.empty()) + { + + BcOp op = ssaWorklist.front(); + ssaWorklist.pop_front(); + + if (op.kind == BcOpKind::Inst) + visitInst(func.inst(op)); + else if (op.kind == BcOpKind::Phi) + visitPhi(func.phi(op)); + } + } + } + + void replaceOperand(BcRef inst, BcOp oldOp, BcOp newOp) + { + for (BcOp& op : inst->ops) + { + if (op == oldOp) + op = newOp; + } + } + + void replacePhiOperand(BcRef phi, BcOp oldOp, BcOp newOp) + { + for (BcOp& op : phi->ops) + { + if (op == oldOp) + op = newOp; + } + } + + bool isLoadInst(BcOp op) + { + if (op.kind != BcOpKind::Inst) + return false; + LuauOpcode opcode = func.inst(op)->op; + return opcode == LOP_LOADK || opcode == LOP_LOADKX || opcode == LOP_LOADN || opcode == LOP_LOADB || opcode == LOP_LOADNIL; + } + + // rewrite a folded instruction in-place to a load instruction + // op identity is preserved so all existing uses remain valid + void rewriteToLoad(BcOp op, const ConstnessLattice& lattice) + { + BcRef inst = func.inst(op); + for (BcOp& usedOp : inst->ops) + { + func.eraseUse(op, usedOp); + } + + inst->ops.clear(); + + if (lattice.kind == Constness::Constant) + { + BcOp op = lattice.constant.value(); + + if (op.kind == BcOpKind::Imm) + { + BcImm& imm = func.immOp(op); + + if (imm.kind == BcImmKind::Boolean) + inst->op = LOP_LOADB; + else if (imm.kind == BcImmKind::Int) + inst->op = LOP_LOADN; + else + LUAU_ASSERT(!"unknown constant"); + + inst->ops.push_back(op); + } + else if (op.kind == BcOpKind::VmConst) + { + inst->op = LOP_LOADK; + inst->ops.push_back(op); + } + else + { + LUAU_ASSERT(!"unknown constant"); + } + } + } + + void removeDeadEdges(BcRef inst) + { + std::vector targets = jumpTargets(inst); + + BcRef block = func.block(inst->block); + + BcOp liveTarget{}; + bool hasLive = false; + + for (const JumpTarget& target : targets) + { + if (target.dead) + { + BcEdges& succs = block->successors; + unsigned writeIdx = 0; + for (unsigned i = 0; i < succs.size(); i++) + { + if (!(succs[i].target == target.blockOp)) + succs[writeIdx++] = succs[i]; + } + succs.resize(writeIdx); + } + else + { + liveTarget = target.blockOp; + hasLive = true; + } + } + + if (!hasLive) + return; + + // ensure the live target has a fallthrough edge + bool hasFallthrough = false; + for (unsigned i = 0; i < block->successors.size(); i++) + { + if (block->successors[i].target == liveTarget) + { + block->successors[i].kind = BcBlockEdgeKind::Fallthrough; + hasFallthrough = true; + break; + } + } + + if (!hasFallthrough) + block->successors.push_back({BcBlockEdgeKind::Fallthrough, liveTarget}); + } + + void replaceUses() + { + for (auto& [op, lattice] : state.opConstness) + { + if (lattice.kind != Constness::Constant) + continue; + if (op.kind != BcOpKind::Inst) + continue; + if (isLoadInst(op)) + continue; + + BcRef inst = func.inst(op); + LUAU_ASSERT(inst->op != LOP_JUMPX); + + // isJumpD is safe to use here because JUMPX is not parsed by the GraphParser + if (isJumpD(inst->op)) + { + removeDeadEdges(inst); + func.eraseOp(op); + } + else + rewriteToLoad(op, lattice); + } + } + + void simplifyPhis() + { + for (BcOp blockOp : flowWorklistSet) + { + BcRef block = func.block(blockOp); + + for (auto it = block->phis.begin(); it != block->phis.end();) + { + BcOp op = *it; + LUAU_ASSERT(op.kind == BcOpKind::Phi); + + BcRef phi = func.phi(op); + if (phi->ops.empty()) + { + ++it; + continue; + } + + BcOp unique = phi->ops[0]; + bool allSame = true; + for (size_t i = 1; i < phi->ops.size(); i++) + { + if (phi->ops[i] != unique) + { + allSame = false; + break; + } + } + + if (!allSame) + { + ++it; + continue; + } + + for (BcOp use : usesOf(func, op)) + { + if (use.kind == BcOpKind::Inst) + replaceOperand(func.inst(use), op, unique); + else if (use.kind == BcOpKind::Phi) + replacePhiOperand(func.phi(use), op, unique); + + // `unique` can be VmReg(it can be param). + func.recordUse(unique, use); + } + phi->uses.clear(); + + for (BcOp operand : phi->ops) + func.eraseUse(op, operand); + phi->ops.clear(); + + it = block->phis.erase(it); + } + } + } + + void updateBlockUses() + { + // mark dead blocks by forward reachability from entry, not by blockUses + // (which can miss blocks depending on worklist ordering) + DenseHashSet reachable; + std::vector worklist; + + uint32_t entryIdx = func.getBlockIndex(*func.block(func.entryBlock)); + uint32_t exitIdx = func.getBlockIndex(*func.block(func.exitBlock)); + reachable.insert(entryIdx); + reachable.insert(exitIdx); + worklist.push_back(entryIdx); + + while (!worklist.empty()) + { + uint32_t idx = worklist.back(); + worklist.pop_back(); + BcBlock& blk = func.blocks[idx]; + for (const BcBlockEdge& edge : blk.successors) + { + uint32_t succIdx = func.getBlockIndex(func.blockOp(edge.target)); + if (!reachable.contains(succIdx)) + { + reachable.insert(succIdx); + worklist.push_back(succIdx); + } + } + } + + for (BcBlock& block : func.blocks) + { + uint32_t blockidx = func.getBlockIndex(block); + block.useCount = static_cast(blockUses[blockidx].size()); + if (!reachable.contains(blockidx)) + block.flags |= BcBlockFlag::Dead; + } + } + + static std::optional arithToKOpcode(LuauOpcode op) + { + switch (op) + { + case LOP_ADD: + return LOP_ADDK; + case LOP_SUB: + return LOP_SUBK; + case LOP_MUL: + return LOP_MULK; + case LOP_DIV: + return LOP_DIVK; + case LOP_MOD: + return LOP_MODK; + case LOP_POW: + return LOP_POWK; + default: + return std::nullopt; + } + } + + // pure value producers can be removed once nothing references their result + bool isPureProducer(LuauOpcode op) const + { + switch (op) + { + case LOP_LOADK: + case LOP_LOADKX: + case LOP_LOADN: + case LOP_LOADB: + case LOP_LOADNIL: + case LOP_GETUPVAL: + return true; + default: + return false; + } + } + + std::optional registerOf(BcOp op) + { + if (op.kind == BcOpKind::VmReg) + return static_cast(op.index); + if (auto it = func.regs.find(op); it != func.regs.end()) + return it->second; + return std::nullopt; + } + + void eraseDeadProducer(BcOp op) + { + if (op.kind != BcOpKind::Inst) + return; + BcRef inst = func.inst(op); + if (!isPureProducer(inst->op)) + return; + if (!inst->uses.empty()) + return; + func.eraseOp(op); + } + + // values that are known constant used in arithmetic operations can be turned into their KR or RK variants + // sub and div have both KR and RK + void arithToK() + { + for (BcBlock& block : func.blocks) + { + uint32_t blockidx = func.getBlockIndex(block); + if (blockUses[blockidx].empty()) + continue; + + std::vector toErase; + toErase.reserve(block.ops.size()); + + for (auto bcOpIt = block.ops.begin(); bcOpIt != block.ops.end(); ++bcOpIt) + { + BcOp& op = *bcOpIt; + + BcRef inst = func.inst(op); + + std::optional kOpcode = arithToKOpcode(inst->op); + if (!kOpcode || inst->ops.size() != 2) + continue; + + BcOp lhs = inst->ops[0]; + BcOp rhs = inst->ops[1]; + // we can safely assume that, at most, one of these can be a Constant + // if they both were constant, the arith would have been folded + ConstnessLattice lhsLat = state.operandLattice(lhs); + ConstnessLattice rhsLat = state.operandLattice(rhs); + + BcOp nonConstantOp; + ConstnessLattice constantK; + bool rk = false; + + auto isConstNumber = [&](const ConstnessLattice& lat) -> bool + { + // Only VmConst are allowed because we don't want to fold a 'compact' LOADN into Tvalue constant + return lat.kind == Constness::Constant && lat.constant && lat.constant->kind == BcOpKind::VmConst && + impl->isNumber(*lat.constant); + }; + + if (isConstNumber(rhsLat) && lhsLat.kind == Constness::NotAConstant) + { + // can fold this to a K variant + nonConstantOp = lhs; + constantK = rhsLat; + } + else if (isConstNumber(lhsLat) && rhsLat.kind == Constness::NotAConstant) + { + if (inst->op != LOP_ADD && inst->op != LOP_MUL && inst->op != LOP_SUB && inst->op != LOP_DIV) + continue; + + nonConstantOp = rhs; + constantK = lhsLat; + + if (inst->op == LOP_SUB) + { + kOpcode = LOP_SUBRK; + rk = true; + } + else if (inst->op == LOP_DIV) + { + kOpcode = LOP_DIVRK; + rk = true; + } + } + else + { + continue; + } + + BcOp prevConstOperand = (nonConstantOp == lhs) ? rhs : lhs; + + const bool constantIsRhs = (nonConstantOp == lhs); + + // we can do some potential folding here now that we know one operand is constant + // for instance, adds of zero, muls of zero or 1, pows of zero or 1, etc + double valueNumber = impl->asNumber(constantK.constant.value()); + if (valueNumber == 0) + { + if (inst->op == LOP_ADD || (inst->op == LOP_SUB && constantIsRhs)) + { + inst->op = LOP_MOVE; + func.setOps(op, inst, {nonConstantOp}); + } + else if (inst->op == LOP_MUL) + { + inst->op = LOP_LOADN; + BcImm imm{BcImmKind::Int}; + imm.valueInt = 0; + func.setOps(op, inst, {func.addImm(imm)}); + } + else if (inst->op == LOP_POW && constantIsRhs) + { + // x ^ 0 == 1 (0 ^ x is not folded: it is 0 for x != 0) + inst->op = LOP_LOADN; + BcImm imm{BcImmKind::Int}; + imm.valueInt = 1; + func.setOps(op, inst, {func.addImm(imm)}); + } + } + else if (valueNumber == 1) + { + if (inst->op == LOP_MUL || (inst->op == LOP_POW && constantIsRhs) || (inst->op == LOP_DIV && constantIsRhs)) + { + inst->op = LOP_MOVE; + func.setOps(op, inst, {nonConstantOp}); + } + } + else + { + inst->op = *kOpcode; + if (!rk) + func.setOps(op, inst, {nonConstantOp, constantK.constant.value()}); + else + // SUBRK and DIVRK expect B as the constant table index + func.setOps(op, inst, {constantK.constant.value(), nonConstantOp}); + } + + toErase.push_back(prevConstOperand); + } + + for (BcOp op : toErase) + { + eraseDeadProducer(op); + } + } + } + + void rewrite() + { + arithToK(); + replaceUses(); + simplifyPhis(); + updateBlockUses(); + } +}; + +template +void foldConstants(BcFunction& func, VmConstOps& impl) +{ + Sccp sccp(func, &impl); + sccp.propagate(); + sccp.rewrite(); +} + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/src/BytecodeBuilder.cpp b/External/luau/Bytecode/src/BytecodeBuilder.cpp new file mode 100644 index 00000000..94f6c83a --- /dev/null +++ b/External/luau/Bytecode/src/BytecodeBuilder.cpp @@ -0,0 +1,3464 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/BytecodeBuilder.h" + +#include "Luau/BytecodeUtils.h" +#include "Luau/SmallVector.h" +#include "Luau/StringUtils.h" + +#include +#include + +#include +#include + +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAGVARIABLE(LuauCompileUndoEmitAdjust) +LUAU_FASTFLAGVARIABLE(LuauEmitCallFeedback) +LUAU_FASTFLAGVARIABLE(LuauCompileRefactorFeedback) +LUAU_FASTFLAGVARIABLE(LuauVirtualBcBuilder) +LUAU_FASTFLAGVARIABLE(LuauBytecodeCostModel) +LUAU_FLAGVERSION(LuauBytecodeCostModel, 2) +LUAU_FASTFLAGVARIABLE(LuauCompileEmitVectorDouble) +LUAU_FLAGVERSION(LuauCompileEmitVectorDouble, 2) +LUAU_FASTFLAGVARIABLE(LuauCompileFastpcall) +LUAU_FLAGVERSION(LuauCompileFastpcall, 2) + +namespace Luau +{ + +static_assert(LBC_VERSION_TARGET >= LBC_VERSION_MIN && LBC_VERSION_TARGET <= LBC_VERSION_MAX, "Invalid bytecode version setup"); +static_assert(LBC_VERSION_MAX <= 127, "Bytecode version should be 7-bit so that we can extend the serialization to use varint transparently"); + +static int log2(int v) +{ + LUAU_ASSERT(v); + + int r = 0; + + while (v >= (2 << r)) + r++; + + return r; +} + +static int ceillog2(int v) +{ + LUAU_ASSERT(v > 0); + + return v == 1 ? 0 : log2(v - 1) + 1; +} + +static void writeByte(std::string& ss, unsigned char value) +{ + ss.append(reinterpret_cast(&value), sizeof(value)); +} + +static void writeInt(std::string& ss, int value) +{ + ss.append(reinterpret_cast(&value), sizeof(value)); +} + +static void writeFloat(std::string& ss, float value) +{ + ss.append(reinterpret_cast(&value), sizeof(value)); +} + +static void writeDouble(std::string& ss, double value) +{ + ss.append(reinterpret_cast(&value), sizeof(value)); +} + +static void writeVarInt(std::string& ss, uint64_t value) +{ + do + { + writeByte(ss, (value & 127) | ((value > 127) << 7)); + value >>= 7; + } while (value); +} + +bool BytecodeBuilder::StringRef::operator==(const StringRef& other) const +{ + return (data && other.data) ? (length == other.length && memcmp(data, other.data, length) == 0) : (data == other.data); +} + +bool BytecodeBuilder::TableShape::operator==(const TableShape& other) const +{ + bool equal = length == other.length && memcmp(keys, other.keys, length * sizeof(keys[0])) == 0 && hasConstants == other.hasConstants; + + if (hasConstants) + { + equal = equal && memcmp(constants, other.constants, length * sizeof(constants[0])) == 0; + } + + return equal; +} + +size_t BytecodeBuilder::StringRefHash::operator()(const StringRef& v) const +{ + return hashRange(v.data, v.length); +} + +size_t BytecodeBuilder::ConstantKeyHash::operator()(const ConstantKey& key) const +{ + if (key.type == Constant::Type_Vectorf) + { + uint32_t i[4]; + static_assert(sizeof(key.value) + sizeof(key.extra1) == sizeof(i), "Expecting vector to have four 32-bit components"); + memcpy(i, &key.value, sizeof(i)); + + // scramble bits to make sure that integer coordinates have entropy in lower bits + i[0] ^= i[0] >> 17; + i[1] ^= i[1] >> 17; + i[2] ^= i[2] >> 17; + i[3] ^= i[3] >> 17; + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + uint32_t h = (i[0] * 73856093) ^ (i[1] * 19349663) ^ (i[2] * 83492791) ^ (i[3] * 39916801); + + return size_t(h); + } + else if (key.type == Constant::Type_Vectord) + { + uint64_t i[4]; + static_assert( + sizeof(key.value) + sizeof(key.extra1) + sizeof(key.extra2) + sizeof(key.extra3) == sizeof(i), + "Expecting vector to have four 64-bit components" + ); + memcpy(i, &key.value, sizeof(i)); + + // scramble bits to make sure that integer coordinates have entropy in lower bits + i[0] ^= i[0] >> 32; + i[1] ^= i[1] >> 32; + i[2] ^= i[2] >> 32; + i[3] ^= i[3] >> 32; + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + uint32_t h = uint32_t(i[0] * 73856093) ^ uint32_t(i[1] * 19349663) ^ uint32_t(i[2] * 83492791) ^ uint32_t(i[3] * 39916801); + + return size_t(h); + } + else + { + // finalizer from MurmurHash64B + const uint32_t m = 0x5bd1e995; + + uint32_t h1 = uint32_t(key.value); + uint32_t h2 = uint32_t(key.value >> 32) ^ (key.type * m); + + h1 ^= h2 >> 18; + h1 *= m; + h2 ^= h1 >> 22; + h2 *= m; + h1 ^= h2 >> 17; + h1 *= m; + h2 ^= h1 >> 19; + h2 *= m; + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return size_t(h2); + } +} + +size_t BytecodeBuilder::TableShapeHash::operator()(const TableShape& v) const +{ + // FNV-1a inspired hash (note that we feed integers instead of bytes) + uint32_t hash = 2166136261; + + for (size_t i = 0; i < v.length; ++i) + { + hash ^= v.keys[i]; + hash *= 16777619; + + if (v.hasConstants) + { + hash ^= v.constants[i]; + hash *= 16777619; + } + } + + return hash; +} + +BytecodeBuilder::BytecodeBuilder(BytecodeEncoder* encoder) + : encoder(encoder) +{ + LUAU_ASSERT(stringTable.find(StringRef{"", 0}) == nullptr); + + // preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays + insns.reserve(32); + lines.reserve(32); + constants.reserve(16); + protos.reserve(16); + functions.reserve(8); +} + +uint32_t BytecodeBuilder::beginFunction(uint8_t numparams, bool isvararg) +{ + LUAU_ASSERT(currentFunction == ~0u); + + uint32_t id = uint32_t(functions.size()); + + Function func; + func.numparams = numparams; + func.isvararg = isvararg; + + functions.push_back(func); + + currentFunction = id; + + hasLongJumps = false; + debugLine = 0; + + return id; +} + +void BytecodeBuilder::clearState() +{ + insns.clear(); + lines.clear(); + constants.clear(); + protos.clear(); + jumps.clear(); + + if (FFlag::LuauCompileRefactorFeedback) + fbSlots.clear(); + else + fbSlots_DEPRECATED.clear(); + + tableShapes.clear(); + + debugLocals.clear(); + debugUpvals.clear(); + + typedLocals.clear(); + typedUpvals.clear(); + + constantMap.clear(); + tableShapeMap.clear(); + protoMap.clear(); + + debugRemarks.clear(); + debugRemarkBuffer.clear(); +} + +void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uint8_t flags, uint64_t cost) +{ + LUAU_ASSERT(currentFunction != ~0u); + + Function& func = functions[currentFunction]; + + func.maxstacksize = maxstacksize; + func.numupvalues = numupvalues; + +#ifdef LUAU_ASSERTENABLED + validate(); +#endif + + // this call is indirect to make sure we only gain link time dependency on dumpCurrentFunction when needed + if (dumpFunctionPtr) + func.dump = (this->*dumpFunctionPtr)(func.dumpinstoffs); + + // very approximate: 4 bytes per instruction for code, 1 byte for debug line, and 1-2 bytes for aux data like constants plus overhead + func.data.reserve(32 + insns.size() * 7); + + if (encoder) + encoder->encode(insns.data(), insns.size()); + + writeFunction(func.data, currentFunction, flags, cost); + + currentFunction = ~0u; + + totalInstructionCount += insns.size(); + if (FFlag::LuauVirtualBcBuilder) + { + clearState(); + } + else + { + insns.clear(); + lines.clear(); + constants.clear(); + protos.clear(); + jumps.clear(); + + if (FFlag::LuauCompileRefactorFeedback) + fbSlots.clear(); + else + fbSlots_DEPRECATED.clear(); + + tableShapes.clear(); + + debugLocals.clear(); + debugUpvals.clear(); + + typedLocals.clear(); + typedUpvals.clear(); + + constantMap.clear(); + tableShapeMap.clear(); + protoMap.clear(); + + debugRemarks.clear(); + debugRemarkBuffer.clear(); + } +} + +void BytecodeBuilder::setMainFunction(uint32_t fid) +{ + LUAU_ASSERT(fid < functions.size()); + + mainFunction = fid; +} + +int32_t BytecodeBuilder::addConstant(const ConstantKey& key, const Constant& value) +{ + if (int32_t* cache = constantMap.find(key)) + return *cache; + + uint32_t id = uint32_t(constants.size()); + + if (id >= kMaxConstantCount) + return -1; + + constantMap[key] = int32_t(id); + constants.push_back(value); + + return int32_t(id); +} + +unsigned int BytecodeBuilder::addStringTableEntry(StringRef value) +{ + unsigned int& index = stringTable[value]; + + // note: bytecode serialization format uses 1-based table indices, 0 is reserved to mean nil + if (index == 0) + { + index = uint32_t(stringTable.size()); + + if ((dumpFlags & Dump_Code) != 0) + debugStrings.push_back(value); + } + + return index; +} + +const char* BytecodeBuilder::tryGetUserdataTypeName(LuauBytecodeType type) const +{ + unsigned index = unsigned((type & ~LBC_TYPE_OPTIONAL_BIT) - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < userdataTypes.size()) + return userdataTypes[index].name.c_str(); + + return nullptr; +} + +int32_t BytecodeBuilder::addConstantNil() +{ + Constant c = {Constant::Type_Nil}; + + ConstantKey k = {Constant::Type_Nil}; + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantBoolean(bool value) +{ + Constant c = {Constant::Type_Boolean}; + c.valueBoolean = value; + + ConstantKey k = {Constant::Type_Boolean, value}; + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantNumber(double value) +{ + Constant c = {Constant::Type_Number}; + c.valueNumber = value; + + ConstantKey k = {Constant::Type_Number}; + static_assert(sizeof(k.value) == sizeof(value), "Expecting double to be 64-bit"); + memcpy(&k.value, &value, sizeof(value)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantInteger(int64_t value) +{ + Constant c = {Constant::Type_Integer}; + c.valueInteger64 = value; + + ConstantKey k = {Constant::Type_Integer}; + static_assert(sizeof(k.value) == sizeof(value), "Expecting integer to be 64-bit"); + memcpy(&k.value, &value, sizeof(value)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantVectorf(float x, float y, float z, float w) +{ + Constant c = {Constant::Type_Vectorf}; + c.valueVectorf[0] = x; + c.valueVectorf[1] = y; + c.valueVectorf[2] = z; + c.valueVectorf[3] = w; + + ConstantKey k = {Constant::Type_Vectorf}; + static_assert( + sizeof(k.value) == sizeof(x) + sizeof(y) && sizeof(k.extra1) == sizeof(z) + sizeof(w), "Expecting vector to have four 32-bit components" + ); + memcpy(&k.value, &x, sizeof(x)); + memcpy((char*)&k.value + sizeof(x), &y, sizeof(y)); + memcpy(&k.extra1, &z, sizeof(z)); + memcpy((char*)&k.extra1 + sizeof(z), &w, sizeof(w)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantVectord(double x, double y, double z, double w) +{ + Constant c = {Constant::Type_Vectord}; + c.valueVectord[0] = x; + c.valueVectord[1] = y; + c.valueVectord[2] = z; + c.valueVectord[3] = w; + + ConstantKey k = {Constant::Type_Vectord}; + static_assert( + sizeof(k.value) == sizeof(x) && sizeof(k.extra1) == sizeof(y) && sizeof(k.extra2) == sizeof(z) && sizeof(k.extra3) == sizeof(w), + "Expecting vector to have four 64-bit components" + ); + memcpy(&k.value, &x, sizeof(x)); + memcpy(&k.extra1, &y, sizeof(y)); + memcpy(&k.extra2, &z, sizeof(z)); + memcpy(&k.extra3, &w, sizeof(w)); + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantString(StringRef value) +{ + unsigned int index = addStringTableEntry(value); + + Constant c = {Constant::Type_String}; + c.valueString = index; + + ConstantKey k = {Constant::Type_String, index}; + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addImport(uint32_t iid) +{ + Constant c = {Constant::Type_Import}; + c.valueImport = iid; + + ConstantKey k = {Constant::Type_Import, iid}; + + return addConstant(k, c); +} + +int32_t BytecodeBuilder::addConstantTable(const TableShape& shape) +{ + if (int32_t* cache = tableShapeMap.find(shape)) + return *cache; + + uint32_t id = uint32_t(constants.size()); + + if (id >= kMaxConstantCount) + return -1; + + Constant value = {Constant::Type_Table}; + value.valueTable = uint32_t(tableShapes.size()); + + tableShapeMap[shape] = int32_t(id); + tableShapes.push_back(shape); + constants.push_back(value); + + return int32_t(id); +} + +int32_t BytecodeBuilder::addConstantClosure(uint32_t fid) +{ + Constant c = {Constant::Type_Closure}; + c.valueClosure = fid; + + ConstantKey k = {Constant::Type_Closure, fid}; + + return addConstant(k, c); +} + +uint32_t BytecodeBuilder::addFbSlot_DEPRECATED(LuauFeedbackType t) +{ + LUAU_ASSERT(!FFlag::LuauCompileRefactorFeedback); + LUAU_ASSERT(t == LuauFeedbackType::LFT_CALLTARGET); + fbSlots_DEPRECATED.push_back(uint32_t(getInstructionCount())); + return uint32_t(fbSlots_DEPRECATED.size() - 1); +} + +uint32_t BytecodeBuilder::addFbSlot_DEPRECATED(LuauFeedbackType t, uint32_t pc) +{ + LUAU_ASSERT(!FFlag::LuauCompileRefactorFeedback); + LUAU_ASSERT(t == LuauFeedbackType::LFT_CALLTARGET); + fbSlots_DEPRECATED.push_back(pc); + return uint32_t(fbSlots_DEPRECATED.size() - 1); +} + +uint32_t BytecodeBuilder::addCallTargetSlot(uint32_t pc) +{ + LUAU_ASSERT(FFlag::LuauCompileRefactorFeedback); + fbSlots.push_back({}); + fbSlots.back().kind = LFT_CALLTARGET; + fbSlots.back().callTarget.pc = pc; + return uint32_t(fbSlots.size() - 1); +} + +int16_t BytecodeBuilder::addChildFunction(uint32_t fid) +{ + if (int16_t* cache = protoMap.find(fid)) + return *cache; + + uint32_t id = uint32_t(protos.size()); + + if (id >= kMaxClosureCount) + return -1; + + protoMap[fid] = int16_t(id); + protos.push_back(fid); + + return int16_t(id); +} + +int32_t BytecodeBuilder::addClassShape(ClassShape shape) +{ + uint32_t id = uint32_t(constants.size()); + + if (id >= kMaxConstantCount) + return -1; + + Constant c = {Constant::Type_ClassShape}; + + c.valueClassShape = uint32_t(classShapes.size()); + + classShapes.emplace_back(std::move(shape)); + + constants.push_back(c); + + return int32_t(id); +} + +void BytecodeBuilder::emitABC(LuauOpcode op, uint8_t a, uint8_t b, uint8_t c) +{ + uint32_t insn = uint32_t(op) | (a << 8) | (b << 16) | (c << 24); + + insns.push_back(insn); + lines.push_back(debugLine); +} + +void BytecodeBuilder::emitAD(LuauOpcode op, uint8_t a, int16_t d) +{ + uint32_t insn = uint32_t(op) | (a << 8) | (uint16_t(d) << 16); + + insns.push_back(insn); + lines.push_back(debugLine); +} + +void BytecodeBuilder::emitE(LuauOpcode op, int32_t e) +{ + uint32_t insn = uint32_t(op) | (uint32_t(e) << 8); + + insns.push_back(insn); + lines.push_back(debugLine); +} + +void BytecodeBuilder::emitAux(uint32_t aux) +{ + insns.push_back(aux); + lines.push_back(debugLine); +} + +void BytecodeBuilder::undoEmit(LuauOpcode op) +{ + LUAU_ASSERT(!insns.empty()); + LUAU_ASSERT((insns.back() & 0xff) == op); + + // Adjust local ranges referencing this instruction + if (FFlag::LuauCompileUndoEmitAdjust) + { + for (size_t i = 0; i < debugLocals.size();) + { + DebugLocal& l = debugLocals[i]; + + // If live range start has been removed, the local never existed + if (l.startpc == insns.size()) + { + debugLocals.erase(debugLocals.begin() + i); + continue; + } + + if (l.endpc == insns.size()) + l.endpc--; + i++; + } + + for (size_t i = 0; i < typedLocals.size();) + { + TypedLocal& l = typedLocals[i]; + + // If live range start has been removed, the local never existed + if (l.startpc == insns.size()) + { + typedLocals.erase(typedLocals.begin() + i); + continue; + } + + if (l.endpc == insns.size()) + l.endpc--; + i++; + } + } + + // Remove the instruction + insns.pop_back(); + lines.pop_back(); +} + +unsigned BytecodeBuilder::lastInstruction() +{ + LUAU_ASSERT(!insns.empty()); + + return insns.back(); +} + +size_t BytecodeBuilder::emitLabel() +{ + return insns.size(); +} + +bool BytecodeBuilder::patchJumpD(size_t jumpLabel, size_t targetLabel) +{ + LUAU_ASSERT(jumpLabel < insns.size()); + + unsigned int jumpInsn = insns[jumpLabel]; + (void)jumpInsn; + + LUAU_ASSERT(isJumpD(LuauOpcode(LUAU_INSN_OP(jumpInsn)))); + LUAU_ASSERT(LUAU_INSN_D(jumpInsn) == 0); + + LUAU_ASSERT(targetLabel <= insns.size()); + + int offset = int(targetLabel) - int(jumpLabel) - 1; + + if (int16_t(offset) == offset) + { + insns[jumpLabel] |= uint16_t(offset) << 16; + } + else if (abs(offset) < kMaxJumpDistance) + { + // our jump doesn't fit into 16 bits; we will need to repatch the bytecode sequence with jump trampolines, see expandJumps + hasLongJumps = true; + } + else + { + return false; + } + + jumps.push_back({uint32_t(jumpLabel), uint32_t(targetLabel)}); + return true; +} + +bool BytecodeBuilder::patchSkipC(size_t jumpLabel, size_t targetLabel) +{ + LUAU_ASSERT(jumpLabel < insns.size()); + + unsigned int jumpInsn = insns[jumpLabel]; + (void)jumpInsn; + + LUAU_ASSERT(isSkipC(LuauOpcode(LUAU_INSN_OP(jumpInsn))) || isFastCall(LuauOpcode(LUAU_INSN_OP(jumpInsn)))); + LUAU_ASSERT(LUAU_INSN_C(jumpInsn) == 0); + + int offset = int(targetLabel) - int(jumpLabel) - 1; + + if (uint8_t(offset) != offset) + { + return false; + } + + insns[jumpLabel] |= offset << 24; + return true; +} + +void BytecodeBuilder::patchAux(size_t targetAux, int32_t newValue) +{ + LUAU_ASSERT(targetAux < insns.size()); + insns[targetAux] = newValue; +} + +void BytecodeBuilder::setFunctionTypeInfo(std::string value) +{ + functions[currentFunction].typeinfo = std::move(value); +} + +void BytecodeBuilder::pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc) +{ + TypedLocal local; + local.type = type; + local.reg = reg; + local.startpc = startpc; + local.endpc = endpc; + + typedLocals.push_back(local); +} + +void BytecodeBuilder::pushUpvalTypeInfo(LuauBytecodeType type) +{ + TypedUpval upval; + upval.type = type; + + typedUpvals.push_back(upval); +} + +uint32_t BytecodeBuilder::addUserdataType(const char* name) +{ + UserdataType ty; + + ty.name = name; + + userdataTypes.push_back(std::move(ty)); + return uint32_t(userdataTypes.size() - 1); +} + +void BytecodeBuilder::useUserdataType(uint32_t index) +{ + userdataTypes[index].used = true; +} + +void BytecodeBuilder::setDebugFunctionName(StringRef name) +{ + unsigned int index = addStringTableEntry(name); + + functions[currentFunction].debugname = index; + + if (dumpFunctionPtr) + functions[currentFunction].dumpname = std::string(name.data, name.length); +} + +void BytecodeBuilder::setDebugFunctionLineDefined(int line) +{ + functions[currentFunction].debuglinedefined = line; +} + +void BytecodeBuilder::setDebugLine(int line) +{ + debugLine = line; +} + +void BytecodeBuilder::pushDebugLocal(StringRef name, uint8_t reg, uint32_t startpc, uint32_t endpc) +{ + unsigned int index = addStringTableEntry(name); + + DebugLocal local; + local.name = index; + local.reg = reg; + local.startpc = startpc; + local.endpc = endpc; + + debugLocals.push_back(local); +} + +void BytecodeBuilder::pushDebugUpval(StringRef name) +{ + unsigned int index = addStringTableEntry(name); + + DebugUpval upval; + upval.name = index; + + debugUpvals.push_back(upval); +} + +size_t BytecodeBuilder::getInstructionCount() const +{ + return insns.size(); +} + +size_t BytecodeBuilder::getTotalInstructionCount() const +{ + return totalInstructionCount; +} + +uint32_t BytecodeBuilder::getDebugPC() const +{ + return uint32_t(insns.size()); +} + +void BytecodeBuilder::addDebugRemark(const char* format, ...) +{ + if ((dumpFlags & Dump_Remarks) == 0) + return; + + size_t offset = debugRemarkBuffer.size(); + + va_list args; + va_start(args, format); + vformatAppend(debugRemarkBuffer, format, args); + va_end(args); + + // we null-terminate all remarks to avoid storing remark length + debugRemarkBuffer += '\0'; + + debugRemarks.emplace_back(uint32_t(insns.size()), uint32_t(offset)); + dumpRemarks.emplace_back(debugLine, debugRemarkBuffer.c_str() + offset); +} + +void BytecodeBuilder::finalize() +{ + LUAU_ASSERT(bytecode.empty()); + + for (auto& ty : userdataTypes) + { + if (ty.used) + ty.nameRef = addStringTableEntry(StringRef({ty.name.c_str(), ty.name.length()})); + } + + // preallocate space for bytecode blob + size_t capacity = 16; + + for (auto& p : stringTable) + capacity += p.first.length + 2; + + for (const Function& func : functions) + capacity += func.data.size(); + + bytecode.reserve(capacity); + + // assemble final bytecode blob + uint8_t version = getVersion(); + LUAU_ASSERT((version >= LBC_VERSION_MIN && version <= LBC_VERSION_MAX) || version == LBC_VERSION_CLASSES); + + bytecode = char(version); + + uint8_t typesversion = getTypeEncodingVersion(); + LUAU_ASSERT(typesversion >= LBC_TYPE_VERSION_MIN && typesversion <= LBC_TYPE_VERSION_MAX); + writeByte(bytecode, typesversion); + + writeStringTable(bytecode); + + { + // Write the mapping between used type name indices and their name + for (uint32_t i = 0; i < uint32_t(userdataTypes.size()); i++) + { + if (userdataTypes[i].used) + { + writeByte(bytecode, i + 1); + writeVarInt(bytecode, userdataTypes[i].nameRef); + } + } + + // 0 marks the end of the mapping + writeByte(bytecode, 0); + } + + writeVarInt(bytecode, uint32_t(functions.size())); + + for (const Function& func : functions) + { + if (FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::LuauCompileFastpcall || FFlag::DebugLuauUserDefinedClasses) + writeVarInt(bytecode, func.data.size()); + bytecode += func.data; + } + + LUAU_ASSERT(mainFunction < functions.size()); + writeVarInt(bytecode, mainFunction); +} + +void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags, uint64_t cost) +{ + LUAU_ASSERT(id < functions.size()); + const Function& func = functions[id]; + + // header + writeByte(ss, func.maxstacksize); + writeByte(ss, func.numparams); + writeByte(ss, func.numupvalues); + writeByte(ss, func.isvararg); + + writeByte(ss, flags); + + if (!func.typeinfo.empty() || !typedUpvals.empty() || !typedLocals.empty()) + { + // collect type info into a temporary string to know the overall size of type data + tempTypeInfo.clear(); + writeVarInt(tempTypeInfo, uint32_t(func.typeinfo.size())); + writeVarInt(tempTypeInfo, uint32_t(typedUpvals.size())); + writeVarInt(tempTypeInfo, uint32_t(typedLocals.size())); + + tempTypeInfo.append(func.typeinfo); + + for (const TypedUpval& l : typedUpvals) + writeByte(tempTypeInfo, l.type); + + for (const TypedLocal& l : typedLocals) + { + writeByte(tempTypeInfo, l.type); + writeByte(tempTypeInfo, l.reg); + writeVarInt(tempTypeInfo, l.startpc); + LUAU_ASSERT(l.endpc >= l.startpc); + writeVarInt(tempTypeInfo, l.endpc - l.startpc); + } + + writeVarInt(ss, uint32_t(tempTypeInfo.size())); + ss.append(tempTypeInfo); + } + else + { + writeVarInt(ss, 0); + } + + // instructions + writeVarInt(ss, uint32_t(insns.size())); + + for (uint32_t insn : insns) + writeInt(ss, insn); + + // constants + writeVarInt(ss, uint32_t(constants.size())); + + for (const Constant& c : constants) + { + switch (c.type) + { + case Constant::Type_Nil: + writeByte(ss, LBC_CONSTANT_NIL); + break; + + case Constant::Type_Boolean: + writeByte(ss, LBC_CONSTANT_BOOLEAN); + writeByte(ss, c.valueBoolean); + break; + + case Constant::Type_Number: + writeByte(ss, LBC_CONSTANT_NUMBER); + writeDouble(ss, c.valueNumber); + break; + + case Constant::Type_Integer: + writeByte(ss, LBC_CONSTANT_INTEGER); + if (c.valueInteger64 < 0) + { + writeByte(ss, 1); + writeVarInt(ss, ~(uint64_t)c.valueInteger64 + 1); + } + else + { + writeByte(ss, 0); + writeVarInt(ss, c.valueInteger64); + } + break; + + case Constant::Type_Vectorf: + writeByte(ss, LBC_CONSTANT_VECTOR); + writeFloat(ss, c.valueVectorf[0]); + writeFloat(ss, c.valueVectorf[1]); + writeFloat(ss, c.valueVectorf[2]); + writeFloat(ss, c.valueVectorf[3]); + break; + + case Constant::Type_Vectord: + if (FFlag::LuauCompileEmitVectorDouble) + { + writeByte(ss, LBC_CONSTANT_VECTORD); + writeDouble(ss, c.valueVectord[0]); + writeDouble(ss, c.valueVectord[1]); + writeDouble(ss, c.valueVectord[2]); + writeDouble(ss, c.valueVectord[3]); + } + else + { + writeByte(ss, LBC_CONSTANT_VECTOR); + writeFloat(ss, float(c.valueVectord[0])); + writeFloat(ss, float(c.valueVectord[1])); + writeFloat(ss, float(c.valueVectord[2])); + writeFloat(ss, float(c.valueVectord[3])); + } + break; + + case Constant::Type_String: + writeByte(ss, LBC_CONSTANT_STRING); + writeVarInt(ss, c.valueString); + break; + + case Constant::Type_Import: + writeByte(ss, LBC_CONSTANT_IMPORT); + writeInt(ss, c.valueImport); + break; + + case Constant::Type_Table: + { + const TableShape& shape = tableShapes[c.valueTable]; + if (shape.hasConstants) + { + writeByte(ss, LBC_CONSTANT_TABLE_WITH_CONSTANTS); + writeVarInt(ss, uint32_t(shape.length)); + for (unsigned int i = 0; i < shape.length; ++i) + { + writeVarInt(ss, shape.keys[i]); + writeInt(ss, shape.constants[i]); + } + } + else + { + writeByte(ss, LBC_CONSTANT_TABLE); + writeVarInt(ss, uint32_t(shape.length)); + for (unsigned int i = 0; i < shape.length; ++i) + writeVarInt(ss, shape.keys[i]); + } + break; + } + + case Constant::Type_Closure: + writeByte(ss, LBC_CONSTANT_CLOSURE); + writeVarInt(ss, c.valueClosure); + break; + + case Constant::Type_ClassShape: + writeByte(ss, LBC_CONSTANT_CLASS_SHAPE); + writeClassShape(ss, classShapes[c.valueClassShape]); + break; + + + default: + LUAU_ASSERT(!"Unsupported constant type"); + } + } + + // child protos + writeVarInt(ss, uint32_t(protos.size())); + + for (uint32_t child : protos) + writeVarInt(ss, child); + + // debug info + writeVarInt(ss, func.debuglinedefined); + writeVarInt(ss, func.debugname); + + bool hasLines = true; + + for (int line : lines) + if (line == 0) + { + hasLines = false; + break; + } + + if (hasLines) + { + writeByte(ss, 1); + + writeLineInfo(ss); + } + else + { + writeByte(ss, 0); + } + + bool hasDebug = !debugLocals.empty() || !debugUpvals.empty(); + + if (hasDebug) + { + writeByte(ss, 1); + + writeVarInt(ss, uint32_t(debugLocals.size())); + + for (const DebugLocal& l : debugLocals) + { + writeVarInt(ss, l.name); + writeVarInt(ss, l.startpc); + writeVarInt(ss, l.endpc); + writeByte(ss, l.reg); + } + + writeVarInt(ss, uint32_t(debugUpvals.size())); + + for (const DebugUpval& l : debugUpvals) + { + writeVarInt(ss, l.name); + } + } + else + { + writeByte(ss, 0); + } + + if (FFlag::LuauEmitCallFeedback) + { + // Feedback Slots + if (FFlag::LuauCompileRefactorFeedback) + { + writeVarInt(ss, fbSlots.size()); + + for (FeedbackSlot& slot : fbSlots) + { + writeByte(ss, slot.kind); + + if (slot.kind == LFT_CALLTARGET) + writeVarInt(ss, slot.callTarget.pc); + } + } + else + { + writeVarInt(ss, fbSlots_DEPRECATED.size()); + + for (uint32_t pc : fbSlots_DEPRECATED) + { + writeByte(ss, LFT_CALLTARGET); + writeVarInt(ss, pc); + } + } + } + else if (FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::LuauCompileFastpcall || FFlag::DebugLuauUserDefinedClasses) + { + writeVarInt(ss, 0); // Empty feedback vector + } + + if ((FFlag::LuauBytecodeCostModel || FFlag::LuauCompileEmitVectorDouble || FFlag::LuauCompileFastpcall || FFlag::DebugLuauUserDefinedClasses) && + (flags & LPF_INLINABLE) != 0) + { + writeVarInt(ss, cost); + } +} + +void BytecodeBuilder::writeClassShape(std::string& ss, const ClassShape& cs) const +{ + writeVarInt(ss, cs.className); + writeVarInt(ss, cs.propertyNames.size()); + writeVarInt(ss, cs.methodNames.size()); + for (const auto propName : cs.propertyNames) + writeVarInt(ss, propName); + for (const auto methodName : cs.methodNames) + writeVarInt(ss, methodName); +} + +int BytecodeBuilder::calcLinesSpan() const +{ + LUAU_ASSERT(!lines.empty()); + + // this function encodes lines inside each span as a 8-bit delta to span baseline + // span is always a power of two; depending on the line info input, it may need to be as low as 1 + int span = 1 << 24; + + // first pass: determine span length + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + int max = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + { + min = std::min(min, lines[next]); + max = std::max(max, lines[next]); + + if (max - min > 255) + break; + } + + if (next < lines.size() && next - offset < size_t(span)) + { + // since not all lines in the range fit in 8b delta, we need to shrink the span + // next iteration will need to reprocess some lines again since span changed + span = 1 << log2(int(next - offset)); + } + } + return span; +} + +void BytecodeBuilder::fillBaselineInfo(int span, int* baseline, size_t baselineSize) const +{ + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + min = std::min(min, lines[next]); + + baseline[offset / span] = min; + } +} + +void BytecodeBuilder::writeLineInfo(std::string& ss) const +{ + LUAU_ASSERT(!lines.empty()); + + // this function encodes lines inside each span as a 8-bit delta to span baseline + // span is always a power of two; depending on the line info input, it may need to be as low as 1 + int span = 1 << 24; + + // first pass: determine span length + if (FFlag::LuauVirtualBcBuilder) + { + span = calcLinesSpan(); + } + else + { + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + int max = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + { + min = std::min(min, lines[next]); + max = std::max(max, lines[next]); + + if (max - min > 255) + break; + } + + if (next < lines.size() && next - offset < size_t(span)) + { + // since not all lines in the range fit in 8b delta, we need to shrink the span + // next iteration will need to reprocess some lines again since span changed + span = 1 << log2(int(next - offset)); + } + } + } + + // second pass: compute span base + int baselineOne = 0; + std::vector baselineScratch; + int* baseline = &baselineOne; + size_t baselineSize = (lines.size() - 1) / span + 1; + + if (baselineSize > 1) + { + // avoid heap allocation for single-element baseline which is most functions (<256 lines) + baselineScratch.resize(baselineSize); + baseline = baselineScratch.data(); + } + + if (FFlag::LuauVirtualBcBuilder) + { + fillBaselineInfo(span, baseline, baselineSize); + } + else + { + for (size_t offset = 0; offset < lines.size(); offset += span) + { + size_t next = offset; + + int min = lines[offset]; + + for (; next < lines.size() && next < offset + span; ++next) + min = std::min(min, lines[next]); + + baseline[offset / span] = min; + } + } + + // third pass: write resulting data + int logspan = log2(span); + + writeByte(ss, uint8_t(logspan)); + + uint8_t lastOffset = 0; + + for (size_t i = 0; i < lines.size(); ++i) + { + int delta = lines[i] - baseline[i >> logspan]; + LUAU_ASSERT(delta >= 0 && delta <= 255); + + writeByte(ss, uint8_t(delta) - lastOffset); + lastOffset = uint8_t(delta); + } + + int lastLine = 0; + + for (size_t i = 0; i < baselineSize; ++i) + { + writeInt(ss, baseline[i] - lastLine); + lastLine = baseline[i]; + } +} + +void BytecodeBuilder::writeStringTable(std::string& ss) const +{ + std::vector strings(stringTable.size()); + + for (auto& p : stringTable) + { + LUAU_ASSERT(p.second > 0 && p.second <= strings.size()); + strings[p.second - 1] = p.first; + } + + writeVarInt(ss, uint32_t(strings.size())); + + for (auto& s : strings) + { + writeVarInt(ss, uint32_t(s.length)); + ss.append(s.data, s.length); + } +} + +uint32_t BytecodeBuilder::getImportId(int32_t id0) +{ + LUAU_ASSERT(unsigned(id0) < 1024); + + return (1u << 30) | (id0 << 20); +} + +uint32_t BytecodeBuilder::getImportId(int32_t id0, int32_t id1) +{ + LUAU_ASSERT(unsigned(id0 | id1) < 1024); + + return (2u << 30) | (id0 << 20) | (id1 << 10); +} + +uint32_t BytecodeBuilder::getImportId(int32_t id0, int32_t id1, int32_t id2) +{ + LUAU_ASSERT(unsigned(id0 | id1 | id2) < 1024); + + return (3u << 30) | (id0 << 20) | (id1 << 10) | id2; +} + +int BytecodeBuilder::decomposeImportId(uint32_t ids, int32_t& id0, int32_t& id1, int32_t& id2) +{ + int count = ids >> 30; + id0 = count > 0 ? int(ids >> 20) & 1023 : -1; + id1 = count > 1 ? int(ids >> 10) & 1023 : -1; + id2 = count > 2 ? int(ids) & 1023 : -1; + return count; +} + +uint32_t BytecodeBuilder::getStringHash(StringRef key) +{ + // This hashing algorithm should match luaS_hash defined in VM/lstring.cpp for short inputs; we can't use that code directly to keep compiler and + // VM independent in terms of compilation/linking. The resulting string hashes are embedded into bytecode binary and result in a better initial + // guess for the field hashes which improves performance during initial code execution. We omit the long string processing here for simplicity, as + // it doesn't really matter on long identifiers. + const char* str = key.data; + size_t len = key.length; + + unsigned int h = unsigned(len); + + // original Lua 5.1 hash for compatibility (exact match when len<32) + for (size_t i = len; i > 0; --i) + h ^= (h << 5) + (h >> 2) + (uint8_t)str[i - 1]; + + return h; +} + +void BytecodeBuilder::foldJumps() +{ + // if our function has long jumps, some processing below can make jump instructions not-jumps (e.g. JUMP->RETURN) + // it's safer to skip this processing + if (hasLongJumps) + return; + + for (Jump& jump : jumps) + { + uint32_t jumpLabel = jump.source; + + uint32_t jumpInsn = insns[jumpLabel]; + + // follow jump target through forward unconditional jumps + // we only follow forward jumps to make sure the process terminates + uint32_t targetLabel = jumpLabel + 1 + LUAU_INSN_D(jumpInsn); + LUAU_ASSERT(targetLabel < insns.size()); + uint32_t targetInsn = insns[targetLabel]; + + while (LUAU_INSN_OP(targetInsn) == LOP_JUMP && LUAU_INSN_D(targetInsn) >= 0) + { + targetLabel = targetLabel + 1 + LUAU_INSN_D(targetInsn); + LUAU_ASSERT(targetLabel < insns.size()); + targetInsn = insns[targetLabel]; + } + + int offset = int(targetLabel) - int(jumpLabel) - 1; + + // for unconditional jumps to RETURN, we can replace JUMP with RETURN + if (LUAU_INSN_OP(jumpInsn) == LOP_JUMP && LUAU_INSN_OP(targetInsn) == LOP_RETURN) + { + insns[jumpLabel] = targetInsn; + } + else if (int16_t(offset) == offset) + { + insns[jumpLabel] &= 0xffff; + insns[jumpLabel] |= uint16_t(offset) << 16; + } + + jump.target = targetLabel; + } +} + +std::vector BytecodeBuilder::expandJumps(bool& hasLongJumpError) +{ + if (!hasLongJumps) + return {}; + + // we have some jump instructions that couldn't be patched which means their offset didn't fit into 16 bits + // our strategy for replacing instructions is as follows: instead of + // OP jumpoffset + // we will synthesize a jump trampoline before our instruction (note that jump offsets are relative to next instruction): + // JUMP +1 + // JUMPX jumpoffset + // OP -2 + // the idea is that during forward execution, we will jump over JUMPX into OP; if OP decides to jump, it will jump to JUMPX + // JUMPX can carry a 24-bit jump offset + + // jump trampolines expand the code size, which can increase existing jump distances. + // because of this, we may need to expand jumps that previously fit into 16-bit just fine. + // the worst-case expansion is 3x, so to be conservative we will repatch all jumps that have an offset >= 32767/3 + const int kMaxJumpDistanceConservative = 32767 / 3; + + // we will need to process jumps in order + std::sort( + jumps.begin(), + jumps.end(), + [](const Jump& lhs, const Jump& rhs) + { + return lhs.source < rhs.source; + } + ); + + // first, let's add jump thunks for every jump with a distance that's too big + // we will create new instruction buffers, with remap table keeping track of the moves: remap[oldpc] = newpc + std::vector remap(insns.size()); + + std::vector newinsns; + std::vector newlines; + + LUAU_ASSERT(insns.size() == lines.size()); + newinsns.reserve(insns.size()); + newlines.reserve(insns.size()); + + size_t currentJump = 0; + size_t pendingTrampolines = 0; + + for (size_t i = 0; i < insns.size();) + { + uint8_t op = LUAU_INSN_OP(insns[i]); + LUAU_ASSERT(op < LOP__COUNT); + + if (currentJump < jumps.size() && jumps[currentJump].source == i) + { + int offset = int(jumps[currentJump].target) - int(jumps[currentJump].source) - 1; + + if (abs(offset) >= kMaxJumpDistanceConservative) + { + // insert jump trampoline as described above; we keep JUMPX offset uninitialized in this pass + newinsns.push_back(LOP_JUMP | (1 << 16)); + newinsns.push_back(LOP_JUMPX); + + newlines.push_back(lines[i]); + newlines.push_back(lines[i]); + + pendingTrampolines++; + } + + currentJump++; + } + + int oplen = getOpLength(LuauOpcode(op)); + + // copy instruction and line info to the new stream + for (int j = 0; j < oplen; ++j) + { + remap[i] = uint32_t(newinsns.size()); + + newinsns.push_back(insns[i]); + newlines.push_back(lines[i]); + + i++; + } + } + + LUAU_ASSERT(currentJump == jumps.size()); + LUAU_ASSERT(pendingTrampolines > 0); + + // now we need to recompute offsets for jump instructions - we could not do this in the first pass because the offsets are between *target* + // instructions + for (Jump& jump : jumps) + { + int offset = int(jump.target) - int(jump.source) - 1; + int newoffset = int(remap[jump.target]) - int(remap[jump.source]) - 1; + + if (abs(newoffset) + 1 >= kMaxJumpDistance) + { + hasLongJumpError = true; + return {}; + } + else if (abs(offset) >= kMaxJumpDistanceConservative) + { + // fix up jump trampoline + uint32_t& insnt = newinsns[remap[jump.source] - 1]; + uint32_t& insnj = newinsns[remap[jump.source]]; + + LUAU_ASSERT(LUAU_INSN_OP(insnt) == LOP_JUMPX); + + // patch JUMPX to JUMPX to target location; note that newoffset is the offset of the jump *relative to OP*, so we need to add 1 to make it + // relative to JUMPX + insnt &= 0xff; + insnt |= uint32_t(newoffset + 1) << 8; + + // patch OP to OP -2 + insnj &= 0xffff; + insnj |= uint16_t(-2) << 16; + + pendingTrampolines--; + } + else + { + uint32_t& insn = newinsns[remap[jump.source]]; + + // make sure jump instruction had the correct offset before we started + LUAU_ASSERT(LUAU_INSN_D(insn) == offset); + + // patch instruction with the new offset + LUAU_ASSERT(int16_t(newoffset) == newoffset); + + insn &= 0xffff; + insn |= uint16_t(newoffset) << 16; + } + } + + LUAU_ASSERT(pendingTrampolines == 0); + + // this was hard, but we're done. + insns.swap(newinsns); + lines.swap(newlines); + + for (DebugLocal& debugLocal : debugLocals) + { + // endpc is exclusive, to get the right remapping, we need to remap the location before the end + if (debugLocal.startpc != debugLocal.endpc) + debugLocal.endpc = remap[debugLocal.endpc - 1] + 1; + else + debugLocal.endpc = remap[debugLocal.endpc]; + + debugLocal.startpc = remap[debugLocal.startpc]; + } + + for (TypedLocal& typedLocal : typedLocals) + { + // endpc is exclusive, to get the right remapping, we need to remap the location before the end + if (typedLocal.startpc != typedLocal.endpc) + typedLocal.endpc = remap[typedLocal.endpc - 1] + 1; + else + typedLocal.endpc = remap[typedLocal.endpc]; + + typedLocal.startpc = remap[typedLocal.startpc]; + } + + return remap; +} + +std::string BytecodeBuilder::getError(const std::string& message) +{ + // 0 acts as a special marker for error bytecode (it's equal to LBC_VERSION_TARGET for valid bytecode blobs) + std::string result; + result += char(0); + result += message; + + return result; +} + +uint8_t BytecodeBuilder::getVersion() +{ + if (FFlag::DebugLuauUserDefinedClasses) + return LBC_VERSION_CLASSES; + + if (FFlag::LuauCompileFastpcall) + return 14; + if (FFlag::LuauCompileEmitVectorDouble) + return 13; + if (FFlag::LuauBytecodeCostModel) + return 12; + + if (FFlag::LuauEmitCallFeedback) + return 11; + + return LBC_VERSION_TARGET; +} + +uint8_t BytecodeBuilder::getTypeEncodingVersion() +{ + return LBC_TYPE_VERSION_TARGET; +} + +// Virtual functions have to be defined even if LUAU_ASSERTENABLED is off. + +void BytecodeBuilder::validateConst(int32_t cid) const +{ + LUAU_ASSERT(unsigned(cid) < constants.size()); +} + +void BytecodeBuilder::validateConst(int32_t cid, Constant::Type constType) const +{ + LUAU_ASSERT(unsigned(cid) < constants.size() && constants[cid].type == constType); +} + +uint8_t BytecodeBuilder::validateProto(int32_t pid) const +{ + LUAU_ASSERT(unsigned(pid) < protos.size()); + LUAU_ASSERT(protos[pid] < functions.size()); + return functions[protos[pid]].numupvalues; +} + +uint8_t BytecodeBuilder::validateClosure(int32_t cid) const +{ + unsigned int proto = constants[cid].valueClosure; + LUAU_ASSERT(proto < functions.size()); + return functions[proto].numupvalues; +} + +#ifdef LUAU_ASSERTENABLED +void BytecodeBuilder::validate() const +{ + validateInstructions(); + validateVariadic(); + validateCaptures(); +} + +void BytecodeBuilder::validateInstructions() const +{ +#define VREG(v) LUAU_ASSERT(unsigned(v) < func.maxstacksize) +#define VREGRANGE(v, count) LUAU_ASSERT(unsigned(v + (count < 0 ? 0 : count)) <= func.maxstacksize) +#define VUPVAL(v) LUAU_ASSERT(unsigned(v) < func.numupvalues) +#define VCONST(v, kind) \ + FFlag::LuauVirtualBcBuilder ? validateConst(v, Constant::Type_##kind) \ + : LUAU_ASSERT(unsigned(v) < constants.size() && constants[v].type == Constant::Type_##kind) +#define VCONSTANY(v) FFlag::LuauVirtualBcBuilder ? validateConst(v) : LUAU_ASSERT(unsigned(v) < constants.size()) +#define VJUMP(v) LUAU_ASSERT(size_t(i + 1 + v) < insns.size() && insnvalid[i + 1 + v]) + + LUAU_ASSERT(currentFunction != ~0u); + + const Function& func = functions[currentFunction]; + + // tag instruction offsets so that we can validate jumps + std::vector insnvalid(insns.size(), 0); + + for (size_t i = 0; i < insns.size();) + { + uint32_t insn = insns[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + insnvalid[i] = true; + + i += getOpLength(op); + LUAU_ASSERT(i <= insns.size()); + } + + // validate individual instructions + for (size_t i = 0; i < insns.size();) + { + uint32_t insn = insns[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + switch (op) + { + case LOP_LOADNIL: + VREG(LUAU_INSN_A(insn)); + break; + + case LOP_LOADB: + VREG(LUAU_INSN_A(insn)); + LUAU_ASSERT(LUAU_INSN_B(insn) == 0 || LUAU_INSN_B(insn) == 1); + VJUMP(LUAU_INSN_C(insn)); + break; + + case LOP_LOADN: + VREG(LUAU_INSN_A(insn)); + break; + + case LOP_LOADK: + VREG(LUAU_INSN_A(insn)); + VCONSTANY(LUAU_INSN_D(insn)); + break; + + case LOP_MOVE: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + break; + + case LOP_GETGLOBAL: + case LOP_SETGLOBAL: + VREG(LUAU_INSN_A(insn)); + VCONST(insns[i + 1], String); + break; + + case LOP_GETUPVAL: + case LOP_SETUPVAL: + VREG(LUAU_INSN_A(insn)); + VUPVAL(LUAU_INSN_B(insn)); + break; + + case LOP_CLOSEUPVALS: + VREG(LUAU_INSN_A(insn)); + break; + + case LOP_GETIMPORT: + { + VREG(LUAU_INSN_A(insn)); + VCONST(LUAU_INSN_D(insn), Import); + uint32_t id = insns[i + 1]; + LUAU_ASSERT((id >> 30) != 0); // import chain with length 1-3 + for (unsigned int j = 0; j < (id >> 30); ++j) + VCONST((id >> (20 - 10 * j)) & 1023, String); + } + break; + + case LOP_GETTABLE: + case LOP_SETTABLE: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VREG(LUAU_INSN_C(insn)); + break; + + case LOP_GETTABLEKS: + case LOP_SETTABLEKS: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(insns[i + 1], String); + break; + + case LOP_GETTABLEN: + case LOP_SETTABLEN: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + break; + + case LOP_NEWCLOSURE: + { + VREG(LUAU_INSN_A(insn)); + unsigned int numupvalues; + if (FFlag::LuauVirtualBcBuilder) + { + numupvalues = validateProto(LUAU_INSN_D(insn)); + } + else + { + LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < protos.size()); + LUAU_ASSERT(protos[LUAU_INSN_D(insn)] < functions.size()); + numupvalues = functions[protos[LUAU_INSN_D(insn)]].numupvalues; + } + + for (unsigned int j = 0; j < numupvalues; ++j) + { + LUAU_ASSERT(i + 1 + j < insns.size()); + uint32_t cinsn = insns[i + 1 + j]; + LUAU_ASSERT(LUAU_INSN_OP(cinsn) == LOP_CAPTURE); + } + } + break; + + case LOP_NAMECALL: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(insns[i + 1], String); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALLFB || LUAU_INSN_OP(insns[i + 2]) == LOP_CALL); + break; + + case LOP_CALL: + case LOP_CALLFB: + { + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + VREG(LUAU_INSN_A(insn)); + VREGRANGE(LUAU_INSN_A(insn) + 1, nparams); // 1..nparams + VREGRANGE(LUAU_INSN_A(insn), nresults); // 1..nresults + } + break; + + case LOP_RETURN: + { + int nresults = LUAU_INSN_B(insn) - 1; + VREGRANGE(LUAU_INSN_A(insn), nresults); // 0..nresults-1 + } + break; + + case LOP_JUMP: + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + VREG(LUAU_INSN_A(insn)); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + VREG(LUAU_INSN_A(insn)); + VREG(insns[i + 1]); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + VREG(LUAU_INSN_A(insn)); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_JUMPXEQKN: + VREG(LUAU_INSN_A(insn)); + VCONST(insns[i + 1] & 0xffffff, Number); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_JUMPXEQKS: + VREG(LUAU_INSN_A(insn)); + VCONST(insns[i + 1] & 0xffffff, String); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_IDIV: + case LOP_MOD: + case LOP_POW: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VREG(LUAU_INSN_C(insn)); + break; + + case LOP_ADDK: + case LOP_SUBK: + case LOP_MULK: + case LOP_DIVK: + case LOP_IDIVK: + case LOP_MODK: + case LOP_POWK: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(LUAU_INSN_C(insn), Number); + break; + + case LOP_SUBRK: + case LOP_DIVRK: + VREG(LUAU_INSN_A(insn)); + VCONST(LUAU_INSN_B(insn), Number); + VREG(LUAU_INSN_C(insn)); + break; + + case LOP_AND: + case LOP_OR: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VREG(LUAU_INSN_C(insn)); + break; + + case LOP_ANDK: + case LOP_ORK: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONSTANY(LUAU_INSN_C(insn)); + break; + + case LOP_CONCAT: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VREG(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_B(insn) <= LUAU_INSN_C(insn)); + break; + + case LOP_NOT: + case LOP_MINUS: + case LOP_LENGTH: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + break; + + case LOP_NEWTABLE: + VREG(LUAU_INSN_A(insn)); + break; + + case LOP_DUPTABLE: + VREG(LUAU_INSN_A(insn)); + VCONST(LUAU_INSN_D(insn), Table); + break; + + case LOP_SETLIST: + { + int count = LUAU_INSN_C(insn) - 1; + VREG(LUAU_INSN_A(insn)); + VREGRANGE(LUAU_INSN_B(insn), count); + } + break; + + case LOP_FORNPREP: + case LOP_FORNLOOP: + // for loop protocol: A, A+1, A+2 are used for iteration + VREG(LUAU_INSN_A(insn) + 2); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_FORGPREP: + // forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, ... are loop variables + VREG(LUAU_INSN_A(insn) + 2 + 1); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_FORGLOOP: + // forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, ... are loop variables + VREG(LUAU_INSN_A(insn) + 2 + uint8_t(insns[i + 1])); + VJUMP(LUAU_INSN_D(insn)); + LUAU_ASSERT(uint8_t(insns[i + 1]) >= 1); + break; + + case LOP_FORGPREP_INEXT: + case LOP_FORGPREP_NEXT: + VREG(LUAU_INSN_A(insn) + 4); // forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, A+4 are loop variables + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_GETVARARGS: + { + int nresults = LUAU_INSN_B(insn) - 1; + VREGRANGE(LUAU_INSN_A(insn), nresults); // 0..nresults-1 + } + break; + + case LOP_DUPCLOSURE: + { + VREG(LUAU_INSN_A(insn)); + VCONST(LUAU_INSN_D(insn), Closure); + unsigned int numupvalues; + if (FFlag::LuauVirtualBcBuilder) + { + numupvalues = validateClosure(LUAU_INSN_D(insn)); + } + else + { + unsigned int proto = constants[LUAU_INSN_D(insn)].valueClosure; + LUAU_ASSERT(proto < functions.size()); + numupvalues = functions[proto].numupvalues; + } + + for (unsigned int j = 0; j < numupvalues; ++j) + { + LUAU_ASSERT(i + 1 + j < insns.size()); + uint32_t cinsn = insns[i + 1 + j]; + LUAU_ASSERT(LUAU_INSN_OP(cinsn) == LOP_CAPTURE); + LUAU_ASSERT(LUAU_INSN_A(cinsn) == LCT_VAL || LUAU_INSN_A(cinsn) == LCT_UPVAL); + } + } + break; + + case LOP_PREPVARARGS: + LUAU_ASSERT(LUAU_INSN_A(insn) == func.numparams); + LUAU_ASSERT(func.isvararg); + break; + + case LOP_BREAK: + break; + + case LOP_JUMPBACK: + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_LOADKX: + VREG(LUAU_INSN_A(insn)); + VCONSTANY(insns[i + 1]); + break; + + case LOP_JUMPX: + VJUMP(LUAU_INSN_E(insn)); + break; + + case LOP_FASTCALL: + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + break; + + case LOP_FASTCALL1: + VREG(LUAU_INSN_B(insn)); + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + break; + + case LOP_FASTCALL2: + VREG(LUAU_INSN_B(insn)); + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + VREG(insns[i + 1]); + break; + + case LOP_FASTCALL2K: + VREG(LUAU_INSN_B(insn)); + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + VCONSTANY(insns[i + 1]); + break; + + case LOP_FASTCALL3: + VREG(LUAU_INSN_B(insn)); + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + VREG(insns[i + 1] & 0xff); + VREG((insns[i + 1] >> 8) & 0xff); + break; + + case LOP_COVERAGE: + break; + + case LOP_CAPTURE: + switch (LUAU_INSN_A(insn)) + { + case LCT_VAL: + VREG(LUAU_INSN_B(insn)); + break; + + case LCT_REF: + VREG(LUAU_INSN_B(insn)); + break; + + case LCT_UPVAL: + VUPVAL(LUAU_INSN_B(insn)); + break; + + default: + LUAU_ASSERT(!"Unsupported capture type"); + } + break; + + case LOP_NEWCLASSMEMBER: + VREG(LUAU_INSN_A(insn)); + LUAU_ASSERT(LUAU_INSN_B(insn) == 0); + VREG(LUAU_INSN_C(insn)); + VCONST(insns[i + 1], String); + break; + + case LOP_GETUDATAKS: + case LOP_SETUDATAKS: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(LUAU_INSN_AUX_KV16(insns[i + 1]), String); + break; + + case LOP_NAMECALLUDATA: + VREG(LUAU_INSN_A(insn)); + VREG(LUAU_INSN_B(insn)); + VCONST(LUAU_INSN_AUX_KV16(insns[i + 1]), String); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALL || LUAU_INSN_OP(insns[i + 2]) == LOP_CALLFB); + break; + + case LOP_CMPPROTO: + VREG(LUAU_INSN_A(insn)); + VJUMP(LUAU_INSN_D(insn)); + break; + + case LOP_FASTPCALL: + LUAU_ASSERT(LUAU_INSN_A(insn) <= 1); + VJUMP(LUAU_INSN_C(insn)); + LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL); + break; + + case LOP_NEWCLASS: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + VREG(LUAU_INSN_A(insn)); + uint8_t super = LUAU_INSN_B(insn); + LUAU_ASSERT(super == 0xff || (unsigned(super) < func.maxstacksize)); + uint8_t flags = LUAU_INSN_C(insn); + LUAU_ASSERT(flags == 0 || flags == 1); + VCONST(insns[i + 1], ClassShape); + } + break; + + default: + LUAU_ASSERT(!"Unsupported opcode"); + } + + i += getOpLength(op); + LUAU_ASSERT(i <= insns.size()); + } + +#undef VREG +#undef VREGEND +#undef VUPVAL +#undef VCONST +#undef VCONSTANY +#undef VJUMP +} + +void BytecodeBuilder::validateVariadic() const +{ + // validate MULTRET sequences: instructions that produce a variadic sequence and consume one must come in pairs + // we classify instructions into four groups: producers, consumers, neutral and others + // any producer (an instruction that produces more than one value) must be followed by 0 or more neutral instructions + // and a consumer (that consumes more than one value); these form a variadic sequence. + // except for producer, no instruction in the variadic sequence may be a jump target. + // from the execution perspective, producer adjusts L->top to point to one past the last result, neutral instructions + // leave L->top unmodified, and consumer adjusts L->top back to the stack frame end. + // consumers invalidate all values after L->top after they execute (which we currently don't validate) + bool variadicSeq = false; + + std::vector insntargets(insns.size(), 0); + + for (size_t i = 0; i < insns.size();) + { + uint32_t insn = insns[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + int target = getJumpTarget(insn, uint32_t(i)); + + if (target >= 0 && !isFastCall(op)) + { + LUAU_ASSERT(unsigned(target) < insns.size()); + + insntargets[target] = true; + } + + i += getOpLength(op); + LUAU_ASSERT(i <= insns.size()); + } + + for (size_t i = 0; i < insns.size();) + { + uint32_t insn = insns[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + if (variadicSeq) + { + // no instruction inside the sequence, including the consumer, may be a jump target + // this guarantees uninterrupted L->top adjustment flow + LUAU_ASSERT(!insntargets[i]); + } + + if (op == LOP_CALL || op == LOP_CALLFB) + { + LUAU_ASSERT(FFlag::LuauEmitCallFeedback || op != LOP_CALLFB); + // note: calls may end one variadic sequence and start a new one + + if (LUAU_INSN_B(insn) == 0) + { + // consumer instruction ends a variadic sequence + LUAU_ASSERT(variadicSeq); + variadicSeq = false; + } + else + { + // CALL is not a neutral instruction so it can't be present in a variadic sequence unless it's a consumer + LUAU_ASSERT(!variadicSeq); + } + + if (LUAU_INSN_C(insn) == 0) + { + // producer instruction starts a variadic sequence + LUAU_ASSERT(!variadicSeq); + variadicSeq = true; + } + } + else if (op == LOP_GETVARARGS && LUAU_INSN_B(insn) == 0) + { + // producer instruction starts a variadic sequence + LUAU_ASSERT(!variadicSeq); + variadicSeq = true; + } + else if ((op == LOP_RETURN && LUAU_INSN_B(insn) == 0) || (op == LOP_SETLIST && LUAU_INSN_C(insn) == 0)) + { + // consumer instruction ends a variadic sequence + LUAU_ASSERT(variadicSeq); + variadicSeq = false; + } + else if (op == LOP_FASTCALL || op == LOP_FASTPCALL) + { + int callTarget = int(i + LUAU_INSN_C(insn) + 1); + LUAU_ASSERT(unsigned(callTarget) < insns.size() && LUAU_INSN_OP(insns[callTarget]) == LOP_CALL); + + if (LUAU_INSN_B(insns[callTarget]) == 0) + { + // consumer instruction ends a variadic sequence; however, we can't terminate it yet because future analysis of CALL will do it + // during FASTCALL fallback, the instructions between this and CALL consumer are going to be executed before L->top so they must + // be neutral; as such, we will defer termination of variadic sequence until CALL analysis + LUAU_ASSERT(variadicSeq); + } + else + { + // FASTCALL is not a neutral instruction so it can't be present in a variadic sequence unless it's linked to CALL consumer + LUAU_ASSERT(!variadicSeq); + } + + // note: if FASTCALL is linked to a CALL producer, the instructions between FASTCALL and CALL are technically not part of an executed + // variadic sequence since they are never executed if FASTCALL does anything, so it's okay to skip their validation until CALL + // (we can't simply start a variadic sequence here because that would trigger assertions during linked CALL validation) + } + else if (op == LOP_CLOSEUPVALS || op == LOP_NAMECALL || op == LOP_NAMECALLUDATA || op == LOP_GETIMPORT || op == LOP_MOVE || + op == LOP_GETUPVAL || op == LOP_GETGLOBAL || op == LOP_GETTABLEKS || op == LOP_COVERAGE) + { + // instructions inside a variadic sequence must be neutral (can't change L->top) + // while there are many neutral instructions like this, here we check that the instruction is one of the few + // that we'd expect to exist in FASTCALL fallback sequences or between consecutive CALLs for encoding reasons + } + else + { + LUAU_ASSERT(!variadicSeq); + } + + i += getOpLength(op); + LUAU_ASSERT(i <= insns.size()); + } + + LUAU_ASSERT(!variadicSeq); +} + +void BytecodeBuilder::validateCaptures() const +{ + // Mark jump targets + std::vector jumpTargets(insns.size(), 0); + + for (int i = 0; i < int(insns.size());) + { + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insns[i])); + + int target = getJumpTarget(insns[i], uint32_t(i)); + + if (target >= 0 && !isFastCall(op)) + jumpTargets[target] = 1; + + i += getOpLength(op); + } + + struct BytecodeBlock + { + int startpc = -1; + int finishpc = -1; + std::array captured; + bool visited = false; + bool inWorklist = false; + SmallVector predecessors; + SmallVector successors; + }; + + // Reconstruct bytecode basic block graph + std::vector blocks; + blocks.push_back(BytecodeBlock{0, -1}); + + int previ = 0; + for (int i = 0; i < int(insns.size());) + { + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insns[i])); + int nexti = i + getOpLength(op); + + // If instruction is a jump target, begin new block starting from it + if (i != 0 && jumpTargets[i] == 1) + { + blocks.back().finishpc = previ; + blocks.push_back(BytecodeBlock{i, -1}); + } + + int target = getJumpTarget(insns[i], uint32_t(i)); + + // Implicit fallthrough terminate the block and might start a new one + if ((target >= 0 && !isFastCall(op)) || op == LOP_RETURN) + { + blocks.back().finishpc = i; + + // Start a new block if there was no explicit jump for the fallthrough + if (nexti < int(insns.size()) && jumpTargets[nexti] == 0) + blocks.push_back(BytecodeBlock{nexti, -1}); + } + + previ = i; + i = nexti; + } + + if (!blocks.empty() && blocks.back().finishpc == -1) + blocks.back().finishpc = previ; + + // Collect mapping from instruction to a block + std::vector ownerBlockIdx(insns.size(), ~0u); + + for (uint32_t blockIdx = 0; blockIdx < uint32_t(blocks.size()); blockIdx++) + { + BytecodeBlock& block = blocks[blockIdx]; + + for (int i = block.startpc; i <= block.finishpc; i += getOpLength(LuauOpcode(LUAU_INSN_OP(insns[i])))) + ownerBlockIdx[i] = blockIdx; + } + + // Collect predecessors and successors + for (uint32_t blockIdx = 0; blockIdx < uint32_t(blocks.size()); blockIdx++) + { + BytecodeBlock& block = blocks[blockIdx]; + + // Loop variables are used after the loop to handle fallthrough + int i = block.startpc; + uint32_t insn = 0; + LuauOpcode op = LOP_NOP; + + while (i <= block.finishpc) + { + insn = insns[i]; + op = LuauOpcode(LUAU_INSN_OP(insn)); + + int target = getJumpTarget(insn, i); + + if (target >= 0 && !isFastCall(op)) + { + uint32_t targetIdx = ownerBlockIdx[target]; + LUAU_ASSERT(targetIdx != ~0u); + block.successors.push_back(targetIdx); + blocks[targetIdx].predecessors.push_back(blockIdx); + } + + i += getOpLength(op); + } + + // Include fallthrough jump as well (excluding non-fallthrough version of LOADB) + if (isFallthrough(op) && !(isSkipC(op) && LUAU_INSN_C(insn) != 0) && i < int(insns.size())) + { + uint32_t targetIdx = ownerBlockIdx[i]; + LUAU_ASSERT(targetIdx != ~0u && blockIdx != targetIdx); + block.successors.push_back(targetIdx); + blocks[targetIdx].predecessors.push_back(blockIdx); + } + } + + // Walk the graph and ensure that at any reachable RETURN, we have all upvalues closed + std::vector worklist; + + worklist.push_back(0); + blocks[0].inWorklist = true; + + while (!worklist.empty()) + { + uint32_t blockIdx = worklist.back(); + worklist.pop_back(); + + BytecodeBlock& block = blocks[blockIdx]; + block.visited = true; + block.inWorklist = false; + + std::array oldCaptured = block.captured; + + // Collect captured registers from each predecessor + for (uint32_t predIdx : block.predecessors) + { + BytecodeBlock& pred = blocks[predIdx]; + + for (int reg = 0; reg < int(pred.captured.size()); reg++) + block.captured[reg] |= pred.captured[reg]; + } + + for (int i = block.startpc; i <= block.finishpc;) + { + uint32_t insn = insns[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + switch (op) + { + case LOP_CLOSEUPVALS: + for (int reg = LUAU_INSN_A(insn); reg < int(block.captured.size()); reg++) + block.captured[reg] = false; + break; + case LOP_CAPTURE: + if (LUAU_INSN_A(insn) == LCT_REF) + block.captured[LUAU_INSN_B(insn)] = true; + break; + case LOP_RETURN: + for (int reg = 0; reg < int(block.captured.size()); reg++) + LUAU_ASSERT(block.captured[reg] == false); + break; + default: + break; + } + + i += getOpLength(op); + } + + bool changed = oldCaptured != block.captured; + + // Check all successors if something changed or if they have not been visited yet + for (uint32_t succIdx : block.successors) + { + BytecodeBlock& succ = blocks[succIdx]; + + if ((!succ.visited || changed) && !succ.inWorklist) + { + worklist.push_back(succIdx); + succ.inWorklist = true; + } + } + } +} + +#endif + +static bool printableStringConstant(const char* str, size_t len) +{ + for (size_t i = 0; i < len; ++i) + { + if (unsigned(str[i]) < ' ') + return false; + } + + return true; +} + +void BytecodeBuilder::dumpConstant(std::string& result, int k, bool detailed) const +{ + LUAU_ASSERT(unsigned(k) < constants.size()); + const Constant& data = constants[k]; + + switch (data.type) + { + case Constant::Type_Nil: + formatAppend(result, "nil"); + break; + case Constant::Type_Boolean: + formatAppend(result, "%s", data.valueBoolean ? "true" : "false"); + break; + case Constant::Type_Number: + formatAppend(result, "%.17g", data.valueNumber); + break; + case Constant::Type_Integer: + formatAppend(result, "%lld", (long long)(int64_t)data.valueInteger64); + break; + case Constant::Type_Vectorf: + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVectorf[3] == 0.0f) + formatAppend(result, "%.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2]); + else + formatAppend(result, "%.9g, %.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2], data.valueVectorf[3]); + break; + case Constant::Type_Vectord: + if (FFlag::LuauCompileEmitVectorDouble) + { + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVectord[3] == 0.0) + formatAppend(result, "%.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2]); + else + formatAppend( + result, "%.17g, %.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2], data.valueVectord[3] + ); + } + else + { + // 3-vectors is the most common configuration, so truncate to three components if possible + if (data.valueVectord[3] == 0.0f) + formatAppend(result, "%.9g, %.9g, %.9g", float(data.valueVectord[0]), float(data.valueVectord[1]), float(data.valueVectord[2])); + else + formatAppend( + result, + "%.9g, %.9g, %.9g, %.9g", + float(data.valueVectord[0]), + float(data.valueVectord[1]), + float(data.valueVectord[2]), + float(data.valueVectord[3]) + ); + } + break; + case Constant::Type_String: + { + const StringRef& str = debugStrings[data.valueString - 1]; + + if (printableStringConstant(str.data, str.length)) + { + if (str.length < 32) + formatAppend(result, "'%.*s'", int(str.length), str.data); + else + formatAppend(result, "'%.*s'...", 32, str.data); + } + else + { + formatAppend(result, "'"); + + for (size_t i = 0; i < str.length && i < 32; ++i) + { + if (unsigned(str.data[i]) < ' ') + formatAppend(result, "\\x%02X", uint8_t(str.data[i])); + else + formatAppend(result, "%c", str.data[i]); + } + + if (str.length >= 32) + formatAppend(result, "'..."); + else + formatAppend(result, "'"); + } + break; + } + case Constant::Type_Import: + { + int32_t id0 = -1, id1 = -1, id2 = -1; + if (int count = decomposeImportId(data.valueImport, id0, id1, id2)) + { + { + const Constant& id = constants[id0]; + LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size()); + + const StringRef& str = debugStrings[id.valueString - 1]; + formatAppend(result, "%.*s", int(str.length), str.data); + } + + if (count > 1) + { + const Constant& id = constants[id1]; + LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size()); + + const StringRef& str = debugStrings[id.valueString - 1]; + formatAppend(result, ".%.*s", int(str.length), str.data); + } + + if (count > 2) + { + const Constant& id = constants[id2]; + LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size()); + + const StringRef& str = debugStrings[id.valueString - 1]; + formatAppend(result, ".%.*s", int(str.length), str.data); + } + } + break; + } + case Constant::Type_Table: + if (detailed) + { + const TableShape& shape = tableShapes[data.valueTable]; + + unsigned sizenode = shape.length > 0 ? (1u << ceillog2(int(shape.length))) : 0; + unsigned mask = sizenode > 0 ? (sizenode - 1) : 0; + + // Compute slot for each key and detect collisions + std::vector slots; + slots.resize(shape.length, 0); + + // Track first key index to claim the slot + std::vector slotOwner; + slotOwner.resize(sizenode, ~0u); + + for (unsigned i = 0; i < shape.length; ++i) + { + const Constant& keyConst = constants[shape.keys[i]]; + LUAU_ASSERT(keyConst.type == Constant::Type_String); + LUAU_ASSERT(keyConst.valueString != 0u); + const StringRef& str = debugStrings[keyConst.valueString - 1]; + + slots[i] = getStringHash(str) & mask; + + if (slotOwner[slots[i]] == ~0u) + slotOwner[slots[i]] = i; + } + + formatAppend(result, "{"); + + for (unsigned i = 0; i < shape.length; ++i) + { + if (i > 0) + formatAppend(result, ", "); + + formatAppend(result, "["); + dumpConstant(result, shape.keys[i], false); + formatAppend(result, "]"); + + if (shape.hasConstants && shape.constants[i] != -1) + { + formatAppend(result, " = "); + dumpConstant(result, shape.constants[i], false); + } + + formatAppend(result, " #%u", slots[i]); + + if (slotOwner[slots[i]] != i) + formatAppend(result, " (conflict)"); + } + + formatAppend(result, "} sizenode=%u", sizenode); + } + else + { + formatAppend(result, "{...}"); + } + break; + case Constant::Type_Closure: + { + const Function& func = functions[data.valueClosure]; + + if (detailed) + { + if (!func.dumpname.empty()) + formatAppend(result, "function %s", func.dumpname.c_str()); + else + formatAppend(result, "function"); + } + else + { + if (!func.dumpname.empty()) + formatAppend(result, "'%s'", func.dumpname.c_str()); + } + break; + } + case Constant::Type_ClassShape: + { + const ClassShape& cs = classShapes[data.valueClassShape]; + const Constant& className = constants[cs.className]; + LUAU_ASSERT(className.type == Constant::Type_String && className.valueString <= debugStrings.size()); + const StringRef& str = debugStrings[className.valueString - 1]; + // This should always be printable, in fact this should always be a + // valid Luau identifier! + LUAU_ASSERT(printableStringConstant(str.data, str.length)); + formatAppend(result, "class %.*s (props: %zu, methods: %zu)", int(str.length), str.data, cs.propertyNames.size(), cs.methodNames.size()); + + if (detailed) + { + if (!cs.propertyNames.empty()) + { + formatAppend(result, "\n props:"); + for (size_t i = 0; i < cs.propertyNames.size(); ++i) + { + formatAppend(result, "\n K%d [", cs.propertyNames[i]); + dumpConstant(result, cs.propertyNames[i], false); + result.append("]"); + } + } + + if (!cs.methodNames.empty()) + { + formatAppend(result, "\n methods:"); + for (size_t i = 0; i < cs.methodNames.size(); ++i) + { + formatAppend(result, "\n K%d [", cs.methodNames[i]); + dumpConstant(result, cs.methodNames[i], false); + result.append("]"); + } + } + } + break; + } + } +} + +void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, int targetLabel) const +{ + uint32_t insn = *code++; + + switch (LUAU_INSN_OP(insn)) + { + case LOP_LOADNIL: + formatAppend(result, "LOADNIL R%d\n", LUAU_INSN_A(insn)); + break; + + case LOP_LOADB: + if (LUAU_INSN_C(insn)) + formatAppend(result, "LOADB R%d %d +%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + else + formatAppend(result, "LOADB R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_LOADN: + formatAppend(result, "LOADN R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + break; + + case LOP_LOADK: + formatAppend(result, "LOADK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); + result.append("]\n"); + break; + + case LOP_MOVE: + formatAppend(result, "MOVE R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_GETGLOBAL: + formatAppend(result, "GETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_SETGLOBAL: + formatAppend(result, "SETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_GETUPVAL: + formatAppend(result, "GETUPVAL R%d U%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_SETUPVAL: + formatAppend(result, "SETUPVAL R%d U%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_CLOSEUPVALS: + formatAppend(result, "CLOSEUPVALS R%d\n", LUAU_INSN_A(insn)); + break; + + case LOP_GETIMPORT: + formatAppend(result, "GETIMPORT R%d %d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); + result.append("]\n"); + code++; // AUX + break; + + case LOP_GETTABLE: + formatAppend(result, "GETTABLE R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_SETTABLE: + formatAppend(result, "SETTABLE R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_GETTABLEKS: + formatAppend(result, "GETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_SETTABLEKS: + formatAppend(result, "SETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_GETTABLEN: + formatAppend(result, "GETTABLEN R%d R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) + 1); + break; + + case LOP_SETTABLEN: + formatAppend(result, "SETTABLEN R%d R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) + 1); + break; + + case LOP_NEWCLOSURE: + formatAppend(result, "NEWCLOSURE R%d P%d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + break; + + case LOP_NAMECALL: + formatAppend(result, "NAMECALL R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_CALL: + formatAppend(result, "CALL R%d %d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1, LUAU_INSN_C(insn) - 1); + break; + + case LOP_CALLFB: + formatAppend(result, "CALLFB R%d %d %d [%d]\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1, LUAU_INSN_C(insn) - 1, static_cast(*code)); + code++; + break; + + case LOP_RETURN: + formatAppend(result, "RETURN R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1); + break; + + case LOP_JUMP: + formatAppend(result, "JUMP L%d\n", targetLabel); + break; + + case LOP_JUMPIF: + formatAppend(result, "JUMPIF R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_JUMPIFNOT: + formatAppend(result, "JUMPIFNOT R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_JUMPIFEQ: + formatAppend(result, "JUMPIFEQ R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_JUMPIFLE: + formatAppend(result, "JUMPIFLE R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_JUMPIFLT: + formatAppend(result, "JUMPIFLT R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_JUMPIFNOTEQ: + formatAppend(result, "JUMPIFNOTEQ R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_JUMPIFNOTLE: + formatAppend(result, "JUMPIFNOTLE R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_JUMPIFNOTLT: + formatAppend(result, "JUMPIFNOTLT R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_ADD: + formatAppend(result, "ADD R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_SUB: + formatAppend(result, "SUB R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_MUL: + formatAppend(result, "MUL R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_DIV: + formatAppend(result, "DIV R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_IDIV: + formatAppend(result, "IDIV R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_MOD: + formatAppend(result, "MOD R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_POW: + formatAppend(result, "POW R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_ADDK: + formatAppend(result, "ADDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_SUBK: + formatAppend(result, "SUBK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_MULK: + formatAppend(result, "MULK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_DIVK: + formatAppend(result, "DIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_IDIVK: + formatAppend(result, "IDIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_MODK: + formatAppend(result, "MODK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_POWK: + formatAppend(result, "POWK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_SUBRK: + formatAppend(result, "SUBRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn), false); + formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); + break; + + case LOP_DIVRK: + formatAppend(result, "DIVRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn), false); + formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); + break; + + case LOP_AND: + formatAppend(result, "AND R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_OR: + formatAppend(result, "OR R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_ANDK: + formatAppend(result, "ANDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_ORK: + formatAppend(result, "ORK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + dumpConstant(result, LUAU_INSN_C(insn), false); + result.append("]\n"); + break; + + case LOP_CONCAT: + formatAppend(result, "CONCAT R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); + break; + + case LOP_NOT: + formatAppend(result, "NOT R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_MINUS: + formatAppend(result, "MINUS R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_LENGTH: + formatAppend(result, "LENGTH R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + break; + + case LOP_NEWTABLE: + formatAppend(result, "NEWTABLE R%d %d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) == 0 ? 0 : 1 << (LUAU_INSN_B(insn) - 1), *code++); + break; + + case LOP_DUPTABLE: + formatAppend(result, "DUPTABLE R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + break; + + case LOP_SETLIST: + formatAppend(result, "SETLIST R%d R%d %d [%d]\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) - 1, *code++); + break; + + case LOP_FORNPREP: + formatAppend(result, "FORNPREP R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_FORNLOOP: + formatAppend(result, "FORNLOOP R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_FORGPREP: + formatAppend(result, "FORGPREP R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_FORGLOOP: + formatAppend(result, "FORGLOOP R%d L%d %d%s\n", LUAU_INSN_A(insn), targetLabel, uint8_t(*code), int(*code) < 0 ? " [inext]" : ""); + code++; + break; + + case LOP_FORGPREP_INEXT: + formatAppend(result, "FORGPREP_INEXT R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_FORGPREP_NEXT: + formatAppend(result, "FORGPREP_NEXT R%d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_GETVARARGS: + formatAppend(result, "GETVARARGS R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1); + break; + + case LOP_DUPCLOSURE: + formatAppend(result, "DUPCLOSURE R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn)); + dumpConstant(result, LUAU_INSN_D(insn), false); + result.append("]\n"); + break; + + case LOP_BREAK: + formatAppend(result, "BREAK\n"); + break; + + case LOP_JUMPBACK: + formatAppend(result, "JUMPBACK L%d\n", targetLabel); + break; + + case LOP_LOADKX: + formatAppend(result, "LOADKX R%d K%d [", LUAU_INSN_A(insn), *code); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_JUMPX: + formatAppend(result, "JUMPX L%d\n", targetLabel); + break; + + case LOP_FASTCALL: + formatAppend(result, "FASTCALL %d L%d\n", LUAU_INSN_A(insn), targetLabel); + break; + + case LOP_FASTCALL1: + formatAppend(result, "FASTCALL1 %d R%d L%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), targetLabel); + break; + + case LOP_FASTCALL2: + formatAppend(result, "FASTCALL2 %d R%d R%d L%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, targetLabel); + code++; + break; + + case LOP_FASTCALL2K: + formatAppend(result, "FASTCALL2K %d R%d K%d L%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, targetLabel); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_FASTCALL3: + formatAppend(result, "FASTCALL3 %d R%d R%d R%d L%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code & 0xff, (*code >> 8) & 0xff, targetLabel); + code++; + break; + + case LOP_COVERAGE: + formatAppend(result, "COVERAGE\n"); + break; + + case LOP_CAPTURE: + formatAppend( + result, + "CAPTURE %s %c%d\n", + LUAU_INSN_A(insn) == LCT_UPVAL ? "UPVAL" + : LUAU_INSN_A(insn) == LCT_REF ? "REF" + : LUAU_INSN_A(insn) == LCT_VAL ? "VAL" + : "", + LUAU_INSN_A(insn) == LCT_UPVAL ? 'U' : 'R', + LUAU_INSN_B(insn) + ); + break; + + case LOP_JUMPXEQKNIL: + formatAppend(result, "JUMPXEQKNIL R%d L%d%s\n", LUAU_INSN_A(insn), targetLabel, *code >> 31 ? " NOT" : ""); + code++; + break; + + case LOP_JUMPXEQKB: + formatAppend(result, "JUMPXEQKB R%d %d L%d%s\n", LUAU_INSN_A(insn), *code & 1, targetLabel, *code >> 31 ? " NOT" : ""); + code++; + break; + + case LOP_JUMPXEQKN: + formatAppend(result, "JUMPXEQKN R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : ""); + dumpConstant(result, *code & 0xffffff, false); + result.append("]\n"); + code++; + break; + + case LOP_JUMPXEQKS: + formatAppend(result, "JUMPXEQKS R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : ""); + dumpConstant(result, *code & 0xffffff, false); + result.append("]\n"); + code++; + break; + + case LOP_GETUDATAKS: + formatAppend(result, "GETUDATAKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_SETUDATAKS: + formatAppend(result, "SETUDATAKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_NAMECALLUDATA: + formatAppend(result, "NAMECALLUDATA R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_AUX_KV16(*code)); + dumpConstant(result, LUAU_INSN_AUX_KV16(*code), false); + result.append("]\n"); + code++; + break; + + case LOP_NEWCLASSMEMBER: + formatAppend(result, "NEWCLASSMEMBER R%d R%d [", LUAU_INSN_A(insn), LUAU_INSN_C(insn)); + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + case LOP_CMPPROTO: + formatAppend(result, "CMPPROTO R%d #%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel); + break; + + case LOP_FASTPCALL: + formatAppend(result, "FASTPCALL %s L%d\n", LUAU_INSN_A(insn) == 0 ? "pcall" : "xpcall", targetLabel); + break; + + case LOP_NEWCLASS: + if (LUAU_INSN_B(insn) == 0xff) + formatAppend(result, "NEWCLASS R%d no_base K%d %d [", LUAU_INSN_A(insn), *code, LUAU_INSN_C(insn)); + else + formatAppend(result, "NEWCLASS R%d R%d K%d %d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, LUAU_INSN_C(insn)); + + dumpConstant(result, *code, false); + result.append("]\n"); + code++; + break; + + default: + LUAU_ASSERT(!"Unsupported opcode"); + } +} + +static const char* getBaseTypeString(uint8_t type) +{ + uint8_t tag = type & ~LBC_TYPE_OPTIONAL_BIT; + switch (tag) + { + case LBC_TYPE_NIL: + return "nil"; + case LBC_TYPE_BOOLEAN: + return "boolean"; + case LBC_TYPE_NUMBER: + return "number"; + case LBC_TYPE_INTEGER: + return "integer"; + case LBC_TYPE_STRING: + return "string"; + case LBC_TYPE_TABLE: + return "table"; + case LBC_TYPE_FUNCTION: + return "function"; + case LBC_TYPE_THREAD: + return "thread"; + case LBC_TYPE_USERDATA: + return "userdata"; + case LBC_TYPE_VECTOR: + return "vector"; + case LBC_TYPE_BUFFER: + return "buffer"; + case LBC_TYPE_ANY: + return "any"; + } + + LUAU_ASSERT(!"Unhandled type in getBaseTypeString"); + return nullptr; +} + +std::string BytecodeBuilder::dumpCurrentFunction(std::vector& dumpinstoffs) const +{ + if ((dumpFlags & (Dump_Code | Dump_Constants)) == 0) + return std::string(); + + int lastLine = -1; + size_t nextRemark = 0; + + std::string result; + + if (dumpFlags & Dump_Locals) + { + for (size_t i = 0; i < debugLocals.size(); ++i) + { + const DebugLocal& l = debugLocals[i]; + + if ((dumpFlags & Dump_Code) != 0) + formatAppend(result, "local %d (%.*s): ", int(i), int(debugStrings[l.name - 1].length), debugStrings[l.name - 1].data); + else + formatAppend(result, "local %d: ", int(i)); + + formatAppend(result, "reg %d, start pc %d line %d, ", l.reg, l.startpc, lines[l.startpc]); + + if (l.startpc == l.endpc) + { + LUAU_ASSERT(l.startpc < lines.size()); + + formatAppend(result, "no live range\n"); + } + else + { + LUAU_ASSERT(l.startpc < l.endpc); + LUAU_ASSERT(l.startpc < lines.size()); + LUAU_ASSERT(l.endpc <= lines.size()); // endpc is exclusive in the debug info, but it's more intuitive to print inclusive data + + formatAppend(result, "end pc %d line %d\n", l.endpc - 1, lines[l.endpc - 1]); + } + } + } + + if (dumpFlags & Dump_Types) + { + const std::string& typeinfo = functions.back().typeinfo; + + // Arguments start from third byte in function typeinfo string + for (uint8_t i = 2; i < typeinfo.size(); ++i) + { + uint8_t et = typeinfo[i]; + + const char* userdata = tryGetUserdataTypeName(LuauBytecodeType(et)); + const char* name = userdata ? userdata : getBaseTypeString(et); + const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "R%d: %s%s [argument]\n", i - 2, name, optional); + } + + for (size_t i = 0; i < typedUpvals.size(); ++i) + { + const TypedUpval& l = typedUpvals[i]; + + const char* userdata = tryGetUserdataTypeName(l.type); + const char* name = userdata ? userdata : getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "U%d: %s%s\n", int(i), name, optional); + } + + for (size_t i = 0; i < typedLocals.size(); ++i) + { + const TypedLocal& l = typedLocals[i]; + + const char* userdata = tryGetUserdataTypeName(l.type); + const char* name = userdata ? userdata : getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, name, optional, l.startpc, l.endpc); + } + } + + if (dumpFlags & Dump_Constants) + { + for (size_t i = 0; i < constants.size(); ++i) + { + formatAppend(result, "K%d: ", int(i)); + dumpConstant(result, int(i), true); + formatAppend(result, "\n"); + } + } + + if (dumpFlags & Dump_Code) + { + std::vector labels(insns.size(), -1); + + // annotate valid jump targets with 0 + for (size_t i = 0; i < insns.size();) + { + int target = getJumpTarget(insns[i], uint32_t(i)); + + if (target >= 0) + { + LUAU_ASSERT(size_t(target) < insns.size()); + labels[target] = 0; + } + + i += getOpLength(LuauOpcode(LUAU_INSN_OP(insns[i]))); + LUAU_ASSERT(i <= insns.size()); + } + + int nextLabel = 0; + + // compute label ids (sequential integers for all jump targets) + for (size_t i = 0; i < labels.size(); ++i) + if (labels[i] == 0) + labels[i] = nextLabel++; + + dumpinstoffs.resize(insns.size() + 1, -1); + + for (size_t i = 0; i < insns.size();) + { + const uint32_t* code = &insns[i]; + uint8_t op = LUAU_INSN_OP(*code); + + dumpinstoffs[i] = int(result.size()); + + if (op == LOP_PREPVARARGS) + { + // Don't emit function header in bytecode - it's used for call dispatching and doesn't contain "interesting" information + i++; + continue; + } + + if (dumpFlags & Dump_Remarks) + { + while (nextRemark < debugRemarks.size() && debugRemarks[nextRemark].first == i) + { + formatAppend(result, "REMARK %s\n", debugRemarkBuffer.c_str() + debugRemarks[nextRemark].second); + nextRemark++; + } + } + + if (dumpFlags & Dump_Source) + { + int line = lines[i]; + + if (line > 0 && line != lastLine) + { + LUAU_ASSERT(size_t(line - 1) < dumpSource.size()); + formatAppend(result, "%5d: %s\n", line, dumpSource[line - 1].c_str()); + lastLine = line; + } + } + + if (dumpFlags & Dump_Lines) + formatAppend(result, "%d: ", lines[i]); + + if (labels[i] != -1) + formatAppend(result, "L%d: ", labels[i]); + + int target = getJumpTarget(*code, uint32_t(i)); + + dumpInstruction(code, result, target >= 0 ? labels[target] : -1); + + i += getOpLength(LuauOpcode(op)); + LUAU_ASSERT(i <= insns.size()); + } + + dumpinstoffs[insns.size()] = int(result.size()); + } + + return result; +} + +void BytecodeBuilder::setDumpSource(const std::string& source) +{ + dumpSource.clear(); + + size_t pos = 0; + + while (pos != std::string::npos) + { + size_t next = source.find('\n', pos); + + if (next == std::string::npos) + { + dumpSource.push_back(source.substr(pos)); + pos = next; + } + else + { + dumpSource.push_back(source.substr(pos, next - pos)); + pos = next + 1; + } + + if (!dumpSource.back().empty() && dumpSource.back().back() == '\r') + dumpSource.back().pop_back(); + } +} + +std::string BytecodeBuilder::dumpFunction(uint32_t id) const +{ + LUAU_ASSERT(id < functions.size()); + + return functions[id].dump; +} + +std::string BytecodeBuilder::dumpEverything() const +{ + std::string result; + + for (size_t i = 0; i < functions.size(); ++i) + { + std::string debugname = functions[i].dumpname.empty() ? "??" : functions[i].dumpname; + + formatAppend(result, "Function %d (%s):\n", int(i), debugname.c_str()); + + result += functions[i].dump; + result += "\n"; + } + + return result; +} + +std::string BytecodeBuilder::dumpSourceRemarks() const +{ + std::string result; + + size_t nextRemark = 0; + + std::vector> remarks = dumpRemarks; + std::sort(remarks.begin(), remarks.end()); + + for (size_t i = 0; i < dumpSource.size(); ++i) + { + const std::string& line = dumpSource[i]; + + size_t indent = 0; + while (indent < line.length() && (line[indent] == ' ' || line[indent] == '\t')) + indent++; + + while (nextRemark < remarks.size() && remarks[nextRemark].first == int(i + 1)) + { + formatAppend(result, "%.*s-- remark: %s\n", int(indent), line.c_str(), remarks[nextRemark].second.c_str()); + nextRemark++; + + // skip duplicate remarks (due to inlining/unrolling) + while (nextRemark < remarks.size() && remarks[nextRemark] == remarks[nextRemark - 1]) + nextRemark++; + } + + result += line; + + if (i + 1 < dumpSource.size()) + result += '\n'; + } + + return result; +} + +std::string BytecodeBuilder::dumpTypeInfo() const +{ + std::string result; + + for (size_t i = 0; i < functions.size(); ++i) + { + const std::string& typeinfo = functions[i].typeinfo; + if (typeinfo.empty()) + continue; + + uint8_t encodedType = typeinfo[0]; + + LUAU_ASSERT(encodedType == LBC_TYPE_FUNCTION); + + formatAppend(result, "%zu: function(", i); + + LUAU_ASSERT(typeinfo.size() >= 2); + + uint8_t numparams = typeinfo[1]; + + LUAU_ASSERT(size_t(1 + numparams - 1) < typeinfo.size()); + + for (uint8_t i = 0; i < numparams; ++i) + { + uint8_t et = typeinfo[2 + i]; + const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + formatAppend(result, "%s%s", getBaseTypeString(et), optional); + + if (i + 1 != numparams) + formatAppend(result, ", "); + } + + formatAppend(result, ")\n"); + } + + return result; +} + +std::vector BytecodeBuilder::getStringTable() +{ + std::vector strings; + strings.resize(stringTable.size()); + for (auto& p : stringTable) + { + LUAU_ASSERT(p.second > 0 && p.second <= strings.size()); + strings[p.second - 1] = std::string_view(p.first.data, p.first.length); + } + return strings; +} + +void BytecodeBuilder::annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const +{ + if ((dumpFlags & Dump_Code) == 0) + return; + + LUAU_ASSERT(fid < functions.size()); + + const Function& function = functions[fid]; + const std::string& dump = function.dump; + const std::vector& dumpinstoffs = function.dumpinstoffs; + + uint32_t next = instpos + 1; + + LUAU_ASSERT(next < dumpinstoffs.size()); + + // Skip locations of multi-dword instructions + while (next < dumpinstoffs.size() && dumpinstoffs[next] == -1) + next++; + + formatAppend(result, "%.*s", dumpinstoffs[next] - dumpinstoffs[instpos], dump.data() + dumpinstoffs[instpos]); +} + +} // namespace Luau diff --git a/External/luau/Bytecode/src/BytecodeDump.cpp b/External/luau/Bytecode/src/BytecodeDump.cpp new file mode 100644 index 00000000..cf7b00bc --- /dev/null +++ b/External/luau/Bytecode/src/BytecodeDump.cpp @@ -0,0 +1,396 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/BytecodeDump.h" + +namespace Luau +{ +namespace Bytecode +{ + +const int kDetailsAlignColumn = 60; + +LUAU_PRINTF_ATTR(2, 3) +void append(std::string& result, const char* fmt, ...) +{ + char buf[256]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + result.append(buf); +} + +void padToDetailColumn(std::string& result, size_t lineStart) +{ + int pad = kDetailsAlignColumn - int(result.size() - lineStart); + + if (pad > 0) + result.append(pad, ' '); +} + +bool isPrintableStringConstant(const char* str, size_t len) +{ + for (size_t i = 0; i < len; ++i) + { + if (unsigned(str[i]) < ' ') + return false; + } + + return true; +} + +int decomposeImportId(uint32_t ids, int32_t& id0, int32_t& id1, int32_t& id2) +{ + int count = ids >> 30; + id0 = count > 0 ? int(ids >> 20) & 1023 : -1; + id1 = count > 1 ? int(ids >> 10) & 1023 : -1; + id2 = count > 2 ? int(ids) & 1023 : -1; + return count; +} + +const char* getBlockEdgeKindName(BcBlockEdgeKind kind) +{ + switch (kind) + { + case BcBlockEdgeKind::Branch: + return "branch"; + case BcBlockEdgeKind::Fallthrough: + return "fallthrough"; + case BcBlockEdgeKind::Loop: + return "loop"; + } + + LUAU_ASSERT(!"unknown BcBlockEdgeKind"); + return "unknown"; +} + +const char* getLuauOpcodeName(LuauOpcode cmd) +{ + switch (cmd) + { + case LOP_LOADNIL: + return "LOADNIL"; + case LOP_LOADB: + return "LOADB"; + case LOP_LOADN: + return "LOADN"; + case LOP_LOADK: + return "LOADK"; + case LOP_MOVE: + return "MOVE"; + case LOP_GETGLOBAL: + return "GETGLOBAL"; + case LOP_SETGLOBAL: + return "SETGLOBAL"; + case LOP_GETUPVAL: + return "GETUPVAL"; + case LOP_SETUPVAL: + return "SETUPVAL"; + case LOP_CLOSEUPVALS: + return "CLOSEUPVALS"; + case LOP_GETIMPORT: + return "GETIMPORT"; + case LOP_GETTABLE: + return "GETTABLE"; + case LOP_SETTABLE: + return "SETTABLE"; + case LOP_GETTABLEKS: + return "GETTABLEKS"; + case LOP_SETTABLEKS: + return "SETTABLEKS"; + case LOP_GETTABLEN: + return "GETTABLEN"; + case LOP_SETTABLEN: + return "SETTABLEN"; + case LOP_NEWCLOSURE: + return "NEWCLOSURE"; + case LOP_NAMECALL: + return "NAMECALL"; + case LOP_CALL: + return "CALL"; + case LOP_CALLFB: + return "CALLFB"; + case LOP_RETURN: + return "RETURN"; + case LOP_JUMP: + return "JUMP"; + case LOP_JUMPIF: + return "JUMPIF"; + case LOP_JUMPIFNOT: + return "JUMPIFNOT"; + case LOP_JUMPIFEQ: + return "JUMPIFEQ"; + case LOP_JUMPIFLE: + return "JUMPIFLE"; + case LOP_JUMPIFLT: + return "JUMPIFLT"; + case LOP_JUMPIFNOTEQ: + return "JUMPIFNOTEQ"; + case LOP_JUMPIFNOTLE: + return "JUMPIFNOTLE"; + case LOP_JUMPIFNOTLT: + return "JUMPIFNOTLT"; + case LOP_ADD: + return "ADD"; + case LOP_SUB: + return "SUB"; + case LOP_MUL: + return "MUL"; + case LOP_DIV: + return "DIV"; + case LOP_IDIV: + return "IDIV"; + case LOP_MOD: + return "MOD"; + case LOP_POW: + return "POW"; + case LOP_ADDK: + return "ADDK"; + case LOP_SUBK: + return "SUBK"; + case LOP_MULK: + return "MULK"; + case LOP_DIVK: + return "DIVK"; + case LOP_IDIVK: + return "IDIVK"; + case LOP_MODK: + return "MODK"; + case LOP_POWK: + return "POWK"; + case LOP_SUBRK: + return "SUBRK"; + case LOP_DIVRK: + return "DIVRK"; + case LOP_AND: + return "AND"; + case LOP_OR: + return "OR"; + case LOP_ANDK: + return "ANDK"; + case LOP_ORK: + return "ORK"; + case LOP_CONCAT: + return "CONCAT"; + case LOP_NOT: + return "NOT"; + case LOP_MINUS: + return "MINUS"; + case LOP_LENGTH: + return "LENGTH"; + case LOP_NEWTABLE: + return "NEWTABLE"; + case LOP_DUPTABLE: + return "DUPTABLE"; + case LOP_SETLIST: + return "SETLIST"; + case LOP_FORNPREP: + return "FORNPREP"; + case LOP_FORNLOOP: + return "FORNLOOP"; + case LOP_FORGPREP: + return "FORGPREP"; + case LOP_FORGLOOP: + return "FORGLOOP"; + case LOP_FORGPREP_INEXT: + return "FORGPREP_INEXT"; + case LOP_FORGPREP_NEXT: + return "FORGPREP_NEXT"; + case LOP_GETVARARGS: + return "GETVARARGS"; + case LOP_DUPCLOSURE: + return "DUPCLOSURE"; + case LOP_PREPVARARGS: + return "PREPVARARGS"; + case LOP_BREAK: + return "BREAK"; + case LOP_JUMPBACK: + return "JUMPBACK"; + case LOP_LOADKX: + return "LOADKX"; + case LOP_JUMPX: + return "JUMPX"; + case LOP_FASTCALL: + return "FASTCALL"; + case LOP_FASTCALL1: + return "FASTCALL1"; + case LOP_FASTCALL2: + return "FASTCALL2"; + case LOP_FASTCALL2K: + return "FASTCALL2K"; + case LOP_FASTCALL3: + return "FASTCALL3"; + case LOP_COVERAGE: + return "COVERAGE"; + case LOP_CAPTURE: + return "CAPTURE"; + case LOP_JUMPXEQKNIL: + return "JUMPXEQKNIL"; + case LOP_JUMPXEQKB: + return "JUMPXEQKB"; + case LOP_JUMPXEQKN: + return "JUMPXEQKN"; + case LOP_JUMPXEQKS: + return "JUMPXEQKS"; + case LOP_GETUDATAKS: + return "GETUDATAKS"; + case LOP_SETUDATAKS: + return "SETUDATAKS"; + case LOP_NAMECALLUDATA: + return "NAMECALLUDATA"; + case LOP_NEWCLASSMEMBER: + return "NEWCLASSMEMBER"; + case LOP_CMPPROTO: + return "CMPPROTO"; + case LOP_FASTPCALL: + return "FASTPCALL"; + case LOP_NEWCLASS: + return "NEWCLASS"; + default: + LUAU_ASSERT(!"Unsupported opcode"); + return "unknown"; + } +} + +void toString(ToStringContext& ctx, const BcImm& imm) +{ + switch (imm.kind) + { + case BcImmKind::Boolean: + append(ctx.result, "%s", imm.valueBoolean ? "true" : "false"); + break; + case BcImmKind::Int: + append(ctx.result, "%d", imm.valueInt); + break; + case BcImmKind::Import: + append(ctx.result, "%u", imm.valueImport); + break; + } +} + +void toString(ToStringContext& ctx, BcFunction& function, const BcVmConst& data, bool detailed) +{ + switch (data.kind) + { + case BcVmConstKind::Nil: + append(ctx.result, "nil"); + break; + case BcVmConstKind::Boolean: + append(ctx.result, "%s", data.valueBoolean ? "true" : "false"); + break; + case BcVmConstKind::Number: + append(ctx.result, "%.17g", data.valueNumber); + break; + case BcVmConstKind::Vectorf: + if (data.valueVectorf[3] == 0.0f) + append(ctx.result, "%.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2]); + else + append(ctx.result, "%.9g, %.9g, %.9g, %.9g", data.valueVectorf[0], data.valueVectorf[1], data.valueVectorf[2], data.valueVectorf[3]); + break; + case BcVmConstKind::Vectord: + if (data.valueVectord[3] == 0.0) + append(ctx.result, "%.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2]); + else + append(ctx.result, "%.17g, %.17g, %.17g, %.17g", data.valueVectord[0], data.valueVectord[1], data.valueVectord[2], data.valueVectord[3]); + break; + case BcVmConstKind::String: + if (isPrintableStringConstant(data.valueString.data(), data.valueString.length())) + { + if (data.valueString.length() < 32) + append(ctx.result, "'%.*s'", int(data.valueString.length()), data.valueString.data()); + else + append(ctx.result, "'%.*s'...", 32, data.valueString.data()); + } + else + { + append(ctx.result, "'"); + + for (size_t i = 0; i < data.valueString.length() && i < 32; ++i) + { + if (unsigned(data.valueString[i]) < ' ') + append(ctx.result, "\\x%02X", uint8_t(data.valueString[i])); + else + append(ctx.result, "%c", data.valueString[i]); + } + + if (data.valueString.length() >= 32) + append(ctx.result, "'..."); + else + append(ctx.result, "'"); + } + break; + case BcVmConstKind::Import: + { + int32_t id0 = -1, id1 = -1, id2 = -1; + if (int count = decomposeImportId(data.valueImport, id0, id1, id2)) + { + { + const BcVmConst& id = function.constants[id0]; + LUAU_ASSERT(id.kind == BcVmConstKind::String); + append(ctx.result, "%.*s", int(id.valueString.length()), id.valueString.data()); + } + + if (count > 1) + { + const BcVmConst& id = function.constants[id1]; + LUAU_ASSERT(id.kind == BcVmConstKind::String); + append(ctx.result, ".%.*s", int(id.valueString.length()), id.valueString.data()); + } + + if (count > 2) + { + const BcVmConst& id = function.constants[id2]; + LUAU_ASSERT(id.kind == BcVmConstKind::String); + append(ctx.result, ".%.*s", int(id.valueString.length()), id.valueString.data()); + } + } + } + break; + case BcVmConstKind::Table: + // TODO: detailed mode + append(ctx.result, "{...}"); + break; + case BcVmConstKind::Closure: + // TODO: detailed mode + append(ctx.result, "%u", data.valueClosure); + break; + case BcVmConstKind::Integer: + append(ctx.result, "%lld", (long long)data.valueInteger); + break; + case BcVmConstKind::ClassShape: + const BytecodeBuilder::ClassShape& cs = function.classShapes[data.valueClassShape]; + const BcVmConst& className = function.constants[cs.className]; + LUAU_ASSERT(className.kind == BcVmConstKind::String); + std::string_view str = className.valueString; + LUAU_ASSERT(isPrintableStringConstant(className.valueString.data(), className.valueString.length())); + append(ctx.result, "class %.*s (props: %zu, methods: %zu)", int(str.length()), str.data(), cs.propertyNames.size(), cs.methodNames.size()); + + // TODO: detailed mode + break; + } +} + +void toString(ToStringContext& ctx, BcFunction& function, TValue* data, bool detailed) +{ + // TODO: runtime representation like in dumpConstant from RuntimeBytecodeBuilder.h +} + +std::string dump(BcFunction& function) +{ + std::string result = toString(function, true); + + printf("%s\n", result.c_str()); + + return result; +} + +std::string dump(BcFunction& function) +{ + std::string result = toString(function, true); + + printf("%s\n", result.c_str()); + + return result; +} + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/src/BytecodeGraph.cpp b/External/luau/Bytecode/src/BytecodeGraph.cpp new file mode 100644 index 00000000..fa01e6fc --- /dev/null +++ b/External/luau/Bytecode/src/BytecodeGraph.cpp @@ -0,0 +1,477 @@ +#include "Luau/BytecodeBuilder.h" +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeWire.h" + +#include "BytecodeGraphParser.h" +#include "BytecodeGraphSerializer.h" + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) +LUAU_FASTFLAG(LuauCostModel) +LUAU_FASTFLAG(LuauCallFeedback) +LUAU_FASTFLAG(LuauCompileRefactorFeedback) + +namespace Luau +{ +namespace Bytecode +{ + +static std::string_view readString(std::vector& strings, const char* data, size_t& offset) +{ + uint32_t stringId = readVarInt(data, offset); + LUAU_ASSERT(stringId <= strings.size()); + if (stringId == 0) + return ""; + return strings[stringId - 1]; +} + +std::optional fromFunctionBytecode(std::string bytecode, std::vector& strings) +{ + CompTimeBcFunction fn; + size_t offset = 0; + const char* data = bytecode.data(); + fn.maxstacksize = read(data, offset); + fn.numparams = read(data, offset); + fn.nups = read(data, offset); + fn.is_vararg = read(data, offset); + fn.flags = read(data, offset); + + uint32_t typesSize = readVarInt(data, offset); + if (typesSize > 0) + { + uint32_t typeInfoSize = readVarInt(data, offset); + uint32_t typedUpvalSize = readVarInt(data, offset); + uint32_t typedLocalSize = readVarInt(data, offset); + fn.typeInfo = bytecode.substr(offset, typeInfoSize); + offset += typeInfoSize; + + fn.upvalueTypes.resize(typedUpvalSize); + for (uint32_t i = 0; i < typedUpvalSize; i++) + fn.upvalueTypes[i] = static_cast(read(data, offset)); + + fn.localTypes.resize(typedLocalSize); + for (uint32_t i = 0; i < typedLocalSize; i++) + { + LuauBytecodeType type = static_cast(read(data, offset)); + uint8_t reg = read(data, offset); + uint32_t startpc = readVarInt(data, offset); + uint32_t endpc = startpc + readVarInt(data, offset); + fn.localTypes[i] = {type, reg, startpc, endpc}; + } + } + + // store pointer to bytecode + int32_t codesize = readVarInt(data, offset); + const Instruction* code = reinterpret_cast(data + offset); + + offset += codesize * sizeof(Instruction); + + // read constants + const uint32_t sizek = readVarInt(data, offset); + fn.constants.resize(sizek); + for (uint32_t i = 0; i < sizek; i++) + { + uint8_t constType = read(data, offset); + switch (constType) + { + case LBC_CONSTANT_NIL: + fn.constants[i].kind = BcVmConstKind::Nil; + break; + + case LBC_CONSTANT_BOOLEAN: + { + fn.constants[i].kind = BcVmConstKind::Boolean; + fn.constants[i].valueBoolean = read(data, offset); + break; + } + + case LBC_CONSTANT_NUMBER: + { + fn.constants[i].kind = BcVmConstKind::Number; + fn.constants[i].valueNumber = read(data, offset); + break; + } + + case LBC_CONSTANT_VECTOR: + { + fn.constants[i].kind = BcVmConstKind::Vectorf; + fn.constants[i].valueVectorf[0] = read(data, offset); + fn.constants[i].valueVectorf[1] = read(data, offset); + fn.constants[i].valueVectorf[2] = read(data, offset); + fn.constants[i].valueVectorf[3] = read(data, offset); + break; + } + + case LBC_CONSTANT_VECTORD: + { + fn.constants[i].kind = BcVmConstKind::Vectord; + fn.constants[i].valueVectord[0] = read(data, offset); + fn.constants[i].valueVectord[1] = read(data, offset); + fn.constants[i].valueVectord[2] = read(data, offset); + fn.constants[i].valueVectord[3] = read(data, offset); + break; + } + + case LBC_CONSTANT_STRING: + { + fn.constants[i].kind = BcVmConstKind::String; + fn.constants[i].valueString = readString(strings, data, offset); + break; + } + + case LBC_CONSTANT_IMPORT: + { + fn.constants[i].kind = BcVmConstKind::Import; + fn.constants[i].valueImport = read(data, offset); + break; + } + + case LBC_CONSTANT_TABLE: + case LBC_CONSTANT_TABLE_WITH_CONSTANTS: + { + fn.constants[i].kind = BcVmConstKind::Table; + fn.constants[i].valueTable = uint32_t(fn.tableShapes.size()); + + BytecodeBuilder::TableShape shape; + shape.length = readVarInt(data, offset); + shape.hasConstants = constType == LBC_CONSTANT_TABLE_WITH_CONSTANTS; + + for (uint32_t i = 0; i < shape.length; ++i) + { + uint32_t key = readVarInt(data, offset); + LUAU_ASSERT(key < sizek); + shape.keys[i] = key; + if (shape.hasConstants) + { + int32_t value = read(data, offset); + LUAU_ASSERT(value < static_cast(sizek)); + shape.constants[i] = value; + } + } + fn.tableShapes.push_back(shape); + break; + } + + case LBC_CONSTANT_CLOSURE: + { + fn.constants[i].kind = BcVmConstKind::Closure; + fn.constants[i].valueClosure = readVarInt(data, offset); + break; + } + + case LBC_CONSTANT_INTEGER: + { + fn.constants[i].kind = BcVmConstKind::Integer; + bool isNegative = read(data, offset); + uint64_t magnitude = readVarInt64(data, offset); + fn.constants[i].valueInteger = isNegative ? (int64_t)(~magnitude + 1) : (int64_t)magnitude; + break; + } + + case LBC_CONSTANT_CLASS_SHAPE: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + fn.constants[i].kind = BcVmConstKind::ClassShape; + fn.constants[i].valueClassShape = uint32_t(fn.classShapes.size()); + + BytecodeBuilder::ClassShape shape; + shape.className = readVarInt(data, offset); + + uint32_t numProps = readVarInt(data, offset); + uint32_t numMethods = readVarInt(data, offset); + + shape.propertyNames.reserve(numProps); + shape.methodNames.reserve(numMethods); + + for (uint32_t i = 0; i < numProps; ++i) + shape.propertyNames.emplace_back(readVarInt(data, offset)); + + for (uint32_t i = 0; i < numMethods; ++i) + shape.methodNames.emplace_back(readVarInt(data, offset)); + + fn.classShapes.push_back(shape); + break; + } + + default: + LUAU_ASSERT(!"Unknown constant type!"); + } + } + + uint32_t psize = readVarInt(data, offset); + fn.protos.resize(psize); + for (uint32_t i = 0; i < psize; i++) + fn.protos[i] = readVarInt(data, offset); + + fn.linedefined = readVarInt(data, offset); + fn.debugname = readString(strings, data, offset); + + uint8_t lineinfo = read(data, offset); + std::vector lines; + + if (lineinfo != 0) + { + uint8_t linegaplog2 = read(data, offset); + + int intervals = ((codesize - 1) >> linegaplog2) + 1; + int absoffset = (codesize + 3) & ~3; + + const int sizelineinfo = absoffset + intervals * sizeof(int); + std::vector lineinfo; + lineinfo.resize(sizelineinfo); + int* abslineinfo = reinterpret_cast(lineinfo.data() + absoffset); + + uint8_t lastoffset = 0; + for (int i = 0; i < codesize; i++) + { + lastoffset += read(data, offset); + lineinfo[i] = lastoffset; + } + + int lastline = 0; + for (int i = 0; i < intervals; i++) + { + lastline += read(data, offset); + abslineinfo[i] = lastline; + } + lines.resize(codesize); + for (int i = 0; i < codesize; i++) + lines[i] = abslineinfo[i >> linegaplog2] + lineinfo[i]; + } + + uint8_t debuginfo = read(data, offset); + + if (debuginfo != 0) + { + const int sizelocvars = readVarInt(data, offset); + fn.locals.resize(sizelocvars); + + for (int i = 0; i < sizelocvars; i++) + { + std::string_view varname = readString(strings, data, offset); + uint32_t startpc = readVarInt(data, offset); + uint32_t endpc = readVarInt(data, offset); + uint8_t reg = read(data, offset); + fn.locals[i] = {varname, reg, startpc, endpc}; + } + + const int sizeupvalues = readVarInt(data, offset); + fn.upvalueNames.resize(sizeupvalues); + + for (int i = 0; i < sizeupvalues; i++) + fn.upvalueNames[i] = readString(strings, data, offset); + } + + if (FFlag::LuauCallFeedback) + { + uint32_t feedbackvecsize = readVarInt(data, offset); + fn.feedbackSlots.resize(feedbackvecsize); + + for (uint32_t j = 0; j < feedbackvecsize; j++) + { + uint8_t slottype = read(data, offset); + + BcFeedbackSlot& slot = fn.feedbackSlots[j]; + slot.kind = static_cast(slottype); + + if (slottype == LFT_CALLTARGET) + { + uint32_t pc = readVarInt(data, offset); + LUAU_ASSERT(pc < uint32_t(codesize)); + LUAU_ASSERT(LUAU_INSN_OP(code[pc]) == LOP_CALLFB); + slot.callTarget.inst = pc; + } + else + { + LUAU_ASSERT(!"unknown feedback slot kind"); + } + } + } + + if (FFlag::LuauCostModel) + { + if ((fn.flags & LPF_INLINABLE) != 0) + readVarInt64(data, offset); + } + + std::vector insnsPC; + BytecodeGraphParser graphParser(fn); + if (!graphParser.rebuildGraph(code, codesize, lines, insnsPC)) + return {}; + + for (BcFeedbackSlot& slot : fn.feedbackSlots) + { + if (slot.kind == LFT_CALLTARGET) + slot.callTarget.inst = insnsPC[slot.callTarget.inst]; + } + + for (TypedLocal& l : fn.localTypes) + { + l.startpc = l.startpc < insnsPC.size() ? insnsPC[l.startpc] : codesize; + l.endpc = l.endpc < insnsPC.size() ? insnsPC[l.endpc] : codesize; + } + + for (DebugLocal& l : fn.locals) + { + l.startpc = l.startpc < insnsPC.size() ? insnsPC[l.startpc] : codesize; + l.endpc = l.endpc < insnsPC.size() ? insnsPC[l.endpc] : codesize; + } + + return {fn}; +} + +struct CompTimeBytecodeGraphSerializer : public BytecodeGraphSerializer +{ + std::vector& consts; + CompTimeBytecodeGraphSerializer(BytecodeBuilder& bcb, CompTimeBcFunction& fn, std::vector& consts) + : BytecodeGraphSerializer(bcb, fn) + , consts(consts) + { + } + + uint32_t getVmConstInputRaw(BcInst& insn, uint8_t index) override + { + uint32_t cid = BytecodeGraphSerializer::getVmConstInputRaw(insn, index); + LUAU_ASSERT(cid < consts.size()); + return consts[cid]; + } +}; + +std::string toFunctionBytecode(BytecodeBuilder& bcb, CompTimeBcFunction& fn) +{ + uint32_t functionId = bcb.beginFunction(fn.numparams, fn.is_vararg); + if (fn.debugname != "") + bcb.setDebugFunctionName({fn.debugname.data(), fn.debugname.size()}); + bcb.setDebugFunctionLineDefined(fn.linedefined); + bcb.setFunctionTypeInfo(fn.typeInfo); + for (LuauBytecodeType t : fn.upvalueTypes) + bcb.pushUpvalTypeInfo(t); + for (auto& upval : fn.upvalueNames) + bcb.pushDebugUpval({upval.data(), upval.size()}); + + std::vector consts; + consts.reserve(fn.constants.size()); + + for (auto& c : fn.constants) + { + switch (c.kind) + { + case BcVmConstKind::Nil: + consts.push_back(bcb.addConstantNil()); + break; + + case BcVmConstKind::Boolean: + consts.push_back(bcb.addConstantBoolean(c.valueBoolean)); + break; + + case BcVmConstKind::Number: + consts.push_back(bcb.addConstantNumber(c.valueNumber)); + break; + + case BcVmConstKind::Vectorf: + consts.push_back(bcb.addConstantVectorf(c.valueVectorf[0], c.valueVectorf[1], c.valueVectorf[2], c.valueVectorf[3])); + break; + + case BcVmConstKind::Vectord: + consts.push_back(bcb.addConstantVectord(c.valueVectord[0], c.valueVectord[1], c.valueVectord[2], c.valueVectord[3])); + break; + + case BcVmConstKind::String: + consts.push_back(bcb.addConstantString({c.valueString.data(), c.valueString.size()})); + break; + + case BcVmConstKind::Import: + consts.push_back(bcb.addImport(c.valueImport)); + break; + + case BcVmConstKind::Table: + { + LUAU_ASSERT(c.valueTable < fn.tableShapes.size()); + consts.push_back(bcb.addConstantTable(fn.tableShapes[c.valueTable])); + break; + } + + case BcVmConstKind::Closure: + consts.push_back(bcb.addConstantClosure(c.valueClosure)); + break; + + case BcVmConstKind::Integer: + consts.push_back(bcb.addConstantInteger(c.valueInteger)); + break; + + case BcVmConstKind::ClassShape: + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + LUAU_ASSERT(c.valueClassShape < fn.classShapes.size()); + consts.push_back(bcb.addClassShape(fn.classShapes[c.valueClassShape])); + break; + } + } + } + + for (auto fid : fn.protos) + bcb.addChildFunction(fid); + + CompTimeBytecodeGraphSerializer serializer(bcb, fn, consts); + std::vector insnsPC = serializer.emitBytecode(); + + for (uint32_t i = 0; i < fn.feedbackSlots.size(); ++i) + { + BcFeedbackSlot& slot = fn.feedbackSlots[i]; + uint32_t slotId = ~0u; + + if (slot.kind == LFT_CALLTARGET) + { + LUAU_ASSERT(slot.callTarget.inst < insnsPC.size()); + LUAU_ASSERT(insnsPC[slot.callTarget.inst] != ~0u); + + if (FFlag::LuauCompileRefactorFeedback) + slotId = bcb.addCallTargetSlot(insnsPC[slot.callTarget.inst]); + else + slotId = bcb.addFbSlot_DEPRECATED(LFT_CALLTARGET, insnsPC[slot.callTarget.inst]); + } + else + { + LUAU_ASSERT(!"unknown feedback slot kind"); + } + + LUAU_ASSERT(slotId == i); + } + + for (auto& local : fn.localTypes) + { + uint32_t startpc = local.startpc < insnsPC.size() ? insnsPC[local.startpc] : bcb.getDebugPC(); + uint32_t endpc = local.endpc < insnsPC.size() ? insnsPC[local.endpc] : bcb.getDebugPC(); + bcb.pushLocalTypeInfo(local.type, local.reg, startpc, endpc); + } + for (auto& local : fn.locals) + { + uint32_t startpc = local.startpc < insnsPC.size() ? insnsPC[local.startpc] : bcb.getDebugPC(); + uint32_t endpc = local.endpc < insnsPC.size() ? insnsPC[local.endpc] : bcb.getDebugPC(); + bcb.pushDebugLocal({local.varname.data(), local.varname.size()}, local.reg, startpc, endpc); + } + + bcb.foldJumps(); + + bool hasLongJumpError = false; + bcb.expandJumps(hasLongJumpError); + + if (hasLongJumpError) + return ""; + + bcb.endFunction(fn.maxstacksize, fn.nups, fn.flags); + + if (serializer.error) + return ""; + + return bcb.getFunctionData(functionId); +} + +std::string toFunctionBytecode(CompTimeBcFunction& fn) +{ + BytecodeBuilder bcb; + return toFunctionBytecode(bcb, fn); +} + +}; // namespace Bytecode +}; // namespace Luau diff --git a/External/luau/Bytecode/src/BytecodeGraphParser.h b/External/luau/Bytecode/src/BytecodeGraphParser.h new file mode 100644 index 00000000..5d0c221b --- /dev/null +++ b/External/luau/Bytecode/src/BytecodeGraphParser.h @@ -0,0 +1,1073 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/Common.h" +#include "Luau/InsertionOrderedMap.h" + +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses) + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BytecodeGraphParser +{ + struct BlockProducers + { + std::unordered_map own; + std::unordered_map cached; + BcOp multiReturn; + Reg multiReturnStart; + int invalidAfter = 255; + // incomplete phi construction state: + // - a block is sealed once all its predecessors have been emitted + // - reads that cross a not-yet-emitted predecessor (a back-edge) + // create an operand-less phi in incompletePhis until the block is sealed and the operands can be filled + // - incomplete phis are filled in with operands that all occupy the same register, after all preds have been emitted + // + // this enables loop phis to be created without a separate pass (Braun "Simple and Efficient Construction of Static Single Assignment Form") + bool sealed = false; + uint32_t unsealedPreds = 0; + InsertionOrderedMap incompletePhis; + }; + + using Producers = std::vector; + + BcFunction& func; + std::unordered_map blockByPC; + Producers producers; + BcOp currentBlock; + std::unordered_map phiBlock; + + BytecodeGraphParser(BcFunction& func) + : func(func) + { + } + + void addSuccessor(BcOp fromOp, BcOp toOp, BcBlockEdgeKind kind) + { + BcBlock& from = func.blockOp(fromOp); + BcBlock& to = func.blockOp(toOp); + from.successors.push_back({kind, toOp}); + to.predecessors.push_back({kind, fromOp}); + } + + BcOp makeBlock(uint32_t pc) + { + BcOp newBlockOp = func.addBlock(); + blockByPC[pc] = newBlockOp; + BcBlock& newBlock = func.blockOp(newBlockOp); + newBlock.sortkey = pc; + return newBlockOp; + } + + bool isJumpTrampoline(uint32_t pc, const Instruction* code, uint32_t codesize) + { + return LuauOpcode(LUAU_INSN_OP(code[pc])) == LOP_JUMP && pc + 1 < codesize && LuauOpcode(LUAU_INSN_OP(code[pc + 1])) == LOP_JUMPX && + static_cast(getJumpTarget(code[pc + 2], pc + 2)) == pc + 1; + } + + size_t rebuildBlocks(const Instruction code[], uint32_t codesize) + { + BcOp entryBlock = func.entryBlock = makeBlock(0); + BcOp exitBlock = func.exitBlock = makeBlock(kBlockNoStartPc); + uint32_t i = 0; + BcOp currentBlock = entryBlock; + size_t instructionCount = 0; + while (i < codesize) + { + Instruction insn = code[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + int target = getJumpTarget(insn, i); + if (target >= 0 && LuauOpcode(LUAU_INSN_OP(code[target])) == LOP_JUMPX) + target = getJumpTarget(code[target], target); + + bool needsBlock = target >= 0 && !isFastCall(op) && op != LOP_JUMPX && !isJumpTrampoline(i, code, codesize); + if (needsBlock) + { + if (blockByPC.count(target) == 0) + { + BcOp newBlockOp = makeBlock(target); + if (target < static_cast(i)) // We are jumping back. + { + // The new block was created in the middle of the existing one. + // We need to maintain predecessor/successor relations. + uint32_t blockStartPc = target - 1; + while (blockByPC.count(blockStartPc) == 0 && blockStartPc-- != 0) + ; + LUAU_ASSERT(blockByPC.count(blockStartPc) > 0); + BcOp prevBlockOp = blockByPC[blockStartPc]; + BcBlock& prevBlock = func.blockOp(prevBlockOp); + BcBlock& newBlock = func.blockOp(newBlockOp); + // Steal successors of the previous block. + newBlock.successors = prevBlock.successors; + // Now it should only fallsthrough to the new block. + prevBlock.successors.clear(); + addSuccessor(prevBlockOp, newBlockOp, BcBlockEdgeKind::Fallthrough); + // Update all successors to have the new block as a predecessor instead of the old one. + for (auto& edge : newBlock.successors) + for (auto& backEdge : func.blockOp(edge.target).predecessors) + if (backEdge.target == prevBlockOp) + backEdge.target = newBlockOp; + } + } + addSuccessor(currentBlock, blockByPC[target], isLoopJump(op) ? BcBlockEdgeKind::Loop : BcBlockEdgeKind::Branch); + } + if (op == LOP_RETURN) + addSuccessor(currentBlock, exitBlock, BcBlockEdgeKind::Fallthrough); + i += getOpLength(op); + if ((needsBlock || (op == LOP_RETURN && i < codesize)) && blockByPC.count(i) == 0) + makeBlock(i); + + if (blockByPC.count(i) != 0) + { + if (isFallthrough(op)) + addSuccessor(currentBlock, blockByPC[i], BcBlockEdgeKind::Fallthrough); + currentBlock = blockByPC[i]; + } + instructionCount++; + } + return instructionCount; + } + + BcOp makePhi(BcOp block, Reg reg) + { + BcOp phiOp = func.addPhi(); + func.regs[phiOp] = reg; + func.blockOp(block).phis.push_back(phiOp); + phiBlock[phiOp] = block; + return phiOp; + } + + std::optional readVariable(BcOp block, Reg reg) + { + BlockProducers& bp = producers.at(block.index); + if (static_cast(reg) > bp.invalidAfter) + return {}; + if (auto it = bp.own.find(reg); it != bp.own.end()) + return it->second; + if (auto it = bp.cached.find(reg); it != bp.cached.end()) + return it->second; + if (bp.multiReturn.kind != BcOpKind::None && reg >= bp.multiReturnStart) + { + // cache the projection so repeated reads (and phi operands across edges) share identity, + // which keeps tryRemoveTrivialPhi able to recognize equal operands + BcOp proj = func.addProj(bp.multiReturn, reg - bp.multiReturnStart); + bp.cached[reg] = proj; + return proj; + } + return readVariableRecursive(block, reg); + } + + BcOp readVariableRecursive(BcOp block, Reg reg) + { + BlockProducers& bp = producers.at(block.index); + BcEdges& preds = func.blockOp(block).predecessors; + + if (!bp.sealed) + { + // predecessors not all emitted yet, so we create an incomplete phi to fill on seal + BcOp phiOp = makePhi(block, reg); + bp.incompletePhis[reg] = phiOp; + bp.cached[reg] = phiOp; + return phiOp; + } + + if (preds.empty()) + return BcOp{BcOpKind::VmReg, reg}; // undefined (entry/unreachable) + + if (preds.size() == 1) + { + // No phi needed, as a single-predecessor block can never a loop header so this can only cycle inside a fully-unreachable single-pred loop + // we cache anyway so if we have a re-entry, we just resolve to undef, rather than recursing forever + bp.cached[reg] = BcOp{BcOpKind::VmReg, reg}; + BcOp val = readVariable(preds[0].target, reg).value_or(BcOp{BcOpKind::VmReg, reg}); + producers.at(block.index).cached[reg] = val; + return val; + } + + // multiple predecessors: create the phi and cache it *before* filling so a back-edge read + // that recurses back into this block resolves to the phi instead of looping forever + BcOp phiOp = makePhi(block, reg); + bp.cached[reg] = phiOp; + BcOp val = addPhiOperands(reg, phiOp, block); + producers.at(block.index).cached[reg] = val; + return val; + } + + BcOp addPhiOperands(Reg reg, BcOp phiOp, BcOp block) + { + LUAU_ASSERT(phiOp.kind == BcOpKind::Phi); + + BcRef phi = func.phi(phiOp); + + // include every predecessor edge, back-edges included + for (auto& [_, pred] : func.blockOp(block).predecessors) + { + if (std::optional v = readVariable(pred, reg)) + { + func.addUse(phi, *v); + } + } + return tryRemoveTrivialPhi(phiOp); + } + + // collapse a phi whose operands (ignoring self-references) are a single distinct value + // NOTE: a self-referential loop phi left in place would make the serializer's getRegister recurse forever + BcOp tryRemoveTrivialPhi(BcOp phiOp) + { + std::optional trivialValue = std::nullopt; + for (BcOp op : func.phiOp(phiOp).ops) + { + if (op == phiOp || (trivialValue.has_value() && op == *trivialValue)) + continue; + + if (trivialValue.has_value()) + return phiOp; // two distinct values, we cannot eliminate this + + trivialValue = op; + } + + Reg reg = static_cast(func.regs.at(phiOp)); + if (!trivialValue.has_value()) + trivialValue = BcOp{BcOpKind::VmReg, reg}; // unreachable or undefined, so we collapse to an VmReg read + + BcRef phiRef = func.phi(phiOp); + std::vector users = std::move(phiRef->uses); + + // we need to now update users to point to the new trivial value + for (BcOp user : users) + { + if (user == phiOp) + continue; + + BcOps& userOps = (user.kind == BcOpKind::Phi) ? func.phiOp(user).ops : func.instOp(user).ops; + for (BcOp& op : userOps) + if (op == phiOp) + { + op = *trivialValue; + + // re-record the reverse edge on whichever def now owns this operand + func.recordUse(*trivialValue, user); + } + } + + // remove the collapsed phi from its block + if (auto bit = phiBlock.find(phiOp); bit != phiBlock.end()) + { + func.blockOp(bit->second).phis.remove(phiOp); + BlockProducers& bp = producers.at(bit->second.index); + if (auto cit = bp.cached.find(reg); cit != bp.cached.end() && cit->second == phiOp) + cit->second = *trivialValue; + phiBlock.erase(bit); + } + + for (BcOp user : users) + if (user.kind == BcOpKind::Phi && user != phiOp) + tryRemoveTrivialPhi(user); + + return *trivialValue; + } + + void sealBlock(BcOp block) + { + BlockProducers& bp = producers.at(block.index); + if (bp.sealed) + return; + // mark sealed first so reads triggered while filling use the normal (non-incomplete) path + bp.sealed = true; + + InsertionOrderedMap pending = std::move(bp.incompletePhis); + bp.incompletePhis.clear(); + + for (const auto& [reg, phiOp] : pending) + { + BcOp val = addPhiOperands(reg, phiOp, block); + BlockProducers& cur = producers.at(block.index); + if (auto cit = cur.cached.find(reg); cit != cur.cached.end() && cit->second == phiOp) + cit->second = val; + } + } + + void finalizeBlock(BcOp block) + { + for (auto& [_, succ] : func.blockOp(block).successors) + { + BlockProducers& sp = producers.at(succ.index); + if (sp.unsealedPreds > 0) + { + --sp.unsealedPreds; + if (sp.unsealedPreds == 0) + sealBlock(succ); + } + } + } + + void sealAllRemaining() + { + for (uint32_t b = 0; b < func.blocks.size(); b++) + if (!producers.at(b).sealed) + sealBlock(BcOp{BcOpKind::Block, b}); + } + + std::vector findProducersUpToTop(BcOp block, Reg reg) + { + // We assume it called only for search of var return calls. + LUAU_ASSERT(block.index < producers.size()); + BlockProducers& blockProducers = producers.at(block.index); + // So we need to find all producers from reg to blockProducers.multiReturnStart. + LUAU_ASSERT(blockProducers.multiReturn.kind == BcOpKind::Inst); + std::vector res; + res.reserve(blockProducers.multiReturnStart - reg + 1); + for (; reg < blockProducers.multiReturnStart; reg++) + { + auto staticRegOp = readVariable(block, reg); + LUAU_ASSERT(staticRegOp); + res.push_back(*staticRegOp); + } + res.push_back(blockProducers.multiReturn); + // multireturn is consumed, clean it up + blockProducers.multiReturn = BcOp{}; + blockProducers.multiReturnStart = 0xFF; + return res; + } + + bool isUnreachable(BcOp blockOp) + { + if (blockOp == func.entryBlock) + return false; + BcBlock& block = func.blockOp(blockOp); + for (auto [ctrl, pred] : block.predecessors) + { + if (ctrl == BcBlockEdgeKind::Loop) + continue; + if (!isUnreachable(pred)) + return false; + } + return true; + } + + void addProducer(Reg reg, BcOp op) + { + BlockProducers& blockProducers = producers[currentBlock.index]; + blockProducers.own[reg] = op; + func.regs[op] = reg; + blockProducers.invalidAfter = std::max(static_cast(reg), blockProducers.invalidAfter); + } + + void applyCall(BlockProducers& producers, BcOp callOp, Reg targetReg, int nresults) + { + for (auto it = producers.own.begin(); it != producers.own.end();) + { + if (it->first >= targetReg) + { + it = producers.own.erase(it); + } + else + { + ++it; + } + } + for (auto it = producers.cached.begin(); it != producers.cached.end();) + { + if (it->first >= targetReg) + { + it = producers.cached.erase(it); + } + else + { + ++it; + } + } + if (nresults < 0) + { + producers.multiReturn = callOp; + producers.multiReturnStart = targetReg; + producers.invalidAfter = 255; + } + else + { + producers.invalidAfter = static_cast(targetReg) - 1 + nresults; + } + } + + void addImmInput(BcRef inst, bool value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Boolean}); + func.immediates.back().valueBoolean = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addImmInput(BcRef inst, int32_t value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Int}); + func.immediates.back().valueInt = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addImmInput(BcRef inst, uint32_t value) + { + BcOp op{BcOpKind::Imm, 0}; + func.immediates.push_back({BcImmKind::Import}); + func.immediates.back().valueImport = value; + op.index = func.immediates.size() - 1; + func.addUse(inst, op); + } + + void addVmConstInput(BcRef inst, uint32_t idx) + { + LUAU_ASSERT(idx < func.constants.size()); + func.addUse(inst, BcOp{BcOpKind::VmConst, idx}); + } + + void addUpvalInput(BcRef inst, uint32_t idx) + { + LUAU_ASSERT(idx < func.nups); + func.addUse(inst, BcOp{BcOpKind::VmUpvalue, idx}); + } + + void addProtoInput(BcRef inst, uint32_t idx) + { + func.addUse(inst, BcOp{BcOpKind::VmProto, idx}); + } + + void addVmRegInput(BcRef inst, Reg reg) + { + std::optional source = readVariable(currentBlock, reg); + if (!source && isUnreachable(currentBlock)) + { + func.addUse(inst, BcOp{BcOpKind::VmReg, reg}); + return; + } + LUAU_ASSERT(source); + func.addUse(inst, *source); + } + + void addJumpInput(BcRef inst, int target) + { + LUAU_ASSERT(!isFastCall(inst->op)); + if (target < 0) + { + LUAU_ASSERT(inst->op == LOP_LOADB); + return; + } + auto it = blockByPC.find(target); + LUAU_ASSERT(it != blockByPC.end()); + func.addUse(inst, it->second); + } + + void addEmptyInput(BcRef inst) + { + func.addUse(inst, BcOp{BcOpKind::None, 0}); + } + + static const uint32_t kMaxCFGBlocks = 1000; + + bool rebuildGraph(const Instruction code[], uint32_t codesize, std::vector& lines, std::vector& pcs) + { + size_t instructionsCount = rebuildBlocks(code, codesize); + if (blockByPC.size() > kMaxCFGBlocks) + return false; + + producers.resize(func.blocks.size()); + pcs.resize(codesize); + + currentBlock = func.entryBlock; + + for (Reg i = 0; i < func.numparams; i++) + addProducer(i, {BcOpKind::VmReg, i}); + + // a block is sealable once all its predecessors are emitted; seed the counts from the CFG + // (already fully built by rebuildBlocks) and seal blocks with no predecessors immediately + for (BcBlock& block : func.blocks) + producers.at(func.getBlockIndex(block)).unsealedPreds = uint32_t(block.predecessors.size()); + for (uint32_t blockIdx = 0; blockIdx < func.blocks.size(); blockIdx++) + if (producers.at(blockIdx).unsealedPreds == 0) + sealBlock(BcOp{BcOpKind::Block, blockIdx}); + + // Create instructions. + currentBlock = func.entryBlock; + func.instructions.reserve(instructionsCount); + + for (uint32_t i = 0; i < codesize;) + { + Instruction insn = code[i]; + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + int opLength = getOpLength(op); + uint32_t aux = (opLength > 1 && i + 1 < codesize) ? code[i + 1] : 0; + BcOp nodeOp = func.addInst(); + func.blockOp(currentBlock).appendInstruction(nodeOp); + BcRef node = func.inst(nodeOp); + node->block = currentBlock; + if (i < lines.size()) + node->line = lines[i]; + node->op = op; + + pcs[i] = nodeOp.index; + + auto parseJump = [&](LuauOpcode op, int jumpTarget) -> void + { + node->op = op; + switch (op) + { + case LOP_JUMPXEQKNIL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + break; + + case LOP_JUMPXEQKB: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + addImmInput(node, static_cast(aux & 0x1)); + break; + + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux >> 31)); + addJumpInput(node, jumpTarget); + addVmConstInput(node, aux & 0xFFFFFF); + break; + + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + addVmRegInput(node, LUAU_INSN_A(insn)); + addJumpInput(node, jumpTarget); + break; + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, aux); + addJumpInput(node, jumpTarget); + break; + + case LOP_FORNPREP: + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + // for loop prep: A..A+2 carry iterator state for both numeric loops and generalized + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addJumpInput(node, jumpTarget); + func.regs[nodeOp] = LUAU_INSN_A(insn); + for (int offset = 0; offset < 3; ++offset) + addProducer(LUAU_INSN_A(insn) + offset, func.addProj(nodeOp, offset)); + break; + + case LOP_FORNLOOP: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addJumpInput(node, jumpTarget); + func.regs[nodeOp] = LUAU_INSN_A(insn); + addProducer(LUAU_INSN_A(insn) + 2, func.addProj(nodeOp, 2)); + break; + + // FORGLOOP updates A+2 for the iterator register and A+3..A+vars for loop variables + case LOP_FORGLOOP: + { + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_A(insn) + 1); + addVmRegInput(node, LUAU_INSN_A(insn) + 2); + addImmInput(node, static_cast(aux >> 31)); + int32_t vars = aux & 0xFF; + addImmInput(node, vars); + addJumpInput(node, jumpTarget); + func.regs[nodeOp] = LUAU_INSN_A(insn); + for (int offset = 2; offset <= 2 + std::max(vars, 2); ++offset) + addProducer(LUAU_INSN_A(insn) + offset, func.addProj(nodeOp, offset)); + break; + } + + default: + LUAU_UNREACHABLE(); + } + }; + switch (op) + { + case LOP_NOP: + case LOP_BREAK: + case LOP_NATIVECALL: + break; + + case LOP_LOADNIL: + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADB: + addImmInput(node, static_cast(LUAU_INSN_B(insn))); + addJumpInput(node, getJumpTarget(insn, i)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADN: + addImmInput(node, static_cast(LUAU_INSN_D(insn))); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_LOADK: + addVmConstInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_MOVE: + addVmRegInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_GETGLOBAL: + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETGLOBAL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + break; + + case LOP_GETUPVAL: + addUpvalInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETUPVAL: + addVmRegInput(node, LUAU_INSN_A(insn)); + addUpvalInput(node, LUAU_INSN_B(insn)); + break; + + case LOP_CLOSEUPVALS: + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + break; + + case LOP_GETIMPORT: + { + addVmConstInput(node, LUAU_INSN_D(insn)); + int32_t componentsCount = aux >> 30; + addImmInput(node, componentsCount); + for (int32_t i = 0; i < componentsCount; i++) + addVmConstInput(node, (aux >> (20 - 10 * i)) & 0x3FF); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + } + + case LOP_GETTABLE: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETTABLE: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + break; + + case LOP_GETUDATAKS: + case LOP_GETTABLEKS: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + if (op == LOP_GETUDATAKS) + { + addVmConstInput(node, LUAU_INSN_AUX_KV16(aux)); + addImmInput(node, static_cast(LUAU_INSN_AUX_SLOT(aux))); + } + else + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETUDATAKS: + case LOP_SETTABLEKS: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + if (op == LOP_SETUDATAKS) + { + addVmConstInput(node, LUAU_INSN_AUX_KV16(aux)); + addImmInput(node, static_cast(LUAU_INSN_AUX_SLOT(aux))); + } + else + addVmConstInput(node, aux); + break; + + case LOP_GETTABLEN: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn) + 1)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_SETTABLEN: + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn) + 1)); + break; + + case LOP_NEWCLOSURE: + addProtoInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_NAMECALLUDATA: + case LOP_NAMECALL: + addVmRegInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + if (op == LOP_NAMECALLUDATA) + { + addVmConstInput(node, LUAU_INSN_AUX_KV16(aux)); + addImmInput(node, static_cast(LUAU_INSN_AUX_SLOT(aux))); + } + else + addVmConstInput(node, aux); + func.regs[nodeOp] = LUAU_INSN_A(insn); + addProducer(LUAU_INSN_A(insn), func.addProj(nodeOp, 0)); + addProducer(LUAU_INSN_A(insn) + 1, func.addProj(nodeOp, 1)); + break; + + case LOP_CALL: + case LOP_CALLFB: + { + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + addImmInput(node, static_cast(nparams)); + addImmInput(node, static_cast(nresults)); + if (op == LOP_CALLFB) + addImmInput(node, static_cast(aux)); + + // Call target. + addVmRegInput(node, LUAU_INSN_A(insn)); + // Fixed arguments. + for (int i = 1; i <= nparams; i++) + addVmRegInput(node, LUAU_INSN_A(insn) + i); + + if (nparams < 0) + { + // all arguments prepared before call in the same block + for (auto& inp : findProducersUpToTop(currentBlock, LUAU_INSN_A(insn) + 1)) + { + func.addUse(node, inp); + } + } + + BlockProducers& blockProducers = producers[currentBlock.index]; + applyCall(blockProducers, nodeOp, LUAU_INSN_A(insn), nresults); + + func.regs[nodeOp] = LUAU_INSN_A(insn); + for (int i = 0; i < nresults; i++) + addProducer(LUAU_INSN_A(insn) + i, func.addProj(nodeOp, i)); + break; + } + + case LOP_RETURN: + { + int nresults = LUAU_INSN_B(insn) - 1; + addImmInput(node, static_cast(nresults)); + for (int i = 0; i < nresults; i++) + addVmRegInput(node, LUAU_INSN_A(insn) + i); + if (nresults < 0) + for (auto& inp : findProducersUpToTop(currentBlock, LUAU_INSN_A(insn))) + func.addUse(node, inp); + if (nresults == 0) + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + break; + } + + case LOP_JUMP: + { + if (isJumpTrampoline(i, code, codesize)) + { + // it is long jump trampoline + int longOffset = LUAU_INSN_E(code[i + 1]); + i += getOpLength(LOP_JUMP) + getOpLength(LOP_JUMPX); + op = LuauOpcode(LUAU_INSN_OP(code[i])); + opLength = getOpLength(op); + aux = (opLength > 1 && i + 1 < codesize) ? code[i + 1] : 0; + parseJump(op, i + longOffset); + } + else + addJumpInput(node, getJumpTarget(insn, i)); + break; + } + + case LOP_JUMPBACK: + // repeat .. until loops use it for back edge. + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + case LOP_FORNPREP: + case LOP_FORNLOOP: + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + case LOP_FORGLOOP: + parseJump(op, getJumpTarget(insn, i)); + break; + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_AND: + case LOP_OR: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_ADDK: + case LOP_SUBK: + case LOP_MULK: + case LOP_DIVK: + case LOP_MODK: + case LOP_POWK: + case LOP_ANDK: + case LOP_ORK: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_CONCAT: + { + LUAU_ASSERT(LUAU_INSN_B(insn) <= LUAU_INSN_C(insn)); + for (Reg param = LUAU_INSN_B(insn); param <= LUAU_INSN_C(insn); param++) + addVmRegInput(node, param); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + } + + case LOP_NOT: + case LOP_MINUS: + case LOP_LENGTH: + addVmRegInput(node, LUAU_INSN_B(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_NEWTABLE: + addImmInput(node, static_cast(LUAU_INSN_B(insn))); + addImmInput(node, static_cast(aux)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_DUPTABLE: + addProducer(LUAU_INSN_A(insn), nodeOp); + addVmConstInput(node, LUAU_INSN_D(insn)); + break; + + case LOP_SETLIST: + { + int count = LUAU_INSN_C(insn) - 1; + addImmInput(node, static_cast(aux)); + addImmInput(node, static_cast(count)); + addVmRegInput(node, LUAU_INSN_A(insn)); + for (Reg param = 0; param < count; param++) + addVmRegInput(node, LUAU_INSN_B(insn) + param); + if (count < 0) + for (auto inp : findProducersUpToTop(currentBlock, LUAU_INSN_B(insn))) + func.addUse(node, inp); + break; + } + + case LOP_FASTCALL: + // Note that FASTCALL will read the actual call arguments, such as argument/result registers and counts, from the CALL instruction + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL1: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL2: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, aux & 0xFF); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL2K: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, aux); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_FASTCALL3: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, aux & 0xFF); + addVmRegInput(node, (aux >> 8) & 0xFF); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + + case LOP_GETVARARGS: + { + func.addUse(node, BcOp{BcOpKind::VmReg, LUAU_INSN_A(insn)}); + int count = LUAU_INSN_B(insn) - 1; + addImmInput(node, static_cast(count)); + func.regs[nodeOp] = LUAU_INSN_A(insn); + if (count < 0) + { + BlockProducers& blockProducers = producers[currentBlock.index]; + blockProducers.multiReturn = nodeOp; + blockProducers.multiReturnStart = LUAU_INSN_A(insn); + blockProducers.invalidAfter = 255; + } + else + for (int i = 0; i < count; i++) + addProducer(LUAU_INSN_A(insn) + i, func.addProj(nodeOp, i)); + break; + } + + case LOP_DUPCLOSURE: + addVmConstInput(node, LUAU_INSN_D(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_PREPVARARGS: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + break; + + case LOP_LOADKX: + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_JUMPX: + LUAU_ASSERT(!"Shouldn't parse it directly"); + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_COVERAGE: + addImmInput(node, static_cast(LUAU_INSN_E(insn))); + break; + + case LOP_CAPTURE: + { + uint8_t captureType = LUAU_INSN_A(insn); + addImmInput(node, static_cast(captureType)); + if (captureType == LCT_VAL || captureType == LCT_REF) + addVmRegInput(node, LUAU_INSN_B(insn)); + else + addUpvalInput(node, LUAU_INSN_B(insn)); + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + } + + case LOP_SUBRK: + case LOP_DIVRK: + addVmConstInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_IDIV: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_IDIVK: + addVmRegInput(node, LUAU_INSN_B(insn)); + addVmConstInput(node, LUAU_INSN_C(insn)); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + case LOP_CMPPROTO: + addVmRegInput(node, LUAU_INSN_A(insn)); + addImmInput(node, static_cast(aux)); + addJumpInput(node, getJumpTarget(insn, i)); + break; + + case LOP_NEWCLASSMEMBER: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + addVmRegInput(node, LUAU_INSN_A(insn)); + addVmRegInput(node, LUAU_INSN_C(insn)); + addVmConstInput(node, aux); + break; + + case LOP_FASTPCALL: + addImmInput(node, static_cast(LUAU_INSN_A(insn))); + addImmInput(node, static_cast(LUAU_INSN_B(insn))); + // turn it in BcOp to CALL BcInst&. + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + break; + case LOP_NEWCLASS: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + if (int reg = LUAU_INSN_B(insn); reg != 0xff) + addVmRegInput(node, LUAU_INSN_B(insn)); + else + addEmptyInput(node); + + addImmInput(node, static_cast(LUAU_INSN_C(insn))); + addVmConstInput(node, aux); + addProducer(LUAU_INSN_A(insn), nodeOp); + break; + + + case LOP__COUNT: + LUAU_UNREACHABLE(); + } + + i += opLength; + if (blockByPC.count(i) > 0) + { + // currentBlock is fully emitted: release it so its successors can seal once all their + // predecessors are emitted (a loop header seals here, after its back-edge source) + finalizeBlock(currentBlock); + currentBlock = blockByPC[i]; + } + } + + finalizeBlock(currentBlock); + sealAllRemaining(); // seal any block whose predecessors were never all emitted + + return true; + } +}; + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/src/BytecodeGraphSerializer.h b/External/luau/Bytecode/src/BytecodeGraphSerializer.h new file mode 100644 index 00000000..0d6d31b1 --- /dev/null +++ b/External/luau/Bytecode/src/BytecodeGraphSerializer.h @@ -0,0 +1,736 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/BytecodeGraph.h" +#include "Luau/BytecodeUtils.h" +#include "Luau/BytecodeOps.h" + +namespace Luau +{ +namespace Bytecode +{ + +template +struct BytecodeGraphSerializer +{ + struct JumpInfo + { + LuauOpcode op; + uint32_t instructionPC; + BcOp targetBlock; + }; + using Jumps = std::vector; + + BytecodeBuilder& bcb; + BcFunction& func; + Jumps jumps; + bool error = false; + + BytecodeGraphSerializer(BytecodeBuilder& bcb, BcFunction& func) + : bcb(bcb) + , func(func) + { + } + + std::vector reschedule() + { + std::vector sortedBlocks; + sortedBlocks.reserve(func.blocks.size()); + for (uint32_t i = 0; i < func.blocks.size(); i++) + if ((func.blocks[i].flags & BcBlockFlag::Dead) == 0) + sortedBlocks.push_back(BcOp{BcOpKind::Block, i}); + + std::sort( + sortedBlocks.begin(), + sortedBlocks.end(), + [&](BcOp opA, BcOp opB) + { + const BcBlock& a = func.blockOp(opA); + const BcBlock& b = func.blockOp(opB); + + if (a.sortkey == b.sortkey) + return a.chainkey < b.chainkey; + + return a.sortkey < b.sortkey; + } + ); + + LUAU_ASSERT(sortedBlocks.back() == func.exitBlock); + sortedBlocks.pop_back(); + + return sortedBlocks; + } + + uint32_t getRegisterRaw(BcOp op) + { + switch (op.kind) + { + case BcOpKind::Phi: + { + BcPhi& phi = func.phiOp(op); + LUAU_ASSERT(phi.ops.size() > 0); + // Parser-built phis record their register at creation in `makePhi` + // Loop-carried phis form operand cycles, as a nested loop's inner and outer accumulator phis are mutual operands, so recursing through + // phi.ops to derive the register would not terminate, so we should use the recorded register instead + // Return-value merges, inserted by the inliner, have no recorded register but are acyclic, so they resolve through their first operand + if (auto it = func.regs.find(op); it != func.regs.end()) + return it->second; + LUAU_ASSERT(phi.ops[0] != op); + return getRegister(phi.ops[0]); + } + case BcOpKind::Inst: + { + auto it = func.regs.find(op); + LUAU_ASSERT(it != func.regs.end()); + return it->second; + } + case BcOpKind::Proj: + { + BcProj& proj = func.projOp(op); + Reg base = getRegister(proj.op); + return base + proj.index; + } + case BcOpKind::VmReg: + return op.index; + default: + LUAU_UNREACHABLE(); + } + LUAU_UNREACHABLE(); + return 0; + } + + uint8_t getRegister(BcOp op) + { + uint32_t reg = getRegisterRaw(op); + + if (reg >= kInvalidReg) + { + LUAU_ASSERT(!"register reference overflow"); + error = true; + } + + if (reg >= func.maxstacksize) + { + LUAU_ASSERT(!"register overflows the function stack"); + error = true; + } + + return uint8_t(reg); + } + + uint8_t getRegInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + return getRegister(insn.ops[index]); + } + + uint8_t getRegInputForRange(BcInst& insn, uint8_t index, int count) + { + LUAU_ASSERT(index < insn.ops.size()); + + uint32_t reg = getRegisterRaw(insn.ops[index]); + + if (count < -1 || count >= 255) + { + LUAU_ASSERT(!"register count overflow"); + error = true; + } + + int range = count < 0 ? 0 : count; + + if (reg >= kInvalidReg) + { + LUAU_ASSERT(!"register reference overflow"); + error = true; + } + + if (reg + range > func.maxstacksize) + { + LUAU_ASSERT(!"register overflows the function stack"); + error = true; + } + + return uint8_t(reg); + } + + BcImm& getImm(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::Imm); + return func.immOp(inp); + } + + int32_t getImmInt(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + return imm.valueInt; + } + + uint8_t getImmIntAsUnsignedABC(BcInst& insn, uint8_t index, int8_t bias) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + + int64_t result = int64_t(imm.valueInt) + bias; + + if (int64_t(uint8_t(result)) != result) + error = true; + + return uint8_t(result); + } + + int16_t getImmIntAsSignedD(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Int); + + if (int32_t(int16_t(imm.valueInt)) != imm.valueInt) + error = true; + + return int16_t(imm.valueInt); + } + + bool getImmBool(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Boolean); + return imm.valueBoolean; + } + + uint32_t getImmImport(BcInst& insn, uint8_t index) + { + BcImm& imm = getImm(insn, index); + LUAU_ASSERT(imm.kind == BcImmKind::Import); + return imm.valueImport; + } + + virtual uint32_t getVmConstInputRaw(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmConst); + LUAU_ASSERT(inp.index < func.constants.size()); + return inp.index; + } + + uint8_t getVmConstInputABC(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid > 0xff) + error = true; + + return uint8_t(cid); + } + + uint16_t getVmConstInputD(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid > 0x7fff) + error = true; + + return uint16_t(cid); + } + + uint32_t getVmConstInputAux16(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid > 0xffff) + error = true; + + return cid; + } + + uint32_t getVmConstInputAux(BcInst& insn, uint8_t index) + { + uint32_t cid = getVmConstInputRaw(insn, index); + + if (cid >= kMaxConstantCount) + error = true; + + return cid; + } + + uint8_t getUpvalInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmUpvalue); + + if (inp.index >= func.nups) + { + LUAU_ASSERT(!"upvalue reference overflows the function upvalue count"); + error = true; + } + + if (inp.index >= kMaxUpvalueCount) + error = true; + + LUAU_ASSERT(inp.index < func.nups); + return uint8_t(inp.index); + } + + uint16_t getProtoInput(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::VmProto); + + if (inp.index >= kMaxClosureCount) + error = true; + + return uint16_t(inp.index); + } + + void recordJump(BcInst& insn, uint8_t index) + { + LUAU_ASSERT(index < insn.ops.size()); + BcOp inp = insn.ops[index]; + LUAU_ASSERT(inp.kind == BcOpKind::Block); + jumps.push_back({insn.op, static_cast(bcb.getInstructionCount()), inp}); + } + + void patchJump(JumpInfo& jump) + { + BcBlock& target = func.blockOp(jump.targetBlock); + LUAU_ASSERT(target.startpc != kBlockNoStartPc); + + if (isJumpD(jump.op)) + { + if (!bcb.patchJumpD(jump.instructionPC, target.startpc)) + error = true; + } + else if (isSkipC(jump.op)) + { + if (!bcb.patchSkipC(jump.instructionPC, target.startpc)) + error = true; + } + } + + void emitInstruction(BcOp insnOp) + { + BcInst& insn = func.instOp(insnOp); + bcb.setDebugLine(insn.line); + switch (insn.op) + { + case LOP_NOP: + case LOP_BREAK: + case LOP_NATIVECALL: + bcb.emitABC(insn.op, 0, 0, 0); + break; + + case LOP_LOADNIL: + bcb.emitABC(LOP_LOADNIL, getRegister(insnOp), 0, 0); + break; + + case LOP_LOADB: + { + if (insn.ops.size() > 1) + recordJump(insn, 1); + bcb.emitABC(LOP_LOADB, getRegister(insnOp), getImmBool(insn, 0), 0); + break; + } + + case LOP_LOADN: + bcb.emitAD(LOP_LOADN, getRegister(insnOp), getImmIntAsSignedD(insn, 0)); + break; + + case LOP_LOADK: + bcb.emitAD(LOP_LOADK, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_MOVE: + bcb.emitABC(LOP_MOVE, getRegister(insnOp), getRegInput(insn, 0), 0); + break; + + case LOP_GETGLOBAL: + bcb.emitABC(LOP_GETGLOBAL, getRegister(insnOp), 0, getImmInt(insn, 0)); + bcb.emitAux(getVmConstInputAux(insn, 1)); + break; + + case LOP_SETGLOBAL: + bcb.emitABC(LOP_SETGLOBAL, getRegInput(insn, 0), 0, getImmInt(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_GETUPVAL: + bcb.emitABC(LOP_GETUPVAL, getRegister(insnOp), getUpvalInput(insn, 0), 0); + break; + + case LOP_SETUPVAL: + bcb.emitABC(LOP_SETUPVAL, getRegInput(insn, 0), getUpvalInput(insn, 1), 0); + break; + + case LOP_CLOSEUPVALS: + bcb.emitABC(LOP_CLOSEUPVALS, getRegInput(insn, 0), 0, 0); + break; + + case LOP_GETIMPORT: + { + bcb.emitAD(LOP_GETIMPORT, getRegister(insnOp), getVmConstInputD(insn, 0)); + uint32_t componentsCount = getImmInt(insn, 1); + LUAU_ASSERT(componentsCount > 0 && componentsCount <= 3); + LUAU_ASSERT(insn.ops.size() - 2 == componentsCount); + uint32_t aux = componentsCount << 30; + for (uint32_t i = 0; i < componentsCount; i++) + { + uint32_t componentId = getVmConstInputRaw(insn, 2 + i); + if (componentId > 0x3FF) + error = true; + aux |= componentId << (20 - 10 * i); + } + bcb.emitAux(aux); + break; + } + + case LOP_GETTABLE: + bcb.emitABC(LOP_GETTABLE, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_SETTABLE: + bcb.emitABC(LOP_SETTABLE, getRegInput(insn, 0), getRegInput(insn, 1), getRegInput(insn, 2)); + break; + + case LOP_GETUDATAKS: + case LOP_GETTABLEKS: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getImmInt(insn, 1)); + + if (insn.op == LOP_GETUDATAKS) + bcb.emitAux(getVmConstInputAux16(insn, 2) | static_cast(getImmInt(insn, 3)) << 16); + else + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_SETUDATAKS: + case LOP_SETTABLEKS: + bcb.emitABC(insn.op, getRegInput(insn, 0), getRegInput(insn, 1), getImmInt(insn, 2)); + + if (insn.op == LOP_SETUDATAKS) + bcb.emitAux(getVmConstInputAux16(insn, 3) | static_cast(getImmInt(insn, 4)) << 16); + else + bcb.emitAux(getVmConstInputAux(insn, 3)); + break; + + case LOP_GETTABLEN: + bcb.emitABC(LOP_GETTABLEN, getRegister(insnOp), getRegInput(insn, 0), getImmIntAsUnsignedABC(insn, 1, -1)); + break; + + case LOP_SETTABLEN: + bcb.emitABC(LOP_SETTABLEN, getRegInput(insn, 0), getRegInput(insn, 1), getImmIntAsUnsignedABC(insn, 2, -1)); + break; + + case LOP_NEWCLOSURE: + bcb.emitAD(LOP_NEWCLOSURE, getRegister(insnOp), getProtoInput(insn, 0)); + break; + + case LOP_NAMECALLUDATA: + case LOP_NAMECALL: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getImmInt(insn, 1)); + + if (insn.op == LOP_NAMECALLUDATA) + bcb.emitAux(getVmConstInputAux16(insn, 2) | static_cast(getImmInt(insn, 3)) << 16); + else + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_CALL: + bcb.emitABC(LOP_CALL, getRegInput(insn, 2), getImmInt(insn, 0) + 1, getImmInt(insn, 1) + 1); + break; + + case LOP_CALLFB: + bcb.emitABC(LOP_CALLFB, getRegInput(insn, 3), getImmInt(insn, 0) + 1, getImmInt(insn, 1) + 1); + bcb.emitAux(getImmInt(insn, 2)); + break; + + case LOP_RETURN: + { + LUAU_ASSERT(insn.ops.size() > 1); + bcb.emitABC(LOP_RETURN, getRegInputForRange(insn, 1, getImmInt(insn, 0)), getImmInt(insn, 0) + 1, 0); + break; + } + + case LOP_JUMP: + recordJump(insn, 0); + bcb.emitAD(LOP_JUMP, 0, 0); + break; + + case LOP_JUMPBACK: + recordJump(insn, 0); + bcb.emitAD(LOP_JUMPBACK, 0, 0); + break; + + case LOP_JUMPIFNOT: + case LOP_JUMPIF: + recordJump(insn, 1); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + break; + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + recordJump(insn, 2); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + bcb.emitAux(getRegInput(insn, 1)); + break; + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_AND: + case LOP_OR: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_ADDK: + case LOP_SUBK: + case LOP_MULK: + case LOP_DIVK: + case LOP_MODK: + case LOP_POWK: + case LOP_ANDK: + case LOP_ORK: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), getVmConstInputABC(insn, 1)); + break; + + case LOP_CONCAT: + LUAU_ASSERT(insn.ops.size() > 0); + bcb.emitABC(LOP_CONCAT, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, insn.ops.size() - 1)); + break; + + case LOP_NOT: + case LOP_MINUS: + case LOP_LENGTH: + bcb.emitABC(insn.op, getRegister(insnOp), getRegInput(insn, 0), 0); + break; + + case LOP_NEWTABLE: + bcb.emitABC(LOP_NEWTABLE, getRegister(insnOp), getImmInt(insn, 0), 0); + bcb.emitAux(getImmInt(insn, 1)); + break; + + case LOP_DUPTABLE: + bcb.emitAD(LOP_DUPTABLE, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_SETLIST: + { + LUAU_ASSERT(insn.ops.size() > 2); + int32_t count = getImmInt(insn, 1); + LUAU_ASSERT(count < 255); + Reg startReg = getRegInput(insn, 2); + if (count != 0) + { + LUAU_ASSERT(insn.ops.size() > 3); + startReg = getRegInput(insn, 3); + } + bcb.emitABC(LOP_SETLIST, getRegInput(insn, 2), startReg, static_cast(count + 1)); + bcb.emitAux(getImmInt(insn, 0)); + break; + } + + case LOP_FORNPREP: + recordJump(insn, 3); + bcb.emitAD(LOP_FORNPREP, getRegInput(insn, 0), 0); + break; + + case LOP_FORNLOOP: + recordJump(insn, 3); + bcb.emitAD(LOP_FORNLOOP, getRegInput(insn, 0), 0); + break; + + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + recordJump(insn, 3); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + break; + + case LOP_FORGLOOP: + recordJump(insn, 5); + bcb.emitAD(LOP_FORGLOOP, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 3)) << 31 | getImmInt(insn, 4)); + break; + + case LOP_FASTCALL: + bcb.emitABC(LOP_FASTCALL, getImmInt(insn, 0), 0, getImmInt(insn, 1)); + break; + + case LOP_FASTCALL1: + bcb.emitABC(LOP_FASTCALL1, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 2)); + break; + + case LOP_FASTCALL2: + bcb.emitABC(LOP_FASTCALL2, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 3)); + bcb.emitAux(getRegInput(insn, 2)); + break; + + case LOP_FASTCALL2K: + bcb.emitABC(LOP_FASTCALL2K, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 3)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_FASTCALL3: + bcb.emitABC(LOP_FASTCALL3, getImmInt(insn, 0), getRegInput(insn, 1), getImmInt(insn, 4)); + bcb.emitAux(getRegInput(insn, 2) | static_cast(getRegInput(insn, 3)) << 8); + break; + + case LOP_GETVARARGS: + LUAU_ASSERT(insn.ops.size() == 2 && insn.ops[0].kind == BcOpKind::VmReg); + bcb.emitABC(LOP_GETVARARGS, getRegInputForRange(insn, 0, getImmInt(insn, 1)), getImmInt(insn, 1) + 1, 0); + break; + + case LOP_DUPCLOSURE: + bcb.emitAD(LOP_DUPCLOSURE, getRegister(insnOp), getVmConstInputD(insn, 0)); + break; + + case LOP_PREPVARARGS: + bcb.emitAD(LOP_PREPVARARGS, getImmInt(insn, 0), 0); + break; + + case LOP_LOADKX: + bcb.emitAD(LOP_LOADKX, getRegister(insnOp), 0); + bcb.emitAux(getVmConstInputAux(insn, 0)); + break; + + case LOP_JUMPX: + recordJump(insn, 0); + bcb.emitE(LOP_JUMPX, 0); + break; + + case LOP_COVERAGE: + bcb.emitE(LOP_COVERAGE, getImmInt(insn, 0)); + break; + + case LOP_CAPTURE: + { + uint8_t captureType = getImmInt(insn, 0); + if (captureType == LCT_VAL || captureType == LCT_REF) + bcb.emitABC(LOP_CAPTURE, captureType, getRegInput(insn, 1), getImmInt(insn, 2)); + else + bcb.emitABC(LOP_CAPTURE, captureType, getUpvalInput(insn, 1), getImmInt(insn, 2)); + break; + } + + case LOP_SUBRK: + case LOP_DIVRK: + bcb.emitABC(insn.op, getRegister(insnOp), getVmConstInputABC(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_JUMPXEQKNIL: + recordJump(insn, 2); + bcb.emitAD(LOP_JUMPXEQKNIL, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31); + break; + + case LOP_JUMPXEQKB: + recordJump(insn, 2); + bcb.emitAD(LOP_JUMPXEQKB, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31 | static_cast(getImmBool(insn, 3))); + break; + + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + recordJump(insn, 2); + bcb.emitAD(insn.op, getRegInput(insn, 0), 0); + bcb.emitAux(static_cast(getImmBool(insn, 1)) << 31 | getVmConstInputAux(insn, 3)); + break; + + case LOP_IDIV: + bcb.emitABC(LOP_IDIV, getRegister(insnOp), getRegInput(insn, 0), getRegInput(insn, 1)); + break; + + case LOP_IDIVK: + bcb.emitABC(LOP_IDIVK, getRegister(insnOp), getRegInput(insn, 0), getVmConstInputABC(insn, 1)); + break; + + case LOP_NEWCLASSMEMBER: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + bcb.emitABC(LOP_NEWCLASSMEMBER, getRegInput(insn, 0), 0, getRegInput(insn, 1)); + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP_CMPPROTO: + recordJump(insn, 2); + bcb.emitAD(LOP_CMPPROTO, getRegInput(insn, 0), 0); + bcb.emitAux(getImmInt(insn, 1)); + break; + + case LOP_FASTPCALL: + bcb.emitABC(LOP_FASTPCALL, getImmInt(insn, 0), getImmInt(insn, 1), getImmInt(insn, 2)); + break; + + case LOP_NEWCLASS: + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + if (insn.ops[0].kind != BcOpKind::None) + bcb.emitABC(LOP_NEWCLASS, getRegister(insnOp), getRegInput(insn, 0), getImmImport(insn, 1)); + else + bcb.emitABC(LOP_NEWCLASS, getRegister(insnOp), 0xff, getImmImport(insn, 1)); + + bcb.emitAux(getVmConstInputAux(insn, 2)); + break; + + case LOP__COUNT: + LUAU_UNREACHABLE(); + } + } + + std::optional getFallthrough(BcBlock& block) + { + for (auto [ctrl, target] : block.successors) + if (ctrl == BcBlockEdgeKind::Fallthrough) + return {target}; + return {}; + } + + std::vector emitBytecode() + { + std::vector schedule = reschedule(); + std::vector insnsPC; + insnsPC.resize(func.instructions.size(), ~0u); + + for (size_t i = 0; i < schedule.size(); i++) + { + BcOp blockOp = schedule[i]; + BcBlock& block = func.blockOp(blockOp); + std::optional fallthrough = getFallthrough(block); + if (fallthrough && *fallthrough != func.exitBlock && !(func.blockOp(*fallthrough).flags & BcBlockFlag::Dead) && + (i + 1 >= schedule.size() || *fallthrough != schedule[i + 1])) + { + BcJump jump = BcJump::create(func); + jump.setTarget(*fallthrough); + jump.appendTo(blockOp); + insnsPC.resize(func.instructions.size()); + } + block.startpc = bcb.getDebugPC(); + for (BcOp op : block.ops) + { + LUAU_ASSERT(op.kind == BcOpKind::Inst); + insnsPC[op.index] = bcb.getDebugPC(); + emitInstruction(op); + } + } + + for (auto& jump : jumps) + patchJump(jump); + + // Serialization failed + if (error) + return {}; + + return insnsPC; + } +}; + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Bytecode/src/Sccp.cpp b/External/luau/Bytecode/src/Sccp.cpp new file mode 100644 index 00000000..4a5b1949 --- /dev/null +++ b/External/luau/Bytecode/src/Sccp.cpp @@ -0,0 +1,433 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Sccp.h" +#include "Luau/BytecodeGraph.h" + +#include + +namespace Luau +{ +namespace Bytecode +{ + +static BcOp findOrAddConst(BcFunction& func, const BcVmConst& value) +{ + for (size_t i = 0; i < func.constants.size(); i++) + { + if (func.constants[i] == value) + return BcOp{BcOpKind::VmConst, static_cast(i)}; + } + return func.addConst(value); +} + +std::optional BcVmConstImpl::evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const +{ + if (isNumber(lhsOp) && isNumber(rhsOp)) + { + if (std::optional resultOpt = evaluateNumberBinaryOp(asNumber(lhsOp), asNumber(rhsOp), op)) + { + BcVmConst result; + result.kind = BcVmConstKind::Number; + result.valueNumber = *resultOpt; + return findOrAddConst(func, result); + } + + return std::nullopt; + } + + // TODO: vector support + return std::nullopt; +} + +bool BcVmConstImpl::falsey(const BcOp& falseyOp) const +{ + if (isNil(falseyOp)) + return true; + + if (isBoolean(falseyOp)) + return asBoolean(falseyOp) == false; + + return false; +} + +bool BcVmConstImpl::compare(const BcOp& lhsOp, const BcOp& rhsOp, BcCondition condition) const +{ + LUAU_ASSERT(isOrderable(lhsOp)); + LUAU_ASSERT(isOrderable(rhsOp)); + + if (isNumber(lhsOp) && isNumber(rhsOp)) + return bcCompare(asNumber(lhsOp), asNumber(rhsOp), condition); + + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + LUAU_ASSERT(lhs.kind == rhs.kind); + + switch (lhs.kind) + { + case BcVmConstKind::String: + return bcCompare(lhs.valueString, rhs.valueString, condition); + default: + LUAU_ASSERT(!"unsupported comparison"); + return false; + } +} + +BcOp BcVmConstImpl::makeNil() const +{ + BcVmConst result{}; + result.kind = BcVmConstKind::Nil; + return findOrAddConst(func, result); +} + +BcOp BcVmConstImpl::makeImmBool(bool value) const +{ + return func.addImmBool(value); +} + +BcRef BcVmConstImpl::asImm(BcOp op) const +{ + return func.imm(op); +} + +bool BcVmConstImpl::isOrderable(const BcOp& op) const +{ + if (op.kind == BcOpKind::Imm) + { + BcImm& imm = func.immOp(op); + return imm.kind == BcImmKind::Int; + } + + if (op.kind == BcOpKind::VmConst) + { + BcVmConst& vmConst = func.constOp(op); + return vmConst.kind == BcVmConstKind::Number || vmConst.kind == BcVmConstKind::String; + } + + return false; +} + +bool BcVmConstImpl::kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + if (isBoolean(lhsOp) && isBoolean(rhsOp)) + return true; + + if (isNumber(lhsOp) && isNumber(rhsOp)) + return true; + + if (lhsOp.kind != BcOpKind::VmConst || rhsOp.kind != BcOpKind::VmConst) + return false; + + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + + return lhs.kind == rhs.kind; +} + +bool BcVmConstImpl::fullyequal(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + if (lhsOp.kind != rhsOp.kind) + return false; + + if (lhsOp.kind == BcOpKind::Imm) + return func.immOp(lhsOp) == func.immOp(rhsOp); + + if (lhsOp.kind == BcOpKind::VmConst) + return lhsOp.index == rhsOp.index; // This relies on findOrAddConst for de-duplication + + LUAU_ASSERT(!"unsupported kind"); + return false; +} + +std::optional BcVmConstImpl::eq(const BcOp& lhsOp, const BcOp& rhsOp) const +{ + if (isNil(lhsOp) && isNil(rhsOp)) + return true; + + if (isBoolean(lhsOp) && isBoolean(rhsOp)) + return asBoolean(lhsOp) == asBoolean(rhsOp); + + if (isNumber(lhsOp) && isNumber(rhsOp)) + return asNumber(lhsOp) == asNumber(rhsOp); + + // Handle other VM constant types + if (lhsOp.kind == BcOpKind::VmConst && rhsOp.kind == BcOpKind::VmConst) + { + BcVmConst& lhs = func.constOp(lhsOp); + BcVmConst& rhs = func.constOp(rhsOp); + + if (lhs.kind != rhs.kind) + return false; + + if (lhs.kind == BcVmConstKind::Integer && rhs.kind == BcVmConstKind::Integer) + return lhs.valueInteger == rhs.valueInteger; + + if (lhs.kind == BcVmConstKind::String && rhs.kind == BcVmConstKind::String) + return lhs.valueString == rhs.valueString; + } + + return std::nullopt; +} + +bool BcVmConstImpl::isNil(const BcOp& op) const +{ + if (op.kind == BcOpKind::VmConst) + return func.constOp(op).kind == BcVmConstKind::Nil; + + return false; +} + +bool BcVmConstImpl::isBoolean(const BcOp& op) const +{ + if (op.kind == BcOpKind::Imm) + return func.immOp(op).kind == BcImmKind::Boolean; + + if (op.kind == BcOpKind::VmConst) + return func.constOp(op).kind == BcVmConstKind::Boolean; + + return false; +} + +bool BcVmConstImpl::isNumber(const BcOp& op) const +{ + if (op.kind == BcOpKind::Imm) + return func.immOp(op).kind == BcImmKind::Int; + + if (op.kind == BcOpKind::VmConst) + return func.constOp(op).kind == BcVmConstKind::Number; + + return false; +} + +bool BcVmConstImpl::asBoolean(const BcOp& op) const +{ + if (op.kind == BcOpKind::Imm) + { + BcImm& imm = func.immOp(op); + LUAU_ASSERT(imm.kind == BcImmKind::Boolean && "use isBoolean first"); + return imm.valueBoolean; + } + + if (op.kind == BcOpKind::VmConst) + { + BcVmConst& vmConst = func.constOp(op); + LUAU_ASSERT(vmConst.kind == BcVmConstKind::Boolean && "use isBoolean first"); + return vmConst.valueBoolean; + } + + LUAU_ASSERT(!"unsupported type, use isBoolean first"); + return false; +} + +double BcVmConstImpl::asNumber(const BcOp& op) const +{ + if (op.kind == BcOpKind::Imm) + { + BcImm& imm = func.immOp(op); + LUAU_ASSERT(imm.kind == BcImmKind::Int && "use isNumber first"); + return double(imm.valueInt); + } + + if (op.kind == BcOpKind::VmConst) + { + BcVmConst& vmConst = func.constOp(op); + LUAU_ASSERT(vmConst.kind == BcVmConstKind::Number && "use isNumber first"); + return vmConst.valueNumber; + } + + LUAU_ASSERT(!"unsupported type, use isNumber first"); + return 0.0; +} + +ConstnessLattice SccpInterpreter::evaluateArith(LuauOpcode opcode, BcRef instRepr) +{ + auto lhs = instRepr->ops[0]; + auto rhs = instRepr->ops[1]; + + ConstnessLattice lhsConstness = this->state->operandLattice(lhs); + ConstnessLattice rhsConstness = this->state->operandLattice(rhs); + + if (lhsConstness.kind == Constness::Constant && rhsConstness.kind == Constness::Constant) + { + std::optional vmConst = impl->evaluate(lhsConstness.constant.value(), rhsConstness.constant.value(), opcode); + if (vmConst) + return ConstnessLattice(Constness::Constant, vmConst.value()); + else + return ConstnessLattice(Constness::NotAConstant); + } + else if (lhsConstness.kind == Constness::Undetermined && rhsConstness.kind == Constness::Undetermined) + { + return ConstnessLattice(Constness::Undetermined); + } + + return ConstnessLattice(Constness::NotAConstant); +} + +ConditionState SccpInterpreter::evaluateComparisonCondition(LuauOpcode op, const BcOp& lhs, const BcOp& rhs) +{ + ConstnessLattice lhsConst = this->state->operandLattice(lhs); + ConstnessLattice rhsConst = this->state->operandLattice(rhs); + + bool isOrderingOp = (op == LOP_JUMPIFLT || op == LOP_JUMPIFLE || op == LOP_JUMPIFNOTLT || op == LOP_JUMPIFNOTLE); + + auto isOrderableLattice = [&](const ConstnessLattice& c) -> bool + { + if (c.kind == Constness::Constant) + return impl->isOrderable(c.constant.value()); + return false; + }; + + if (isOrderingOp && (!isOrderableLattice(lhsConst) || !isOrderableLattice(rhsConst))) + return ConditionState::Unknown; + + if (lhsConst.kind == Constness::Constant && rhsConst.kind == Constness::Constant) + { + if (isOrderingOp) + { + if (!impl->kindEquals(lhsConst.constant.value(), rhsConst.constant.value())) + return ConditionState::Unknown; + + bool condTrue = impl->compare(lhsConst.constant.value(), rhsConst.constant.value(), opcodeToCondition(op)); + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + else if (std::optional condTrueOpt = impl->eq(lhsConst.constant.value(), rhsConst.constant.value())) + { + bool condTrue = op == LOP_JUMPIFEQ ? *condTrueOpt : !*condTrueOpt; + return condTrue ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + } + + return ConditionState::Unknown; +} + +ConditionState SccpInterpreter::evaluateXeqkCondition(BcRef inst) +{ + ConstnessLattice valConst = this->state->operandLattice(inst->ops[0]); + + switch (inst->op) + { + case LOP_JUMPXEQKNIL: + if (valConst.kind == Constness::Constant) + return impl->isNil(*valConst.constant) ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + break; + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + { + if (valConst.kind == Constness::Constant) + { + if (std::optional eq = impl->eq(valConst.constant.value(), inst->ops[3])) + return *eq ? ConditionState::AlwaysTrue : ConditionState::AlwaysFalse; + } + break; + } + default: + break; + } + + return ConditionState::Unknown; +} + +ConditionState SccpInterpreter::evaluateCondition(const BcOp& op) +{ + ConstnessLattice lhs = this->state->operandLattice(op); + if (lhs.kind == Constness::Constant) + return impl->falsey(lhs.constant.value()) ? ConditionState::AlwaysFalse : ConditionState::AlwaysTrue; + return ConditionState::Unknown; +} + +ConstnessLattice SccpInterpreter::evaluate(LuauOpcode op, BcRef instRepr) +{ + switch (op) + { + case LOP_LOADK: + case LOP_LOADKX: + { + const BcOp& op = instRepr->ops[0]; + LUAU_ASSERT(op.kind == BcOpKind::VmConst); + return ConstnessLattice(Constness::Constant, op); + } + case LOP_LOADB: + { + const BcOp& op = instRepr->ops[0]; + LUAU_ASSERT(op.kind == BcOpKind::Imm); + LUAU_ASSERT(impl->asImm(op)->kind == BcImmKind::Boolean); + return ConstnessLattice(Constness::Constant, op); + } + case LOP_LOADN: + { + const BcOp& op = instRepr->ops[0]; + LUAU_ASSERT(op.kind == BcOpKind::Imm); + LUAU_ASSERT(impl->asImm(op)->kind == BcImmKind::Int); + return ConstnessLattice(Constness::Constant, op); + } + case LOP_LOADNIL: + { + BcOp nilConst = impl->makeNil(); + return ConstnessLattice(Constness::Constant, nilConst); + } + + case LOP_ADD: + case LOP_SUB: + case LOP_MUL: + case LOP_DIV: + case LOP_MOD: + case LOP_POW: + case LOP_IDIV: + { + return evaluateArith(op, instRepr); + } + case LOP_MOVE: + { + return this->state->operandLattice(instRepr->ops[0]); + } + + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + { + ConditionState cond = evaluateCondition(instRepr->ops[0]); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0]})); + + bool jumpsOnTrue = (instRepr->op == LOP_JUMPIF); + bool takesJump = (cond == ConditionState::AlwaysTrue) == jumpsOnTrue; + return ConstnessLattice(Constness::Constant, impl->makeImmBool(takesJump)); + } + + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + { + ConditionState cond = evaluateComparisonCondition(instRepr->op, instRepr->ops[0], instRepr->ops[1]); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0], instRepr->ops[1]})); + + return ConstnessLattice(Constness::Constant, impl->makeImmBool(cond == ConditionState::AlwaysTrue)); + } + + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + { + ConditionState cond = evaluateXeqkCondition(instRepr); + if (cond == ConditionState::Unknown) + return ConstnessLattice(this->state->unknownConditionConstness({instRepr->ops[0]})); + + const BcOp& negImmOp = instRepr->ops[1]; + bool negated = !impl->falsey(negImmOp); + bool takesJump = (cond == ConditionState::AlwaysTrue) != negated; + + return ConstnessLattice(Constness::Constant, impl->makeImmBool(takesJump)); + } + + case LOP_JUMP: + case LOP_JUMPBACK: + default: + return ConstnessLattice(Constness::NotAConstant); + } +} + +} // namespace Bytecode +} // namespace Luau diff --git a/External/luau/Common/include/Luau/Bytecode.h b/External/luau/Common/include/Luau/Bytecode.h new file mode 100644 index 00000000..ae38a8d1 --- /dev/null +++ b/External/luau/Common/include/Luau/Bytecode.h @@ -0,0 +1,786 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +// clang-format off + +// This header contains the bytecode definition for Luau interpreter +// Creating the bytecode is outside the scope of this file and is handled by bytecode builder (BytecodeBuilder.h) and bytecode compiler (Compiler.h) +// Note that ALL enums declared in this file are order-sensitive since the values are baked into bytecode that needs to be processed by legacy clients. + +// # Bytecode definitions +// Bytecode instructions are using "word code" - each instruction is one or many 32-bit words. +// The first word in the instruction is always the instruction header, and *must* contain the opcode (enum below) in the least significant byte. +// +// Instruction word can be encoded using one of the following encodings: +// ABC - least-significant byte for the opcode, followed by three bytes, A, B and C; each byte declares a register index, small index into some other table or an unsigned integral value +// AD - least-significant byte for the opcode, followed by A byte, followed by D half-word (16-bit integer). D is a signed integer that commonly specifies constant table index or jump offset +// E - least-significant byte for the opcode, followed by E (24-bit integer). E is a signed integer that commonly specifies a jump offset +// +// Instruction word is sometimes followed by one extra word, indicated as AUX - this is just a 32-bit word and is decoded according to the specification for each opcode. +// For each opcode the encoding is *static* - that is, based on the opcode you know apriori how large the instruction is, with the exception of NEWCLOSURE + +// # Bytecode indices +// Bytecode instructions commonly refer to integer values that define offsets or indices for various entities. For each type, there's a maximum encodable value. +// Note that in some cases, the compiler will set a lower limit than the maximum encodable value is to prevent fragile code into bumping against the limits whenever we change the compilation details. +// Additionally, in some specific instructions such as ANDK, the limit on the encoded value is smaller; this means that if a value is larger, a different instruction must be selected. +// +// Registers: 0-254. Registers refer to the values on the function's stack frame, including arguments. +// Upvalues: 0-199. Upvalues refer to the values stored in the closure object. +// Constants: 0-2^23-1. Constants are stored in a table allocated with each proto; to allow for future bytecode tweaks the encodable value is limited to 23 bits. +// Closures: 0-2^15-1. Closures are created from child protos via a child index; the limit is for the number of closures immediately referenced in each function. +// Jumps: -2^23..2^23. Jump offsets are specified in word increments, so jumping over an instruction may sometimes require an offset of 2 or more. Note that for jump instructions with AUX, the AUX word is included as part of the jump offset. + +// # Bytecode versions +// Bytecode serialized format embeds a version number, that dictates both the serialized form as well as the allowed instructions. As long as the bytecode version falls into supported +// range (indicated by LBC_BYTECODE_MIN / LBC_BYTECODE_MAX) and was produced by Luau compiler, it should load and execute correctly. +// +// Note that Luau runtime doesn't provide indefinite bytecode compatibility: support for older versions gets removed over time. As such, bytecode isn't a durable storage format and it's expected +// that Luau users can recompile bytecode from source on Luau version upgrades if necessary. + +// # Bytecode version history +// +// Note: due to limitations of the versioning scheme, some bytecode blobs that carry version 2 are using features from version 3. Starting from version 3, version should be sufficient to indicate bytecode compatibility. +// +// Version 1: Baseline version for the open-source release. Supported until 0.521. +// Version 2: Adds Proto::linedefined. Supported until 0.544. +// Version 3: Adds FORGPREP/JUMPXEQK* and enhances AUX encoding for FORGLOOP. Removes FORGLOOP_NEXT/INEXT and JUMPIFEQK/JUMPIFNOTEQK. Currently supported. +// Version 4: Adds Proto::flags, typeinfo, and floor division opcodes IDIV/IDIVK. Currently supported. +// Version 5: Adds SUBRK/DIVRK and vector constants. Currently supported. +// Version 6: Adds FASTCALL3. Currently supported. +// Version 7: Adds LBC_CONSTANT_TABLE_WITH_CONSTANTS for DUPTABLE with pre-filled constant values. Currently supported. +// Version 8: Adds LBC_CONSTANT_INTEGER for 64-bit integer constants. Currently supported. +// Version 9: Adds atom-based userdata field access acceleration. Currently supported. +// Version 10: Adds LBC_CONSTANT_CLASS_SHAPE and NEWCLASSMEMBER for use with Luau Classes. Experimental. +// Version 11: Adds CALLFB, CMPPROTO and feedback vector description. Experimental. +// Version 12: Adds cost function serialized for proto and prepend each proto with size in bytes. Experimental. +// Version 13: Adds support for double-precision vector constants. Experimental. +// Version 14: Adds FASTPCALL. Currently supported. + +// WIP Versions: Used for in-progress features that might require multiple changes to bytecode. Since these versions are higher than the non-WIP versions, they are responsible for maintaining compatibility with them. For example, tests exercising WIP bytecode versions may need to enable flags for unreleased but non-WIP bytecode versions. +// Version 100: Adds NEWCLASS for use with Luau Classes. Future class-related bytecode changes should go in this version before release. Experimental. + +// # Bytecode type information history +// Version 1: (from bytecode version 4) Type information for function signature. Currently supported. +// Version 2: (from bytecode version 4) Type information for arguments, upvalues, locals and some temporaries. Currently supported. +// Version 3: (from bytecode version 5) Type information for userdata type names and their index mapping. Currently supported. + +// Bytecode opcode, part of the instruction header +enum LuauOpcode +{ + // NOP: noop + LOP_NOP, + + // BREAK: debugger break + LOP_BREAK, + + // LOADNIL: sets register to nil + // A: target register + LOP_LOADNIL, + + // LOADB: sets register to boolean and jumps to a given short offset (used to compile comparison results into a boolean) + // A: target register + // B: value (0/1) + // C: jump offset + LOP_LOADB, + + // LOADN: sets register to a number literal + // A: target register + // D: value (-32768..32767) + LOP_LOADN, + + // LOADK: sets register to an entry from the constant table from the proto (number/vector/string) + // A: target register + // D: constant table index (0..32767) + LOP_LOADK, + + // MOVE: move (copy) value from one register to another + // A: target register + // B: source register + LOP_MOVE, + + // GETGLOBAL: load value from global table using constant string as a key + // A: target register + // C: predicted slot index (based on hash) + // AUX: constant table index + LOP_GETGLOBAL, + + // SETGLOBAL: set value in global table using constant string as a key + // A: source register + // C: predicted slot index (based on hash) + // AUX: constant table index + LOP_SETGLOBAL, + + // GETUPVAL: load upvalue from the upvalue table for the current function + // A: target register + // B: upvalue index + LOP_GETUPVAL, + + // SETUPVAL: store value into the upvalue table for the current function + // A: target register + // B: upvalue index + LOP_SETUPVAL, + + // CLOSEUPVALS: close (migrate to heap) all upvalues that were captured for registers >= target + // A: target register + LOP_CLOSEUPVALS, + + // GETIMPORT: load imported global table global from the constant table + // A: target register + // D: constant table index (0..32767); we assume that imports are loaded into the constant table + // AUX: 3 10-bit indices of constant strings that, combined, constitute an import path; length of the path is set by the top 2 bits (1,2,3) + LOP_GETIMPORT, + + // GETTABLE: load value from table into target register using key from register + // A: target register + // B: table register + // C: index register + LOP_GETTABLE, + + // SETTABLE: store source register into table using key from register + // A: source register + // B: table register + // C: index register + LOP_SETTABLE, + + // GETTABLEKS: load value from table into target register using constant string as a key + // A: target register + // B: table register + // C: predicted slot index (based on hash) + // AUX: constant table index + LOP_GETTABLEKS, + + // SETTABLEKS: store source register into table using constant string as a key + // A: source register + // B: table register + // C: predicted slot index (based on hash) + // AUX: constant table index + LOP_SETTABLEKS, + + // GETTABLEN: load value from table into target register using small integer index as a key + // A: target register + // B: table register + // C: index-1 (index is 1..256) + LOP_GETTABLEN, + + // SETTABLEN: store source register into table using small integer index as a key + // A: source register + // B: table register + // C: index-1 (index is 1..256) + LOP_SETTABLEN, + + // NEWCLOSURE: create closure from a child proto; followed by a CAPTURE instruction for each upvalue + // A: target register + // D: child proto index (0..32767) + LOP_NEWCLOSURE, + + // NAMECALL: prepare to call specified method by name by loading function from source register using constant index into target register and copying source register into target register + 1 + // A: target register + // B: source register + // C: predicted slot index (based on hash) + // AUX: constant table index + // Note that this instruction must be followed directly by CALL; it prepares the arguments + // This instruction is roughly equivalent to GETTABLEKS + MOVE pair, but we need a special instruction to support custom __namecall metamethod + LOP_NAMECALL, + + // CALL: call specified function + // A: register where the function object lives, followed by arguments; results are placed starting from the same register + // B: argument count + 1, or 0 to preserve all arguments up to top (MULTRET) + // C: result count + 1, or 0 to preserve all values and adjust top (MULTRET) + LOP_CALL, + + // RETURN: returns specified values from the function + // A: register where the returned values start + // B: number of returned values + 1, or 0 to return all values up to top (MULTRET) + LOP_RETURN, + + // JUMP: jumps to target offset + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + LOP_JUMP, + + // JUMPBACK: jumps to target offset; this is equivalent to JUMP but is used as a safepoint to be able to interrupt while/repeat loops + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + LOP_JUMPBACK, + + // JUMPIF: jumps to target offset if register is not nil/false + // A: source register + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + LOP_JUMPIF, + + // JUMPIFNOT: jumps to target offset if register is nil/false + // A: source register + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + LOP_JUMPIFNOT, + + // JUMPIFEQ, JUMPIFLE, JUMPIFLT, JUMPIFNOTEQ, JUMPIFNOTLE, JUMPIFNOTLT: jumps to target offset if the comparison is true (or false, for NOT variants) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: source register 2 + LOP_JUMPIFEQ, + LOP_JUMPIFLE, + LOP_JUMPIFLT, + LOP_JUMPIFNOTEQ, + LOP_JUMPIFNOTLE, + LOP_JUMPIFNOTLT, + + // ADD, SUB, MUL, DIV, MOD, POW: compute arithmetic operation between two source registers and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + LOP_ADD, + LOP_SUB, + LOP_MUL, + LOP_DIV, + LOP_MOD, + LOP_POW, + + // ADDK, SUBK, MULK, DIVK, MODK, POWK: compute arithmetic operation between the source register and a constant and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255); must refer to a number + LOP_ADDK, + LOP_SUBK, + LOP_MULK, + LOP_DIVK, + LOP_MODK, + LOP_POWK, + + // AND, OR: perform `and` or `or` operation (selecting first or second register based on whether the first one is truthy) and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + LOP_AND, + LOP_OR, + + // ANDK, ORK: perform `and` or `or` operation (selecting source register or constant based on whether the source register is truthy) and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255) + LOP_ANDK, + LOP_ORK, + + // CONCAT: concatenate all strings between B and C (inclusive) and put the result into A + // A: target register + // B: source register start + // C: source register end + LOP_CONCAT, + + // NOT, MINUS, LENGTH: compute unary operation for source register and put the result into target register + // A: target register + // B: source register + LOP_NOT, + LOP_MINUS, + LOP_LENGTH, + + // NEWTABLE: create table in target register + // A: target register + // B: table size, stored as 0 for v=0 and ceil(log2(v))+1 for v!=0 + // AUX: array size + LOP_NEWTABLE, + + // DUPTABLE: duplicate table using the constant table template to target register + // A: target register + // D: constant table index (0..32767) + LOP_DUPTABLE, + + // SETLIST: set a list of values to table in target register + // A: target register + // B: source register start + // C: value count + 1, or 0 to use all values up to top (MULTRET) + // AUX: table index to start from + LOP_SETLIST, + + // FORNPREP: prepare a numeric for loop, jump over the loop if first iteration doesn't need to run + // A: target register; numeric for loops assume a register layout [limit, step, index, variable] + // D: jump offset (-32768..32767) + // limit/step are immutable, index isn't visible to user code since it's copied into variable + LOP_FORNPREP, + + // FORNLOOP: adjust loop variables for one iteration, jump back to the loop header if loop needs to continue + // A: target register; see FORNPREP for register layout + // D: jump offset (-32768..32767) + LOP_FORNLOOP, + + // FORGLOOP: adjust loop variables for one iteration of a generic for loop, jump back to the loop header if loop needs to continue + // A: target register; generic for loops assume a register layout [generator, state, index, variables...] + // D: jump offset (-32768..32767) + // AUX: variable count (1..255) in the low 8 bits, high bit indicates whether to use ipairs-style traversal in the fast path + // loop variables are adjusted by calling generator(state, index) and expecting it to return a tuple that's copied to the user variables + // the first variable is then copied into index; generator/state are immutable, index isn't visible to user code + LOP_FORGLOOP, + + // FORGPREP_INEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_inext, and jump to FORGLOOP + // A: target register (see FORGLOOP for register layout) + // D: jump offset (-32768..32767) + LOP_FORGPREP_INEXT, + + // FASTCALL3: perform a fast call of a built-in function using 3 register arguments + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: source register 2 in least-significant byte + // AUX: source register 3 in second least-significant byte + LOP_FASTCALL3, + + // FORGPREP_NEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_next, and jump to FORGLOOP + // A: target register (see FORGLOOP for register layout) + // D: jump offset (-32768..32767) + LOP_FORGPREP_NEXT, + + // NATIVECALL: start executing new function in native code + // this is a pseudo-instruction that is never emitted by bytecode compiler, but can be constructed at runtime to accelerate native code dispatch + LOP_NATIVECALL, + + // GETVARARGS: copy variables into the target register from vararg storage for current function + // A: target register + // B: variable count + 1, or 0 to copy all variables and adjust top (MULTRET) + LOP_GETVARARGS, + + // DUPCLOSURE: create closure from a pre-created function object (reusing it unless environments diverge) + // A: target register + // D: constant table index (0..32767) + LOP_DUPCLOSURE, + + // PREPVARARGS: prepare stack for variadic functions so that GETVARARGS works correctly + // A: number of fixed arguments + LOP_PREPVARARGS, + + // LOADKX: sets register to an entry from the constant table from the proto (number/string) + // A: target register + // AUX: constant table index + LOP_LOADKX, + + // JUMPX: jumps to the target offset; like JUMPBACK, supports interruption + // E: jump offset (-2^23..2^23; 0 means "next instruction" aka "don't jump") + LOP_JUMPX, + + // FASTCALL: perform a fast call of a built-in function + // A: builtin function id (see LuauBuiltinFunction) + // C: jump offset to get to following CALL + // FASTCALL is followed by one of (GETIMPORT, MOVE, GETUPVAL) instructions and by CALL instruction + // This is necessary so that if FASTCALL can't perform the call inline, it can continue normal execution + // If FASTCALL *can* perform the call, it jumps over the instructions *and* over the next CALL + // Note that FASTCALL will read the actual call arguments, such as argument/result registers and counts, from the CALL instruction + LOP_FASTCALL, + + // COVERAGE: update coverage information stored in the instruction + // E: hit count for the instruction (0..2^23-1) + // The hit count is incremented by VM every time the instruction is executed, and saturates at 2^23-1 + LOP_COVERAGE, + + // CAPTURE: capture a local or an upvalue as an upvalue into a newly created closure; only valid after NEWCLOSURE + // A: capture type, see LuauCaptureType + // B: source register (for VAL/REF) or upvalue index (for UPVAL) + LOP_CAPTURE, + + // SUBRK, DIVRK: compute arithmetic operation between the constant and a source register and put the result into target register + // A: target register + // B: constant table index (0..255); must refer to a number + // C: source register + LOP_SUBRK, + LOP_DIVRK, + + // FASTCALL1: perform a fast call of a built-in function using 1 register argument + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + LOP_FASTCALL1, + + // FASTCALL2: perform a fast call of a built-in function using 2 register arguments + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: source register 2 in least-significant byte + LOP_FASTCALL2, + + // FASTCALL2K: perform a fast call of a built-in function using 1 register argument and 1 constant argument + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: constant index + LOP_FASTCALL2K, + + // FORGPREP: prepare loop variables for a generic for loop, jump to the loop backedge unconditionally + // A: target register; generic for loops assume a register layout [generator, state, index, variables...] + // D: jump offset (-32768..32767) + LOP_FORGPREP, + + // JUMPXEQKNIL, JUMPXEQKB: jumps to target offset if the comparison with constant is true (or false, see AUX) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: constant value (for boolean) in low bit, NOT flag (that flips comparison result) in high bit + LOP_JUMPXEQKNIL, + LOP_JUMPXEQKB, + + // JUMPXEQKN, JUMPXEQKS: jumps to target offset if the comparison with constant is true (or false, see AUX) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: constant table index in low 24 bits, NOT flag (that flips comparison result) in high bit + LOP_JUMPXEQKN, + LOP_JUMPXEQKS, + + // IDIV: compute floor division between two source registers and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + LOP_IDIV, + + // IDIVK compute floor division between the source register and a constant and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255) + LOP_IDIVK, + + // Atom-based userdata field access acceleration + // These are equivalent to their GETTABLEKS/SETTABLEKS/NAMECALL counterparts, except tailored towards userdata field accesses + // If the user has registered metamethods for a userdata tag, callbacks will be called by these instructions + // NOTE: it uses only lower 2 bytes in AUX for constant index. Higher bytes are used for runtime cache. + LOP_GETUDATAKS, + LOP_SETUDATAKS, + LOP_NAMECALLUDATA, + + // NEWCLASSMEMBER: register this method on a class object. + // A: target register of class + // B: reserved + // C: initial value of this member. currently must be a function. + // AUX: The name of this member as a constant string + LOP_NEWCLASSMEMBER, + + // CALLFB: call specified function with collecting runtime stats in a feedback slot + // A: register where the function object lives, followed by arguments; results are placed starting from the same register + // B: argument count + 1, or 0 to preserve all arguments up to top (MULTRET) + // C: result count + 1, or 0 to preserve all values and adjust top (MULTRET) + // AUX: feedback slot id. 0xFFFFFFFF - sealed + LOP_CALLFB, + + // CMPPROTO: check if a register contains a closure with a specified Luau function proto id + // A: closure register + // D: jump offset if proto doesn't match + // AUX: proto id + LOP_CMPPROTO, + + // FASTPCALL: perform a fastcall of a built-in protected call function + // A: protected function id (0 - pcall, 1 - xpcall) + // B: number of explicit arguments before a variadic tail + // C: jump offset to get to following CALL + LOP_FASTPCALL, + + // NEWCLASS: reify a class object + // A: target register of class + // B: source register of superclass, or 0xFF if no superclass + // C: bottom bit is 1 if the class is open, else 0; upper 7 bits are reserved + // AUX: constant table index of unreified class object + LOP_NEWCLASS, + + // Enum entry for number of opcodes, not a valid opcode by itself! + LOP__COUNT +}; + +// Bytecode instruction header: it's always a 32-bit integer, with low byte (first byte in little endian) containing the opcode +// Some instruction types require more data and have more 32-bit integers following the header +#define LUAU_INSN_OP(insn) ((insn) & 0xff) + +// ABC encoding: three 8-bit values, containing registers or small numbers +#define LUAU_INSN_A(insn) (((insn) >> 8) & 0xff) +#define LUAU_INSN_B(insn) (((insn) >> 16) & 0xff) +#define LUAU_INSN_C(insn) (((insn) >> 24) & 0xff) + +// AD encoding: one 8-bit value, one signed 16-bit value +#define LUAU_INSN_D(insn) (int32_t(insn) >> 16) + +// E encoding: one signed 24-bit value +#define LUAU_INSN_E(insn) (int32_t(insn) >> 8) + +// Auxiliary AB: two 8-bit values, containing registers or small numbers +// Used in FASTCALL3 +#define LUAU_INSN_AUX_A(aux) ((aux) & 0xff) +#define LUAU_INSN_AUX_B(aux) (((aux) >> 8) & 0xff) + +// Auxiliary KV: unsigned 24-bit constant index +// Used in LOP_JUMPXEQK* instructions +#define LUAU_INSN_AUX_KV(aux) ((aux) & 0xffffff) + +// Auxiliary KB: 1-bit constant value +// Used in LOP_JUMPXEQKB instruction +#define LUAU_INSN_AUX_KB(aux) ((aux) & 0x1) + +// Auxiliary NOT: 1-bit negation flag +// Used in LOP_JUMPXEQK* instructions +#define LUAU_INSN_AUX_NOT(aux) ((aux) >> 31) + +// Auxiliary 16-bit constant index and 16-bit cachedslot +// Used in LOP_GETUDATAKS, LOP_SETUDATAKS and LOP_NAMECALLUDATA +#define LUAU_INSN_AUX_KV16(aux) ((aux) & 0xffffu) +#define LUAU_INSN_AUX_SLOT(aux) ((aux) >> 16) + +#define LUAU_INSN_FBSLOT_SEALED 0xFFFFFFFF + +// Bytecode tags, used internally for bytecode encoded as a string +enum LuauBytecodeTag +{ + // Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled + LBC_VERSION_MIN = 3, + LBC_VERSION_MAX = 14, + LBC_VERSION_TARGET = 9, + LBC_VERSION_CLASSES = 100, + // Type encoding version + LBC_TYPE_VERSION_MIN = 1, + LBC_TYPE_VERSION_MAX = 3, + LBC_TYPE_VERSION_TARGET = 3, + // Types of constant table entries + LBC_CONSTANT_NIL = 0, + LBC_CONSTANT_BOOLEAN, + LBC_CONSTANT_NUMBER, + LBC_CONSTANT_STRING, + LBC_CONSTANT_IMPORT, + LBC_CONSTANT_TABLE, + LBC_CONSTANT_CLOSURE, + LBC_CONSTANT_VECTOR, + LBC_CONSTANT_TABLE_WITH_CONSTANTS, + LBC_CONSTANT_INTEGER, + LBC_CONSTANT_CLASS_SHAPE, + LBC_CONSTANT_VECTORD, + + /** WARNING: This must always be last. */ + LBC_CONSTANT__COUNT +}; + +// Type table tags +enum LuauBytecodeType +{ + LBC_TYPE_NIL = 0, + LBC_TYPE_BOOLEAN, + LBC_TYPE_NUMBER, + LBC_TYPE_STRING, + LBC_TYPE_TABLE, + LBC_TYPE_FUNCTION, + LBC_TYPE_THREAD, + LBC_TYPE_USERDATA, + LBC_TYPE_VECTOR, + LBC_TYPE_BUFFER, + LBC_TYPE_INTEGER, + + LBC_TYPE_ANY = 15, + + LBC_TYPE_TAGGED_USERDATA_BASE = 64, + LBC_TYPE_TAGGED_USERDATA_END = 64 + 32, + + LBC_TYPE_OPTIONAL_BIT = 1 << 7, + + LBC_TYPE_INVALID = 256, +}; + +// Builtin function ids, used in LOP_FASTCALL +enum LuauBuiltinFunction +{ + LBF_NONE = 0, + + // assert() + LBF_ASSERT, + + // math. + LBF_MATH_ABS, + LBF_MATH_ACOS, + LBF_MATH_ASIN, + LBF_MATH_ATAN2, + LBF_MATH_ATAN, + LBF_MATH_CEIL, + LBF_MATH_COSH, + LBF_MATH_COS, + LBF_MATH_DEG, + LBF_MATH_EXP, + LBF_MATH_FLOOR, + LBF_MATH_FMOD, + LBF_MATH_FREXP, + LBF_MATH_LDEXP, + LBF_MATH_LOG10, + LBF_MATH_LOG, + LBF_MATH_MAX, + LBF_MATH_MIN, + LBF_MATH_MODF, + LBF_MATH_POW, + LBF_MATH_RAD, + LBF_MATH_SINH, + LBF_MATH_SIN, + LBF_MATH_SQRT, + LBF_MATH_TANH, + LBF_MATH_TAN, + + // bit32. + LBF_BIT32_ARSHIFT, + LBF_BIT32_BAND, + LBF_BIT32_BNOT, + LBF_BIT32_BOR, + LBF_BIT32_BXOR, + LBF_BIT32_BTEST, + LBF_BIT32_EXTRACT, + LBF_BIT32_LROTATE, + LBF_BIT32_LSHIFT, + LBF_BIT32_REPLACE, + LBF_BIT32_RROTATE, + LBF_BIT32_RSHIFT, + + // type() + LBF_TYPE, + + // string. + LBF_STRING_BYTE, + LBF_STRING_CHAR, + LBF_STRING_LEN, + + // typeof() + LBF_TYPEOF, + + // string. + LBF_STRING_SUB, + + // math. + LBF_MATH_CLAMP, + LBF_MATH_SIGN, + LBF_MATH_ROUND, + + // raw* + LBF_RAWSET, + LBF_RAWGET, + LBF_RAWEQUAL, + + // table. + LBF_TABLE_INSERT, + LBF_TABLE_UNPACK, + + // vector ctor + LBF_VECTOR, + + // bit32.count + LBF_BIT32_COUNTLZ, + LBF_BIT32_COUNTRZ, + + // select(_, ...) + LBF_SELECT_VARARG, + + // rawlen + LBF_RAWLEN, + + // bit32.extract(_, k, k) + LBF_BIT32_EXTRACTK, + + // get/setmetatable + LBF_GETMETATABLE, + LBF_SETMETATABLE, + + // tonumber/tostring + LBF_TONUMBER, + LBF_TOSTRING, + + // bit32.byteswap(n) + LBF_BIT32_BYTESWAP, + + // buffer. + LBF_BUFFER_READI8, + LBF_BUFFER_READU8, + LBF_BUFFER_WRITEU8, + LBF_BUFFER_READI16, + LBF_BUFFER_READU16, + LBF_BUFFER_WRITEU16, + LBF_BUFFER_READI32, + LBF_BUFFER_READU32, + LBF_BUFFER_WRITEU32, + LBF_BUFFER_READF32, + LBF_BUFFER_WRITEF32, + LBF_BUFFER_READF64, + LBF_BUFFER_WRITEF64, + + // vector. + LBF_VECTOR_MAGNITUDE, + LBF_VECTOR_NORMALIZE, + LBF_VECTOR_CROSS, + LBF_VECTOR_DOT, + LBF_VECTOR_FLOOR, + LBF_VECTOR_CEIL, + LBF_VECTOR_ABS, + LBF_VECTOR_SIGN, + LBF_VECTOR_CLAMP, + LBF_VECTOR_MIN, + LBF_VECTOR_MAX, + + // math.lerp + LBF_MATH_LERP, + + // vector.lerp + LBF_VECTOR_LERP, + + // math. + LBF_MATH_ISNAN, + LBF_MATH_ISINF, + LBF_MATH_ISFINITE, + + // integer + LBF_INTEGER_CREATE, + LBF_INTEGER_TONUMBER, + LBF_INTEGER_NEG, + LBF_INTEGER_ADD, + LBF_INTEGER_SUB, + LBF_INTEGER_MUL, + LBF_INTEGER_DIV, + LBF_INTEGER_MIN, + LBF_INTEGER_MAX, + LBF_INTEGER_REM, + LBF_INTEGER_IDIV, + LBF_INTEGER_UDIV, + LBF_INTEGER_UREM, + LBF_INTEGER_MOD, + LBF_INTEGER_CLAMP, + LBF_INTEGER_BAND, + LBF_INTEGER_BOR, + LBF_INTEGER_BNOT, + LBF_INTEGER_BXOR, + LBF_INTEGER_LT, + LBF_INTEGER_LE, + LBF_INTEGER_ULT, + LBF_INTEGER_ULE, + LBF_INTEGER_GT, + LBF_INTEGER_GE, + LBF_INTEGER_UGT, + LBF_INTEGER_UGE, + LBF_INTEGER_LSHIFT, + LBF_INTEGER_RSHIFT, + LBF_INTEGER_ARSHIFT, + LBF_INTEGER_LROTATE, + LBF_INTEGER_RROTATE, + LBF_INTEGER_EXTRACT, + LBF_INTEGER_BTEST, + LBF_INTEGER_COUNTRZ, + LBF_INTEGER_COUNTLZ, + LBF_INTEGER_BSWAP, + + // buffer.readinteger / buffer.writeinteger (int64_t) + LBF_BUFFER_READINTEGER, + LBF_BUFFER_WRITEINTEGER, +}; + +// Capture type, used in LOP_CAPTURE +enum LuauCaptureType +{ + LCT_VAL = 0, + LCT_REF, + LCT_UPVAL, +}; + +// Proto flag bitmask, stored in Proto::flags +enum LuauProtoFlag +{ + // used to tag main proto for modules with --!native + LPF_NATIVE_MODULE = 1 << 0, + // used to tag individual protos as not profitable to compile natively + LPF_NATIVE_COLD = 1 << 1, + // used to tag main proto for modules that have at least one function with native attribute + LPF_NATIVE_FUNCTION = 1 << 2, + // function can be inlined + LPF_INLINABLE = 1 << 3, + // top-level function uses export statements and returns the export table + LPF_USES_EXPORT = 1 << 4, +}; + +enum LuauFeedbackType +{ + LFT_CALLTARGET = 0 +}; diff --git a/External/luau/Common/include/Luau/BytecodeUtils.h b/External/luau/Common/include/Luau/BytecodeUtils.h new file mode 100644 index 00000000..a640e21d --- /dev/null +++ b/External/luau/Common/include/Luau/BytecodeUtils.h @@ -0,0 +1,159 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Bytecode.h" + +#include + +namespace Luau +{ + +inline int getOpLength(LuauOpcode op) +{ + switch (op) + { + case LOP_GETGLOBAL: + case LOP_SETGLOBAL: + case LOP_GETIMPORT: + case LOP_GETTABLEKS: + case LOP_SETTABLEKS: + case LOP_NAMECALL: + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + case LOP_NEWTABLE: + case LOP_SETLIST: + case LOP_FORGLOOP: + case LOP_LOADKX: + case LOP_FASTCALL2: + case LOP_FASTCALL2K: + case LOP_FASTCALL3: + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + case LOP_GETUDATAKS: + case LOP_SETUDATAKS: + case LOP_NAMECALLUDATA: + case LOP_NEWCLASSMEMBER: + case LOP_CALLFB: + case LOP_CMPPROTO: + case LOP_NEWCLASS: + return 2; + + default: + return 1; + } +} + +inline bool isFastCall(LuauOpcode op) +{ + switch (op) + { + case LOP_FASTCALL: + case LOP_FASTCALL1: + case LOP_FASTCALL2: + case LOP_FASTCALL2K: + case LOP_FASTCALL3: + case LOP_FASTPCALL: + return true; + + default: + return false; + } +} + +inline bool isJumpD(LuauOpcode op) +{ + switch (op) + { + case LOP_JUMP: + case LOP_JUMPIF: + case LOP_JUMPIFNOT: + case LOP_JUMPIFEQ: + case LOP_JUMPIFLE: + case LOP_JUMPIFLT: + case LOP_JUMPIFNOTEQ: + case LOP_JUMPIFNOTLE: + case LOP_JUMPIFNOTLT: + case LOP_FORNPREP: + case LOP_FORNLOOP: + case LOP_FORGPREP: + case LOP_FORGLOOP: + case LOP_FORGPREP_INEXT: + case LOP_FORGPREP_NEXT: + case LOP_JUMPBACK: + case LOP_JUMPXEQKNIL: + case LOP_JUMPXEQKB: + case LOP_JUMPXEQKN: + case LOP_JUMPXEQKS: + case LOP_CMPPROTO: + return true; + + default: + return false; + } +} + +inline bool isSkipC(LuauOpcode op) +{ + switch (op) + { + case LOP_LOADB: + return true; + + default: + return false; + } +} + +inline int getJumpTarget(uint32_t insn, uint32_t pc) +{ + LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn)); + + if (isJumpD(op)) + return int(pc + LUAU_INSN_D(insn) + 1); + else if (isFastCall(op)) + return int(pc + LUAU_INSN_C(insn) + 2); + else if (isSkipC(op) && LUAU_INSN_C(insn)) + return int(pc + LUAU_INSN_C(insn) + 1); + else if (op == LOP_JUMPX) + return int(pc + LUAU_INSN_E(insn) + 1); + else + return -1; +} + +inline bool isFallthrough(LuauOpcode op) +{ + switch (op) + { + case LOP_RETURN: + case LOP_JUMP: + case LOP_JUMPBACK: + case LOP_JUMPX: + case LOP_FORGPREP: + case LOP_FORGPREP_NEXT: + case LOP_FORGPREP_INEXT: + return false; + default: + return true; + } +} + +inline bool isLoopJump(LuauOpcode op) +{ + switch (op) + { + case LOP_JUMPBACK: + case LOP_FORGLOOP: + case LOP_FORNLOOP: + return true; + default: + return false; + } +} + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/BytecodeWire.h b/External/luau/Common/include/Luau/BytecodeWire.h new file mode 100644 index 00000000..0f9161d5 --- /dev/null +++ b/External/luau/Common/include/Luau/BytecodeWire.h @@ -0,0 +1,24 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include +#include +#include + +namespace Luau +{ + +template +T read(const char* data, size_t& offset) +{ + T result; + memcpy(&result, data + offset, sizeof(T)); + offset += sizeof(T); + + return result; +} + +unsigned int readVarInt(const char* data, size_t& offset); +uint64_t readVarInt64(const char* data, size_t& offset); + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/Common.h b/External/luau/Common/include/Luau/Common.h new file mode 100644 index 00000000..7b269e51 --- /dev/null +++ b/External/luau/Common/include/Luau/Common.h @@ -0,0 +1,184 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +// Compiler codegen control macros +#ifdef _MSC_VER +#define LUAU_NORETURN __declspec(noreturn) +#define LUAU_NOINLINE __declspec(noinline) +#define LUAU_MAYBE_UNUSED +#define LUAU_FORCEINLINE __forceinline +#define LUAU_LIKELY(x) x +#define LUAU_UNLIKELY(x) x +#define LUAU_UNREACHABLE() __assume(false) +#define LUAU_DEBUGBREAK() __debugbreak() +#else +#define LUAU_NORETURN __attribute__((__noreturn__)) +#define LUAU_NOINLINE __attribute__((noinline)) +#define LUAU_MAYBE_UNUSED __attribute__((unused)) +#define LUAU_FORCEINLINE inline __attribute__((always_inline)) +#define LUAU_LIKELY(x) __builtin_expect(x, 1) +#define LUAU_UNLIKELY(x) __builtin_expect(x, 0) +#define LUAU_UNREACHABLE() __builtin_unreachable() +#define LUAU_DEBUGBREAK() __builtin_trap() +#endif + +// LUAU_FALLTHROUGH is a C++11-compatible alternative to [[fallthrough]] for use in the VM library +#if defined(__clang__) && defined(__has_warning) +#if __has_feature(cxx_attributes) && __has_warning("-Wimplicit-fallthrough") +#define LUAU_FALLTHROUGH [[clang::fallthrough]] +#else +#define LUAU_FALLTHROUGH +#endif +#elif defined(__GNUC__) && __GNUC__ >= 7 +#define LUAU_FALLTHROUGH [[gnu::fallthrough]] +#else +#define LUAU_FALLTHROUGH +#endif + +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define LUAU_BIG_ENDIAN +#endif + +namespace Luau +{ + +using AssertHandler = int (*)(const char* expression, const char* file, int line, const char* function); + +inline AssertHandler& assertHandler() +{ + static AssertHandler handler = nullptr; + return handler; +} + +// We want 'inline' to correctly link this function declared in the header +// But we also want to prevent compiler from inlining this function when optimization and assertions are enabled together +// Reason for that is that compilation times can increase significantly in such a configuration +LUAU_NOINLINE inline int assertCallHandler(const char* expression, const char* file, int line, const char* function) +{ + if (AssertHandler handler = assertHandler()) + return handler(expression, file, line, function); + + return 1; +} + +} // namespace Luau + +#if !defined(NDEBUG) || defined(LUAU_ENABLE_ASSERT) +#define LUAU_ASSERT(expr) ((void)(!!(expr) || (Luau::assertCallHandler(#expr, __FILE__, __LINE__, __FUNCTION__) && (LUAU_DEBUGBREAK(), 0)))) +#define LUAU_ASSERTENABLED +#else +#define LUAU_ASSERT(expr) (void)sizeof(!!(expr)) +#endif + +namespace Luau +{ + +template +struct FValue +{ + static FValue* list; + + T value; + bool dynamic; + const char* name; + FValue* next; + unsigned int version = 0; + + FValue(const char* name, T def, bool dynamic) + : value(def) + , dynamic(dynamic) + , name(name) + , next(list) + { + list = this; + } + + LUAU_FORCEINLINE operator T() const + { + return value; + } +}; + +template +FValue* FValue::list = nullptr; + +struct FValueVersionSetter +{ + FValueVersionSetter(const char* name, unsigned int version) + { + bool found = false; + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (strcmp(flag->name, name) == 0) + { + flag->version = version; + found = true; + } + } + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + if (strcmp(flag->name, name) == 0) + { + flag->version = version; + found = true; + } + } + LUAU_ASSERT(found && "LUAU_FLAGVERSION must appear after the flag definition in the same source file"); + } +}; + +} // namespace Luau + +#define LUAU_FASTFLAG(flag) \ + namespace FFlag \ + { \ + extern Luau::FValue flag; \ + } +#define LUAU_FASTFLAGVARIABLE(flag) \ + namespace FFlag \ + { \ + Luau::FValue flag(#flag, false, false); \ + } +#define LUAU_FASTINT(flag) \ + namespace FInt \ + { \ + extern Luau::FValue flag; \ + } +#define LUAU_FASTINTVARIABLE(flag, def) \ + namespace FInt \ + { \ + Luau::FValue flag(#flag, def, false); \ + } + +#define LUAU_DYNAMIC_FASTFLAG(flag) \ + namespace DFFlag \ + { \ + extern Luau::FValue flag; \ + } +#define LUAU_DYNAMIC_FASTFLAGVARIABLE(flag, def) \ + namespace DFFlag \ + { \ + Luau::FValue flag(#flag, def, true); \ + } +#define LUAU_DYNAMIC_FASTINT(flag) \ + namespace DFInt \ + { \ + extern Luau::FValue flag; \ + } +#define LUAU_DYNAMIC_FASTINTVARIABLE(flag, def) \ + namespace DFInt \ + { \ + Luau::FValue flag(#flag, def, true); \ + } + +#define LUAU_FLAGVERSION(flag, version) \ + static_assert((version) != 0, "LUAU_FLAGVERSION version cannot be 0"); \ + static Luau::FValueVersionSetter flag##_VersionSetter(#flag, version); + +#if defined(__GNUC__) +#define LUAU_PRINTF_ATTR(fmt, arg) __attribute__((format(printf, fmt, arg))) +#else +#define LUAU_PRINTF_ATTR(fmt, arg) +#endif diff --git a/External/luau/Common/include/Luau/DenseHash.h b/External/luau/Common/include/Luau/DenseHash.h new file mode 100644 index 00000000..78673718 --- /dev/null +++ b/External/luau/Common/include/Luau/DenseHash.h @@ -0,0 +1,942 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/HashUtil.h" +#include "Luau/Common.h" + +#include +#include +#include +#include +#include + +namespace Luau +{ + +// Internal implementation of DenseHashSet and DenseHashMap +namespace detail +{ + +inline size_t countTrailingZeroes(uint64_t word) +{ + // Caller must ensure that word is not 0 + LUAU_ASSERT(word != 0); +#if defined(__GNUC__) || defined(__clang__) + return __builtin_ctzll(word); +#elif defined(_MSC_VER) && defined(_M_X64) + unsigned long idx; + _BitScanForward64(&idx, word); + return idx; +#else + size_t n = 0; + while ((word & 1) == 0) + { + word >>= 1; + ++n; + } + return n; +#endif +} + +// This is an implementation of a hash table that does not require a tombstone element and supports erase. +// In order to avoid the use of tombstones we use a Dense BitSet to store presence/absence of elements. +// The hashtable uses fibonacci hashing and linear probing to ensure pretty good scattering. The usage of fibonacci hashing +// means we can just use the default std::hash and there is less burden on us to select a good hash function. For degenerate cases, the +// option still remains to supply your own hash. +// Invariants: +// - capacity is a power of two to support fast integer modulo (%) +// - doubling the capacity of the map allows us to amortize the cost of lookups and better scatter elements throughout the map +// - every element can either be reached in the map by computing its hash, or from a linear probe of elements (that wraps around) from the hash +// location +// - when the map gets to 3/4 full(our load factor), we will double the size of the hash. +// Interesting details: +// Rehashing uses the count trailing zeroes intrinsic in order to quickly skip to the elements that have to be rehashed. +// We can do this because the bitset data structure encodes presence/absence in a uint64_t, so we could theoretically +// skip up to 64 empty elements per loop iteration, instead of one element per loop + +template +class DenseHashTable +{ + +private: + struct BitSet + { + // UsedTable stores an array of uint64_t, where the ith' uint64_t represents the presence/absence of + // elements at indices [64 * (i - 1) - 64 * i). + // Each uint64_t is examined from LSB to MSB (right to left). + // For example, if the 1st uint64 looks like 0'11[61 0's...]1, then we interpret indices 0, 62, and 63 are occupied in + // the hashtable. + + using BitsetT = uint64_t; + // These constants are used to make implementing the contains/set methods magic constant agnostic + static constexpr size_t numElements = sizeof(BitsetT) * 8; + // Num elements must be a power of 2 + static_assert((numElements & (numElements - 1)) == 0); + // If you want to change the backing structure from 32 bit unsigned integer to 64 / 8 /... etc, you'll have to update this variable + static constexpr size_t numElementsLog2 = 6; + + explicit BitSet(size_t capacity = 0) + : capacity(capacity) + , count(0) + , data(nullptr) + { + LUAU_ASSERT((capacity & (capacity - 1)) == 0); + if (capacity != 0u) + { + count = capacity < numElements ? 1 : capacity >> numElementsLog2; + data = static_cast(::operator new(sizeof(BitsetT) * count)); + memset(data, 0, sizeof(BitsetT) * count); + } + } + + ~BitSet() + { + ::operator delete(data); + } + + BitSet(const BitSet& other) + : capacity(other.capacity) + , count(other.count) + , data(nullptr) + { + if (count != 0u) + { + data = static_cast(::operator new(sizeof(BitsetT) * count)); + memcpy(data, other.data, sizeof(BitsetT) * count); + } + } + + BitSet(BitSet&& other) noexcept + : capacity(other.capacity) + , count(other.count) + , data(other.data) + { + other.capacity = 0; + other.count = 0; + other.data = nullptr; + } + + BitSet& operator=(const BitSet& other) + { + if (this != &other) + { + BitSet copy(other); + *this = std::move(copy); + } + return *this; + } + + BitSet& operator=(BitSet&& other) noexcept + { + if (this != &other) + { + ::operator delete(data); + data = other.data; + capacity = other.capacity; + count = other.count; + other.data = nullptr; + other.capacity = 0; + other.count = 0; + } + return *this; + } + + bool contains(size_t bucket) const + { + size_t whichBitvec = bucket >> numElementsLog2; + size_t bvOffset = bucket & (numElements - 1); + return ((data[whichBitvec] >> bvOffset) & 1u) == 1u; + } + + void clear() + { + if (count != 0u) + memset(data, 0, sizeof(BitsetT) * count); + } + + void set(size_t bucket, bool v) + { + size_t whichBitvec = bucket >> numElementsLog2; + size_t offset = bucket & (numElements - 1); + if (v) + data[whichBitvec] |= (BitsetT(1) << offset); + else + data[whichBitvec] &= ~(BitsetT(1) << offset); + } + + BitsetT wordAt(size_t idx) const + { + LUAU_ASSERT(idx < count); + return data[idx]; + } + + size_t numWords() const + { + return count; + } + + // This iterator produces a stream of indices representing indices in bitset that are on, not each bit in the set + // Insertions to this set / mutations will invalidate this iterator + class iterator + { + public: + iterator() + : data(nullptr) + , wordCount(0) + , wordIdx(0) + , word(0) + { + } + + iterator(const BitsetT* data, size_t wordCount, size_t wordIdx) + : data(data) + , wordCount(wordCount) + , wordIdx(wordIdx) + , word(0) + { + // Skip all words that are 0's + while (this->wordIdx < this->wordCount && (this->word = data[this->wordIdx]) == 0) + this->wordIdx++; + + if (this->wordIdx < this->wordCount) + currentBucket = this->wordIdx * numElements + countTrailingZeroes(word); + } + + size_t operator*() const + { + return currentBucket; + } + + iterator& operator++() + { + // When you want the `next` occupied bucket, zero out the lowest 1 in the current word in the bitset (since that's where you are + // currently) If the word is 0, skip to the next one - there aren't anymore elements in the current block of 64 Otherwise, the + // currentBucket is the index of the word * numElements per word + the trailing zeroes in that word + word &= word - 1; + while (word == 0) + { + wordIdx++; + if (wordIdx >= wordCount) + return *this; + word = data[wordIdx]; + } + currentBucket = wordIdx * numElements + countTrailingZeroes(word); + return *this; + } + + bool operator==(const iterator& other) const + { + return wordIdx == other.wordIdx && word == other.word; + } + + bool operator!=(const iterator& other) const + { + return wordIdx != other.wordIdx || word != other.word; + } + + private: + const BitsetT* data; + size_t wordCount; + size_t wordIdx; + BitsetT word; + size_t currentBucket = 0; + }; + + iterator begin() const + { + return iterator(data, count, 0); + } + + iterator end() const + { + return iterator(data, count, count); + } + + private: + size_t capacity; + size_t count; + BitsetT* data; + }; + +public: + class const_iterator; + class iterator; + + explicit DenseHashTable(size_t buckets = 0) + : data(nullptr) + , usedTable(buckets) + , capacity(0) + , count(0) + , hashShift(64) + { + // buckets has to be power-of-two or zero + LUAU_ASSERT((buckets & (buckets - 1)) == 0); + + if (buckets != 0u) + { + data = static_cast(::operator new(sizeof(Item) * buckets)); + capacity = buckets; + // This cast is fine because count trailing zeroes on a uint64_t returns a number in [0, 64) + // Easily fits in a uint8_t + hashShift = 64 - static_cast(detail::countTrailingZeroes(static_cast(buckets))); + } + } + + ~DenseHashTable() + { + if (data) + destroy(); + } + + DenseHashTable(const DenseHashTable& other) + : data(nullptr) + , usedTable(0) + , capacity(0) + , count(other.count) + , hashShift(other.hashShift) + { + if (other.capacity) + { + data = static_cast(::operator new(sizeof(Item) * other.capacity)); + usedTable = BitSet{other.capacity}; + + for (size_t bucket : other.usedTable) + { + new (&data[bucket]) Item(other.data[bucket]); + usedTable.set(bucket, true); // if Item copy throws, used table will note the initialized objects for destroy() to clean up + } + + capacity = other.capacity; + LUAU_ASSERT((capacity & (capacity - 1)) == 0); + } + } + + DenseHashTable(DenseHashTable&& other) noexcept + : data(other.data) + , usedTable(std::move(other.usedTable)) + , capacity(other.capacity) + , count(other.count) + , hashShift(other.hashShift) + { + other.data = nullptr; + other.capacity = 0; + other.count = 0; + other.hashShift = 64; + } + + DenseHashTable& operator=(DenseHashTable&& other) noexcept + { + if (this != &other) + { + if (data) + destroy(); + + data = other.data; + usedTable = std::move(other.usedTable); + capacity = other.capacity; + count = other.count; + hashShift = other.hashShift; + + other.data = nullptr; + other.capacity = 0; + other.count = 0; + other.hashShift = 64; + } + + return *this; + } + + DenseHashTable& operator=(const DenseHashTable& other) + { + if (this != &other) + { + DenseHashTable copy(other); + *this = std::move(copy); + } + + return *this; + } + + void clear(size_t thresholdToDestroy = 32) + { + if (count == 0) + return; + + if (capacity > thresholdToDestroy) + { + destroy(); + } + else + { + for (size_t bucket : usedTable) + data[bucket].~Item(); + usedTable.clear(); + } + + count = 0; + } + + void destroy() + { + for (size_t bucket : usedTable) + data[bucket].~Item(); + + ::operator delete(data); + data = nullptr; + usedTable = BitSet{}; + + capacity = 0; + hashShift = 64; + } + + size_t doHash(const Key& key) const + { + // DenseHash2 uses fibonacci hashing for much better scattering + // https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ + // The hashShift is 64(num bits) - log2(capacity). + // Shifting the hashed value * constant by this much maps the resulting value into the range [0, capacity) + // For example: if capacity is 4096, we shift right by 52 (64 - 12). This preserves the upper 12 bits of data with the remaining 52 bits being + // 0 Your value will look like: [ 52 bits of zeroes | 12 bits of 'value'] This 12 bits of value will map you in the range [0, 2^12 -1], which + // is exactly [0, capacity] One of the reasons for taking the upper order bits is that multiplications shifts information upwards, so those + // are probably the interesting bits to keep + // This is nice to do because it means the user of dense hash doesn't have to design a good hash function - e.g. if you're hashing integers + // you can just return the integer. If you tried to do this with DenseHash(old), you'd end up with a lot of clustering, and it means we can + // just use std::hash as the default hash everywhere + return static_cast((static_cast(hasher(key)) * 11400714819323198485ull) >> hashShift); + } + + struct BucketResult + { + size_t bucket; + bool found; + }; + + void erase(const Key& key) + { + if (count == 0) + return; + + auto [bucket, found] = getBucket(key); + if (found) + doErase(bucket); + } + + Item* insert_unsafe(const Key& key) + { + auto [bucket, found] = getBucket(key); + + if (!found) + { + usedTable.set(bucket, true); + ItemInterface::setKey(data[bucket], key); + count++; + } + + return &data[bucket]; + } + + const Item* find(const Key& key) const + { + if (count == 0) + return NULL; + + auto [bucket, found] = getBucket(key); + return found ? &data[bucket] : NULL; + } + + void grow() + { + size_t newsize = capacity == 0 ? 16 : capacity * 2; + + DenseHashTable newtable(newsize); + + // We can leverage the structure of the bitvector here to skip contiguous chunks of empty elements + for (size_t bucket : usedTable) + { + const Key& key = ItemInterface::getKey(data[bucket]); + // ItemInterface::setKey default constructs the value type. If we use insert_unsafe here, we then pay for one unnecessary construction + // which is immediately overwritten. Instead, we manually insert these items into the destination table + auto [dest, found] = newtable.getBucket(key); + LUAU_ASSERT(!found); + newtable.usedTable.set(dest, true); + ++newtable.count; + new (&newtable.data[dest]) Item(std::move(data[bucket])); + } + + LUAU_ASSERT(count == newtable.count); + + std::swap(data, newtable.data); + std::swap(usedTable, newtable.usedTable); + std::swap(capacity, newtable.capacity); + std::swap(hashShift, newtable.hashShift); + } + + void rehash_if_full(const Key& key) + { + if (count >= capacity * 3 / 4 && !find(key)) + { + grow(); + } + } + + const_iterator begin() const + { + return const_iterator(this, usedTable.begin()); + } + + const_iterator end() const + { + return const_iterator(this, usedTable.end()); + } + + iterator begin() + { + return iterator(this, usedTable.begin()); + } + + iterator end() + { + return iterator(this, usedTable.end()); + } + + size_t size() const + { + return count; + } + + class const_iterator + { + public: + using value_type = Item; + using reference = Item&; + using pointer = Item*; + using difference_type = ptrdiff_t; + using iterator_category = std::forward_iterator_tag; + + const_iterator() = default; + + const_iterator(const DenseHashTable* set, typename BitSet::iterator bucketIt) + : set(set) + , bucketIt(bucketIt) + { + } + + const Item& operator*() const + { + return set->data[*bucketIt]; + } + + const Item* operator->() const + { + return &set->data[*bucketIt]; + } + + bool operator==(const const_iterator& other) const + { + return set == other.set && bucketIt == other.bucketIt; + } + + bool operator!=(const const_iterator& other) const + { + return set != other.set || bucketIt != other.bucketIt; + } + + const_iterator& operator++() + { + ++bucketIt; + return *this; + } + + const_iterator operator++(int) + { + const_iterator res = *this; + ++*this; + return res; + } + + private: + const DenseHashTable* set = nullptr; + typename BitSet::iterator bucketIt; + }; + + class iterator + { + public: + using value_type = MutableItem; + using reference = MutableItem&; + using pointer = MutableItem*; + using difference_type = ptrdiff_t; + using iterator_category = std::forward_iterator_tag; + + iterator() = default; + + iterator(DenseHashTable* set, typename BitSet::iterator bucketIt) + : set(set) + , bucketIt(bucketIt) + { + } + + MutableItem& operator*() const + { + return *reinterpret_cast(&set->data[*bucketIt]); + } + + MutableItem* operator->() const + { + return reinterpret_cast(&set->data[*bucketIt]); + } + + bool operator==(const iterator& other) const + { + return set == other.set && bucketIt == other.bucketIt; + } + + bool operator!=(const iterator& other) const + { + return set != other.set || bucketIt != other.bucketIt; + } + + iterator& operator++() + { + ++bucketIt; + return *this; + } + + iterator operator++(int) + { + iterator res = *this; + ++*this; + return res; + } + + private: + DenseHashTable* set = nullptr; + typename BitSet::iterator bucketIt; + }; + +private: + // Returns the bucket where `key` was found, or the first empty bucket if not present. + inline BucketResult getBucket(const Key& key) const + { + // Guarantees that this function will terminate - in the worst case, the linear probe + // is guaranteed to hit an empty slot + LUAU_ASSERT(count < capacity); + size_t hashmod = capacity - 1; + size_t bucket = doHash(key); + + for (;;) + { + if (!usedTable.contains(bucket)) + { + return {bucket, false}; + } + + if (eq(ItemInterface::getKey(data[bucket]), key)) + return {bucket, true}; + bucket = (bucket + 1) & hashmod; + } + + LUAU_ASSERT(false); + return {0, false}; + } + + void doErase(size_t bucket) + { + // doErase implements algorithm R deletion as described in TAOCP volume 3, 6.4 (although in TAOCP, probes go towards lower indices) + // This is the implementation described here: https://maskray.me/blog/2026-06-07-recent-llvm-hash-table-improvements + size_t i = bucket; + size_t j = bucket; + size_t hashmod = capacity - 1; + while (true) + { + // Slightly subtle - our capacity is always guaranteed to be a power of two, which is why + // we can use bitwise & to perform a quick modulo (%) operation. The ensures that the index pointer `j` + // will always be in range + j = (j + 1) & hashmod; + const Item& curr = data[j]; + if (!usedTable.contains(j)) + break; + size_t r = doHash(ItemInterface::getKey(curr)); + // The invariant here is that every element must be reachable from a contiguous probe sequence. + // When considering whether to shift the element at j down to the element at i, we look at the original + // hash position. If the original hash position lies in between i and j, then shifting this element would break + // the probe sequence. Thus, modulo the size of the hash table, the original hash position must be closer to i than j + // to avoid breaking the probe sequence. + size_t left = (i - r) & hashmod; + size_t right = (j - r) & hashmod; + + if (left < right) + { + usedTable.set(i, true); + usedTable.set(j, false); + // Slot i currently holds a live element: the just-erased element on the first shift, or an + // already-moved-from element on later shifts. Destroy it before move-constructing over it, + // otherwise value types that own resources (e.g. std::unique_ptr) leak the erased element. + data[i].~Item(); + new (&data[i]) Item(std::move(data[j])); + i = j; + } + } + + usedTable.set(i, false); + data[i].~Item(); + --count; + } + + Item* data; + BitSet usedTable; + size_t capacity; + size_t count; + uint8_t hashShift; // 64 - log2(capacity); used for Fibonacci hashing + Hash hasher; + Eq eq; +}; + +template +struct ItemInterfaceSet2 +{ + static const Key& getKey(const Key& item) + { + return item; + } + + static void setKey(Key& item, const Key& key) + { + new (&item) Key(key); + } +}; + +template +struct ItemInterfaceMap2 +{ + static const Key& getKey(const std::pair& item) + { + return item.first; + } + + static void setKey(std::pair& item, const Key& key) + { + new (&item.first) Key(key); + new (&item.second) Value(); + } +}; + +} // namespace detail + +// This is a faster alternative of unordered_set +template, typename Eq = std::equal_to> +class DenseHashSet +{ + using Impl = detail::DenseHashTable, Hash, Eq>; + Impl impl; + +public: + using const_iterator = typename Impl::const_iterator; + using iterator = typename Impl::iterator; + + DenseHashSet(size_t buckets = 0) + : impl(buckets) + { + } + + void clear() + { + impl.clear(); + } + + const Key& insert(const Key& key) + { + impl.rehash_if_full(key); + return *impl.insert_unsafe(key); + } + + bool try_insert(const Key& key) + { + const size_t oldSize = impl.size(); + insert(key); + return impl.size() != oldSize; + } + + const Key* find(const Key& key) const + { + return impl.find(key); + } + + bool contains(const Key& key) const + { + return impl.find(key) != 0; + } + + void erase(const Key& key) + { + impl.erase(key); + } + + size_t size() const + { + return impl.size(); + } + + bool empty() const + { + return impl.size() == 0; + } + + const_iterator begin() const + { + return impl.begin(); + } + + const_iterator end() const + { + return impl.end(); + } + + iterator begin() + { + return impl.begin(); + } + + iterator end() + { + return impl.end(); + } + + bool operator==(const DenseHashSet& other) const + { + if (size() != other.size()) + return false; + + for (const Key& k : *this) + { + if (!other.contains(k)) + return false; + } + + return true; + } + + bool operator!=(const DenseHashSet& other) const + { + return !(*this == other); + } +}; + +// This is a faster alternative of unordered_map +template, typename Eq = std::equal_to> +class DenseHashMap +{ + using Impl = detail::DenseHashTable, std::pair, detail::ItemInterfaceMap2, Hash, Eq>; + Impl impl; + +public: + using const_iterator = typename Impl::const_iterator; + using iterator = typename Impl::iterator; + + DenseHashMap(size_t buckets = 0) + : impl(buckets) + { + } + + void clear(size_t thresholdToDestroy = 32) + { + impl.clear(thresholdToDestroy); + } + + // Note: this reference is invalidated by any insert operation (i.e. operator[]) + Value& operator[](const Key& key) + { + impl.rehash_if_full(key); + return impl.insert_unsafe(key)->second; + } + + // Note: this pointer is invalidated by any insert operation (i.e. operator[]) + const Value* find(const Key& key) const + { + const std::pair* result = impl.find(key); + + return result ? &result->second : NULL; + } + + // Note: this pointer is invalidated by any insert operation (i.e. operator[]) + Value* find(const Key& key) + { + const std::pair* result = impl.find(key); + + return result ? const_cast(&result->second) : NULL; + } + + bool contains(const Key& key) const + { + return impl.find(key) != 0; + } + + void erase(const Key& key) + { + impl.erase(key); + } + + std::pair try_insert(const Key& key, const Value& value) + { + impl.rehash_if_full(key); + + size_t before = impl.size(); + std::pair* slot = impl.insert_unsafe(key); + + // Value is fresh if container count has increased + bool fresh = impl.size() > before; + + if (fresh) + slot->second = value; + + return std::make_pair(std::ref(slot->second), fresh); + } + + std::pair try_insert(const Key& key, Value&& value) + { + impl.rehash_if_full(key); + + size_t before = impl.size(); + std::pair* slot = impl.insert_unsafe(key); + + // Value is fresh if container count has increased + bool fresh = impl.size() > before; + + if (fresh) + slot->second = std::move(value); + + return std::make_pair(std::ref(slot->second), fresh); + } + + size_t size() const + { + return impl.size(); + } + + bool empty() const + { + return impl.size() == 0; + } + + const_iterator begin() const + { + return impl.begin(); + } + + const_iterator end() const + { + return impl.end(); + } + + iterator begin() + { + return impl.begin(); + } + + iterator end() + { + return impl.end(); + } +}; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/ExperimentalFlags.h b/External/luau/Common/include/Luau/ExperimentalFlags.h new file mode 100644 index 00000000..90173723 --- /dev/null +++ b/External/luau/Common/include/Luau/ExperimentalFlags.h @@ -0,0 +1,32 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +namespace Luau +{ + +inline bool isAnalysisFlagExperimental(const char* flag) +{ + // Flags in this list are disabled by default in various command-line tools. They may have behavior that is not fully final, + // or critical bugs that are found after the code has been submitted. This list is intended _only_ for flags that affect + // Luau's type checking. Flags that may change runtime behavior (e.g.: parser or VM flags) are not appropriate for this list. + static const char* const kList[] = { + "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code + "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative + "LuauSolverV2", + "UseNewLuauTypeSolverDefaultEnabled", // This can change the default solver used in cli applications, so it also needs to be disabled. Will + // require fixes in lua-apps code + "LuauRefactorStringSemanticSubtyping", // Requires fixes to lua-apps code. + // makes sure we always have at least one entry + nullptr, + }; + + for (const char* item : kList) + if (item && strcmp(item, flag) == 0) + return true; + + return false; +} + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/HashUtil.h b/External/luau/Common/include/Luau/HashUtil.h new file mode 100644 index 00000000..c615bf66 --- /dev/null +++ b/External/luau/Common/include/Luau/HashUtil.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace Luau +{ + +struct DenseHashPointer +{ + size_t operator()(const void* key) const + { + // The idea to use this hash function was suggested here originally: https://maskray.me/blog/2026-06-07-recent-llvm-hash-table-improvements + // Hash function implementation is detailed here: https://github.com/MaskRay/llvm-project/blob/main/llvm/include/llvm/ADT/DenseMapInfo.h + // This hash produces better scattering for arena allocated types, because the pointers usually share the higher order bits. + // When inserting lots of keys, quadratic probing is not enough to save DenseHash, although it usually takes many more elements, + // before it becomes a problem + uint64_t u = static_cast(uintptr_t(key)); + u *= 0xbf58476d1ce4e5b9u; + u ^= u >> 31; + // On 32-bit platforms uint64_t to size_t is a narrowing, so we need + // to static cast here. + return static_cast(u); + } +}; + +namespace detail +{ + +template +using DenseHashDefault = std::conditional_t, DenseHashPointer, std::hash>; + +} // namespace detail + +inline void hashCombine(size_t& seed, size_t hash) +{ + // Golden Ratio constant used for better hash scattering + // See https://softwareengineering.stackexchange.com/a/402543 + seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2); +} + +template, typename H2 = detail::DenseHashDefault> +struct PairHash +{ + std::size_t operator()(const std::pair& p) const noexcept + { + std::size_t seed = 0; + hashCombine(seed, h1(p.first)); + hashCombine(seed, h2(p.second)); + return seed; + } + +private: + H1 h1; + H2 h2; +}; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/InsertionOrderedMap.h b/External/luau/Common/include/Luau/InsertionOrderedMap.h new file mode 100644 index 00000000..86279153 --- /dev/null +++ b/External/luau/Common/include/Luau/InsertionOrderedMap.h @@ -0,0 +1,147 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include +#include +#include + +namespace Luau +{ + +template +struct InsertionOrderedMap +{ + static_assert(std::is_trivially_copyable_v, "key must be trivially copyable"); + +private: + using vec = std::vector>; + +public: + using iterator = typename vec::iterator; + using const_iterator = typename vec::const_iterator; + + void insert(K k, V v) + { + if (indices.count(k) != 0) + return; + + pairs.push_back(std::make_pair(k, std::move(v))); + indices[k] = pairs.size() - 1; + } + + void clear() + { + pairs.clear(); + indices.clear(); + } + + size_t size() const + { + LUAU_ASSERT(pairs.size() == indices.size()); + return pairs.size(); + } + + bool contains(const K& k) const + { + return indices.count(k) > 0; + } + + const V* get(const K& k) const + { + auto it = indices.find(k); + if (it == indices.end()) + return nullptr; + else + return &pairs.at(it->second).second; + } + + V* get(const K& k) + { + auto it = indices.find(k); + if (it == indices.end()) + return nullptr; + else + return &pairs.at(it->second).second; + } + + V& operator[](const K& k) + { + auto it = indices.find(k); + if (it == indices.end()) + { + pairs.push_back(std::make_pair(k, V())); + indices[k] = pairs.size() - 1; + return pairs.back().second; + } + else + return pairs.at(it->second).second; + } + + const_iterator begin() const + { + return pairs.begin(); + } + + const_iterator end() const + { + return pairs.end(); + } + + iterator begin() + { + return pairs.begin(); + } + + iterator end() + { + return pairs.end(); + } + + const_iterator find(K k) const + { + auto indicesIt = indices.find(k); + if (indicesIt == indices.end()) + return end(); + else + return begin() + indicesIt->second; + } + + iterator find(K k) + { + auto indicesIt = indices.find(k); + if (indicesIt == indices.end()) + return end(); + else + return begin() + indicesIt->second; + } + + void erase(iterator it) + { + if (it == pairs.end()) + return; + + K k = it->first; + auto indexIt = indices.find(k); + if (indexIt == indices.end()) + return; + + size_t removed = indexIt->second; + indices.erase(indexIt); + pairs.erase(it); + + for (auto& [_, index] : indices) + { + if (index > removed) + --index; + } + } + +private: + vec pairs; + std::unordered_map indices; +}; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/ScopedSeenSet.h b/External/luau/Common/include/Luau/ScopedSeenSet.h new file mode 100644 index 00000000..8d335674 --- /dev/null +++ b/External/luau/Common/include/Luau/ScopedSeenSet.h @@ -0,0 +1,29 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +namespace Luau +{ + +template +struct ScopedSeenSet +{ + SetType& seen; + Key key; + + ScopedSeenSet(SetType& seen, Key key) + : seen(seen) + , key(key) + { + seen.insert(key); + } + + ~ScopedSeenSet() + { + seen.erase(key); + } + + ScopedSeenSet(const ScopedSeenSet&) = delete; + ScopedSeenSet& operator=(const ScopedSeenSet&) = delete; +}; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/SmallVector.h b/External/luau/Common/include/Luau/SmallVector.h new file mode 100644 index 00000000..5e41cd18 --- /dev/null +++ b/External/luau/Common/include/Luau/SmallVector.h @@ -0,0 +1,322 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include +#include +#include +#include + +namespace Luau +{ + +template +class SmallVector +{ + static_assert(std::is_nothrow_move_constructible::value, "T must be nothrow move constructible"); + +public: + using iterator = T*; + using const_iterator = const T*; + using value_type = T; + using reference = T&; + using const_reference = const T&; + + SmallVector() + { + ptr = reinterpret_cast(storage); + } + + SmallVector(std::initializer_list init) + : SmallVector() + { + LUAU_ASSERT(unsigned(init.size()) == init.size()); + + reserve(unsigned(init.size())); + + std::uninitialized_copy(init.begin(), init.end(), ptr); + count = unsigned(init.size()); + } + + SmallVector(const SmallVector& other) + : SmallVector() + { + reserve(other.count); + + std::uninitialized_copy(other.begin(), other.end(), ptr); + count = other.count; + } + + SmallVector(SmallVector&& other) noexcept + : SmallVector() + { + if (!other.is_heap()) + { + std::uninitialized_move(other.begin(), other.end(), ptr); + count = other.count; + + other.clear(); + } + else + { + ptr = other.ptr; + max = other.max; + count = other.count; + + other.ptr = reinterpret_cast(other.storage); + other.max = N; + other.count = 0; + } + } + + ~SmallVector() + { + clear(); + + if (is_heap()) + ::operator delete(ptr); + } + + SmallVector& operator=(const SmallVector& other) + { + if (this == &other) + return *this; + + if (other.count <= count) + { + std::copy(other.begin(), other.end(), begin()); + + while (count > other.count) + ptr[--count].~T(); + } + else + { + std::copy(other.begin(), other.begin() + count, begin()); + + reserve(other.count); + + std::uninitialized_copy(other.begin() + count, other.end(), ptr + count); + count = other.count; + } + + return *this; + } + + SmallVector& operator=(SmallVector&& other) noexcept + { + if (this != &other) + { + clear(); + + if (!other.is_heap()) + { + std::uninitialized_move(other.begin(), other.end(), begin()); + count = other.count; + + other.clear(); + } + else + { + if (is_heap()) + ::operator delete(ptr); + + ptr = other.ptr; + max = other.max; + count = other.count; + + other.ptr = reinterpret_cast(other.storage); + other.max = N; + other.count = 0; + } + } + + return *this; + } + + template + reference emplace_back(Args&&... args) + { + if (count == max) + grow(count + 1); + + new (&ptr[count]) T(std::forward(args)...); + return ptr[count++]; + } + + void push_back(const T& val) + { + if (count == max) + grow(count + 1); + + new (&ptr[count]) T(val); + count++; + } + + void push_back(T&& val) + { + if (count == max) + grow(count + 1); + + new (&ptr[count]) T(std::move(val)); + count++; + } + + T& front() + { + LUAU_ASSERT(count > 0); + return ptr[0]; + } + + const T& front() const + { + LUAU_ASSERT(count > 0); + return ptr[0]; + } + + T& back() + { + LUAU_ASSERT(count > 0); + return ptr[count - 1]; + } + + const T& back() const + { + LUAU_ASSERT(count > 0); + return ptr[count - 1]; + } + + bool empty() const noexcept + { + return count == 0; + } + + unsigned size() const noexcept + { + return count; + } + + unsigned capacity() const noexcept + { + return max; + } + + void pop_back() + { + LUAU_ASSERT(count > 0); + ptr[--count].~T(); + } + + void clear() + { + while (count > 0) + ptr[--count].~T(); + } + + T* begin() + { + return ptr; + } + + const T* begin() const + { + return ptr; + } + + T* end() + { + return ptr + count; + } + + const T* end() const + { + return ptr + count; + } + + T* data() + { + return ptr; + } + const T* data() const + { + return ptr; + } + + T& operator[](size_t index) + { + LUAU_ASSERT(index < count); + + return ptr[index]; + } + + const T& operator[](size_t index) const + { + LUAU_ASSERT(index < count); + + return ptr[index]; + } + + void resize(unsigned newSize) + { + if (newSize > count) + { + if (newSize > max) + grow(newSize); + + for (unsigned i = count; i < newSize; ++i) + new (&ptr[i]) T(); + } + else + { + while (count > newSize) + ptr[--count].~T(); + } + + count = newSize; + } + + void reserve(unsigned reserveSize) + { + if (reserveSize > max) + grow(reserveSize); + } + +private: + bool is_heap() const + { + return ptr != reinterpret_cast(storage); + } + + void grow(unsigned newSize) + { + if (max + (max >> 1) > newSize) + newSize = max + (max >> 1); + else + newSize += 4; + + LUAU_ASSERT(newSize < 0x40000000); + + void* raw = ::operator new(newSize * sizeof(T)); + T* newData = static_cast(raw); + + std::uninitialized_move(ptr, ptr + count, newData); + + for (unsigned i = 0; i < count; ++i) + ptr[i].~T(); + + if (is_heap()) + ::operator delete(ptr); + + ptr = newData; + max = newSize; + } + + T* ptr = nullptr; + unsigned count = 0; + unsigned max = N; + + alignas(T) unsigned char storage[N * sizeof(T)]; +}; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/StringUtils.h b/External/luau/Common/include/Luau/StringUtils.h new file mode 100644 index 00000000..baf39957 --- /dev/null +++ b/External/luau/Common/include/Luau/StringUtils.h @@ -0,0 +1,39 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include + +#include + +namespace Luau +{ + +std::string format(const char* fmt, ...) LUAU_PRINTF_ATTR(1, 2); +std::string vformat(const char* fmt, va_list args); + +void formatAppend(std::string& str, const char* fmt, ...) LUAU_PRINTF_ATTR(2, 3); +void vformatAppend(std::string& ret, const char* fmt, va_list args); + +std::string join(const std::vector& segments, std::string_view delimiter); +std::string join(const std::vector& segments, std::string_view delimiter); + +std::vector split(std::string_view s, char delimiter); + +// Computes the Damerau-Levenshtein distance of A and B. +// https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance#Distance_with_adjacent_transpositions +size_t editDistance(std::string_view a, std::string_view b); + +bool startsWith(std::string_view haystack, std::string_view needle); +bool equalsLower(std::string_view lhs, std::string_view rhs); + +size_t hashRange(const char* data, size_t size); + +std::string escape(std::string_view s, bool escapeForInterpString = false); +bool isIdentifier(std::string_view s); + +std::string_view strip(std::string_view s); + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/TimeTrace.h b/External/luau/Common/include/Luau/TimeTrace.h new file mode 100644 index 00000000..7f76684d --- /dev/null +++ b/External/luau/Common/include/Luau/TimeTrace.h @@ -0,0 +1,240 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include + +#include +#include + +LUAU_FASTFLAG(DebugLuauTimeTracing) + +namespace Luau +{ +namespace TimeTrace +{ +double getClock(); +uint32_t getClockMicroseconds(); +} // namespace TimeTrace +} // namespace Luau + +#if defined(LUAU_ENABLE_TIME_TRACE) + +namespace Luau +{ +namespace TimeTrace +{ +struct Token +{ + const char* name; + const char* category; +}; + +enum class EventType : uint8_t +{ + Enter, + Leave, + + ArgName, + ArgValue, +}; + +struct Event +{ + EventType type; + uint16_t token; + + union + { + uint32_t microsec; // 1 hour trace limit + uint32_t dataPos; + } data; +}; + +struct GlobalContext; +struct ThreadContext; + +std::shared_ptr getGlobalContext(); + +uint16_t createToken(GlobalContext& context, const char* name, const char* category); +uint32_t createThread(GlobalContext& context, ThreadContext* threadContext); +void releaseThread(GlobalContext& context, ThreadContext* threadContext); +void flushEvents(GlobalContext& context, uint32_t threadId, const std::vector& events, const std::vector& data); + +struct ThreadContext +{ + ThreadContext() + : globalContext(getGlobalContext()) + { + threadId = createThread(*globalContext, this); + } + + ~ThreadContext() + { + if (!events.empty()) + flushEvents(); + + releaseThread(*globalContext, this); + } + + void flushEvents() + { + static uint16_t flushToken = createToken(*globalContext, "flushEvents", "TimeTrace"); + + events.push_back({EventType::Enter, flushToken, {getClockMicroseconds()}}); + + TimeTrace::flushEvents(*globalContext, threadId, events, data); + + events.clear(); + data.clear(); + + events.push_back({EventType::Leave, 0, {getClockMicroseconds()}}); + } + + void eventEnter(uint16_t token) + { + eventEnter(token, getClockMicroseconds()); + } + + void eventEnter(uint16_t token, uint32_t microsec) + { + events.push_back({EventType::Enter, token, {microsec}}); + } + + void eventLeave() + { + eventLeave(getClockMicroseconds()); + } + + void eventLeave(uint32_t microsec) + { + events.push_back({EventType::Leave, 0, {microsec}}); + + if (events.size() > kEventFlushLimit) + flushEvents(); + } + + void eventArgument(const char* name, const char* value) + { + uint32_t pos = uint32_t(data.size()); + data.insert(data.end(), name, name + strlen(name) + 1); + events.push_back({EventType::ArgName, 0, {pos}}); + + pos = uint32_t(data.size()); + data.insert(data.end(), value, value + strlen(value) + 1); + events.push_back({EventType::ArgValue, 0, {pos}}); + } + + std::shared_ptr globalContext; + uint32_t threadId; + std::vector events; + std::vector data; + + static constexpr size_t kEventFlushLimit = 8192; +}; + +using ThreadContextProvider = ThreadContext& (*)(); + +inline ThreadContextProvider& threadContextProvider() +{ + static ThreadContextProvider handler = nullptr; + return handler; +} + +ThreadContext& getThreadContext(); + +struct Scope +{ + explicit Scope(uint16_t token) + : context(getThreadContext()) + { + if (!FFlag::DebugLuauTimeTracing) + return; + + context.eventEnter(token); + } + + ~Scope() + { + if (!FFlag::DebugLuauTimeTracing) + return; + + context.eventLeave(); + } + + ThreadContext& context; +}; + +struct OptionalTailScope +{ + explicit OptionalTailScope(uint16_t token, uint32_t threshold) + : context(getThreadContext()) + , token(token) + , threshold(threshold) + { + if (!FFlag::DebugLuauTimeTracing) + return; + + pos = uint32_t(context.events.size()); + microsec = getClockMicroseconds(); + } + + ~OptionalTailScope() + { + if (!FFlag::DebugLuauTimeTracing) + return; + + if (pos == context.events.size()) + { + uint32_t curr = getClockMicroseconds(); + + if (curr - microsec > threshold) + { + context.eventEnter(token, microsec); + context.eventLeave(curr); + } + } + } + + ThreadContext& context; + uint16_t token; + uint32_t threshold; + uint32_t microsec; + uint32_t pos; +}; + +LUAU_NOINLINE uint16_t createScopeData(const char* name, const char* category); + +} // namespace TimeTrace +} // namespace Luau + +// Regular scope +#define LUAU_TIMETRACE_SCOPE(name, category) \ + static uint16_t lttScopeStatic = Luau::TimeTrace::createScopeData(name, category); \ + Luau::TimeTrace::Scope lttScope(lttScopeStatic) + +// A scope without nested scopes that may be skipped if the time it took is less than the threshold +#define LUAU_TIMETRACE_OPTIONAL_TAIL_SCOPE(name, category, microsec) \ + static uint16_t lttScopeStaticOptTail = Luau::TimeTrace::createScopeData(name, category); \ + Luau::TimeTrace::OptionalTailScope lttScope(lttScopeStaticOptTail, microsec) + +// Extra key/value data can be added to regular scopes +#define LUAU_TIMETRACE_ARGUMENT(name, value) \ + do \ + { \ + if (FFlag::DebugLuauTimeTracing) \ + lttScope.context.eventArgument(name, value); \ + } while (false) + +#else + +#define LUAU_TIMETRACE_SCOPE(name, category) +#define LUAU_TIMETRACE_OPTIONAL_TAIL_SCOPE(name, category, microsec) +#define LUAU_TIMETRACE_ARGUMENT(name, value) \ + do \ + { \ + } while (false) + +#endif diff --git a/External/luau/Common/include/Luau/Variant.h b/External/luau/Common/include/Luau/Variant.h new file mode 100644 index 00000000..4810ad80 --- /dev/null +++ b/External/luau/Common/include/Luau/Variant.h @@ -0,0 +1,304 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include +#include +#include +#include + +#include + +namespace Luau +{ + +template +class Variant +{ + static_assert(sizeof...(Ts) > 0, "variant must have at least 1 type (empty variants are ill-formed)"); + static_assert(std::disjunction_v...> == false, "variant does not allow void as an alternative type"); + static_assert(std::disjunction_v...> == false, "variant does not allow references as an alternative type"); + static_assert(std::disjunction_v...> == false, "variant does not allow arrays as an alternative type"); + +public: + template + static constexpr int getTypeId() + { + using TT = std::decay_t; + + constexpr int N = sizeof...(Ts); + constexpr bool is[N] = {std::is_same_v...}; + + for (int i = 0; i < N; ++i) + if (is[i]) + return i; + + return -1; + } + +private: + template + struct First + { + using type = T; + }; + +public: + using first_alternative = typename First::type; + + template + static constexpr bool is_part_of_v = std::disjunction_v, T>...>; + + Variant() + { + static_assert(std::is_default_constructible_v, "first alternative type must be default constructible"); + typeId = 0; + new (&storage) first_alternative(); + } + + template + Variant(T&& value, std::enable_if_t() >= 0>* = 0) + { + using TT = std::decay_t; + + constexpr int tid = getTypeId(); + typeId = tid; + new (&storage) TT(std::forward(value)); + } + + Variant(const Variant& other) + { + static constexpr FnCopy table[sizeof...(Ts)] = {&fnCopy...}; + + typeId = other.typeId; + table[typeId](&storage, &other.storage); + } + + Variant(Variant&& other) noexcept + { + typeId = other.typeId; + tableMove[typeId](&storage, &other.storage); + } + + ~Variant() + { + tableDtor[typeId](&storage); + } + + Variant& operator=(const Variant& other) + { + Variant copy(other); + // static_cast is equivalent to std::move() but faster in Debug + return *this = static_cast(copy); + } + + Variant& operator=(Variant&& other) noexcept + { + if (this != &other) + { + tableDtor[typeId](&storage); + typeId = other.typeId; + tableMove[typeId](&storage, &other.storage); // nothrow + } + return *this; + } + + template + T& emplace(Args&&... args) + { + using TT = std::decay_t; + constexpr int tid = getTypeId(); + static_assert(tid >= 0, "unsupported T"); + + tableDtor[typeId](&storage); + typeId = tid; + new (&storage) TT{std::forward(args)...}; + + return *reinterpret_cast(&storage); + } + + template + const T* get_if() const + { + constexpr int tid = getTypeId(); + static_assert(tid >= 0, "unsupported T"); + + return tid == typeId ? reinterpret_cast(&storage) : nullptr; + } + + template + T* get_if() + { + constexpr int tid = getTypeId(); + static_assert(tid >= 0, "unsupported T"); + + return tid == typeId ? reinterpret_cast(&storage) : nullptr; + } + + bool valueless_by_exception() const + { + return false; + } + + int index() const + { + return typeId; + } + + bool operator==(const Variant& other) const + { + static constexpr FnPred table[sizeof...(Ts)] = {&fnPredEq...}; + + return typeId == other.typeId && table[typeId](&storage, &other.storage); + } + + bool operator!=(const Variant& other) const + { + return !(*this == other); + } + +private: + static constexpr size_t cmax(std::initializer_list l) + { + size_t res = 0; + for (size_t i : l) + res = (res < i) ? i : res; + return res; + } + + static constexpr size_t storageSize = cmax({sizeof(Ts)...}); + static constexpr size_t storageAlign = cmax({alignof(Ts)...}); + + using FnCopy = void (*)(void*, const void*); + using FnMove = void (*)(void*, void*) noexcept; + using FnDtor = void (*)(void*); + using FnPred = bool (*)(const void*, const void*); + + template + static void fnCopy(void* dst, const void* src) + { + new (dst) T(*static_cast(src)); + } + + template + static void fnMove(void* dst, void* src) noexcept + { + // static_cast is equivalent to std::move() but faster in Debug + new (dst) T(static_cast(*static_cast(src))); + } + + template + static void fnDtor(void* dst) + { + static_cast(dst)->~T(); + } + + template + static bool fnPredEq(const void* lhs, const void* rhs) + { + return *static_cast(lhs) == *static_cast(rhs); + } + + static constexpr FnMove tableMove[sizeof...(Ts)] = {&fnMove...}; + static constexpr FnDtor tableDtor[sizeof...(Ts)] = {&fnDtor...}; + + int typeId; + alignas(storageAlign) char storage[storageSize]; + + template + friend auto visit(Visitor&& vis, const Variant<_Ts...>& var); + template + friend auto visit(Visitor&& vis, Variant<_Ts...>& var); +}; + +template +const T* get_if(const Variant* var) +{ + return var ? var->template get_if() : nullptr; +} + +template +T* get_if(Variant* var) +{ + return var ? var->template get_if() : nullptr; +} + +template +static void fnVisitR(Visitor& vis, Result& dst, std::conditional_t, const void, void>* src) +{ + dst = vis(*static_cast(src)); +} + +template +static void fnVisitV(Visitor& vis, std::conditional_t, const void, void>* src) +{ + vis(*static_cast(src)); +} + +template +auto visit(Visitor&& vis, const Variant& var) +{ + static_assert(std::conjunction_v...>, "visitor must accept every alternative as an argument"); + + using Result = std::invoke_result_t::first_alternative>; + static_assert( + std::conjunction_v>...>, "visitor result type must be consistent between alternatives" + ); + + if constexpr (std::is_same_v) + { + using FnVisitV = void (*)(Visitor&, const void*); + static const FnVisitV tableVisit[sizeof...(Ts)] = {&fnVisitV...}; + + tableVisit[var.typeId](vis, &var.storage); + } + else + { + using FnVisitR = void (*)(Visitor&, Result&, const void*); + static const FnVisitR tableVisit[sizeof...(Ts)] = {&fnVisitR...}; + + Result res; + tableVisit[var.typeId](vis, res, &var.storage); + return res; + } +} + +template +auto visit(Visitor&& vis, Variant& var) +{ + static_assert(std::conjunction_v...>, "visitor must accept every alternative as an argument"); + + using Result = std::invoke_result_t::first_alternative&>; + static_assert( + std::conjunction_v>...>, "visitor result type must be consistent between alternatives" + ); + + if constexpr (std::is_same_v) + { + using FnVisitV = void (*)(Visitor&, void*); + static const FnVisitV tableVisit[sizeof...(Ts)] = {&fnVisitV...}; + + tableVisit[var.typeId](vis, &var.storage); + } + else + { + using FnVisitR = void (*)(Visitor&, Result&, void*); + static const FnVisitR tableVisit[sizeof...(Ts)] = {&fnVisitR...}; + + Result res; + tableVisit[var.typeId](vis, res, &var.storage); + return res; + } +} + +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + +template +inline constexpr bool always_false_v = false; + +} // namespace Luau diff --git a/External/luau/Common/include/Luau/VecDeque.h b/External/luau/Common/include/Luau/VecDeque.h new file mode 100644 index 00000000..32e0ea0a --- /dev/null +++ b/External/luau/Common/include/Luau/VecDeque.h @@ -0,0 +1,501 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Luau +{ +// `VecDeque` is a general double-ended implementation designed as a drop-in replacement for the +// standard library `std::deque`. It's backed by a growable ring buffer, rather than using the +// segmented queue design of `std::deque` which can degrade into a linked list in the worst case. +// The motivation for `VecDeque` as a replacement is to maintain the asymptotic complexity of +// `std::deque` while reducing overall allocations and promoting better usage of the cache. Its API +// is intended to be compatible with `std::deque` and `std::vector` as appropriate, and as such +// provides corresponding method definitions and supports the use of custom allocators. +// +// `VecDeque` offers pushing and popping from both ends with an amortized O(1) complexity. It also +// supports `std::vector`-style random-access in O(1). The implementation of buffer resizing uses +// a growth factor of 1.5x to enable better memory reuse after resizing, and reduce overall memory +// fragmentation when using the queue. +// +// Since `VecDeque` is a ring buffer, its elements are not necessarily contiguous in memory. To +// describe this, we refer to the two portions of the buffer as the `head` and the `tail`. The +// `head` is the initial portion of the queue that is on the range `[head, capacity)` and the tail +// is the (optionally) remaining portion on the range `[0, head + size - capacity)` whenever the +// `head + size` exceeds the capacity of the buffer. +// +// `VecDeque` does not currently support iteration since its primary focus is on providing +// double-ended queue functionality specifically, but it can be reasonably expanded to provide +// an iterator if we have a use-case for one in the future. +template> +class VecDeque : Allocator +{ +private: + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + T* buffer = nullptr; // the existing allocation we have backing this queue + size_t buffer_capacity = 0; // the size of our allocation + + size_t head = 0; // the index of the head of the queue + size_t queue_size = 0; // the size of the queue + + void destroyElements() noexcept + { + size_t head_size = + std::min(queue_size, capacity() - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + // we have to destroy every element in the head portion + for (size_t index = head; index < head + head_size; index++) + buffer[index].~T(); + + // and any in the tail portion, if one exists + for (size_t index = 0; index < tail_size; index++) + buffer[index].~T(); + } + + bool is_full() + { + return queue_size == capacity(); + } + + void grow() + { + size_t old_capacity = capacity(); + + // we use a growth factor of 1.5x (plus a constant) here in order to enable the + // previous memory to be reused after a certain number of calls to grow. + // see: https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md#memory-handling + size_t new_capacity = (old_capacity > 0) ? old_capacity * 3 / 2 + 1 : 4; + + // check that it's a legal allocation + if (new_capacity > max_size()) + throw std::bad_array_new_length(); + + // allocate a new backing buffer + T* new_buffer = this->allocate(new_capacity); + + // we should not be growing if the capacity is not the current size + LUAU_ASSERT(old_capacity == queue_size); + + size_t head_size = + std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + // move the head into the new buffer + if (head_size != 0) + std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); + + // move the tail into the new buffer immediately after + if (tail_size != 0) + std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); + + // destroy the old elements + destroyElements(); + // deallocate the old buffer + this->deallocate(buffer, old_capacity); + + // set up the queue to be backed by the new buffer + buffer = new_buffer; + buffer_capacity = new_capacity; + head = 0; + } + + size_t logicalToPhysical(size_t pos) + { + return (head + pos) % capacity(); + } + +public: + VecDeque() = default; + + explicit VecDeque(const Allocator& alloc) noexcept + : Allocator{alloc} + { + } + + VecDeque(const VecDeque& other) + : buffer(this->allocate(other.buffer_capacity)) + , buffer_capacity(other.buffer_capacity) + , head(other.head) + , queue_size(other.queue_size) + { + // copy the initialized contents of the other buffer to this one + size_t head_size = std::min( + other.queue_size, + other.buffer_capacity - other.head + ); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + if (head_size != 0) + std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head); + + if (tail_size != 0) + std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer); + } + + VecDeque(const VecDeque& other, const Allocator& alloc) + : Allocator{alloc} + , buffer(this->allocate(other.buffer_capacity)) + , buffer_capacity(other.buffer_capacity) + , head(other.head) + , queue_size(other.queue_size) + { + // copy the initialized contents of the other buffer to this one + size_t head_size = std::min( + other.queue_size, + other.buffer_capacity - other.head + ); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + if (head_size != 0) + std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head); + + if (tail_size != 0) + std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer); + } + + VecDeque(VecDeque&& other) noexcept + : buffer(std::exchange(other.buffer, nullptr)) + , buffer_capacity(std::exchange(other.buffer_capacity, 0)) + , head(std::exchange(other.head, 0)) + , queue_size(std::exchange(other.queue_size, 0)) + { + } + + VecDeque(VecDeque&& other, const Allocator& alloc) noexcept + : Allocator{alloc} + , buffer(std::exchange(other.buffer, nullptr)) + , buffer_capacity(std::exchange(other.buffer_capacity, 0)) + , head(std::exchange(other.head, 0)) + , queue_size(std::exchange(other.queue_size, 0)) + { + } + + VecDeque(std::initializer_list init, const Allocator& alloc = Allocator()) + : Allocator{alloc} + { + buffer = this->allocate(init.size()); + buffer_capacity = init.size(); + queue_size = init.size(); + + std::uninitialized_copy(init.begin(), init.end(), buffer); + } + + ~VecDeque() noexcept + { + // destroy any elements that exist + destroyElements(); + // free the allocated buffer + this->deallocate(buffer, buffer_capacity); + } + + VecDeque& operator=(const VecDeque& other) + { + if (this == &other) + return *this; + + // destroy all of the existing elements + destroyElements(); + + if (buffer_capacity < other.size()) + { + // free the current buffer + this->deallocate(buffer, buffer_capacity); + + buffer = this->allocate(other.buffer_capacity); + buffer_capacity = other.buffer_capacity; + } + + size_t head_size = std::min( + other.queue_size, + other.buffer_capacity - other.head + ); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + // Assignment doesn't try to match the capacity of 'other' and thus makes the buffer contiguous + head = 0; + queue_size = other.queue_size; + + if (head_size != 0) + std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer); + + if (tail_size != 0) + std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size); + + return *this; + } + + VecDeque& operator=(VecDeque&& other) + { + if (this == &other) + return *this; + + // destroy all of the existing elements + destroyElements(); + // free the current buffer + this->deallocate(buffer, buffer_capacity); + + buffer = std::exchange(other.buffer, nullptr); + buffer_capacity = std::exchange(other.buffer_capacity, 0); + head = std::exchange(other.head, 0); + queue_size = std::exchange(other.queue_size, 0); + + return *this; + } + + Allocator get_allocator() const noexcept + { + return this; + } + + // element access + + T& at(size_t pos) + { + if (pos >= queue_size) + throw std::out_of_range("VecDeque"); + + return buffer[logicalToPhysical(pos)]; + } + + const T& at(size_t pos) const + { + if (pos >= queue_size) + throw std::out_of_range("VecDeque"); + + return buffer[logicalToPhysical(pos)]; + } + + [[nodiscard]] T& operator[](size_t pos) noexcept + { + LUAU_ASSERT(pos < queue_size); + + return buffer[logicalToPhysical(pos)]; + } + + [[nodiscard]] const T& operator[](size_t pos) const noexcept + { + LUAU_ASSERT(pos < queue_size); + + return buffer[logicalToPhysical(pos)]; + } + + T& front() + { + LUAU_ASSERT(!empty()); + + return buffer[head]; + } + + const T& front() const + { + LUAU_ASSERT(!empty()); + + return buffer[head]; + } + + T& back() + { + LUAU_ASSERT(!empty()); + + size_t back = logicalToPhysical(queue_size - 1); + return buffer[back]; + } + + const T& back() const + { + LUAU_ASSERT(!empty()); + + size_t back = logicalToPhysical(queue_size - 1); + return buffer[back]; + } + + // capacity + + bool empty() const noexcept + { + return queue_size == 0; + } + + size_t size() const noexcept + { + return queue_size; + } + + size_t max_size() const noexcept + { + return std::numeric_limits::max() / sizeof(T); + } + + void reserve(size_t new_capacity) + { + // error if this allocation would be illegal + if (new_capacity > max_size()) + throw std::length_error("too large"); + + size_t old_capacity = capacity(); + + // do nothing if we're requesting a capacity that would not cause growth + if (new_capacity <= old_capacity) + return; + + size_t head_size = + std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + // allocate a new backing buffer + T* new_buffer = this->allocate(new_capacity); + + // move the head into the new buffer + if (head_size != 0) + std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); + + // move the tail into the new buffer immediately after + if (tail_size != 0) + std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); + + // destroy all the existing elements before freeing the old buffer + destroyElements(); + + // deallocate the old buffer + this->deallocate(buffer, old_capacity); + + // set up the queue to be backed by the new buffer + buffer = new_buffer; + buffer_capacity = new_capacity; + head = 0; + } + + size_t capacity() const noexcept + { + return buffer_capacity; + } + + void shrink_to_fit() + { + size_t old_capacity = capacity(); + size_t new_capacity = queue_size; + + if (old_capacity == new_capacity) + return; + + size_t head_size = + std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer) + size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) + + // allocate a new backing buffer + T* new_buffer = this->allocate(new_capacity); + + // move the head into the new buffer + if (head_size != 0) + std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); + + // move the tail into the new buffer immediately after, if we have one + if (tail_size != 0) + std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); + + // destroy all the existing elements before freeing the old buffer + destroyElements(); + // deallocate the old buffer + this->deallocate(buffer, old_capacity); + + // set up the queue to be backed by the new buffer + buffer = new_buffer; + buffer_capacity = new_capacity; + head = 0; + } + + [[nodiscard]] bool is_contiguous() const noexcept + { + // this is an overflow-safe alternative to writing `head + size <= capacity`. + return head <= capacity() - queue_size; + } + + // modifiers + + void clear() noexcept + { + destroyElements(); + + head = 0; + queue_size = 0; + } + + void push_back(const T& value) + { + if (is_full()) + grow(); + + size_t next_back = logicalToPhysical(queue_size); + new (buffer + next_back) T(value); + queue_size++; + } + + template + T& emplace_back(Args&&... args) + { + if (is_full()) + grow(); + + size_t next_back = logicalToPhysical(queue_size); + new (buffer + next_back) T(std::forward(args)...); + queue_size++; + return buffer[next_back]; + } + + + void pop_back() + { + LUAU_ASSERT(!empty()); + + queue_size--; + size_t next_back = logicalToPhysical(queue_size); + buffer[next_back].~T(); + } + + void push_front(const T& value) + { + if (is_full()) + grow(); + + head = (head == 0) ? capacity() - 1 : head - 1; + new (buffer + head) T(value); + queue_size++; + } + + template + T& emplace_front(Args&&... args) + { + if (is_full()) + grow(); + + head = (head == 0) ? capacity() - 1 : head - 1; + new (buffer + head) T(std::forward(args)...); + queue_size++; + return buffer[head]; + } + + void pop_front() + { + LUAU_ASSERT(!empty()); + + buffer[head].~T(); + head++; + queue_size--; + + if (head == capacity()) + head = 0; + } +}; + +} // namespace Luau diff --git a/External/luau/Common/src/BytecodeWire.cpp b/External/luau/Common/src/BytecodeWire.cpp new file mode 100644 index 00000000..68a3dfe0 --- /dev/null +++ b/External/luau/Common/src/BytecodeWire.cpp @@ -0,0 +1,31 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/BytecodeWire.h" + +#include "Luau/Common.h" + +namespace Luau +{ + +uint64_t readVarInt64(const char* data, size_t& offset) +{ + uint64_t result = 0; + unsigned int shift = 0; + + uint8_t byte; + + do + { + byte = read(data, offset); + result |= static_cast(byte & 127) << shift; + shift += 7; + } while (byte & 128); + + return result; +} + +unsigned int readVarInt(const char* data, size_t& offset) +{ + return static_cast(readVarInt64(data, offset)); +} + +} // namespace Luau \ No newline at end of file diff --git a/External/luau/Common/src/StringUtils.cpp b/External/luau/Common/src/StringUtils.cpp new file mode 100644 index 00000000..8059e208 --- /dev/null +++ b/External/luau/Common/src/StringUtils.cpp @@ -0,0 +1,315 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/StringUtils.h" + +#include "Luau/Common.h" + +#include +#include +#include +#include +#include + +namespace Luau +{ + +void vformatAppend(std::string& ret, const char* fmt, va_list args) +{ + va_list argscopy; + va_copy(argscopy, args); +#ifdef _MSC_VER + int actualSize = _vscprintf(fmt, argscopy); +#else + int actualSize = vsnprintf(NULL, 0, fmt, argscopy); +#endif + va_end(argscopy); + + if (actualSize <= 0) + return; + + size_t sz = ret.size(); + ret.resize(sz + actualSize); + vsnprintf(&ret[0] + sz, actualSize + 1, fmt, args); +} + +std::string format(const char* fmt, ...) +{ + std::string result; + va_list args; + va_start(args, fmt); + vformatAppend(result, fmt, args); + va_end(args); + return result; +} + +void formatAppend(std::string& str, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + vformatAppend(str, fmt, args); + va_end(args); +} + +std::string vformat(const char* fmt, va_list args) +{ + std::string ret; + vformatAppend(ret, fmt, args); + return ret; +} + +template +static std::string joinImpl(const std::vector& segments, std::string_view delimiter) +{ + if (segments.empty()) + return ""; + + size_t len = (segments.size() - 1) * delimiter.size(); + for (const auto& sv : segments) + len += sv.size(); + + std::string result; + result.resize(len); + char* dest = const_cast(result.data()); // This const_cast is only necessary until C++17 + + auto it = segments.begin(); + memcpy(dest, it->data(), it->size()); + dest += it->size(); + ++it; + for (; it != segments.end(); ++it) + { + memcpy(dest, delimiter.data(), delimiter.size()); + dest += delimiter.size(); + memcpy(dest, it->data(), it->size()); + dest += it->size(); + } + + LUAU_ASSERT(dest == result.data() + len); + + return result; +} + +std::string join(const std::vector& segments, std::string_view delimiter) +{ + return joinImpl(segments, delimiter); +} + +std::string join(const std::vector& segments, std::string_view delimiter) +{ + return joinImpl(segments, delimiter); +} + +std::vector split(std::string_view s, char delimiter) +{ + std::vector result; + + while (!s.empty()) + { + auto index = s.find(delimiter); + if (index == std::string::npos) + { + result.push_back(s); + break; + } + result.push_back(s.substr(0, index)); + s.remove_prefix(index + 1); + } + + return result; +} + +size_t editDistance(std::string_view a, std::string_view b) +{ + // When there are matching prefix and suffix, they end up computing as zero cost, effectively making it no-op. We drop these characters. + while (!a.empty() && !b.empty() && a.front() == b.front()) + { + a.remove_prefix(1); + b.remove_prefix(1); + } + + while (!a.empty() && !b.empty() && a.back() == b.back()) + { + a.remove_suffix(1); + b.remove_suffix(1); + } + + // Since we know the edit distance is the difference of the length of A and B discounting the matching prefixes and suffixes, + // it is therefore pointless to run the rest of this function to find that out. We immediately infer this size and return it. + if (a.empty()) + return b.size(); + if (b.empty()) + return a.size(); + + size_t maxDistance = a.size() + b.size(); + + std::vector distances((a.size() + 2) * (b.size() + 2), 0); + auto getPos = [b](size_t x, size_t y) -> size_t + { + return (x * (b.size() + 2)) + y; + }; + + distances[0] = maxDistance; + + for (size_t x = 0; x <= a.size(); ++x) + { + distances[getPos(x + 1, 0)] = maxDistance; + distances[getPos(x + 1, 1)] = x; + } + + for (size_t y = 0; y <= b.size(); ++y) + { + distances[getPos(0, y + 1)] = maxDistance; + distances[getPos(1, y + 1)] = y; + } + + std::array seenCharToRow; + seenCharToRow.fill(0); + + for (size_t x = 1; x <= a.size(); ++x) + { + size_t lastMatchedY = 0; + + for (size_t y = 1; y <= b.size(); ++y) + { + // The value of b[N] can be negative with unicode characters + unsigned char bSeenCharIndex = static_cast(b[y - 1]); + size_t x1 = seenCharToRow[bSeenCharIndex]; + size_t y1 = lastMatchedY; + + size_t cost = 1; + if (a[x - 1] == b[y - 1]) + { + cost = 0; + lastMatchedY = y; + } + + size_t transposition = distances[getPos(x1, y1)] + (x - x1 - 1) + 1 + (y - y1 - 1); + size_t substitution = distances[getPos(x, y)] + cost; + size_t insertion = distances[getPos(x, y + 1)] + 1; + size_t deletion = distances[getPos(x + 1, y)] + 1; + + // It's more performant to use std::min(size_t, size_t) rather than the initializer_list overload. + // Until proven otherwise, please do not change this. + distances[getPos(x + 1, y + 1)] = std::min(std::min(insertion, deletion), std::min(substitution, transposition)); + } + + // The value of a[N] can be negative with unicode characters + unsigned char aSeenCharIndex = static_cast(a[x - 1]); + seenCharToRow[aSeenCharIndex] = x; + } + + return distances[getPos(a.size() + 1, b.size() + 1)]; +} + +bool startsWith(std::string_view haystack, std::string_view needle) +{ + // ::starts_with is C++20 + return haystack.size() >= needle.size() && haystack.substr(0, needle.size()) == needle; +} + +bool equalsLower(std::string_view lhs, std::string_view rhs) +{ + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0; i < lhs.size(); ++i) + if (tolower(uint8_t(lhs[i])) != tolower(uint8_t(rhs[i]))) + return false; + + return true; +} + +size_t hashRange(const char* data, size_t size) +{ + // FNV-1a + uint32_t hash = 2166136261; + + for (size_t i = 0; i < size; ++i) + { + hash ^= uint8_t(data[i]); + hash *= 16777619; + } + + return hash; +} + +bool isIdentifier(std::string_view s) +{ + return (s.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_") == std::string::npos); +} + +std::string escape(std::string_view s, bool escapeForInterpString) +{ + std::string r; + r.reserve(s.size() + 50); // arbitrary number to guess how many characters we'll be inserting + + for (uint8_t c : s) + { + if (c >= ' ' && c != '\\' && c != '\'' && c != '\"' && c != '`' && c != '{') + r += c; + else + { + r += '\\'; + + if (escapeForInterpString && (c == '`' || c == '{')) + { + r += c; + continue; + } + + switch (c) + { + case '\a': + r += 'a'; + break; + case '\b': + r += 'b'; + break; + case '\f': + r += 'f'; + break; + case '\n': + r += 'n'; + break; + case '\r': + r += 'r'; + break; + case '\t': + r += 't'; + break; + case '\v': + r += 'v'; + break; + case '\'': + r += '\''; + break; + case '\"': + r += '\"'; + break; + case '\\': + r += '\\'; + break; + default: + Luau::formatAppend(r, "%03u", c); + } + } + } + + return r; +} + +static bool isWhitespace(char c) +{ + return c == ' ' || c == '\n' || c == '\r' || c == '\t'; +} + +std::string_view strip(std::string_view s) +{ + while (!s.empty() && isWhitespace(s.front())) + s.remove_prefix(1); + + while (!s.empty() && isWhitespace(s.back())) + s.remove_suffix(1); + + return s; +} + +} // namespace Luau diff --git a/External/luau/Common/src/TimeTrace.cpp b/External/luau/Common/src/TimeTrace.cpp new file mode 100644 index 00000000..1a0f3e7b --- /dev/null +++ b/External/luau/Common/src/TimeTrace.cpp @@ -0,0 +1,278 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TimeTrace.h" + +#include "Luau/StringUtils.h" + +#include +#include +#include + +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +#ifdef __APPLE__ +#include +#include +#endif + +#include + +LUAU_FASTFLAGVARIABLE(DebugLuauTimeTracing) +namespace Luau +{ +namespace TimeTrace +{ +static double getClockPeriod() +{ +#if defined(_WIN32) + LARGE_INTEGER result = {}; + QueryPerformanceFrequency(&result); + return 1.0 / double(result.QuadPart); +#elif defined(__APPLE__) + mach_timebase_info_data_t result = {}; + mach_timebase_info(&result); + return double(result.numer) / double(result.denom) * 1e-9; +#elif defined(__linux__) || defined(__FreeBSD__) + return 1e-9; +#else + return 1.0 / double(CLOCKS_PER_SEC); +#endif +} + +static double getClockTimestamp() +{ +#if defined(_WIN32) + LARGE_INTEGER result = {}; + QueryPerformanceCounter(&result); + return double(result.QuadPart); +#elif defined(__APPLE__) + return double(mach_absolute_time()); +#elif defined(__linux__) || defined(__FreeBSD__) + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1e9 + now.tv_nsec; +#else + return double(clock()); +#endif +} + +double getClock() +{ + static double period = getClockPeriod(); + static double start = getClockTimestamp(); + + return (getClockTimestamp() - start) * period; +} + +uint32_t getClockMicroseconds() +{ + static double period = getClockPeriod() * 1e6; + static double start = getClockTimestamp(); + + return uint32_t((getClockTimestamp() - start) * period); +} +} // namespace TimeTrace +} // namespace Luau + +#if defined(LUAU_ENABLE_TIME_TRACE) + +namespace Luau +{ +namespace TimeTrace +{ +struct GlobalContext +{ + ~GlobalContext() + { + if (traceFile) + fclose(traceFile); + } + + std::mutex mutex; + std::vector threads; + uint32_t nextThreadId = 0; + std::vector tokens; + FILE* traceFile = nullptr; + +private: + friend std::shared_ptr getGlobalContext(); + GlobalContext() = default; +}; + +std::shared_ptr getGlobalContext() +{ + static std::shared_ptr context = std::shared_ptr{new GlobalContext}; + return context; +} + +uint16_t createToken(GlobalContext& context, const char* name, const char* category) +{ + std::scoped_lock lock(context.mutex); + + LUAU_ASSERT(context.tokens.size() < 64 * 1024); + + context.tokens.push_back({name, category}); + return uint16_t(context.tokens.size() - 1); +} + +uint32_t createThread(GlobalContext& context, ThreadContext* threadContext) +{ + std::scoped_lock lock(context.mutex); + + context.threads.push_back(threadContext); + + return ++context.nextThreadId; +} + +void releaseThread(GlobalContext& context, ThreadContext* threadContext) +{ + std::scoped_lock lock(context.mutex); + + if (auto it = std::find(context.threads.begin(), context.threads.end(), threadContext); it != context.threads.end()) + context.threads.erase(it); +} + +void flushEvents(GlobalContext& context, uint32_t threadId, const std::vector& events, const std::vector& data) +{ + std::scoped_lock lock(context.mutex); + + if (!context.traceFile) + { + context.traceFile = fopen("trace.json", "w"); + + if (!context.traceFile) + return; + + fprintf(context.traceFile, "[\n"); + } + + std::string temp; + const unsigned tempReserve = 64 * 1024; + temp.reserve(tempReserve); + + const char* rawData = data.data(); + + // Formatting state + bool unfinishedEnter = false; + bool unfinishedArgs = false; + + for (const Event& ev : events) + { + switch (ev.type) + { + case EventType::Enter: + { + if (unfinishedArgs) + { + formatAppend(temp, "}"); + unfinishedArgs = false; + } + + if (unfinishedEnter) + { + formatAppend(temp, "},\n"); + unfinishedEnter = false; + } + + Token& token = context.tokens[ev.token]; + + formatAppend( + temp, + R"({"name": "%s", "cat": "%s", "ph": "B", "ts": %u, "pid": 0, "tid": %u)", + token.name, + token.category, + ev.data.microsec, + threadId + ); + unfinishedEnter = true; + } + break; + case EventType::Leave: + if (unfinishedArgs) + { + formatAppend(temp, "}"); + unfinishedArgs = false; + } + if (unfinishedEnter) + { + formatAppend(temp, "},\n"); + unfinishedEnter = false; + } + + formatAppend( + temp, + R"({"ph": "E", "ts": %u, "pid": 0, "tid": %u},)" + "\n", + ev.data.microsec, + threadId + ); + break; + case EventType::ArgName: + LUAU_ASSERT(unfinishedEnter); + + if (!unfinishedArgs) + { + formatAppend(temp, R"(, "args": { "%s": )", rawData + ev.data.dataPos); + unfinishedArgs = true; + } + else + { + formatAppend(temp, R"(, "%s": )", rawData + ev.data.dataPos); + } + break; + case EventType::ArgValue: + LUAU_ASSERT(unfinishedArgs); + formatAppend(temp, R"("%s")", rawData + ev.data.dataPos); + break; + } + + // Don't want to hit the string capacity and reallocate + if (temp.size() > tempReserve - 1024) + { + fwrite(temp.data(), 1, temp.size(), context.traceFile); + temp.clear(); + } + } + + if (unfinishedArgs) + { + formatAppend(temp, "}"); + unfinishedArgs = false; + } + if (unfinishedEnter) + { + formatAppend(temp, "},\n"); + unfinishedEnter = false; + } + + fwrite(temp.data(), 1, temp.size(), context.traceFile); + fflush(context.traceFile); +} + +ThreadContext& getThreadContext() +{ + // Check custom provider that which might implement a custom TLS + if (auto provider = threadContextProvider()) + return provider(); + + thread_local ThreadContext context; + return context; +} + +uint16_t createScopeData(const char* name, const char* category) +{ + return createToken(*Luau::TimeTrace::getGlobalContext(), name, category); +} +} // namespace TimeTrace +} // namespace Luau + +#endif diff --git a/External/luau/Compiler/include/Luau/Compiler.h b/External/luau/Compiler/include/Luau/Compiler.h new file mode 100644 index 00000000..b5b7db78 --- /dev/null +++ b/External/luau/Compiler/include/Luau/Compiler.h @@ -0,0 +1,115 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/ParseOptions.h" +#include "Luau/Location.h" +#include "Luau/StringUtils.h" +#include "Luau/Common.h" + +namespace Luau +{ +class AstNameTable; +struct ParseResult; +class BytecodeBuilder; +class BytecodeEncoder; + +using CompileConstant = void*; + +// return a type identifier for a global library member +// values are defined by 'enum LuauBytecodeType' in Bytecode.h +using LibraryMemberTypeCallback = int (*)(const char* library, const char* member); + +// setup a value of a constant for a global library member +// use setCompileConstant*** set of functions for values +using LibraryMemberConstantCallback = void (*)(const char* library, const char* member, CompileConstant* constant); + +// Note: this structure is duplicated in luacode.h, don't forget to change these in sync! +struct CompileOptions +{ + // 0 - no optimization + // 1 - baseline optimization level that doesn't prevent debuggability + // 2 - includes optimizations that harm debuggability such as inlining + int optimizationLevel = 1; + + // 0 - no debugging support + // 1 - line info & function names only; sufficient for backtraces + // 2 - full debug info with local & upvalue names; necessary for debugger + int debugLevel = 1; + + // type information is used to guide native code generation decisions + // information includes testable typeArguments for function arguments, locals, upvalues and some temporaries + // 0 - generate for native modules + // 1 - generate for all modules + int typeInfoLevel = 0; + + // 0 - no code coverage support + // 1 - statement coverage + // 2 - statement and expression coverage (verbose) + int coverageLevel = 0; + + // alternative global builtin to construct vectors, in addition to default builtin 'vector.create' + const char* vectorLib = nullptr; + const char* vectorCtor = nullptr; + + // alternative vector type name for type tables, in addition to default type 'vector' + const char* vectorType = nullptr; + + // 0 - 32-bit float vector components + // 1 - 64-bit double vector components + int vectorPrecision = 0; + + // null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these + const char* const* mutableGlobals = nullptr; + + // null-terminated array of userdata typeArguments that will be included in the type information + const char* const* userdataTypes = nullptr; + + // null-terminated array of globals which act as libraries and have members with known type and/or constant value + // when an import of one of these libraries is accessed, callbacks below will be called to receive that information + const char* const* librariesWithKnownMembers = nullptr; + LibraryMemberTypeCallback libraryMemberTypeCb = nullptr; + LibraryMemberConstantCallback libraryMemberConstantCb = nullptr; + + // null-terminated array of library functions that should not be compiled into a built-in fastcall ("name" "lib.name") + const char* const* disabledBuiltins = nullptr; +}; + +class CompileError : public std::exception +{ +public: + CompileError(const Location& location, std::string message); + + ~CompileError() throw() override; + + const char* what() const throw() override; + + const Location& getLocation() const; + + static LUAU_NORETURN void raise(const Location& location, const char* format, ...) LUAU_PRINTF_ATTR(2, 3); + +private: + Location location; + std::string message; +}; + +// compiles bytecode into bytecode builder using either a pre-parsed AST or parsing it from source; throws on errors +void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, AstNameTable& names, const CompileOptions& options = {}); +void compileOrThrow(BytecodeBuilder& bytecode, const std::string& source, const CompileOptions& options = {}, const ParseOptions& parseOptions = {}); + +// compiles bytecode into a bytecode blob, that either contains the valid bytecode or an encoded error that luau_load can decode +std::string compile( + const std::string& source, + const CompileOptions& options = {}, + const ParseOptions& parseOptions = {}, + BytecodeEncoder* encoder = nullptr +); + +void setCompileConstantNil(CompileConstant* constant); +void setCompileConstantBoolean(CompileConstant* constant, bool b); +void setCompileConstantNumber(CompileConstant* constant, double n); +void setCompileConstantInteger64(CompileConstant* constant, int64_t l); +void setCompileConstantVector(CompileConstant* constant, float x, float y, float z, float w); +void setCompileConstantVectord(CompileConstant* constant, double x, double y, double z, double w); +void setCompileConstantString(CompileConstant* constant, const char* s, size_t l); + +} // namespace Luau diff --git a/External/luau/Compiler/include/luacode.h b/External/luau/Compiler/include/luacode.h new file mode 100644 index 00000000..2875e915 --- /dev/null +++ b/External/luau/Compiler/include/luacode.h @@ -0,0 +1,85 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include +#include + +// can be used to reconfigure visibility/exports for public APIs +#ifndef LUACODE_API +#define LUACODE_API extern +#endif + +typedef struct lua_CompileOptions lua_CompileOptions; +typedef void* lua_CompileConstant; + +// return a type identifier for a global library member +// values are defined by 'enum LuauBytecodeType' in Bytecode.h +typedef int (*lua_LibraryMemberTypeCallback)(const char* library, const char* member); + +// setup a value of a constant for a global library member +// use luau_set_compile_constant_*** set of functions for values +typedef void (*lua_LibraryMemberConstantCallback)(const char* library, const char* member, lua_CompileConstant* constant); + +struct lua_CompileOptions +{ + // 0 - no optimization + // 1 - baseline optimization level that doesn't prevent debuggability + // 2 - includes optimizations that harm debuggability such as inlining + int optimizationLevel; // default=1 + + // 0 - no debugging support + // 1 - line info & function names only; sufficient for backtraces + // 2 - full debug info with local & upvalue names; necessary for debugger + int debugLevel; // default=1 + + // type information is used to guide native code generation decisions + // information includes testable types for function arguments, locals, upvalues and some temporaries + // 0 - generate for native modules + // 1 - generate for all modules + int typeInfoLevel; // default=0 + + // 0 - no code coverage support + // 1 - statement coverage + // 2 - statement and expression coverage (verbose) + int coverageLevel; // default=0 + + // alternative global builtin to construct vectors, in addition to default builtin 'vector.create' + const char* vectorLib; + const char* vectorCtor; + + // alternative vector type name for type tables, in addition to default type 'vector' + const char* vectorType; + + // 0 - 32-bit float vector components + // 1 - 64-bit double vector components + int vectorPrecision; // default=0 + + // null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these + const char* const* mutableGlobals; + + // null-terminated array of userdata types that will be included in the type information + const char* const* userdataTypes; + + // null-terminated array of globals which act as libraries and have members with known type and/or constant value + // when an import of one of these libraries is accessed, callbacks below will be called to receive that information + const char* const* librariesWithKnownMembers; + lua_LibraryMemberTypeCallback libraryMemberTypeCb; + lua_LibraryMemberConstantCallback libraryMemberConstantCb; + + // null-terminated array of library functions that should not be compiled into a built-in fastcall ("name" "lib.name") + const char* const* disabledBuiltins; +}; + +// compile source to bytecode; when source compilation fails, the resulting bytecode contains the encoded error. use free() to destroy +LUACODE_API char* luau_compile(const char* source, size_t size, lua_CompileOptions* options, size_t* outsize); + +// when libraryMemberConstantCb is called, these methods can be used to set a value of the opaque lua_CompileConstant struct +// vector component 'w' is not visible to VM runtime configured with LUA_VECTOR_SIZE == 3, but can affect constant folding during compilation +// string storage must outlive the invocation of 'luau_compile' which used the callback +LUACODE_API void luau_set_compile_constant_nil(lua_CompileConstant* constant); +LUACODE_API void luau_set_compile_constant_boolean(lua_CompileConstant* constant, int b); +LUACODE_API void luau_set_compile_constant_number(lua_CompileConstant* constant, double n); +LUACODE_API void luau_set_compile_constant_integer64(lua_CompileConstant* constant, int64_t l); +LUACODE_API void luau_set_compile_constant_vector(lua_CompileConstant* constant, float x, float y, float z, float w); +LUACODE_API void luau_set_compile_constant_vectord(lua_CompileConstant* constant, double x, double y, double z, double w); +LUACODE_API void luau_set_compile_constant_string(lua_CompileConstant* constant, const char* s, size_t l); diff --git a/External/luau/Compiler/src/BuiltinFolding.cpp b/External/luau/Compiler/src/BuiltinFolding.cpp new file mode 100644 index 00000000..6ce9a7f1 --- /dev/null +++ b/External/luau/Compiler/src/BuiltinFolding.cpp @@ -0,0 +1,692 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "BuiltinFolding.h" + +#include "Luau/Bytecode.h" +#include "Luau/Lexer.h" + +#include +#include +#include + +namespace Luau +{ +namespace Compile +{ + +const double kPi = 3.14159265358979323846; +const double kRadDeg = kPi / 180.0; +const double kNan = std::numeric_limits::quiet_NaN(); +const double kE = 2.71828182845904523536; +const double kPhi = 1.61803398874989484820; +const double kSqrt2 = 1.41421356237309504880; +const double kTau = 6.28318530717958647692; + +constexpr size_t kStringCharFoldLimit = 128; + +static Constant cvar() +{ + return Constant(); +} + +static Constant cbool(bool v) +{ + Constant res = {Constant::Type_Boolean}; + res.valueBoolean = v; + return res; +} + +static Constant cnum(double v) +{ + Constant res = {Constant::Type_Number}; + res.valueNumber = v; + return res; +} + +static Constant cvectorf(float x, float y, float z, float w) +{ + Constant res = {Constant::Type_Vectorf}; + res.valueVectorf[0] = x; + res.valueVectorf[1] = y; + res.valueVectorf[2] = z; + res.valueVectorf[3] = w; + return res; +} + +static Constant cvectord(double x, double y, double z, double w) +{ + Constant res = {Constant::Type_Vectord}; + res.valueVectord[0] = x; + res.valueVectord[1] = y; + res.valueVectord[2] = z; + res.valueVectord[3] = w; + return res; +} + +static Constant cstring(const char* v) +{ + Constant res = {Constant::Type_String}; + res.stringLength = unsigned(strlen(v)); + res.valueString = v; + return res; +} + +static Constant cstring(const char* v, size_t len) +{ + Constant res = {Constant::Type_String}; + res.stringLength = unsigned(len); + res.valueString = v; + return res; +} + +static Constant ctype(const Constant& c) +{ + LUAU_ASSERT(c.type != Constant::Type_Unknown); + + switch (c.type) + { + case Constant::Type_Nil: + return cstring("nil"); + + case Constant::Type_Boolean: + return cstring("boolean"); + + case Constant::Type_Number: + return cstring("number"); + + case Constant::Type_Integer: + return cstring("integer"); + + case Constant::Type_Vectorf: + case Constant::Type_Vectord: + return cstring("vector"); + + case Constant::Type_String: + return cstring("string"); + + default: + LUAU_ASSERT(!"Unsupported constant type"); + return cvar(); + } +} + +static Constant ctypeof(const Constant& c) +{ + LUAU_ASSERT(c.type != Constant::Type_Unknown); + + switch (c.type) + { + case Constant::Type_Nil: + return cstring("nil"); + + case Constant::Type_Boolean: + return cstring("boolean"); + + case Constant::Type_Number: + return cstring("number"); + + case Constant::Type_Integer: + return cstring("integer"); + + case Constant::Type_Vectorf: + case Constant::Type_Vectord: + return cvar(); // vector can have a custom typeof name at runtime + + case Constant::Type_String: + return cstring("string"); + + default: + LUAU_ASSERT(!"Unsupported constant type"); + return cvar(); + } +} + +static uint32_t bit32(double v) +{ + // convert through signed 64-bit integer to match runtime behavior and gracefully truncate negative integers + return uint32_t(int64_t(v)); +} + +Constant foldBuiltin(AstNameTable& stringTable, int bfid, const Constant* args, size_t count, bool vectorDoublePrecision) +{ + switch (bfid) + { + case LBF_MATH_ABS: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(fabs(args[0].valueNumber)); + break; + + case LBF_MATH_ACOS: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(acos(args[0].valueNumber)); + break; + + case LBF_MATH_ASIN: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(asin(args[0].valueNumber)); + break; + + case LBF_MATH_ATAN2: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + return cnum(atan2(args[0].valueNumber, args[1].valueNumber)); + break; + + case LBF_MATH_ATAN: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(atan(args[0].valueNumber)); + break; + + case LBF_MATH_CEIL: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(ceil(args[0].valueNumber)); + break; + + case LBF_MATH_COSH: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(cosh(args[0].valueNumber)); + break; + + case LBF_MATH_COS: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(cos(args[0].valueNumber)); + break; + + case LBF_MATH_DEG: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(args[0].valueNumber / kRadDeg); + break; + + case LBF_MATH_EXP: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(exp(args[0].valueNumber)); + break; + + case LBF_MATH_FLOOR: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(floor(args[0].valueNumber)); + break; + + case LBF_MATH_FMOD: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + return cnum(fmod(args[0].valueNumber, args[1].valueNumber)); + break; + + // Note: FREXP isn't folded since it returns multiple values + + case LBF_MATH_LDEXP: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + return cnum(ldexp(args[0].valueNumber, int(args[1].valueNumber))); + break; + + case LBF_MATH_LOG10: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(log10(args[0].valueNumber)); + break; + + case LBF_MATH_LOG: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(log(args[0].valueNumber)); + else if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + if (args[1].valueNumber == 2.0) + return cnum(log2(args[0].valueNumber)); + else if (args[1].valueNumber == 10.0) + return cnum(log10(args[0].valueNumber)); + else + return cnum(log(args[0].valueNumber) / log(args[1].valueNumber)); + } + break; + + case LBF_MATH_MAX: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + double r = args[0].valueNumber; + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + double a = args[i].valueNumber; + + r = (a > r) ? a : r; + } + + return cnum(r); + } + break; + + case LBF_MATH_MIN: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + double r = args[0].valueNumber; + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + double a = args[i].valueNumber; + + r = (a < r) ? a : r; + } + + return cnum(r); + } + break; + + // Note: MODF isn't folded since it returns multiple values + + case LBF_MATH_POW: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + return cnum(pow(args[0].valueNumber, args[1].valueNumber)); + break; + + case LBF_MATH_RAD: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(args[0].valueNumber * kRadDeg); + break; + + case LBF_MATH_SINH: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(sinh(args[0].valueNumber)); + break; + + case LBF_MATH_SIN: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(sin(args[0].valueNumber)); + break; + + case LBF_MATH_SQRT: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(sqrt(args[0].valueNumber)); + break; + + case LBF_MATH_TANH: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(tanh(args[0].valueNumber)); + break; + + case LBF_MATH_TAN: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(tan(args[0].valueNumber)); + break; + + case LBF_BIT32_ARSHIFT: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + uint32_t u = bit32(args[0].valueNumber); + int s = int(args[1].valueNumber); + + if (unsigned(s) < 32) + return cnum(double(uint32_t(int32_t(u) >> s))); + } + break; + + case LBF_BIT32_BAND: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + uint32_t r = bit32(args[0].valueNumber); + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + r &= bit32(args[i].valueNumber); + } + + return cnum(double(r)); + } + break; + + case LBF_BIT32_BNOT: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(double(uint32_t(~bit32(args[0].valueNumber)))); + break; + + case LBF_BIT32_BOR: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + uint32_t r = bit32(args[0].valueNumber); + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + r |= bit32(args[i].valueNumber); + } + + return cnum(double(r)); + } + break; + + case LBF_BIT32_BXOR: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + uint32_t r = bit32(args[0].valueNumber); + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + r ^= bit32(args[i].valueNumber); + } + + return cnum(double(r)); + } + break; + + case LBF_BIT32_BTEST: + if (count >= 1 && args[0].type == Constant::Type_Number) + { + uint32_t r = bit32(args[0].valueNumber); + + for (size_t i = 1; i < count; ++i) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + r &= bit32(args[i].valueNumber); + } + + return cbool(r != 0); + } + break; + + case LBF_BIT32_EXTRACT: + if (count >= 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && + (count == 2 || args[2].type == Constant::Type_Number)) + { + uint32_t u = bit32(args[0].valueNumber); + int f = int(args[1].valueNumber); + int w = count == 2 ? 1 : int(args[2].valueNumber); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + + return cnum(double((u >> f) & m)); + } + } + break; + + case LBF_BIT32_LROTATE: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + uint32_t u = bit32(args[0].valueNumber); + int s = int(args[1].valueNumber); + + return cnum(double((u << (s & 31)) | (u >> ((32 - s) & 31)))); + } + break; + + case LBF_BIT32_LSHIFT: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + uint32_t u = bit32(args[0].valueNumber); + int s = int(args[1].valueNumber); + + if (unsigned(s) < 32) + return cnum(double(u << s)); + } + break; + + case LBF_BIT32_REPLACE: + if (count >= 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number && + (count == 3 || args[3].type == Constant::Type_Number)) + { + uint32_t n = bit32(args[0].valueNumber); + uint32_t v = bit32(args[1].valueNumber); + int f = int(args[2].valueNumber); + int w = count == 3 ? 1 : int(args[3].valueNumber); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + + return cnum(double((n & ~(m << f)) | ((v & m) << f))); + } + } + break; + + case LBF_BIT32_RROTATE: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + uint32_t u = bit32(args[0].valueNumber); + int s = int(args[1].valueNumber); + + return cnum(double((u >> (s & 31)) | (u << ((32 - s) & 31)))); + } + break; + + case LBF_BIT32_RSHIFT: + if (count == 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + uint32_t u = bit32(args[0].valueNumber); + int s = int(args[1].valueNumber); + + if (unsigned(s) < 32) + return cnum(double(u >> s)); + } + break; + + case LBF_TYPE: + if (count == 1 && args[0].type != Constant::Type_Unknown) + return ctype(args[0]); + break; + + case LBF_STRING_BYTE: + if (count == 1 && args[0].type == Constant::Type_String) + { + if (args[0].stringLength > 0) + return cnum(double(uint8_t(args[0].valueString[0]))); + } + else if (count == 2 && args[0].type == Constant::Type_String && args[1].type == Constant::Type_Number) + { + int i = int(args[1].valueNumber); + + if (i > 0 && unsigned(i) <= args[0].stringLength) + return cnum(double(uint8_t(args[0].valueString[i - 1]))); + } + break; + + case LBF_STRING_CHAR: + if (count < kStringCharFoldLimit) + { + std::array buf{}; + + for (size_t i = 0; i < count; i++) + { + if (args[i].type != Constant::Type_Number) + return cvar(); + + int ch = int(args[i].valueNumber); + + if ((unsigned char)(ch) != ch) + return cvar(); + + buf[i] = ch; + } + + if (count == 0) + return cstring(""); + + AstName name = stringTable.getOrAdd(buf.data(), count); + return cstring(name.value, count); + } + break; + + case LBF_STRING_LEN: + if (count == 1 && args[0].type == Constant::Type_String) + return cnum(double(args[0].stringLength)); + break; + + case LBF_TYPEOF: + if (count == 1 && args[0].type != Constant::Type_Unknown) + return ctypeof(args[0]); + break; + + case LBF_STRING_SUB: + if (count >= 2 && args[0].type == Constant::Type_String && args[1].type == Constant::Type_Number) + { + if (count >= 3 && args[2].type != Constant::Type_Number) + return cvar(); + + const char* str = args[0].valueString; + unsigned len = args[0].stringLength; + + int start = int(args[1].valueNumber); + int end = count >= 3 ? int(args[2].valueNumber) : int(len); + + // Relative string position: negative means back from end + if (start < 0) + start += int(len) + 1; + if (end < 0) + end += int(len) + 1; + + // If end is before the start of the string, substring is empty + if (end < 1) + return cstring(""); + + // Start clamped to the start of the string, end is clamped to the end + start = start < 1 ? 1 : start; + end = end > int(len) ? int(len) : end; + + if (start <= end) + { + AstName name = stringTable.getOrAdd(str + (start - 1), end - start + 1); + return cstring(name.value, end - start + 1); + } + + return cstring(""); + } + break; + + case LBF_MATH_CLAMP: + if (count == 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number) + { + double min = args[1].valueNumber; + double max = args[2].valueNumber; + + if (min <= max) + { + double v = args[0].valueNumber; + v = v < min ? min : v; + v = v > max ? max : v; + + return cnum(v); + } + } + break; + + case LBF_MATH_SIGN: + if (count == 1 && args[0].type == Constant::Type_Number) + { + double v = args[0].valueNumber; + + return cnum(v > 0.0 ? 1.0 : v < 0.0 ? -1.0 : 0.0); + } + break; + + case LBF_MATH_ROUND: + if (count == 1 && args[0].type == Constant::Type_Number) + return cnum(round(args[0].valueNumber)); + break; + + case LBF_VECTOR: + if (count >= 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number) + { + if (vectorDoublePrecision) + { + if (count == 2) + return cvectord(args[0].valueNumber, args[1].valueNumber, 0.0, 0.0); + else if (count == 3 && args[2].type == Constant::Type_Number) + return cvectord(args[0].valueNumber, args[1].valueNumber, args[2].valueNumber, 0.0); + else if (count == 4 && args[2].type == Constant::Type_Number && args[3].type == Constant::Type_Number) + return cvectord(args[0].valueNumber, args[1].valueNumber, args[2].valueNumber, args[3].valueNumber); + } + else + { + if (count == 2) + return cvectorf(float(args[0].valueNumber), float(args[1].valueNumber), 0.0f, 0.0f); + else if (count == 3 && args[2].type == Constant::Type_Number) + return cvectorf(float(args[0].valueNumber), float(args[1].valueNumber), float(args[2].valueNumber), 0.0f); + else if (count == 4 && args[2].type == Constant::Type_Number && args[3].type == Constant::Type_Number) + return cvectorf(float(args[0].valueNumber), float(args[1].valueNumber), float(args[2].valueNumber), float(args[3].valueNumber)); + } + } + break; + + case LBF_MATH_LERP: + if (count == 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number) + { + double a = args[0].valueNumber; + double b = args[1].valueNumber; + double t = args[2].valueNumber; + + double v = (t == 1.0) ? b : a + (b - a) * t; + return cnum(v); + } + break; + + case LBF_MATH_ISNAN: + if (count == 1 && args[0].type == Constant::Type_Number) + { + double x = args[0].valueNumber; + + return cbool(isnan(x)); + } + break; + + case LBF_MATH_ISINF: + if (count == 1 && args[0].type == Constant::Type_Number) + { + double x = args[0].valueNumber; + + return cbool(isinf(x)); + } + break; + + case LBF_MATH_ISFINITE: + if (count == 1 && args[0].type == Constant::Type_Number) + { + double x = args[0].valueNumber; + + return cbool(isfinite(x)); + } + break; + } + + return cvar(); +} + +Constant foldBuiltinMath(AstName index) +{ + if (index == "pi") + return cnum(kPi); + + if (index == "huge") + return cnum(HUGE_VAL); + + if (index == "nan") + return cnum(kNan); + + if (index == "e") + return cnum(kE); + + if (index == "phi") + return cnum(kPhi); + + if (index == "sqrt2") + return cnum(kSqrt2); + + if (index == "tau") + return cnum(kTau); + + return cvar(); +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/BuiltinFolding.h b/External/luau/Compiler/src/BuiltinFolding.h new file mode 100644 index 00000000..659b5353 --- /dev/null +++ b/External/luau/Compiler/src/BuiltinFolding.h @@ -0,0 +1,18 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "ConstantFolding.h" + +namespace Luau +{ + +class AstNameTable; + +namespace Compile +{ + +Constant foldBuiltin(AstNameTable& stringTable, int bfid, const Constant* args, size_t count, bool vectorDoublePrecision); +Constant foldBuiltinMath(AstName index); + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/Builtins.cpp b/External/luau/Compiler/src/Builtins.cpp new file mode 100644 index 00000000..3b476235 --- /dev/null +++ b/External/luau/Compiler/src/Builtins.cpp @@ -0,0 +1,739 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Builtins.h" + +#include "Luau/Bytecode.h" +#include "Luau/Compiler.h" +#include "Luau/Lexer.h" + +#include + +LUAU_FASTFLAGVARIABLE(LuauIntegerFastcalls) +LUAU_FASTFLAGVARIABLE(LuauIntegerBufferFastcalls) + +namespace Luau +{ +namespace Compile +{ + +Builtin getBuiltin(AstExpr* node, const DenseHashMap& globals, const DenseHashMap& variables) +{ + if (AstExprLocal* expr = node->as()) + { + const Variable* v = variables.find(expr->local); + + return v && !v->written && v->init ? getBuiltin(v->init, globals, variables) : Builtin(); + } + else if (AstExprIndexName* expr = node->as()) + { + if (AstExprLocal* object = expr->expr->as()) + { + const Variable* v = variables.find(object->local); + + // Local that is initialized and not modified might hold the built-in library itself + if (v && !v->written && v->init) + { + AstExprGlobal* object = nullptr; + + // Look for patterns like 'local m = math' and 'local math = math or replacement' + // Fastcall is used in safe env where libraries like 'math' are built-in + // This means that if we are still in safe env, 'math' was truthy and local was initialized to the library value + // If safe env is false, 'math' could be a polyfill or something else and fastcall takes the fallback, preserving the behavior + if (AstExprGlobal* global = v->init->as()) + object = global; + else if (AstExprBinary* cond = v->init->as(); cond && cond->op == AstExprBinary::Or) + object = cond->left->as(); + + if (object) + return getGlobalState(globals, object->name) == Global::Default ? Builtin{object->name, expr->index} : Builtin(); + } + } + + if (AstExprGlobal* object = expr->expr->as()) + { + return getGlobalState(globals, object->name) == Global::Default ? Builtin{object->name, expr->index} : Builtin(); + } + else + { + return Builtin(); + } + } + else if (AstExprGlobal* expr = node->as()) + { + return getGlobalState(globals, expr->name) == Global::Default ? Builtin{AstName(), expr->name} : Builtin(); + } + else + { + return Builtin(); + } +} + +static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& options) +{ + if (builtin.isGlobal("assert")) + return LBF_ASSERT; + + if (builtin.isGlobal("type")) + return LBF_TYPE; + + if (builtin.isGlobal("typeof")) + return LBF_TYPEOF; + + if (builtin.isGlobal("rawset")) + return LBF_RAWSET; + if (builtin.isGlobal("rawget")) + return LBF_RAWGET; + if (builtin.isGlobal("rawequal")) + return LBF_RAWEQUAL; + if (builtin.isGlobal("rawlen")) + return LBF_RAWLEN; + + if (builtin.isGlobal("unpack")) + return LBF_TABLE_UNPACK; + + if (builtin.isGlobal("select")) + return LBF_SELECT_VARARG; + + if (builtin.isGlobal("getmetatable")) + return LBF_GETMETATABLE; + if (builtin.isGlobal("setmetatable")) + return LBF_SETMETATABLE; + + if (builtin.isGlobal("tonumber")) + return LBF_TONUMBER; + if (builtin.isGlobal("tostring")) + return LBF_TOSTRING; + + if (builtin.object == "math") + { + if (builtin.method == "abs") + return LBF_MATH_ABS; + if (builtin.method == "acos") + return LBF_MATH_ACOS; + if (builtin.method == "asin") + return LBF_MATH_ASIN; + if (builtin.method == "atan2") + return LBF_MATH_ATAN2; + if (builtin.method == "atan") + return LBF_MATH_ATAN; + if (builtin.method == "ceil") + return LBF_MATH_CEIL; + if (builtin.method == "cosh") + return LBF_MATH_COSH; + if (builtin.method == "cos") + return LBF_MATH_COS; + if (builtin.method == "deg") + return LBF_MATH_DEG; + if (builtin.method == "exp") + return LBF_MATH_EXP; + if (builtin.method == "floor") + return LBF_MATH_FLOOR; + if (builtin.method == "fmod") + return LBF_MATH_FMOD; + if (builtin.method == "frexp") + return LBF_MATH_FREXP; + if (builtin.method == "ldexp") + return LBF_MATH_LDEXP; + if (builtin.method == "log10") + return LBF_MATH_LOG10; + if (builtin.method == "log") + return LBF_MATH_LOG; + if (builtin.method == "max") + return LBF_MATH_MAX; + if (builtin.method == "min") + return LBF_MATH_MIN; + if (builtin.method == "modf") + return LBF_MATH_MODF; + if (builtin.method == "pow") + return LBF_MATH_POW; + if (builtin.method == "rad") + return LBF_MATH_RAD; + if (builtin.method == "sinh") + return LBF_MATH_SINH; + if (builtin.method == "sin") + return LBF_MATH_SIN; + if (builtin.method == "sqrt") + return LBF_MATH_SQRT; + if (builtin.method == "tanh") + return LBF_MATH_TANH; + if (builtin.method == "tan") + return LBF_MATH_TAN; + if (builtin.method == "clamp") + return LBF_MATH_CLAMP; + if (builtin.method == "sign") + return LBF_MATH_SIGN; + if (builtin.method == "round") + return LBF_MATH_ROUND; + if (builtin.method == "lerp") + return LBF_MATH_LERP; + if (builtin.method == "isnan") + return LBF_MATH_ISNAN; + if (builtin.method == "isinf") + return LBF_MATH_ISINF; + if (builtin.method == "isfinite") + return LBF_MATH_ISFINITE; + } + + if (builtin.object == "bit32") + { + if (builtin.method == "arshift") + return LBF_BIT32_ARSHIFT; + if (builtin.method == "band") + return LBF_BIT32_BAND; + if (builtin.method == "bnot") + return LBF_BIT32_BNOT; + if (builtin.method == "bor") + return LBF_BIT32_BOR; + if (builtin.method == "bxor") + return LBF_BIT32_BXOR; + if (builtin.method == "btest") + return LBF_BIT32_BTEST; + if (builtin.method == "extract") + return LBF_BIT32_EXTRACT; + if (builtin.method == "lrotate") + return LBF_BIT32_LROTATE; + if (builtin.method == "lshift") + return LBF_BIT32_LSHIFT; + if (builtin.method == "replace") + return LBF_BIT32_REPLACE; + if (builtin.method == "rrotate") + return LBF_BIT32_RROTATE; + if (builtin.method == "rshift") + return LBF_BIT32_RSHIFT; + if (builtin.method == "countlz") + return LBF_BIT32_COUNTLZ; + if (builtin.method == "countrz") + return LBF_BIT32_COUNTRZ; + if (builtin.method == "byteswap") + return LBF_BIT32_BYTESWAP; + } + + if (builtin.object == "string") + { + if (builtin.method == "byte") + return LBF_STRING_BYTE; + if (builtin.method == "char") + return LBF_STRING_CHAR; + if (builtin.method == "len") + return LBF_STRING_LEN; + if (builtin.method == "sub") + return LBF_STRING_SUB; + } + + if (builtin.object == "table") + { + if (builtin.method == "insert") + return LBF_TABLE_INSERT; + if (builtin.method == "unpack") + return LBF_TABLE_UNPACK; + } + + if (builtin.object == "buffer") + { + if (builtin.method == "readi8") + return LBF_BUFFER_READI8; + if (builtin.method == "readu8") + return LBF_BUFFER_READU8; + if (builtin.method == "writei8" || builtin.method == "writeu8") + return LBF_BUFFER_WRITEU8; + if (builtin.method == "readi16") + return LBF_BUFFER_READI16; + if (builtin.method == "readu16") + return LBF_BUFFER_READU16; + if (builtin.method == "writei16" || builtin.method == "writeu16") + return LBF_BUFFER_WRITEU16; + if (builtin.method == "readi32") + return LBF_BUFFER_READI32; + if (builtin.method == "readu32") + return LBF_BUFFER_READU32; + if (builtin.method == "writei32" || builtin.method == "writeu32") + return LBF_BUFFER_WRITEU32; + if (builtin.method == "readf32") + return LBF_BUFFER_READF32; + if (builtin.method == "writef32") + return LBF_BUFFER_WRITEF32; + if (builtin.method == "readf64") + return LBF_BUFFER_READF64; + if (builtin.method == "writef64") + return LBF_BUFFER_WRITEF64; + if (FFlag::LuauIntegerFastcalls && FFlag::LuauIntegerBufferFastcalls && builtin.method == "readinteger") + return LBF_BUFFER_READINTEGER; + if (FFlag::LuauIntegerFastcalls && FFlag::LuauIntegerBufferFastcalls && builtin.method == "writeinteger") + return LBF_BUFFER_WRITEINTEGER; + } + + if (builtin.object == "vector") + { + if (builtin.method == "create") + return LBF_VECTOR; + if (builtin.method == "magnitude") + return LBF_VECTOR_MAGNITUDE; + if (builtin.method == "normalize") + return LBF_VECTOR_NORMALIZE; + if (builtin.method == "cross") + return LBF_VECTOR_CROSS; + if (builtin.method == "dot") + return LBF_VECTOR_DOT; + if (builtin.method == "floor") + return LBF_VECTOR_FLOOR; + if (builtin.method == "ceil") + return LBF_VECTOR_CEIL; + if (builtin.method == "abs") + return LBF_VECTOR_ABS; + if (builtin.method == "sign") + return LBF_VECTOR_SIGN; + if (builtin.method == "clamp") + return LBF_VECTOR_CLAMP; + if (builtin.method == "min") + return LBF_VECTOR_MIN; + if (builtin.method == "max") + return LBF_VECTOR_MAX; + if (builtin.method == "lerp") + return LBF_VECTOR_LERP; + } + + if (FFlag::LuauIntegerFastcalls && builtin.object == "integer") + { + if (builtin.method == "add") + return LBF_INTEGER_ADD; + if (builtin.method == "sub") + return LBF_INTEGER_SUB; + if (builtin.method == "mod") + return LBF_INTEGER_MOD; + if (builtin.method == "mul") + return LBF_INTEGER_MUL; + if (builtin.method == "div") + return LBF_INTEGER_DIV; + if (builtin.method == "idiv") + return LBF_INTEGER_IDIV; + if (builtin.method == "udiv") + return LBF_INTEGER_UDIV; + if (builtin.method == "rem") + return LBF_INTEGER_REM; + if (builtin.method == "urem") + return LBF_INTEGER_UREM; + if (builtin.method == "min") + return LBF_INTEGER_MIN; + if (builtin.method == "max") + return LBF_INTEGER_MAX; + if (builtin.method == "neg") + return LBF_INTEGER_NEG; + if (builtin.method == "create") + return LBF_INTEGER_CREATE; + if (builtin.method == "clamp") + return LBF_INTEGER_CLAMP; + if (builtin.method == "band") + return LBF_INTEGER_BAND; + if (builtin.method == "bor") + return LBF_INTEGER_BOR; + if (builtin.method == "bxor") + return LBF_INTEGER_BXOR; + if (builtin.method == "bnot") + return LBF_INTEGER_BNOT; + if (builtin.method == "btest") + return LBF_INTEGER_BTEST; + if (builtin.method == "bswap") + return LBF_INTEGER_BSWAP; + if (builtin.method == "lt") + return LBF_INTEGER_LT; + if (builtin.method == "le") + return LBF_INTEGER_LE; + if (builtin.method == "ult") + return LBF_INTEGER_ULT; + if (builtin.method == "ule") + return LBF_INTEGER_ULE; + if (builtin.method == "gt") + return LBF_INTEGER_GT; + if (builtin.method == "ge") + return LBF_INTEGER_GE; + if (builtin.method == "ugt") + return LBF_INTEGER_UGT; + if (builtin.method == "uge") + return LBF_INTEGER_UGE; + if (builtin.method == "lshift") + return LBF_INTEGER_LSHIFT; + if (builtin.method == "rshift") + return LBF_INTEGER_RSHIFT; + if (builtin.method == "arshift") + return LBF_INTEGER_ARSHIFT; + if (builtin.method == "lrotate") + return LBF_INTEGER_LROTATE; + if (builtin.method == "rrotate") + return LBF_INTEGER_RROTATE; + if (builtin.method == "countrz") + return LBF_INTEGER_COUNTRZ; + if (builtin.method == "countlz") + return LBF_INTEGER_COUNTLZ; + if (builtin.method == "extract") + return LBF_INTEGER_EXTRACT; + if (builtin.method == "tonumber") + return LBF_INTEGER_TONUMBER; + } + + if (options.vectorCtor) + { + if (options.vectorLib) + { + if (builtin.isMethod(options.vectorLib, options.vectorCtor)) + return LBF_VECTOR; + } + else + { + if (builtin.isGlobal(options.vectorCtor)) + return LBF_VECTOR; + } + } + + return -1; +} + +struct BuiltinVisitor : AstVisitor +{ + DenseHashMap& result; + std::array builtinIsDisabled; + + const DenseHashMap& globals; + const DenseHashMap& variables; + + const CompileOptions& options; + const AstNameTable& names; + + BuiltinVisitor( + DenseHashMap& result, + const DenseHashMap& globals, + const DenseHashMap& variables, + const CompileOptions& options, + const AstNameTable& names + ) + : result(result) + , globals(globals) + , variables(variables) + , options(options) + , names(names) + { + builtinIsDisabled.fill(false); + + if (const char* const* ptr = options.disabledBuiltins) + { + for (; *ptr; ++ptr) + { + if (const char* dot = strchr(*ptr, '.')) + { + AstName library = names.getWithType(*ptr, dot - *ptr).first; + AstName name = names.get(dot + 1); + + if (library.value && name.value && getGlobalState(globals, name) == Global::Default) + { + Builtin builtin = Builtin{library, name}; + + if (int bfid = getBuiltinFunctionId(builtin, options); bfid >= 0) + builtinIsDisabled[bfid] = true; + } + } + else + { + if (AstName name = names.get(*ptr); name.value && getGlobalState(globals, name) == Global::Default) + { + Builtin builtin = Builtin{AstName(), name}; + + if (int bfid = getBuiltinFunctionId(builtin, options); bfid >= 0) + builtinIsDisabled[bfid] = true; + } + } + } + } + } + + bool visit(AstExprCall* node) override + { + Builtin builtin = node->self ? Builtin() : getBuiltin(node->func, globals, variables); + if (builtin.empty()) + return true; + + int bfid = getBuiltinFunctionId(builtin, options); + + if (bfid >= 0 && builtinIsDisabled[bfid]) + bfid = -1; + + // getBuiltinFunctionId optimistically assumes all select() calls are builtin but actually the second argument must be a vararg + if (bfid == LBF_SELECT_VARARG && !(node->args.size == 2 && node->args.data[1]->is())) + bfid = -1; + + if (bfid >= 0) + result[node] = bfid; + + return true; // propagate to nested calls + } +}; + +void analyzeBuiltins( + DenseHashMap& result, + const DenseHashMap& globals, + const DenseHashMap& variables, + const CompileOptions& options, + AstNode* root, + const AstNameTable& names +) +{ + BuiltinVisitor visitor{result, globals, variables, options, names}; + root->visit(&visitor); +} + +BuiltinInfo getBuiltinInfo(int bfid) +{ + switch (LuauBuiltinFunction(bfid)) + { + case LBF_NONE: + return {-1, -1}; + + case LBF_ASSERT: + return {-1, -1}; // assert() returns all values when first value is truthy + + case LBF_MATH_ABS: + case LBF_MATH_ACOS: + case LBF_MATH_ASIN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_ATAN2: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_ATAN: + case LBF_MATH_CEIL: + case LBF_MATH_COSH: + case LBF_MATH_COS: + case LBF_MATH_DEG: + case LBF_MATH_EXP: + case LBF_MATH_FLOOR: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_FMOD: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_FREXP: + return {1, 2, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_LDEXP: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_LOG10: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_LOG: + return {-1, 1}; // 1 or 2 parameters + + case LBF_MATH_MAX: + case LBF_MATH_MIN: + return {-1, 1}; // variadic + + case LBF_MATH_MODF: + return {1, 2, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_POW: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_RAD: + case LBF_MATH_SINH: + case LBF_MATH_SIN: + case LBF_MATH_SQRT: + case LBF_MATH_TANH: + case LBF_MATH_TAN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BIT32_ARSHIFT: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BIT32_BAND: + return {-1, 1}; // variadic + + case LBF_BIT32_BNOT: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BIT32_BOR: + case LBF_BIT32_BXOR: + case LBF_BIT32_BTEST: + return {-1, 1}; // variadic + + case LBF_BIT32_EXTRACT: + return {-1, 1}; // 2 or 3 parameters + + case LBF_BIT32_LROTATE: + case LBF_BIT32_LSHIFT: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BIT32_REPLACE: + return {-1, 1}; // 3 or 4 parameters + + case LBF_BIT32_RROTATE: + case LBF_BIT32_RSHIFT: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_TYPE: + return {1, 1}; + + case LBF_STRING_BYTE: + return {-1, -1}; // 1, 2 or 3 parameters + + case LBF_STRING_CHAR: + return {-1, 1}; // variadic + + case LBF_STRING_LEN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_TYPEOF: + return {1, 1}; + + case LBF_STRING_SUB: + return {-1, 1}; // 2 or 3 parameters + + case LBF_MATH_CLAMP: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_SIGN: + case LBF_MATH_ROUND: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_RAWSET: + return {3, 1}; + + case LBF_RAWGET: + case LBF_RAWEQUAL: + return {2, 1}; + + case LBF_TABLE_INSERT: + return {-1, 0}; // 2 or 3 parameters + + case LBF_TABLE_UNPACK: + return {-1, -1}; // 1, 2 or 3 parameters + + case LBF_VECTOR: + return {-1, 1}; // 3 or 4 parameters in some configurations + + case LBF_BIT32_COUNTLZ: + case LBF_BIT32_COUNTRZ: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_SELECT_VARARG: + return {-1, -1}; // variadic + + case LBF_RAWLEN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BIT32_EXTRACTK: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_GETMETATABLE: + return {1, 1}; + + case LBF_SETMETATABLE: + return {2, 1}; + + case LBF_TONUMBER: + return {-1, 1}; // 1 or 2 parameters + + case LBF_TOSTRING: + return {1, 1}; + + case LBF_BIT32_BYTESWAP: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BUFFER_READI8: + case LBF_BUFFER_READU8: + case LBF_BUFFER_READI16: + case LBF_BUFFER_READU16: + case LBF_BUFFER_READI32: + case LBF_BUFFER_READU32: + case LBF_BUFFER_READF32: + case LBF_BUFFER_READF64: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BUFFER_WRITEU8: + case LBF_BUFFER_WRITEU16: + case LBF_BUFFER_WRITEU32: + case LBF_BUFFER_WRITEF32: + case LBF_BUFFER_WRITEF64: + case LBF_BUFFER_WRITEINTEGER: + return {3, 0, BuiltinInfo::Flag_NoneSafe}; + + case LBF_BUFFER_READINTEGER: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_VECTOR_MAGNITUDE: + case LBF_VECTOR_NORMALIZE: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_VECTOR_CROSS: + case LBF_VECTOR_DOT: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_VECTOR_FLOOR: + case LBF_VECTOR_CEIL: + case LBF_VECTOR_ABS: + case LBF_VECTOR_SIGN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_VECTOR_CLAMP: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_VECTOR_MIN: + case LBF_VECTOR_MAX: + return {-1, 1}; // variadic + case LBF_VECTOR_LERP: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_MATH_LERP: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_MATH_ISNAN: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_MATH_ISINF: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + case LBF_MATH_ISFINITE: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_INTEGER_BAND: + case LBF_INTEGER_BOR: + case LBF_INTEGER_BXOR: + case LBF_INTEGER_BTEST: + case LBF_INTEGER_MIN: + case LBF_INTEGER_MAX: + return {-1, 1}; // variadic + + case LBF_INTEGER_EXTRACT: + return {-1, 1}; // 2 or 3 parameters + + case LBF_INTEGER_BNOT: + case LBF_INTEGER_BSWAP: + case LBF_INTEGER_NEG: + case LBF_INTEGER_COUNTLZ: + case LBF_INTEGER_COUNTRZ: + case LBF_INTEGER_TONUMBER: + case LBF_INTEGER_CREATE: + return {1, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_INTEGER_CLAMP: + return {3, 1, BuiltinInfo::Flag_NoneSafe}; + + case LBF_INTEGER_ADD: + case LBF_INTEGER_SUB: + case LBF_INTEGER_DIV: + case LBF_INTEGER_REM: + case LBF_INTEGER_UDIV: + case LBF_INTEGER_UREM: + case LBF_INTEGER_MOD: + case LBF_INTEGER_MUL: + case LBF_INTEGER_IDIV: + case LBF_INTEGER_LT: + case LBF_INTEGER_LE: + case LBF_INTEGER_ULT: + case LBF_INTEGER_ULE: + case LBF_INTEGER_GT: + case LBF_INTEGER_GE: + case LBF_INTEGER_UGT: + case LBF_INTEGER_UGE: + case LBF_INTEGER_LSHIFT: + case LBF_INTEGER_RSHIFT: + case LBF_INTEGER_ARSHIFT: + case LBF_INTEGER_LROTATE: + case LBF_INTEGER_RROTATE: + return {2, 1, BuiltinInfo::Flag_NoneSafe}; + } + + LUAU_UNREACHABLE(); +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/Builtins.h b/External/luau/Compiler/src/Builtins.h new file mode 100644 index 00000000..ca4acfcc --- /dev/null +++ b/External/luau/Compiler/src/Builtins.h @@ -0,0 +1,66 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "ValueTracking.h" + +namespace Luau +{ +struct CompileOptions; +} + +namespace Luau +{ +namespace Compile +{ + +struct Builtin +{ + AstName object; + AstName method; + + bool empty() const + { + return object == AstName() && method == AstName(); + } + + bool isGlobal(const char* name) const + { + return object == AstName() && method == name; + } + + bool isMethod(const char* table, const char* name) const + { + return object == table && method == name; + } +}; + +Builtin getBuiltin(AstExpr* node, const DenseHashMap& globals, const DenseHashMap& variables); + +void analyzeBuiltins( + DenseHashMap& result, + const DenseHashMap& globals, + const DenseHashMap& variables, + const CompileOptions& options, + AstNode* root, + const AstNameTable& names +); + +struct BuiltinInfo +{ + enum Flags + { + // none-safe builtins are builtins that have the same behavior for arguments that are nil or none + // this allows the compiler to compile calls to builtins more efficiently in certain cases + // for example, math.abs(x()) may compile x() as if it returns one value; if it returns no values, abs() will get nil instead of none + Flag_NoneSafe = 1 << 0, + }; + + int params; + int results; + unsigned int flags; +}; + +BuiltinInfo getBuiltinInfo(int bfid); + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/Compiler.cpp b/External/luau/Compiler/src/Compiler.cpp new file mode 100644 index 00000000..ab52cc46 --- /dev/null +++ b/External/luau/Compiler/src/Compiler.cpp @@ -0,0 +1,6138 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Compiler.h" + +#include "Luau/Ast.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Common.h" +#include "Luau/Parser.h" +#include "Luau/InsertionOrderedMap.h" +#include "Luau/StringUtils.h" +#include "Luau/TimeTrace.h" + +#include "Builtins.h" +#include "ConstantFolding.h" +#include "CostModel.h" +#include "TableShape.h" +#include "Types.h" +#include "Utils.h" +#include "ValueTracking.h" + +#include +#include +#include + +#include +#include + +LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThreshold, 25) +LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThresholdMaxBoost, 300) + +LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) +LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) +LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) + +LUAU_FASTFLAG(LuauExportValueSyntax) +LUAU_FASTFLAGVARIABLE(LuauCompileMoveElision) +LUAU_FASTFLAGVARIABLE(LuauCompileCleanBlockDeadClose) +LUAU_FASTFLAGVARIABLE(LuauCompileLoopUnrollZero) +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAGVARIABLE(LuauCompileConcatTargetTop) +LUAU_FASTFLAG(LuauCompileRefactorFeedback) +LUAU_FASTFLAG(DebugLuauNoInline) +LUAU_FASTFLAG(LuauEmitCallFeedback) +LUAU_FASTFLAGVARIABLE(LuauOptimizeExportTable) +LUAU_FASTFLAG(LuauCompileFastpcall) +LUAU_FASTFLAGVARIABLE(LuauExportedTypesParticipateInScc) +LUAU_FLAGVERSION(LuauExportedTypesParticipateInScc, 2) +LUAU_FASTFLAG(LuauExperimentalIfLocalSyntax) +LUAU_FASTFLAGVARIABLE(LuauCompileReuseLocalRegs) + +namespace Luau +{ + +using namespace Luau::Compile; + +static const uint32_t kDefaultAllocPc = ~0u; + +void escapeAndAppend(std::string& buffer, const char* str, size_t len) +{ + if (memchr(str, '%', len)) + { + for (size_t characterIndex = 0; characterIndex < len; ++characterIndex) + { + char character = str[characterIndex]; + buffer.push_back(character); + + if (character == '%') + buffer.push_back('%'); + } + } + else + buffer.append(str, len); +} + +CompileError::CompileError(const Location& location, std::string message) + : location(location) + , message(std::move(message)) +{ +} + +CompileError::~CompileError() throw() {} + +const char* CompileError::what() const throw() +{ + return message.c_str(); +} + +const Location& CompileError::getLocation() const +{ + return location; +} + +// NOINLINE is used to limit the stack cost of this function due to std::string object / exception plumbing +LUAU_NOINLINE void CompileError::raise(const Location& location, const char* format, ...) +{ + va_list args; + va_start(args, format); + std::string message = vformat(format, args); + va_end(args); + + throw CompileError(location, message); +} + +static BytecodeBuilder::StringRef sref(AstName name) +{ + LUAU_ASSERT(name.value); + return {name.value, strlen(name.value)}; +} + +static BytecodeBuilder::StringRef sref(AstArray data) +{ + LUAU_ASSERT(data.data); + return {data.data, data.size}; +} + +static BytecodeBuilder::StringRef sref(AstArray data) +{ + LUAU_ASSERT(data.data); + return {data.data, data.size}; +} + +struct Compiler +{ + struct RegScope; + + Compiler(BytecodeBuilder& bytecode, const CompileOptions& options, AstNameTable& names) + : bytecode(bytecode) + , options(options) + , builtinTypes(options.vectorType) + , names(names) + , exports(AstLocal(names.getOrAdd("__EXP"), Location(), nullptr, 0, 0, nullptr, true)) + { + // preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays + localStack.reserve(16); + upvals.reserve(16); + } + + void checkConstant(int32_t constant, const Location& location) + { + if (constant < 0) + CompileError::raise(location, "Exceeded constant limit; simplify the code to compile"); + } + + int getLocalReg(AstLocal* local) + { + Local* l = locals.find(local); + + return l && l->allocated ? l->reg : -1; + } + + uint8_t getUpval(AstLocal* local) + { + for (size_t uid = 0; uid < upvals.size(); ++uid) + if (upvals[uid] == local) + return uint8_t(uid); + + if (upvals.size() >= kMaxUpvalueCount) + CompileError::raise( + local->location, "Out of upvalue registers when trying to allocate %s: exceeded limit %d", local->name.value, kMaxUpvalueCount + ); + + // mark local as captured so that closeLocals emits LOP_CLOSEUPVALS accordingly + Variable* v = variables.find(local); + + if (v && v->written) + { + if (FFlag::LuauCompileMoveElision) + { + Local& l = locals[local]; + l.captured = true; + + if (l.allocated) + regCaptured[l.reg] = true; + } + else + { + locals[local].captured = true; + } + } + + upvals.push_back(local); + + return uint8_t(upvals.size() - 1); + } + + bool atTopLevel() const + { + return currentFunction != nullptr && currentFunction->functionDepth == 0 && blockDepth == 0 && loops.empty(); + } + + void checkExportedLocal(AstLocal* local, const Location& location) + { + if (local->isExported) + { + if (!atTopLevel()) + { + // We can catch some non top-level usages in the parser, but for others, like in loops, we also catch them here + CompileError::raise(location, "'export' may only be applied to top-level statements"); + } + } + } + + void ensureExportTable(AstNode* node) + { + exports.hasExports = true; + + if (locals.contains(&exports.exportTableLocal)) + return; + + LUAU_ASSERT(atTopLevel()); + + uint8_t tableReg = allocReg(node, 1u); + if (FFlag::LuauOptimizeExportTable && exports.exportedTableCid != -1 && exports.exportedTableCid < 32768) + { + bytecode.emitAD(LOP_DUPTABLE, tableReg, static_cast(exports.exportedTableCid)); + } + else + { + bytecode.emitABC(LOP_NEWTABLE, tableReg, encodeHashSize(0), 0); + bytecode.emitAux(0); + } + + pushLocal(&exports.exportTableLocal, tableReg, kDefaultAllocPc); + } + + uint8_t getExportTableReg(AstNode* node) + { + if (int reg = getLocalReg(&exports.exportTableLocal); reg >= 0) + return uint8_t(reg); + + uint8_t upval = getUpval(&exports.exportTableLocal); + uint8_t reg = allocReg(node, 1u); + bytecode.emitABC(LOP_GETUPVAL, reg, upval, 0); + return reg; + } + + void buildExportTableShape() + { + LUAU_ASSERT(FFlag::LuauOptimizeExportTable); + + BytecodeBuilder::TableShape exportedShape; + + if (exports.exportedVariables.size() >= BytecodeBuilder::TableShape::kMaxLength) + return; + + for (const auto& exportedLocal : exports.exportedVariables) + { + Variable* variable = variables.find(exportedLocal); + // TODO: this may be fine to just be an assert + if (variable == nullptr) + { + CompileError::raise(exportedLocal->location, "Local does not have corresponding variable"); + continue; + } + + int32_t keyCid = bytecode.addConstantString(sref(exportedLocal->name)); + if (keyCid < 0) + // typically we throw a compiler error, but optimistically we can just not build a shape preemptively + return; + + size_t idx = exportedShape.length; + exportedShape.keys[idx] = keyCid; + exportedShape.constants[idx] = -1; + if (variable->constant && !variable->written) + { + int32_t valueCid = getConstantIndex(variable->init); + // If the constant pool is full we can't embed this value in the shape + // Bail out so exportedTableCid stays -1 and compileStatLocal falls back to SETTABLEKS + if (valueCid < 0) + return; + + exportedShape.constants[idx] = valueCid; + exportedShape.hasConstants = true; + } + exportedShape.length++; + } + + if (exportedShape.length > 0) + exports.exportedTableCid = bytecode.addConstantTable(exportedShape); + } + + bool alwaysTerminates(AstStat* node) const + { + return Compile::alwaysTerminates(constants, node); + } + + void emitLoadK(uint8_t target, int32_t cid) + { + LUAU_ASSERT(cid >= 0); + + if (cid < 32768) + { + bytecode.emitAD(LOP_LOADK, target, int16_t(cid)); + } + else + { + bytecode.emitAD(LOP_LOADKX, target, 0); + bytecode.emitAux(cid); + } + } + + AstExpr* tryIndexConstantTable(AstExprIndexName* expr) + { + // If we are referring to a local + AstExprLocal* tableLocal = unwrapExprOfType(expr->expr); + if (!tableLocal) + return nullptr; + + // And it's not mutable and has an initializer + Variable* lv = variables.find(tableLocal->local); + if (!lv || lv->written || !lv->init) + return nullptr; + + // And the local is a constant table + TableConstantKind* tableKind = tableConstants.find(tableLocal->local); + if (!tableKind || *tableKind != ConstantTable) + return nullptr; + + AstExprTable* table = unwrapExprOfType(lv->init); + if (!table) + return nullptr; + + // Look into the initializer to find the function value + AstExpr* match = nullptr; + + for (const AstExprTable::Item& item : table->items) + { + if (item.kind == AstExprTable::Item::Kind::Record || item.kind == AstExprTable::Item::Kind::General) + { + Constant* keyConstant = constants.find(item.key); + + // Dynamic key can alias any of the constant keys + if (!keyConstant) + { + match = nullptr; + } + else if (keyConstant->type == Constant::Type_String && keyConstant->stringLength != 0) + { + AstName keyName = names.getOrAdd(keyConstant->valueString, keyConstant->stringLength); + + // No break as last match determines the lookup result if there are duplicates + if (keyName == expr->index) + match = item.value; + } + } + } + + return match; + } + + AstExprFunction* getFunctionExpr(AstExpr* node) + { + if (AstExprLocal* expr = node->as()) + { + Variable* lv = variables.find(expr->local); + + if (!lv || lv->written || !lv->init) + return nullptr; + + return getFunctionExpr(lv->init); + } + else if (AstExprIndexName* expr = node->as()) + { + if (AstExpr* value = tryIndexConstantTable(expr)) + return getFunctionExpr(value); + + return nullptr; + } + else if (AstExprGroup* expr = node->as()) + return getFunctionExpr(expr->expr); + else if (AstExprTypeAssertion* expr = node->as()) + return getFunctionExpr(expr->expr); + else if (AstExprInstantiate* expr = node->as()) + return getFunctionExpr(expr->expr); + else + return node->as(); + } + + void compileExportTable() + { + LUAU_ASSERT(currentFunction); + + AstExprFunction* locNode = currentFunction; + int8_t tableReg; + + if (!exports.isEmpty()) + { + // this arises when we have a module that is only exporting classes + ensureExportTable(currentFunction); + + tableReg = getLocalReg(&exports.exportTableLocal); + LUAU_ASSERT(tableReg >= 0); + + if (FFlag::DebugLuauUserDefinedClasses) + { + for (auto& [classLocal, classReg] : exports.exportedClasses) + { + BytecodeBuilder::StringRef classNameRef = sref(classLocal->name); + int32_t classNameCid = bytecode.addConstantString(classNameRef); + if (classNameCid < 0) + CompileError::raise(locNode->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(LOP_SETTABLEKS, classReg, tableReg, uint8_t(BytecodeBuilder::getStringHash(classNameRef))); + bytecode.emitAux(classNameCid); + } + } + + if (FFlag::LuauOptimizeExportTable) + { + for (auto& funcLocal : exports.exportedFunctions) + { + int32_t cid = bytecode.addConstantString(sref(funcLocal->name)); + if (cid < 0) + CompileError::raise(funcLocal->location, "Exceeded constant limit; simplify the code to compile"); + + uint8_t funcReg = getLocalReg(funcLocal); + + bytecode.emitABC(LOP_SETTABLEKS, funcReg, tableReg, uint8_t(BytecodeBuilder::getStringHash(sref(funcLocal->name)))); + bytecode.emitAux(cid); + } + } + } + else + { + // Type-only exports with no return: emit a fresh empty table + LUAU_ASSERT(FFlag::LuauExportedTypesParticipateInScc); + LUAU_ASSERT(exports.hasTypeExports); + + tableReg = allocReg(locNode, 1u); + bytecode.emitABC(LOP_NEWTABLE, tableReg, encodeHashSize(0), 0); + bytecode.emitAux(0); + } + + uint8_t freezeReg = allocReg(locNode, 2u); + AstName freezeName = names.getOrAdd("freeze"); + int32_t freezeCid = bytecode.addConstantString(sref(freezeName)); + if (freezeCid < 0) + CompileError::raise(locNode->location, "Exceeded constant limit; simplify the code to compile"); + + AstName tableName = names.getOrAdd("table"); + int32_t tableCid = bytecode.addConstantString(sref(tableName)); + if (tableCid < 0) + CompileError::raise(locNode->location, "Exceeded constant limit; simplify the code to compile"); + + // Note: GETIMPORT encoding is limited to 10 bits per object id component + // otherwise we can fallback to getglobal + bool getGlobalFallback = true; + if (tableCid < 1024 && freezeCid < 1024) + { + uint32_t iid = BytecodeBuilder::getImportId(tableCid, freezeCid); + int32_t cid = bytecode.addImport(iid); + if (cid >= 0 && cid < 32768) + { + bytecode.emitAD(LOP_GETIMPORT, freezeReg, int16_t(cid)); + bytecode.emitAux(iid); + getGlobalFallback = false; + } + } + + if (getGlobalFallback) + { + bytecode.emitABC(LOP_GETGLOBAL, freezeReg, 0, uint8_t(BytecodeBuilder::getStringHash(sref(tableName)))); + bytecode.emitAux(tableCid); + + bytecode.emitABC(LOP_GETTABLEKS, freezeReg, freezeReg, uint8_t(BytecodeBuilder::getStringHash(sref(freezeName)))); + bytecode.emitAux(freezeCid); + } + + bytecode.emitABC(LOP_MOVE, uint8_t(freezeReg + 1), tableReg, 0); + bytecode.emitABC(LOP_CALL, freezeReg, 2, 2); + + closeLocals(0); + bytecode.emitABC(LOP_RETURN, freezeReg, 2, 0); + } + + uint32_t compileFunction(AstExprFunction* func, uint8_t& protoflags) + { + LUAU_TIMETRACE_SCOPE("Compiler::compileFunction", "Compiler"); + + if (func->debugname.value) + LUAU_TIMETRACE_ARGUMENT("name", func->debugname.value); + + LUAU_ASSERT(!functions.contains(func)); + LUAU_ASSERT(regTop == 0 && stackSize == 0 && localStack.empty() && upvals.empty()); + if (FFlag::LuauExportValueSyntax) + currentFunction = func; + + if (FFlag::LuauExportValueSyntax && FFlag::LuauOptimizeExportTable && atTopLevel()) + buildExportTableShape(); + + RegScope rs(this); + + bool self = func->self != 0; + uint32_t fid = bytecode.beginFunction(uint8_t(self + func->args.size), func->vararg); + + setDebugLine(func); + + if (func->vararg) + bytecode.emitABC(LOP_PREPVARARGS, uint8_t(self + func->args.size), 0, 0); + + uint8_t args = allocReg(func, self + unsigned(func->args.size)); + + if (func->self) + pushLocal(func->self, args, kDefaultAllocPc); + + for (size_t i = 0; i < func->args.size; ++i) + pushLocal(func->args.data[i], uint8_t(args + self + i), kDefaultAllocPc); + + argCount = localStack.size(); + + AstStatBlock* stat = func->body; + + bool terminatesEarly = false; + Location terminationLocation; + currentFunction = func; + + if (FFlag::DebugLuauUserDefinedClasses && atTopLevel()) + preallocateHoistedClasses(stat); + + for (size_t i = 0; i < stat->body.size; ++i) + { + AstStat* bodyStat = stat->body.data[i]; + + if (FFlag::LuauCompileReuseLocalRegs) + popStatLocals(stat, bodyStat, 0u); + + compileStat(bodyStat); + + if (alwaysTerminates(bodyStat)) + { + terminatesEarly = true; + break; + } + } + + // valid function bytecode must always end with RETURN + // we elide this if we're guaranteed to hit a RETURN statement regardless of the control flow + if (FFlag::LuauExportValueSyntax) + { + setDebugLineEnd(stat); + // in main + if (!exports.isEmpty() && atTopLevel()) + { + compileExportTable(); + } + else if (FFlag::LuauExportedTypesParticipateInScc && exports.hasTypeExports && !terminatesEarly && atTopLevel()) + { + compileExportTable(); + } + else + { + if (!terminatesEarly) + { + closeLocals(0); + + bytecode.emitABC(LOP_RETURN, 0, 1, 0); + } + } + } + else + { + if (!terminatesEarly) + { + setDebugLineEnd(stat); + closeLocals(0); + + bytecode.emitABC(LOP_RETURN, 0, 1, 0); + } + } + + // constant folding may remove some upvalue refs from bytecode, so this puts them back + if (options.optimizationLevel >= 1 && options.debugLevel >= 2) + gatherConstUpvals(func); + + bytecode.setDebugFunctionLineDefined(func->location.begin.line + 1); + + if (options.debugLevel >= 1 && func->debugname.value) + bytecode.setDebugFunctionName(sref(func->debugname)); + + if (options.debugLevel >= 2 && !upvals.empty()) + { + for (AstLocal* l : upvals) + bytecode.pushDebugUpval(sref(l->name)); + } + + if (options.typeInfoLevel >= 1) + { + for (AstLocal* l : upvals) + { + LuauBytecodeType ty = LBC_TYPE_ANY; + + if (LuauBytecodeType* recordedTy = localTypes.find(l)) + ty = *recordedTy; + + bytecode.pushUpvalTypeInfo(ty); + } + } + + if (options.optimizationLevel >= 1) + bytecode.foldJumps(); + + bool hasLongJumpError = false; + bytecode.expandJumps(hasLongJumpError); + + if (hasLongJumpError) + CompileError::raise(func->location, "Exceeded jump distance limit; simplify the code to compile"); + + popLocals(0); + + if (bytecode.getInstructionCount() > kMaxInstructionCount) + CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile"); + + // note: we move types out of typeMap which is safe because compileFunction is only called once per function + if (std::string* funcType = functionTypes.find(func)) + bytecode.setFunctionTypeInfo(std::move(*funcType)); + + // top-level code only executes once so it can be marked as cold if it has no loops; code with loops might be profitable to compile natively + if (func->functionDepth == 0 && !hasLoops) + protoflags |= LPF_NATIVE_COLD; + + if (func->hasNativeAttribute()) + protoflags |= LPF_NATIVE_FUNCTION; + + if (FFlag::LuauExportValueSyntax && !exports.isEmpty() && func->functionDepth == 0) + protoflags |= LPF_USES_EXPORT; + + if (FFlag::LuauExportedTypesParticipateInScc && exports.hasTypeExports && !terminatesEarly && func->functionDepth == 0) + protoflags |= LPF_USES_EXPORT; + + bool isInlinable = !hasMultiRet && !getfenvUsed && !setfenvUsed; + uint64_t costModel = 0; + if (FFlag::LuauEmitCallFeedback && isInlinable && upvals.empty()) + { + protoflags |= LPF_INLINABLE; + costModel = modelCost(func->body, func->args.data, func->args.size, builtins, constants); + } + + bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()), protoflags, costModel); + + Function& f = functions[func]; + f.id = fid; + f.upvals = upvals; + + // record information for inlining + if (options.optimizationLevel >= 2 && !func->vararg && !func->self && !getfenvUsed && !setfenvUsed) + { + if (FFlag::DebugLuauNoInline && func->hasAttribute(AstAttr::Type::DebugNoinline)) + { + f.canInline = false; + } + else + { + f.canInline = true; + } + f.stackSize = stackSize; + f.costModel = costModel == 0 ? modelCost(func->body, func->args.data, func->args.size, builtins, constants) : costModel; + + // track functions that only ever return a single value so that we can convert multret calls to fixedret calls + if (alwaysTerminates(func->body)) + { + ReturnVisitor returnVisitor(this, func); + stat->visit(&returnVisitor); + f.returnsOne = returnVisitor.returnsOne; + + if (FFlag::LuauCompileMoveElision) + { + f.returnKind = returnVisitor.returnKind; + f.returnLocal = returnVisitor.returnLocal; + + if (f.returnLocal) + { + if (Local* lc = locals.find(f.returnLocal); lc && lc->captured) + f.returnLocalCaptured = true; + } + } + } + } + + upvals.clear(); // note: instead of std::move above, we copy & clear to preserve capacity for future pushes + stackSize = 0; + + if (FFlag::LuauCompileReuseLocalRegs) + freeLocalRegs.clear(); + + argCount = 0; + + hasLoops = false; + hasMultiRet = false; + currentFunction = nullptr; + + return fid; + } + + // returns true if node can return multiple values; may conservatively return true even if expr is known to return just a single value + bool isExprMultRet(AstExpr* node) + { + AstExprCall* expr = node->as(); + if (!expr) + return node->is(); + + // conservative version, optimized for compilation throughput + if (options.optimizationLevel <= 1) + return true; + + // handles builtin calls that can be constant-folded + // without this we may omit some optimizations eg compiling fast calls without use of FASTCALL2K + if (isConstant(expr)) + return false; + + // handles builtin calls that can't be constant-folded but are known to return one value + // note: optimizationLevel check is technically redundant but it's important that we never optimize based on builtins in O1 + if (options.optimizationLevel >= 2) + { + if (int* bfid = builtins.find(expr); bfid && *bfid != LBF_NONE) + return getBuiltinInfo(*bfid).results != 1; + } + + // handles local function calls where we know only one argument is returned + AstExprFunction* func = getFunctionExpr(expr->func); + Function* fi = func ? functions.find(func) : nullptr; + + if (fi && fi->returnsOne) + return false; + + // unrecognized call, so we conservatively assume multret + return true; + } + + // note: this doesn't just clobber target (assuming it's temp), but also clobbers *all* allocated registers >= target! + // this is important to be able to support "multret" semantics due to Lua call frame structure + bool compileExprTempMultRet(AstExpr* node, uint8_t target) + { + if (AstExprCall* expr = node->as()) + { + // Optimization: convert multret calls that always return one value to fixedret calls; this facilitates inlining/constant folding + if (options.optimizationLevel >= 2 && !isExprMultRet(node)) + { + compileExprTemp(node, target); + return false; + } + + // We temporarily swap out regTop to have targetTop work correctly... + // This is a crude hack but it's necessary for correctness :( + RegScope rs(this, target); + compileExprCall(expr, target, /* targetCount= */ 0, /* targetTop= */ true, /* multRet= */ true); + return true; + } + else if (AstExprVarargs* expr = node->as()) + { + // We temporarily swap out regTop to have targetTop work correctly... + // This is a crude hack but it's necessary for correctness :( + RegScope rs(this, target); + compileExprVarargs(expr, target, /* targetCount= */ 0, /* multRet= */ true); + return true; + } + else + { + compileExprTemp(node, target); + return false; + } + } + + // note: this doesn't just clobber target (assuming it's temp), but also clobbers *all* allocated registers >= target! + // this is important to be able to emit code that takes fewer registers and runs faster + void compileExprTempTop(AstExpr* node, uint8_t target) + { + // We temporarily swap out regTop to have targetTop work correctly... + // This is a crude hack but it's necessary for performance :( + // It makes sure that nested call expressions can use targetTop optimization and don't need to have too many registers + RegScope rs(this, target + 1); + compileExprTemp(node, target); + } + + void compileExprVarargs(AstExprVarargs* expr, uint8_t target, uint8_t targetCount, bool multRet = false) + { + LUAU_ASSERT(targetCount < 255); + LUAU_ASSERT(!multRet || unsigned(target + targetCount) == regTop); + + setDebugLine(expr); // normally compileExpr sets up line info, but compileExprVarargs can be called directly + + bytecode.emitABC(LOP_GETVARARGS, target, multRet ? 0 : uint8_t(targetCount + 1), 0); + } + + void compileExprSelectVararg(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs) + { + LUAU_ASSERT(targetCount == 1); + LUAU_ASSERT(!expr->self); + LUAU_ASSERT(expr->args.size == 2 && expr->args.data[1]->is()); + + AstExpr* arg = expr->args.data[0]; + + uint8_t argreg; + + if (int reg = getExprLocalReg(arg); reg >= 0) + argreg = uint8_t(reg); + else + { + argreg = uint8_t(regs + 1); + compileExprTempTop(arg, argreg); + } + + size_t fastcallLabel = bytecode.emitLabel(); + + bytecode.emitABC(LOP_FASTCALL1, LBF_SELECT_VARARG, argreg, 0); + + // note, these instructions are normally not executed and are used as a fallback for FASTCALL + // we can't use TempTop variant here because we need to make sure the arguments we already computed aren't overwritten + compileExprTemp(expr->func, regs); + + if (argreg != regs + 1) + bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1), argreg, 0); + + bytecode.emitABC(LOP_GETVARARGS, uint8_t(regs + 2), 0, 0); + + size_t callLabel = bytecode.emitLabel(); + if (!bytecode.patchSkipC(fastcallLabel, callLabel)) + CompileError::raise(expr->func->location, "Exceeded jump distance limit; simplify the code to compile"); + + // note, this is always multCall (last argument is variadic) + bytecode.emitABC(LOP_CALL, regs, 0, multRet ? 0 : uint8_t(targetCount + 1)); + + // if we didn't output results directly to target, we need to move them + if (!targetTop) + { + for (size_t i = 0; i < targetCount; ++i) + bytecode.emitABC(LOP_MOVE, uint8_t(target + i), uint8_t(regs + i), 0); + } + } + + void compileExprFastcallN( + AstExprCall* expr, + uint8_t target, + uint8_t targetCount, + bool targetTop, + bool multRet, + uint8_t regs, + int bfid, + int bfK = -1 + ) + { + LUAU_ASSERT(!expr->self); + LUAU_ASSERT(expr->args.size >= 1); + LUAU_ASSERT(expr->args.size <= 3); + LUAU_ASSERT(bfid == LBF_BIT32_EXTRACTK ? bfK >= 0 : bfK < 0); + LUAU_ASSERT(targetCount < 255); + + LuauOpcode opc = LOP_NOP; + + if (expr->args.size == 1) + opc = LOP_FASTCALL1; + else if (bfK >= 0 || (expr->args.size == 2 && isConstant(expr->args.data[1]))) + opc = LOP_FASTCALL2K; + else if (expr->args.size == 2) + opc = LOP_FASTCALL2; + else + opc = LOP_FASTCALL3; + + uint32_t args[3] = {}; + + for (size_t i = 0; i < expr->args.size; ++i) + { + if (i > 0 && opc == LOP_FASTCALL2K) + { + int32_t cid = getConstantIndex(expr->args.data[i]); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + args[i] = cid; + } + else if (int reg = getExprLocalReg(expr->args.data[i]); reg >= 0) + { + args[i] = uint8_t(reg); + } + else + { + args[i] = uint8_t(regs + 1 + i); + + if (FFlag::LuauCompileMoveElision) + { + // We temporarily swap out regTop to have targetTop work correctly here, see compileExprTempTop for info + RegScope rs(this, args[i] + 1); + args[i] = compileExprAutoTemp(expr->args.data[i], uint8_t(args[i])); + } + else + { + compileExprTempTop(expr->args.data[i], uint8_t(args[i])); + } + } + } + + size_t fastcallLabel = bytecode.emitLabel(); + + bytecode.emitABC(opc, uint8_t(bfid), uint8_t(args[0]), 0); + + if (opc == LOP_FASTCALL3) + { + LUAU_ASSERT(bfK < 0); + bytecode.emitAux(args[1] | (args[2] << 8)); + } + else if (opc != LOP_FASTCALL1) + { + bytecode.emitAux(bfK >= 0 ? bfK : args[1]); + } + + // Set up a traditional Lua stack for the subsequent LOP_CALL. + // Note, as with other instructions that immediately follow FASTCALL, these are normally not executed and are used as a fallback for + // these FASTCALL variants. + for (size_t i = 0; i < expr->args.size; ++i) + { + if (i > 0 && opc == LOP_FASTCALL2K) + emitLoadK(uint8_t(regs + 1 + i), args[i]); + else if (args[i] != regs + 1 + i) + bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0); + } + + // note, these instructions are normally not executed and are used as a fallback for FASTCALL + // we can't use TempTop variant here because we need to make sure the arguments we already computed aren't overwritten + compileExprTemp(expr->func, regs); + + size_t callLabel = bytecode.emitLabel(); + + // FASTCALL will skip over the instructions needed to compute function and jump over CALL which must immediately follow the instruction + // sequence after FASTCALL + if (!bytecode.patchSkipC(fastcallLabel, callLabel)) + CompileError::raise(expr->func->location, "Exceeded jump distance limit; simplify the code to compile"); + + bytecode.emitABC(LOP_CALL, regs, uint8_t(expr->args.size + 1), multRet ? 0 : uint8_t(targetCount + 1)); + + // if we didn't output results directly to target, we need to move them + if (!targetTop) + { + for (size_t i = 0; i < targetCount; ++i) + bytecode.emitABC(LOP_MOVE, uint8_t(target + i), uint8_t(regs + i), 0); + } + } + + bool tryCompileInlinedCall( + AstExprCall* expr, + AstExprFunction* func, + uint8_t target, + uint8_t targetCount, + bool multRet, + int thresholdBase, + int thresholdMaxBoost, + int depthLimit + ) + { + Function* fi = functions.find(func); + LUAU_ASSERT(fi); + + // make sure we have enough register space + if (regTop > 128 || fi->stackSize > 32) + { + bytecode.addDebugRemark("inlining failed: high register pressure"); + return false; + } + + // we should ideally aggregate the costs during recursive inlining, but for now simply limit the depth + if (int(inlineFrames.size()) >= depthLimit) + { + bytecode.addDebugRemark("inlining failed: too many inlined frames"); + return false; + } + + // compiling recursive inlining is difficult because we share constant/variable state but need to bind variables to different registers + for (InlineFrame& frame : inlineFrames) + if (frame.func == func) + { + bytecode.addDebugRemark("inlining failed: can't inline recursive calls"); + return false; + } + + // we can't inline multret functions because the caller expects L->top to be adjusted: + // - inlined return compiles to a JUMP, and we don't have an instruction that adjusts L->top arbitrarily + // - even if we did, right now all L->top adjustments are immediately consumed by the next instruction, and for now we want to preserve that + // - additionally, we can't easily compile multret expressions into designated target as computed call arguments will get clobbered + if (multRet) + { + bytecode.addDebugRemark("inlining failed: can't convert fixed returns to multret"); + return false; + } + + // compute constant bitvector for all arguments to feed the cost model + bool varc[8] = {}; + bool hasConstant = false; + for (size_t i = 0; i < func->args.size && i < expr->args.size && i < 8; ++i) + { + if (isConstant(expr->args.data[i])) + { + varc[i] = true; + hasConstant = true; + } + } + + // if the last argument only returns a single value, all following arguments are nil + if (expr->args.size != 0 && !isExprMultRet(expr->args.data[expr->args.size - 1])) + { + for (size_t i = expr->args.size; i < func->args.size && i < 8; ++i) + { + varc[i] = true; + hasConstant = true; + } + } + + // If we had constant arguments that can affect the cost model of this specific call in non-trivial ways + uint64_t callCostModel = fi->costModel; + + if (hasConstant) + callCostModel = costModelInlinedCall(expr, func); + + // we use a dynamic cost threshold that's based on the fixed limit boosted by the cost advantage we gain due to inlining + int inlinedCost = computeCost(callCostModel, varc, std::min(int(func->args.size), 8)); + int baselineCost = computeCost(fi->costModel, nullptr, 0) + 3; + int inlineProfit = (inlinedCost == 0) ? thresholdMaxBoost : std::min(thresholdMaxBoost, 100 * baselineCost / inlinedCost); + + int threshold = thresholdBase * inlineProfit / 100; + bool isIife = unwrapExprOfType(expr->func) != nullptr; + + if (inlinedCost > threshold && !isIife) + { + bytecode.addDebugRemark("inlining failed: too expensive (cost %d, profit %.2fx)", inlinedCost, double(inlineProfit) / 100); + return false; + } + + bytecode.addDebugRemark( + "inlining succeeded (cost %d, profit %.2fx, depth %d)", inlinedCost, double(inlineProfit) / 100, int(inlineFrames.size()) + ); + + compileInlinedCall(expr, func, target, targetCount); + return true; + } + + uint64_t costModelInlinedCall(AstExprCall* expr, AstExprFunction* func) + { + for (size_t i = 0; i < func->args.size; ++i) + { + AstLocal* var = func->args.data[i]; + AstExpr* arg = i < expr->args.size ? expr->args.data[i] : nullptr; + + // last expression is a multret, there are no constant for it and it will fill all values + if (i + 1 == expr->args.size && func->args.size > expr->args.size && isExprMultRet(arg)) + break; + + // variable gets mutated at some point, so we do not have a constant for it + if (Variable* vv = variables.find(var); vv && vv->written) + continue; + + if (arg == nullptr) + locstants[var] = {Constant::Type_Nil}; + else if (const Constant* cv = constants.find(arg); cv && cv->type != Constant::Type_Unknown) + locstants[var] = *cv; + } + + // fold constant values updated above into expressions in the function body, recording changes for undo + exprChanges.clear(); + localChanges.clear(); + + foldConstants( + constants, + variables, + locstants, + builtinsFold, + builtinsFoldLibraryK, + options.vectorPrecision == 1, + options.libraryMemberConstantCb, + func->body, + names, + tableConstants, + &exprChanges, + &localChanges + ); + + // model the cost of the function evaluated with current constants + uint64_t cost = modelCost(func->body, func->args.data, func->args.size, builtins, constants); + + // clean up constant state for future inlining attempts + for (size_t i = 0; i < func->args.size; ++i) + { + if (Constant* var = locstants.find(func->args.data[i])) + var->type = Constant::Type_Unknown; + } + + Compile::undoChanges(constants, exprChanges); + Compile::undoChanges(locstants, localChanges); + + return cost; + } + + struct FindRegisterUseByLocal : AstVisitor + { + FindRegisterUseByLocal(Compiler* self, uint8_t reg) + : self(self) + , reg(reg) + { + } + + bool visit(AstExprLocal* node) override + { + if (self->getLocalReg(node->local) == reg) + used = true; + + return !used; + } + + Compiler* self = nullptr; + uint8_t reg = 0; + bool used = false; + }; + + bool isRegisterUsedInCallArguments(AstExprCall* expr, size_t startArg, uint8_t reg) + { + FindRegisterUseByLocal visitor(this, reg); + + for (size_t i = startArg; i < expr->args.size && !visitor.used; ++i) + expr->args.data[i]->visit(&visitor); + + return visitor.used; + } + + void compileInlinedCall(AstExprCall* expr, AstExprFunction* func, uint8_t target, uint8_t targetCount) + { + Function* fi = FFlag::LuauCompileMoveElision ? functions.find(func) : nullptr; + + // We try to write result directly to the target register when possible, but there are a few blockers + // - function captures a reference to the local that is being returned and might close it + // - there might not be a target register (return value is unused) + // - target register is captured and might be read before inlined function completes + bool noTargetMoveElision = FFlag::LuauCompileMoveElision && (fi->returnLocalCaptured || targetCount == 0 || regCaptured[target]); + + RegScope rs(this); + + size_t oldLocals = localStack.size(); + + std::vector args; + args.reserve(func->args.size); + + // evaluate all arguments; note that we don't emit code for constant arguments (relying on constant folding) + // note that compiler state (variable registers/values) does not change here - we defer that to a separate loop below to handle nested calls + for (size_t i = 0; i < func->args.size; ++i) + { + AstLocal* var = func->args.data[i]; + AstExpr* arg = i < expr->args.size ? expr->args.data[i] : nullptr; + bool canUseParameterTarget = + FFlag::LuauCompileMoveElision && fi->returnKind == ReturnKind::Parameter && fi->returnLocal == var && !noTargetMoveElision; + + if (i + 1 == expr->args.size && func->args.size > expr->args.size && isExprMultRet(arg)) + { + // if the last argument can return multiple values, we need to compute all of them into the remaining arguments + unsigned int tail = unsigned(func->args.size - expr->args.size) + 1; + uint8_t reg = allocReg(arg, tail); + uint32_t allocpc = bytecode.getDebugPC(); + + if (AstExprCall* expr = arg->as()) + compileExprCall(expr, reg, tail, /* targetTop= */ true); + else if (AstExprVarargs* expr = arg->as()) + compileExprVarargs(expr, reg, tail); + else + LUAU_ASSERT(!"Unexpected expression type"); + + for (size_t j = i; j < func->args.size; ++j) + args.push_back({func->args.data[j], uint8_t(reg + (j - i)), {Constant::Type_Unknown}, allocpc}); + + // all remaining function arguments have been allocated and assigned to + break; + } + else if (Variable* vv = variables.find(var); vv && vv->written) + { + if (canUseParameterTarget && !isRegisterUsedInCallArguments(expr, i + 1, target)) + { + if (arg) + compileExprTemp(arg, target); + else + bytecode.emitABC(LOP_LOADNIL, target, 0, 0); + + args.push_back({var, target, {Constant::Type_Unknown}, kDefaultAllocPc}); + } + else + { + // if the argument is mutated, we need to allocate a fresh register even if it's a constant + uint8_t reg = allocReg(arg, 1u); + uint32_t allocpc = bytecode.getDebugPC(); + + if (arg) + compileExprTemp(arg, reg); + else + bytecode.emitABC(LOP_LOADNIL, reg, 0, 0); + + args.push_back({var, reg, {Constant::Type_Unknown}, allocpc}); + } + } + else if (arg == nullptr) + { + // since the argument is not mutated, we can simply fold the value into the expressions that need it + args.push_back({var, kInvalidReg, {Constant::Type_Nil}}); + } + else if (const Constant* cv = constants.find(arg); cv && cv->type != Constant::Type_Unknown) + { + // since the argument is not mutated, we can simply fold the value into the expressions that need it + args.push_back({var, kInvalidReg, *cv}); + } + else + { + AstExprLocal* le = getExprLocal(arg); + Variable* lv = le ? variables.find(le->local) : nullptr; + + // if the argument is a local that isn't mutated, we will simply reuse the existing register + if (int reg = le ? getExprLocalReg(le) : -1; reg >= 0 && (!lv || !lv->written)) + { + args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc, lv ? lv->init : nullptr}); + } + else if (canUseParameterTarget && !isRegisterUsedInCallArguments(expr, i + 1, target)) + { + compileExprTemp(arg, target); + + args.push_back({var, target, {Constant::Type_Unknown}, kDefaultAllocPc}); + } + else + { + uint8_t temp = allocReg(arg, 1u); + uint32_t allocpc = bytecode.getDebugPC(); + + if (FFlag::LuauCompileMoveElision) + temp = compileExprAutoTemp(arg, temp, target); + else + compileExprTemp(arg, temp); + + args.push_back({var, temp, {Constant::Type_Unknown}, allocpc, arg}); + } + } + } + + // evaluate extra expressions for side effects + for (size_t i = func->args.size; i < expr->args.size; ++i) + compileExprSide(expr->args.data[i]); + + // apply all evaluated arguments to the compiler state + // note: locals use current startpc for debug info, although some of them have been computed earlier; this is similar to compileStatLocal + for (InlineArg& arg : args) + { + if (arg.value.type == Constant::Type_Unknown) + { + pushLocal(arg.local, arg.reg, arg.allocpc); + + if (arg.init) + { + if (Variable* lv = variables.find(arg.local)) + lv->init = arg.init; + } + } + else + { + locstants[arg.local] = arg.value; + } + } + + AstLocal* resultLocal = + FFlag::LuauCompileMoveElision && fi->returnKind == ReturnKind::Local && !noTargetMoveElision ? fi->returnLocal : nullptr; + + // the inline frame will be used to compile return statements as well as to reject recursive inlining attempts + inlineFrames.push_back({func, oldLocals, target, targetCount, resultLocal}); + + // this pass tracks which calls are builtins and can be compiled more efficiently + analyzeBuiltins(inlineBuiltins, globals, variables, options, func->body, names); + + // If we found new builtins, apply them, but record which expressions we changed so we can undo later + if (!inlineBuiltins.empty()) + { + for (auto [callExpr, bfid] : inlineBuiltins) + { + int& builtin = builtins[callExpr]; // If there was no builtin previously, we will get LBF_NONE + + if (bfid != builtin) + { + inlineBuiltinsBackup[callExpr] = builtin; + builtin = bfid; + } + } + + inlineBuiltins.clear(); + } + + // fold constant values updated above into expressions in the function body, recording changes for undo + exprChanges.clear(); + localChanges.clear(); + + foldConstants( + constants, + variables, + locstants, + builtinsFold, + builtinsFoldLibraryK, + options.vectorPrecision == 1, + options.libraryMemberConstantCb, + func->body, + names, + tableConstants, + &exprChanges, + &localChanges + ); + + bool terminatesEarly = false; + + for (size_t i = 0; i < func->body->body.size; ++i) + { + AstStat* stat = func->body->body.data[i]; + + if (FFlag::LuauCompileReuseLocalRegs) + popStatLocals(func->body, stat, oldLocals); + + compileStat(stat); + + if (alwaysTerminates(stat)) + { + terminatesEarly = true; + + // Remove the last jump which jumps directly to the next instruction + InlineFrame& currFrame = inlineFrames.back(); + if (!currFrame.returnJumps.empty() && currFrame.returnJumps.back() == bytecode.emitLabel() - 1) + { + bytecode.undoEmit(LOP_JUMP); + currFrame.returnJumps.pop_back(); + } + break; + } + } + + // for the fallthrough path we need to ensure we clear out target registers + if (!terminatesEarly) + { + for (size_t i = 0; i < targetCount; ++i) + bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0); + + closeLocals(oldLocals); + } + + popLocals(oldLocals); + + size_t returnLabel = bytecode.emitLabel(); + patchJumps(expr, inlineFrames.back().returnJumps, returnLabel); + + inlineFrames.pop_back(); + + // clean up constant state for future inlining attempts + for (size_t i = 0; i < func->args.size; ++i) + { + AstLocal* local = func->args.data[i]; + + if (Constant* var = locstants.find(local)) + var->type = Constant::Type_Unknown; + + if (Variable* lv = variables.find(local)) + lv->init = nullptr; + } + + if (!inlineBuiltinsBackup.empty()) + { + for (auto [callExpr, bfid] : inlineBuiltinsBackup) + builtins[callExpr] = bfid; + + inlineBuiltinsBackup.clear(); + } + + Compile::undoChanges(constants, exprChanges); + Compile::undoChanges(locstants, localChanges); + } + + void compileExprCall(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop = false, bool multRet = false) + { + LUAU_ASSERT(targetCount < 255); + LUAU_ASSERT(!targetTop || unsigned(target + targetCount) == regTop); + + setDebugLine(expr); // normally compileExpr sets up line info, but compileExprCall can be called directly + + // try inlining the function + if (options.optimizationLevel >= 2 && !expr->self) + { + AstExprFunction* func = getFunctionExpr(expr->func); + Function* fi = func ? functions.find(func) : nullptr; + + if (fi && fi->canInline && + tryCompileInlinedCall( + expr, + func, + target, + targetCount, + multRet, + FInt::LuauCompileInlineThreshold, + FInt::LuauCompileInlineThresholdMaxBoost, + FInt::LuauCompileInlineDepth + )) + return; + + // add a debug remark for cases when we didn't even call tryCompileInlinedCall + if (func && !(fi && fi->canInline)) + { + if (func->vararg) + bytecode.addDebugRemark("inlining failed: function is variadic"); + else if (!fi) + bytecode.addDebugRemark("inlining failed: can't inline recursive calls"); + else if (getfenvUsed || setfenvUsed) + bytecode.addDebugRemark("inlining failed: module uses getfenv/setfenv"); + } + } + + RegScope rs(this); + + unsigned int regCount = std::max(unsigned(1 + expr->self + expr->args.size), unsigned(targetCount)); + + // Optimization: if target points to the top of the stack, we can start the call at oldTop - 1 and won't need MOVE at the end + uint8_t regs = targetTop ? allocReg(expr, regCount - targetCount) - targetCount : allocReg(expr, regCount); + + uint8_t selfreg = 0; + + int bfid = -1; + + if (options.optimizationLevel >= 1 && !expr->self) + { + if (const int* id = builtins.find(expr); id && *id != LBF_NONE) + bfid = *id; + } + + if (bfid >= 0 && bytecode.needsDebugRemarks()) + { + Builtin builtin = getBuiltin(expr->func, globals, variables); + bool lastMult = expr->args.size > 0 && isExprMultRet(expr->args.data[expr->args.size - 1]); + + if (builtin.object.value) + bytecode.addDebugRemark("builtin %s.%s/%d%s", builtin.object.value, builtin.method.value, int(expr->args.size), lastMult ? "+" : ""); + else if (builtin.method.value) + bytecode.addDebugRemark("builtin %s/%d%s", builtin.method.value, int(expr->args.size), lastMult ? "+" : ""); + } + + if (bfid == LBF_SELECT_VARARG) + { + // Optimization: compile select(_, ...) as FASTCALL1; the builtin will read variadic arguments directly + // note: for now we restrict this to single-return expressions since our runtime code doesn't deal with general cases + if (multRet == false && targetCount == 1) + return compileExprSelectVararg(expr, target, targetCount, targetTop, multRet, regs); + else + bfid = -1; + } + + // Optimization: for bit32.extract with constant in-range f/w we compile using FASTCALL2K and a special builtin + if (bfid == LBF_BIT32_EXTRACT && expr->args.size == 3 && isConstant(expr->args.data[1]) && isConstant(expr->args.data[2])) + { + Constant fc = getConstant(expr->args.data[1]); + Constant wc = getConstant(expr->args.data[2]); + + int fi = fc.type == Constant::Type_Number ? int(fc.valueNumber) : -1; + int wi = wc.type == Constant::Type_Number ? int(wc.valueNumber) : -1; + + if (fi >= 0 && wi > 0 && fi + wi <= 32) + { + int fwp = fi | ((wi - 1) << 5); + int32_t cid = bytecode.addConstantNumber(fwp); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, LBF_BIT32_EXTRACTK, cid); + } + } + + unsigned maxFastcallArgs = 2; + + // Fastcall with 3 arguments is only used if it can help save one or more move instructions + if (bfid >= 0 && expr->args.size == 3) + { + for (size_t i = 0; i < expr->args.size; ++i) + { + if (int reg = getExprLocalReg(expr->args.data[i]); reg >= 0) + { + maxFastcallArgs = 3; + break; + } + } + } + + // Optimization: for 1/2/3 argument fast calls use specialized opcodes + if (bfid >= 0 && expr->args.size >= 1 && expr->args.size <= maxFastcallArgs) + { + if (!isExprMultRet(expr->args.data[expr->args.size - 1])) + { + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); + } + else if (options.optimizationLevel >= 2) + { + // when a builtin is none-safe with matching arity, even if the last expression returns 0 or >1 arguments, + // we can rely on the behavior of the function being the same (none-safe means nil and none are interchangeable) + BuiltinInfo info = getBuiltinInfo(bfid); + if (int(expr->args.size) == info.params && (info.flags & BuiltinInfo::Flag_NoneSafe) != 0) + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); + } + } + + // Optimization: pcall/xpcall functions have a special fastcall instruction + int fastPcallId = -1; + if (FFlag::LuauCompileFastpcall && options.optimizationLevel >= 1 && !expr->self) + { + if (AstExprGlobal* g = expr->func->as(); g && canImport(g)) + { + if (g->name == "pcall" && expr->args.size >= 1) + fastPcallId = 0; + else if (g->name == "xpcall" && expr->args.size >= 2) + fastPcallId = 1; + } + } + + if (expr->self) + { + AstExprIndexName* fi = expr->func->as(); + LUAU_ASSERT(fi); + + // Optimization: use local register directly in NAMECALL if possible + if (int reg = getExprLocalReg(fi->expr); reg >= 0) + { + selfreg = uint8_t(reg); + } + else + { + // Note: to be able to compile very deeply nested self call chains (obj:method1():method2():...), we need to be able to do this in + // finite stack space NAMECALL will happily move object from regs to regs+1 but we need to compute it into regs so that + // compileExprTempTop doesn't increase stack usage for every recursive call + selfreg = regs; + + compileExprTempTop(fi->expr, selfreg); + } + } + else if (bfid < 0 && (!FFlag::LuauCompileFastpcall || fastPcallId < 0)) + { + compileExprTempTop(expr->func, regs); + } + + bool multCall = false; + + for (size_t i = 0; i < expr->args.size; ++i) + if (i + 1 == expr->args.size) + multCall = compileExprTempMultRet(expr->args.data[i], uint8_t(regs + 1 + expr->self + i)); + else + compileExprTempTop(expr->args.data[i], uint8_t(regs + 1 + expr->self + i)); + + setDebugLineEnd(expr->func); + + if (expr->self) + { + AstExprIndexName* fi = expr->func->as(); + LUAU_ASSERT(fi); + + setDebugLine(fi->indexLocation); + + BytecodeBuilder::StringRef iname = sref(fi->index); + int32_t cid = bytecode.addConstantString(iname); + if (cid < 0) + CompileError::raise(fi->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(LOP_NAMECALL, regs, selfreg, uint8_t(BytecodeBuilder::getStringHash(iname))); + bytecode.emitAux(cid); + + hintTemporaryExprRegType(fi->expr, selfreg, LBC_TYPE_TABLE, /* instLength */ 2); + } + else if (bfid >= 0 || (FFlag::LuauCompileFastpcall && fastPcallId >= 0)) + { + size_t fastcallLabel = bytecode.emitLabel(); + + if (FFlag::LuauCompileFastpcall && fastPcallId >= 0) + { + uint8_t explicitArgs = uint8_t(expr->args.size - (multCall ? 1 : 0)); + bytecode.emitABC(LOP_FASTPCALL, uint8_t(fastPcallId), explicitArgs, 0); + } + else + { + bytecode.emitABC(LOP_FASTCALL, uint8_t(bfid), 0, 0); + } + + // note, these instructions are normally not executed and are used as a fallback for FASTCALL + // we can't use TempTop variant here because we need to make sure the arguments we already computed aren't overwritten + compileExprTemp(expr->func, regs); + + size_t callLabel = bytecode.emitLabel(); + + // FASTCALL will skip over the instructions needed to compute function and jump over CALL which must immediately follow the instruction + // sequence after FASTCALL + if (!bytecode.patchSkipC(fastcallLabel, callLabel)) + CompileError::raise(expr->func->location, "Exceeded jump distance limit; simplify the code to compile"); + } + + // Without deoptimization we cannot break VARARG sequences. + // So VARARG producer or consumer cannot be inlined, because it creates a diamond(with slow path). + bool canInline = currentFunction->functionDepth != 0 && !multCall && !multRet; + if (FFlag::LuauEmitCallFeedback && (bfid < 0 && (!FFlag::LuauCompileFastpcall || fastPcallId < 0)) && canInline) + { + uint32_t fbSlot = FFlag::LuauCompileRefactorFeedback ? bytecode.addCallTargetSlot(uint32_t(bytecode.getInstructionCount())) + : bytecode.addFbSlot_DEPRECATED(LuauFeedbackType::LFT_CALLTARGET); + bytecode.emitABC(LOP_CALLFB, regs, multCall ? 0 : uint8_t(expr->self + expr->args.size + 1), multRet ? 0 : uint8_t(targetCount + 1)); + bytecode.emitAux(fbSlot); + } + else + { + bytecode.emitABC(LOP_CALL, regs, multCall ? 0 : uint8_t(expr->self + expr->args.size + 1), multRet ? 0 : uint8_t(targetCount + 1)); + } + + // if we didn't output results directly to target, we need to move them + if (!targetTop) + { + for (size_t i = 0; i < targetCount; ++i) + bytecode.emitABC(LOP_MOVE, uint8_t(target + i), uint8_t(regs + i), 0); + } + } + + bool shouldShareClosure(AstExprFunction* func) + { + const Function* f = functions.find(func); + if (!f) + return false; + + for (AstLocal* uv : f->upvals) + { + Variable* ul = variables.find(uv); + + if (!ul) + return false; + + if (ul->written) + return false; + + // it's technically safe to share closures whenever all upvalues are immutable + // this is because of a runtime equality check in DUPCLOSURE. + // however, this results in frequent de-optimization and increases the set of reachable objects, making some temporary objects permanent + // instead we apply a heuristic: we share closures if they refer to top-level upvalues, or closures that refer to top-level upvalues + // this will only de-optimize (outside of fenv changes) if top level code is executed twice with different results. + if (uv->functionDepth != 0 || uv->loopDepth != 0) + { + AstExprFunction* uf = ul->init ? ul->init->as() : nullptr; + if (!uf) + return false; + + if (uf != func && !shouldShareClosure(uf)) + return false; + } + } + + return true; + } + + void compileExprFunction(AstExprFunction* expr, uint8_t target) + { + RegScope rs(this); + + const Function* f = functions.find(expr); + LUAU_ASSERT(f); + + // when the closure has upvalues we'll use this to create the closure at runtime + // when the closure has no upvalues, we use constant closures that technically don't rely on the child function list + // however, it's still important to add the child function because debugger relies on the function hierarchy when setting breakpoints + int16_t pid = bytecode.addChildFunction(f->id); + if (pid < 0) + CompileError::raise(expr->location, "Exceeded closure limit; simplify the code to compile"); + + // we use a scratch vector to reduce allocations; this is safe since compileExprFunction is not reentrant + captures.clear(); + captures.reserve(f->upvals.size()); + + for (AstLocal* uv : f->upvals) + { + LUAU_ASSERT(uv->functionDepth < expr->functionDepth); + + if (int reg = getLocalReg(uv); reg >= 0) + { + // note: we can't check if uv is an upvalue in the current frame because inlining can migrate from upvalues to locals + Variable* ul = variables.find(uv); + bool immutable = !ul || !ul->written; + + captures.push_back({immutable ? LCT_VAL : LCT_REF, uint8_t(reg)}); + } + else if (const Constant* uc = locstants.find(uv); uc && uc->type != Constant::Type_Unknown) + { + // inlining can result in an upvalue capture of a constant, in which case we can't capture without a temporary register + uint8_t reg = allocReg(expr, 1u); + compileExprConstant(expr, uc, reg); + + captures.push_back({LCT_VAL, reg}); + } + else + { + LUAU_ASSERT(uv->functionDepth < expr->functionDepth - 1); + + // get upvalue from parent frame + // note: this will add uv to the current upvalue list if necessary + uint8_t uid = getUpval(uv); + + captures.push_back({LCT_UPVAL, uid}); + } + } + + // Optimization: when closure has no upvalues, or upvalues are safe to share, instead of allocating it every time we can share closure + // objects (this breaks assumptions about function identity which can lead to setfenv not working as expected, so we disable this when it + // is used) + int16_t shared = -1; + + if (options.optimizationLevel >= 1 && shouldShareClosure(expr) && !setfenvUsed) + { + int32_t cid = bytecode.addConstantClosure(f->id); + + if (cid >= 0 && cid < 32768) + shared = int16_t(cid); + } + + if (shared < 0) + bytecode.addDebugRemark("allocation: closure with %d upvalues", int(captures.size())); + + if (shared >= 0) + bytecode.emitAD(LOP_DUPCLOSURE, target, shared); + else + bytecode.emitAD(LOP_NEWCLOSURE, target, pid); + + for (const Capture& c : captures) + { + bytecode.emitABC(LOP_CAPTURE, uint8_t(c.type), c.data, 0); + } + } + + void compileClassDeclaration(AstStatClass* decl) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + // CLI-194693: We probably need to add something here to prevent: + // + // class Foobar + // public foobar + // function foobar() end + // end + // + // ... properties and methods need to share a namespace. + + AstLocal** classLocal = classLocals.find(decl->name->name); + LUAU_ASSERT(classLocal); + int destReg = getLocalReg(*classLocal); + LUAU_ASSERT(destReg >= 0); + uint8_t dest = uint8_t(destReg); + + if (FFlag::LuauExportValueSyntax && decl->exported) + { + // we want to eagerly insert into the exported classes map, as the class may be referenced by one of its methods + ensureExportTable(decl); + exports.exportedClasses[decl->name] = dest; + } + + RegScope _(this); + + // The bottom bit of the C slot indicates whether the class is open. + uint8_t instrCVal = decl->open ? 1u : 0u; + + if (decl->super) + { + // If the superclass is already in a local register, we can reference it directly + int superReg = getExprLocalReg(decl->super); + uint8_t superDest = allocReg(decl, decl->super && superReg < 0 ? 1u : 0u); + + if (superReg >= 0) + bytecode.emitABC(LOP_NEWCLASS, dest, uint8_t(superReg), instrCVal); + else + { + compileExpr(decl->super, superDest); + bytecode.emitABC(LOP_NEWCLASS, dest, superDest, instrCVal); + } + } + else // The range of valid registers is 0-254, so we use 0xFF (255) to indicate the absence of a superclass. + bytecode.emitABC(LOP_NEWCLASS, dest, kInvalidReg, instrCVal); + + // We want to load the class constant up front, but in order to load + // the class constant we need to build it first. To avoid a second + // pass, we start by emitting the LOADKX bytecode and a dummy + // constant (0xDEADBEEF), which we will patch later once we + // have added the class constant to the constant table. + size_t auxOffset = bytecode.emitLabel(); + bytecode.emitAux(0xDEADBEEF); + + BytecodeBuilder::ClassShape shape; + shape.className = bytecode.addConstantString(sref(decl->name->name)); + checkConstant(shape.className, decl->name->location); + + // We use this as temporary storage while we make all of the closures + // associated with this particular class. Another option would be to + // refactor class construction to be more like a function call and take + // N registers. + auto temp = allocReg(decl, 1u); + + bool hasExplicitConstructor = false; + + for (const auto& member : decl->members) + { + Luau::visit( + overloaded{ + [&](const AstClassProperty& prop) + { + // Properties we only need to store the name, for now. + int propNameCid = bytecode.addConstantString(sref(prop.name)); + checkConstant(propNameCid, prop.nameLocation); + shape.propertyNames.emplace_back(propNameCid); + }, + [&](const AstClassMethod& method) + { + // For a method: + // + // function foobar(a, b, c) + // end + // + // ... in a class declaration, we compile it as if it were + // a free floating function, but instead of assigning it to + // a local or a global, we use `NEWCLASSMEMBER` to add it to + // our class definition. + compileExprFunction(method.function, temp); + int methodNameCid = bytecode.addConstantString(sref(method.functionName)); + checkConstant(methodNameCid, method.function->location); + shape.methodNames.emplace_back(methodNameCid); + bytecode.emitABC(LOP_NEWCLASSMEMBER, dest, 0, temp); + bytecode.emitAux(methodNameCid); + + if (method.functionName == "__init") + hasExplicitConstructor = true; + } + }, + member + ); + } + + // All classes have `__init` methods. + if (!hasExplicitConstructor) + { + int initCid = bytecode.addConstantString(sref(names.getOrAdd("__init"))); + checkConstant(initCid, decl->location); + shape.methodNames.emplace_back(initCid); + } + + // Finally, we create the class constant and patch the AUX slot + // from before. + int32_t classConst = bytecode.addClassShape(std::move(shape)); + checkConstant(classConst, decl->location); + bytecode.patchAux(auxOffset, classConst); + } + + LuauOpcode getUnaryOp(AstExprUnary::Op op) + { + switch (op) + { + case AstExprUnary::Op::Not: + return LOP_NOT; + + case AstExprUnary::Op::Minus: + return LOP_MINUS; + + case AstExprUnary::Op::Len: + return LOP_LENGTH; + + default: + LUAU_ASSERT(!"Unexpected unary operation"); + return LOP_NOP; + } + } + + LuauOpcode getBinaryOpArith(AstExprBinary::Op op, bool k = false) + { + switch (op) + { + case AstExprBinary::Add: + return k ? LOP_ADDK : LOP_ADD; + + case AstExprBinary::Sub: + return k ? LOP_SUBK : LOP_SUB; + + case AstExprBinary::Mul: + return k ? LOP_MULK : LOP_MUL; + + case AstExprBinary::Div: + return k ? LOP_DIVK : LOP_DIV; + + case AstExprBinary::FloorDiv: + return k ? LOP_IDIVK : LOP_IDIV; + + case AstExprBinary::Mod: + return k ? LOP_MODK : LOP_MOD; + + case AstExprBinary::Pow: + return k ? LOP_POWK : LOP_POW; + + default: + LUAU_ASSERT(!"Unexpected binary operation"); + return LOP_NOP; + } + } + + LuauOpcode getJumpOpCompare(AstExprBinary::Op op, bool not_ = false) + { + switch (op) + { + case AstExprBinary::CompareNe: + return not_ ? LOP_JUMPIFEQ : LOP_JUMPIFNOTEQ; + + case AstExprBinary::CompareEq: + return not_ ? LOP_JUMPIFNOTEQ : LOP_JUMPIFEQ; + + case AstExprBinary::CompareLt: + case AstExprBinary::CompareGt: + return not_ ? LOP_JUMPIFNOTLT : LOP_JUMPIFLT; + + case AstExprBinary::CompareLe: + case AstExprBinary::CompareGe: + return not_ ? LOP_JUMPIFNOTLE : LOP_JUMPIFLE; + + default: + LUAU_ASSERT(!"Unexpected binary operation"); + return LOP_NOP; + } + } + + bool isConstant(AstExpr* node) + { + const Constant* cv = constants.find(node); + + return (cv != nullptr) && cv->type != Constant::Type_Unknown; + } + + bool isConstantTrue(AstExpr* node) + { + return Compile::isConstantTrue(constants, node); + } + + bool isConstantFalse(AstExpr* node) + { + return Compile::isConstantFalse(constants, node); + } + + bool isConstantVector(AstExpr* node) + { + const Constant* cv = constants.find(node); + + return (cv != nullptr) && (cv->type == Constant::Type_Vectorf || cv->type == Constant::Type_Vectord); + } + + bool isConstantInteger(AstExpr* node) + { + const Constant* cv = constants.find(node); + + return cv && cv->type == Constant::Type_Integer; + } + + Constant getConstant(AstExpr* node) + { + const Constant* cv = constants.find(node); + + return cv ? *cv : Constant{Constant::Type_Unknown}; + } + + size_t compileCompareJump(AstExprBinary* expr, bool not_ = false) + { + RegScope rs(this); + + bool isEq = (expr->op == AstExprBinary::CompareEq || expr->op == AstExprBinary::CompareNe); + AstExpr* left = expr->left; + AstExpr* right = expr->right; + + bool operandIsConstant = isConstant(right); + if (isEq && !operandIsConstant) + { + operandIsConstant = isConstant(left); + if (operandIsConstant) + std::swap(left, right); + } + + // disable fast path for vectors and integers because supporting it would require a new opcode + if (operandIsConstant && (isConstantVector(right) || (FFlag::LuauIntegerType2 && isConstantInteger(right)))) + operandIsConstant = false; + + uint8_t rl = compileExprAuto(left, rs); + + if (isEq && operandIsConstant) + { + const Constant* cv = constants.find(right); + LUAU_ASSERT(cv && cv->type != Constant::Type_Unknown); + + LuauOpcode opc = LOP_NOP; + int32_t cid = -1; + uint32_t flip = (expr->op == AstExprBinary::CompareEq) == not_ ? 0x80000000 : 0; + + switch (cv->type) + { + case Constant::Type_Nil: + opc = LOP_JUMPXEQKNIL; + cid = 0; + break; + + case Constant::Type_Boolean: + opc = LOP_JUMPXEQKB; + cid = cv->valueBoolean; + break; + + case Constant::Type_Number: + opc = LOP_JUMPXEQKN; + cid = getConstantIndex(right); + break; + + case Constant::Type_String: + opc = LOP_JUMPXEQKS; + cid = getConstantIndex(right); + break; + + default: + LUAU_ASSERT(!"Unexpected constant type"); + } + + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + size_t jumpLabel = bytecode.emitLabel(); + + bytecode.emitAD(opc, rl, 0); + bytecode.emitAux(cid | flip); + + return jumpLabel; + } + else + { + LuauOpcode opc = getJumpOpCompare(expr->op, not_); + + uint8_t rr = compileExprAuto(right, rs); + + size_t jumpLabel = bytecode.emitLabel(); + + if (expr->op == AstExprBinary::CompareGt || expr->op == AstExprBinary::CompareGe) + { + bytecode.emitAD(opc, rr, 0); + bytecode.emitAux(rl); + } + else + { + bytecode.emitAD(opc, rl, 0); + bytecode.emitAux(rr); + } + + return jumpLabel; + } + } + + int32_t getConstantNumber(AstExpr* node) + { + const Constant* c = constants.find(node); + + if (c && c->type == Constant::Type_Number) + { + int cid = bytecode.addConstantNumber(c->valueNumber); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + return cid; + } + + return -1; + } + + int32_t getConstantIndex(AstExpr* node) + { + const Constant* c = constants.find(node); + + if (!c || c->type == Constant::Type_Unknown) + return -1; + + int cid = -1; + + switch (c->type) + { + case Constant::Type_Nil: + cid = bytecode.addConstantNil(); + break; + + case Constant::Type_Boolean: + cid = bytecode.addConstantBoolean(c->valueBoolean); + break; + + case Constant::Type_Number: + cid = bytecode.addConstantNumber(c->valueNumber); + break; + + case Constant::Type_Integer: + cid = bytecode.addConstantInteger(c->valueInteger64); + break; + + case Constant::Type_Vectorf: + cid = bytecode.addConstantVectorf(c->valueVectorf[0], c->valueVectorf[1], c->valueVectorf[2], c->valueVectorf[3]); + break; + + case Constant::Type_Vectord: + cid = bytecode.addConstantVectord(c->valueVectord[0], c->valueVectord[1], c->valueVectord[2], c->valueVectord[3]); + break; + + case Constant::Type_String: + cid = bytecode.addConstantString(sref(c->getString())); + break; + + default: + LUAU_ASSERT(!"Unexpected constant type"); + return -1; + } + + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + return cid; + } + + // compile expr to target temp register + // if the expr (or not expr if onlyTruth is false) is truthy, jump via skipJump + // if the expr (or not expr if onlyTruth is false) is falsy, fall through (target isn't guaranteed to be updated in this case) + // if target is omitted, then the jump behavior is the same - skipJump or fallthrough depending on the truthiness of the expression + void compileConditionValue(AstExpr* node, const uint8_t* target, std::vector& skipJump, bool onlyTruth) + { + // Optimization: we don't need to compute constant values + if (const Constant* cv = constants.find(node); cv && cv->type != Constant::Type_Unknown) + { + // note that we only need to compute the value if it's truthy; otherwise we cal fall through + if (cv->isTruthful() == onlyTruth) + { + if (target) + compileExprTemp(node, *target); + + skipJump.push_back(bytecode.emitLabel()); + bytecode.emitAD(LOP_JUMP, 0, 0); + } + return; + } + + if (AstExprBinary* expr = node->as()) + { + switch (expr->op) + { + case AstExprBinary::And: + case AstExprBinary::Or: + { + // disambiguation: there's 4 cases (we only need truthy or falsy results based on onlyTruth) + // onlyTruth = 1: a and b transforms to a ? b : dontcare + // onlyTruth = 1: a or b transforms to a ? a : b + // onlyTruth = 0: a and b transforms to !a ? a : b + // onlyTruth = 0: a or b transforms to !a ? b : dontcare + if (onlyTruth == (expr->op == AstExprBinary::And)) + { + // we need to compile the left hand side, and skip to "dontcare" (aka fallthrough of the entire statement) if it's not the same as + // onlyTruth if it's the same then the result of the expression is the right hand side because of this, we *never* care about the + // result of the left hand side + std::vector elseJump; + compileConditionValue(expr->left, nullptr, elseJump, !onlyTruth); + + // fallthrough indicates that we need to compute & return the right hand side + // we use compileConditionValue again to process any extra and/or statements directly + compileConditionValue(expr->right, target, skipJump, onlyTruth); + + size_t elseLabel = bytecode.emitLabel(); + + patchJumps(expr, elseJump, elseLabel); + } + else + { + // we need to compute the left hand side first; note that we will jump to skipJump if we know the answer + compileConditionValue(expr->left, target, skipJump, onlyTruth); + + // we will fall through if computing the left hand didn't give us an "interesting" result + // we still use compileConditionValue to recursively optimize any and/or/compare statements + compileConditionValue(expr->right, target, skipJump, onlyTruth); + } + return; + } + break; + + case AstExprBinary::CompareNe: + case AstExprBinary::CompareEq: + case AstExprBinary::CompareLt: + case AstExprBinary::CompareLe: + case AstExprBinary::CompareGt: + case AstExprBinary::CompareGe: + { + if (target) + { + // since target is a temp register, we'll initialize it to 1, and then jump if the comparison is true + // if the comparison is false, we'll fallthrough and target will still be 1 but target has unspecified value for falsy results + // when we only care about falsy values instead of truthy values, the process is the same but with flipped conditionals + bytecode.emitABC(LOP_LOADB, *target, onlyTruth ? 1 : 0, 0); + } + + size_t jumpLabel = compileCompareJump(expr, /* not= */ !onlyTruth); + + skipJump.push_back(jumpLabel); + return; + } + break; + + // fall-through to default path below + default:; + } + } + + if (AstExprUnary* expr = node->as()) + { + // if we *do* need to compute the target, we'd have to inject "not" ops on every return path + // this is possible but cumbersome; so for now we only optimize not expression when we *don't* need the value + if (!target && expr->op == AstExprUnary::Op::Not) + { + compileConditionValue(expr->expr, target, skipJump, !onlyTruth); + return; + } + } + + if (AstExprGroup* expr = node->as()) + { + compileConditionValue(expr->expr, target, skipJump, onlyTruth); + return; + } + + RegScope rs(this); + uint8_t reg; + + if (target) + { + reg = *target; + compileExprTemp(node, reg); + } + else + { + reg = compileExprAuto(node, rs); + } + + skipJump.push_back(bytecode.emitLabel()); + bytecode.emitAD(onlyTruth ? LOP_JUMPIF : LOP_JUMPIFNOT, reg, 0); + } + + // checks if compiling the expression as a condition value generates code that's faster than using compileExpr + bool isConditionFast(AstExpr* node) + { + const Constant* cv = constants.find(node); + + if (cv && cv->type != Constant::Type_Unknown) + return true; + + if (AstExprBinary* expr = node->as()) + { + switch (expr->op) + { + case AstExprBinary::And: + case AstExprBinary::Or: + return true; + + case AstExprBinary::CompareNe: + case AstExprBinary::CompareEq: + case AstExprBinary::CompareLt: + case AstExprBinary::CompareLe: + case AstExprBinary::CompareGt: + case AstExprBinary::CompareGe: + return true; + + default: + return false; + } + } + + if (AstExprGroup* expr = node->as()) + return isConditionFast(expr->expr); + + return false; + } + + void compileExprAndOr(AstExprBinary* expr, uint8_t target, bool targetTemp) + { + bool and_ = (expr->op == AstExprBinary::And); + + RegScope rs(this); + + // Optimization: when left hand side is a constant, we can emit left hand side or right hand side + if (const Constant* cl = constants.find(expr->left); cl && cl->type != Constant::Type_Unknown) + { + compileExpr(and_ == cl->isTruthful() ? expr->right : expr->left, target, targetTemp); + return; + } + + // Note: two optimizations below can lead to inefficient codegen when the left hand side is a condition + if (!isConditionFast(expr->left)) + { + // Optimization: when right hand side is a local variable, we can use AND/OR + if (int reg = getExprLocalReg(expr->right); reg >= 0) + { + uint8_t lr = compileExprAuto(expr->left, rs); + uint8_t rr = uint8_t(reg); + + bytecode.emitABC(and_ ? LOP_AND : LOP_OR, target, lr, rr); + return; + } + + // Optimization: when right hand side is a constant, we can use ANDK/ORK + int32_t cid = getConstantIndex(expr->right); + + if (cid >= 0 && cid <= 255) + { + uint8_t lr = compileExprAuto(expr->left, rs); + + bytecode.emitABC(and_ ? LOP_ANDK : LOP_ORK, target, lr, uint8_t(cid)); + return; + } + } + + // Optimization: if target is a temp register, we can clobber it which allows us to compute the result directly into it + // If it's not a temp register, then something like `a = a > 1 or a + 2` may clobber `a` while evaluating left hand side, and `a+2` will break + uint8_t reg = targetTemp ? target : allocReg(expr, 1u); + + std::vector skipJump; + compileConditionValue(expr->left, ®, skipJump, /* onlyTruth= */ !and_); + + compileExprTemp(expr->right, reg); + + size_t moveLabel = bytecode.emitLabel(); + + patchJumps(expr, skipJump, moveLabel); + + if (target != reg) + bytecode.emitABC(LOP_MOVE, target, reg, 0); + } + + void compileExprUnary(AstExprUnary* expr, uint8_t target) + { + RegScope rs(this); + + // Special case for integer constants, like -1000000000i + AstExprConstantInteger* cint = expr->expr->as(); + if (FFlag::LuauIntegerType2 && (expr->op == AstExprUnary::Op::Minus) && (cint != nullptr)) + { + int32_t cid = bytecode.addConstantInteger((int64_t)(~(uint64_t)cint->value + 1)); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + return; + } + + uint8_t re = compileExprAuto(expr->expr, rs); + + bytecode.emitABC(getUnaryOp(expr->op), target, re, 0); + } + + static void unrollConcats(std::vector& args) + { + for (;;) + { + AstExprBinary* be = args.back()->as(); + + if (!be || be->op != AstExprBinary::Concat) + break; + + args.back() = be->left; + args.push_back(be->right); + } + } + + void compileExprBinary(AstExprBinary* expr, uint8_t target, bool targetTemp) + { + RegScope rs(this); + + switch (expr->op) + { + case AstExprBinary::Add: + case AstExprBinary::Sub: + case AstExprBinary::Mul: + case AstExprBinary::Div: + case AstExprBinary::FloorDiv: + case AstExprBinary::Mod: + case AstExprBinary::Pow: + { + int32_t rc = getConstantNumber(expr->right); + + if (rc >= 0 && rc <= 255) + { + uint8_t rl = compileExprAuto(expr->left, rs); + + bytecode.emitABC(getBinaryOpArith(expr->op, /* k= */ true), target, rl, uint8_t(rc)); + + hintTemporaryExprRegType(expr->left, rl, LBC_TYPE_NUMBER, /* instLength */ 1); + } + else + { + if (expr->op == AstExprBinary::Sub || expr->op == AstExprBinary::Div) + { + int32_t lc = getConstantNumber(expr->left); + + if (lc >= 0 && lc <= 255) + { + uint8_t rr = compileExprAuto(expr->right, rs); + LuauOpcode op = (expr->op == AstExprBinary::Sub) ? LOP_SUBRK : LOP_DIVRK; + + bytecode.emitABC(op, target, uint8_t(lc), uint8_t(rr)); + + hintTemporaryExprRegType(expr->right, rr, LBC_TYPE_NUMBER, /* instLength */ 1); + return; + } + } + else if (options.optimizationLevel >= 2 && (expr->op == AstExprBinary::Add || expr->op == AstExprBinary::Mul)) + { + // Optimization: replace k*r with r*k when r is known to be a number (otherwise metamethods may be called) + if (LuauBytecodeType* ty = exprTypes.find(expr)) + { + // Note: for vectors, it only makes sense to do for a multiplication as number+vector is an error + if (*ty == LBC_TYPE_NUMBER || (*ty == LBC_TYPE_VECTOR && expr->op == AstExprBinary::Mul)) + { + int32_t lc = getConstantNumber(expr->left); + + if (lc >= 0 && lc <= 255) + { + uint8_t rr = compileExprAuto(expr->right, rs); + + bytecode.emitABC(getBinaryOpArith(expr->op, /* k= */ true), target, rr, uint8_t(lc)); + + hintTemporaryExprRegType(expr->right, rr, LBC_TYPE_NUMBER, /* instLength */ 1); + return; + } + } + } + } + + uint8_t rl = compileExprAuto(expr->left, rs); + uint8_t rr = compileExprAuto(expr->right, rs); + + bytecode.emitABC(getBinaryOpArith(expr->op), target, rl, rr); + + hintTemporaryExprRegType(expr->left, rl, LBC_TYPE_NUMBER, /* instLength */ 1); + hintTemporaryExprRegType(expr->right, rr, LBC_TYPE_NUMBER, /* instLength */ 1); + } + } + break; + + case AstExprBinary::Concat: + { + std::vector args = {expr->left, expr->right}; + + // unroll the tree of concats down the right hand side to be able to do multiple ops + unrollConcats(args); + + uint8_t regs = allocReg(expr, unsigned(args.size())); + + for (size_t i = 0; i < args.size(); ++i) + if (FFlag::LuauCompileConcatTargetTop) + compileExprTempTop(args[i], uint8_t(regs + i)); + else + compileExprTemp(args[i], uint8_t(regs + i)); + + bytecode.emitABC(LOP_CONCAT, target, regs, uint8_t(regs + args.size() - 1)); + } + break; + + case AstExprBinary::CompareNe: + case AstExprBinary::CompareEq: + case AstExprBinary::CompareLt: + case AstExprBinary::CompareLe: + case AstExprBinary::CompareGt: + case AstExprBinary::CompareGe: + { + size_t jumpLabel = compileCompareJump(expr); + + // note: this skips over the next LOADB instruction because of "1" in the C slot + bytecode.emitABC(LOP_LOADB, target, 0, 1); + + size_t thenLabel = bytecode.emitLabel(); + + bytecode.emitABC(LOP_LOADB, target, 1, 0); + + patchJump(expr, jumpLabel, thenLabel); + } + break; + + case AstExprBinary::And: + case AstExprBinary::Or: + { + compileExprAndOr(expr, target, targetTemp); + } + break; + + default: + LUAU_ASSERT(!"Unexpected binary operation"); + } + } + + void compileExprIfElseAndOr(bool and_, uint8_t creg, AstExpr* other, uint8_t target) + { + int32_t cid = getConstantIndex(other); + + if (cid >= 0 && cid <= 255) + { + bytecode.emitABC(and_ ? LOP_ANDK : LOP_ORK, target, creg, uint8_t(cid)); + } + else + { + RegScope rs(this); + uint8_t oreg = compileExprAuto(other, rs); + + bytecode.emitABC(and_ ? LOP_AND : LOP_OR, target, creg, oreg); + } + } + + void compileExprIfElseLocal(AstExprIfElse* expr, uint8_t target, bool targetTemp) + { + LUAU_ASSERT(expr->conditionLocal); + + bool skipElse = isConstantTrue(expr->condition); + size_t oldLocals = localStack.size(); + size_t elseJump = 0; + + { + RegScope rs(this); + + uint8_t reg = allocReg(expr, 1u); + uint32_t allocpc = bytecode.getDebugPC(); + + compileExprTemp(expr->condition, reg); + pushLocal(expr->conditionLocal, reg, allocpc); + + if (!skipElse) + { + elseJump = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMPIFNOT, reg, 0); + } + + compileExpr(expr->trueExpr, target, targetTemp); + + closeLocals(oldLocals); + popLocals(oldLocals); + } + + if (skipElse) + return; + + // Jump over else expression evaluation + size_t thenLabel = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMP, 0, 0); + + size_t elseLabel = bytecode.emitLabel(); + compileExpr(expr->falseExpr, target, targetTemp); + size_t endLabel = bytecode.emitLabel(); + + patchJump(expr, elseJump, elseLabel); + patchJump(expr, thenLabel, endLabel); + } + + void compileExprIfElse(AstExprIfElse* expr, uint8_t target, bool targetTemp) + { + if (FFlag::LuauExperimentalIfLocalSyntax && expr->conditionLocal) + { + compileExprIfElseLocal(expr, target, targetTemp); + return; + } + + if (isConstant(expr->condition)) + { + if (isConstantTrue(expr->condition)) + { + compileExpr(expr->trueExpr, target, targetTemp); + } + else + { + compileExpr(expr->falseExpr, target, targetTemp); + } + } + else + { + // Optimization: convert some if..then..else expressions into and/or when the other side has no side effects and is very cheap to compute + // if v then v else e => v or e + // if v then e else v => v and e + if (int creg = getExprLocalReg(expr->condition); creg >= 0) + { + if (creg == getExprLocalReg(expr->trueExpr) && (getExprLocalReg(expr->falseExpr) >= 0 || isConstant(expr->falseExpr))) + return compileExprIfElseAndOr(/* and_= */ false, uint8_t(creg), expr->falseExpr, target); + else if (creg == getExprLocalReg(expr->falseExpr) && (getExprLocalReg(expr->trueExpr) >= 0 || isConstant(expr->trueExpr))) + return compileExprIfElseAndOr(/* and_= */ true, uint8_t(creg), expr->trueExpr, target); + } + + std::vector elseJump; + compileConditionValue(expr->condition, nullptr, elseJump, false); + compileExpr(expr->trueExpr, target, targetTemp); + + // Jump over else expression evaluation + size_t thenLabel = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMP, 0, 0); + + size_t elseLabel = bytecode.emitLabel(); + compileExpr(expr->falseExpr, target, targetTemp); + size_t endLabel = bytecode.emitLabel(); + + patchJumps(expr, elseJump, elseLabel); + patchJump(expr, thenLabel, endLabel); + } + } + + void compileExprInterpString(AstExprInterpString* expr, uint8_t target, bool targetTemp) + { + size_t formatCapacity = 0; + for (AstArray string : expr->strings) + { + formatCapacity += string.size + std::count(string.data, string.data + string.size, '%'); + } + + size_t skippedSubExpr = 0; + for (size_t index = 0; index < expr->expressions.size; ++index) + { + const Constant* c = constants.find(expr->expressions.data[index]); + if (c && c->type == Constant::Type::Type_String) + { + formatCapacity += c->stringLength + std::count(c->valueString, c->valueString + c->stringLength, '%'); + skippedSubExpr++; + } + else + formatCapacity += 2; // "%*" + } + + std::string formatString; + formatString.reserve(formatCapacity); + + LUAU_ASSERT(expr->strings.size == expr->expressions.size + 1); + for (size_t idx = 0; idx < expr->strings.size; idx++) + { + AstArray string = expr->strings.data[idx]; + escapeAndAppend(formatString, string.data, string.size); + + if (idx < expr->expressions.size) + { + const Constant* c = constants.find(expr->expressions.data[idx]); + if (c && c->type == Constant::Type::Type_String) + escapeAndAppend(formatString, c->valueString, c->stringLength); + else + formatString += "%*"; + } + } + + int32_t formatStringIndex = -1; + + if (formatString.empty()) + { + formatStringIndex = bytecode.addConstantString({"", 0}); + } + else + { + AstName interned = names.getOrAdd(formatString.c_str(), formatString.size()); + AstArray formatStringArray{interned.value, formatString.size()}; + formatStringIndex = bytecode.addConstantString(sref(formatStringArray)); + } + + if (formatStringIndex < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + RegScope rs(this); + + unsigned int regCount = unsigned(2 + expr->expressions.size - skippedSubExpr); + + // Optimization: have the format call place the result directly into the target to avoid an extra MOVE + bool targetTop = targetTemp && target == regTop - 1; + uint8_t baseReg = targetTop ? allocReg(expr, regCount - 1) - 1 : allocReg(expr, regCount); + + emitLoadK(baseReg, formatStringIndex); + + size_t skipped = 0; + for (size_t index = 0; index < expr->expressions.size; ++index) + { + AstExpr* subExpr = expr->expressions.data[index]; + const Constant* c = constants.find(subExpr); + if (!c || c->type != Constant::Type::Type_String) + compileExprTempTop(subExpr, uint8_t(baseReg + 2 + index - skipped)); + else + skipped++; + } + + BytecodeBuilder::StringRef formatMethod = sref(AstName("format")); + + int32_t formatMethodIndex = bytecode.addConstantString(formatMethod); + if (formatMethodIndex < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(LOP_NAMECALL, baseReg, baseReg, uint8_t(BytecodeBuilder::getStringHash(formatMethod))); + bytecode.emitAux(formatMethodIndex); + bytecode.emitABC(LOP_CALL, baseReg, uint8_t(expr->expressions.size + 2 - skippedSubExpr), 2); + if (target != baseReg) + bytecode.emitABC(LOP_MOVE, target, baseReg, 0); + } + + static uint8_t encodeHashSize(unsigned int hashSize) + { + size_t hashSizeLog2 = 0; + while ((1u << hashSizeLog2) < hashSize) + hashSizeLog2++; + + return hashSize == 0 ? 0 : uint8_t(hashSizeLog2 + 1); + } + + void compileExprTable(AstExprTable* expr, uint8_t target, bool targetTemp) + { + // Optimization: if the table is empty, we can compute it directly into the target + if (expr->items.size == 0) + { + TableShape shape = tableShapes[expr]; + + bytecode.addDebugRemark("allocation: table hash %d", shape.hashSize); + + bytecode.emitABC(LOP_NEWTABLE, target, encodeHashSize(shape.hashSize), 0); + bytecode.emitAux(shape.arraySize); + return; + } + + unsigned int arraySize = 0; + unsigned int hashSize = 0; + unsigned int recordSize = 0; + unsigned int indexSize = 0; + + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + + arraySize += (item.kind == AstExprTable::Item::Kind::List); + hashSize += (item.kind != AstExprTable::Item::Kind::List); + recordSize += (item.kind == AstExprTable::Item::Kind::Record); + } + + // Optimization: allocate sequential explicitly specified numeric indices ([1]) as arrays + if (arraySize == 0 && hashSize > 0) + { + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + LUAU_ASSERT(item.key); // no list portion => all items have keys + + const Constant* ckey = constants.find(item.key); + + indexSize += (ckey && ckey->type == Constant::Type_Number && ckey->valueNumber == double(indexSize + 1)); + } + + // we only perform the optimization if we don't have any other []-keys + // technically it's "safe" to do this even if we have other keys, but doing so changes iteration order and may break existing code + if (hashSize == recordSize + indexSize) + hashSize = recordSize; + else + indexSize = 0; + } + + int encodedHashSize = encodeHashSize(hashSize); + + RegScope rs(this); + + // Optimization: if target is a temp register, we can clobber it which allows us to compute the result directly into it + uint8_t reg = targetTemp ? target : allocReg(expr, 1u); + + // flattening operation where we only load the last element + // this optimizes for tables like: { data = 43, data = "true", data = 9 } + // this does not optimize for tables such as: { data = 43, data = function() end, data = 9} + // in this case, we know that data = 9 should be the element, so we can just skip the rest + InsertionOrderedMap lastKeyVal; + // Optimization: when all items are record fields, use template tables to compile expression + if (arraySize == 0 && indexSize == 0 && hashSize == recordSize && recordSize >= 1 && recordSize <= BytecodeBuilder::TableShape::kMaxLength) + { + BytecodeBuilder::TableShape shape; + + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + LUAU_ASSERT(item.kind == AstExprTable::Item::Kind::Record); + + AstExprConstantString* ckey = item.key->as(); + LUAU_ASSERT(ckey); + + int keyCid = bytecode.addConstantString(sref(ckey->value)); + if (keyCid < 0) + CompileError::raise(ckey->location, "Exceeded constant limit; simplify the code to compile"); + + int32_t valueCid = getConstantIndex(item.value); + if (lastKeyVal.contains(keyCid) && lastKeyVal[keyCid] == -1) + continue; + + lastKeyVal[keyCid] = valueCid; + } + + for (auto& [keyCid, valueCid] : lastKeyVal) + { + LUAU_ASSERT(shape.length < BytecodeBuilder::TableShape::kMaxLength); + + size_t idx = shape.length; + shape.keys[idx] = keyCid; + + shape.constants[idx] = valueCid; + if (valueCid >= 0) + { + shape.hasConstants = true; + } + + shape.length++; + } + + int32_t tid = bytecode.addConstantTable(shape); + if (tid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.addDebugRemark("allocation: table template %d", hashSize); + + if (tid < 32768) + { + bytecode.emitAD(LOP_DUPTABLE, reg, int16_t(tid)); + } + else + { + // must disable duptable constant optimization here, as we're defaulting back to new table + shape.hasConstants = false; + lastKeyVal.clear(); + + bytecode.emitABC(LOP_NEWTABLE, reg, uint8_t(encodedHashSize), 0); + bytecode.emitAux(0); + } + } + else + { + // Optimization: instead of allocating one extra element when the last element of the table literal is ..., let SETLIST allocate the + // correct amount of storage + const AstExprTable::Item* last = expr->items.size > 0 ? &expr->items.data[expr->items.size - 1] : nullptr; + + bool trailingVarargs = last && last->kind == AstExprTable::Item::Kind::List && last->value->is(); + LUAU_ASSERT(!trailingVarargs || arraySize > 0); + + unsigned int arrayAllocation = arraySize - trailingVarargs + indexSize; + + if (hashSize == 0) + bytecode.addDebugRemark("allocation: table array %d", arrayAllocation); + else if (arrayAllocation == 0) + bytecode.addDebugRemark("allocation: table hash %d", hashSize); + else + bytecode.addDebugRemark("allocation: table hash %d array %d", hashSize, arrayAllocation); + + bytecode.emitABC(LOP_NEWTABLE, reg, uint8_t(encodedHashSize), 0); + bytecode.emitAux(arrayAllocation); + } + + unsigned int arrayChunkSize = std::min(16u, arraySize); + uint8_t arrayChunkReg = allocReg(expr, arrayChunkSize); + unsigned int arrayChunkCurrent = 0; + + unsigned int arrayIndex = 1; + bool multRet = false; + + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + + AstExpr* key = item.key; + AstExpr* value = item.value; + + if (lastKeyVal.size() > 0 && key && key->is()) + { + AstExprConstantString* ckey = item.key->as(); + LUAU_ASSERT(ckey); + + int keyCid = bytecode.addConstantString(sref(ckey->value)); + if (const int32_t* valueCid = lastKeyVal.get(keyCid)) + { + // do not generate assignments for constants + if (*valueCid >= 0) + { + continue; + } + } + } + + + // some key/value pairs don't require us to compile the expressions, so we need to setup the line info here + setDebugLine(value); + + if (options.coverageLevel >= 2) + { + bytecode.emitABC(LOP_COVERAGE, 0, 0, 0); + } + + // flush array chunk on overflow or before hash keys to maintain insertion order + if (arrayChunkCurrent > 0 && (key || arrayChunkCurrent == arrayChunkSize)) + { + bytecode.emitABC(LOP_SETLIST, reg, arrayChunkReg, uint8_t(arrayChunkCurrent + 1)); + bytecode.emitAux(arrayIndex); + arrayIndex += arrayChunkCurrent; + arrayChunkCurrent = 0; + } + + // items with a key are set one by one via SETTABLE/SETTABLEKS/SETTABLEN + if (key) + { + RegScope rsi(this); + + LValue lv = compileLValueIndex(reg, key, rsi); + uint8_t rv = compileExprAuto(value, rsi); + + compileAssign(lv, rv, nullptr); + } + // items without a key are set using SETLIST so that we can initialize large arrays quickly + else + { + uint8_t temp = uint8_t(arrayChunkReg + arrayChunkCurrent); + + if (i + 1 == expr->items.size) + multRet = compileExprTempMultRet(value, temp); + else + compileExprTempTop(value, temp); + + arrayChunkCurrent++; + } + } + + // flush last array chunk; note that this needs multret handling if the last expression was multret + if (arrayChunkCurrent) + { + bytecode.emitABC(LOP_SETLIST, reg, arrayChunkReg, multRet ? 0 : uint8_t(arrayChunkCurrent + 1)); + bytecode.emitAux(arrayIndex); + } + + if (target != reg) + bytecode.emitABC(LOP_MOVE, target, reg, 0); + } + + bool canImport(AstExprGlobal* expr) + { + return options.optimizationLevel >= 1 && getGlobalState(globals, expr->name) != Global::Written; + } + + bool canImportChain(AstExprGlobal* expr) + { + return options.optimizationLevel >= 1 && getGlobalState(globals, expr->name) == Global::Default; + } + + void compileExprIndexName(AstExprIndexName* expr, uint8_t target, bool targetTemp = false) + { + setDebugLine(expr); // normally compileExpr sets up line info, but compileExprIndexName can be called directly + + // Optimization: index chains that start from global variables can be compiled into GETIMPORT statement + AstExprGlobal* importRoot = 0; + AstExprIndexName* import1 = 0; + AstExprIndexName* import2 = 0; + + if (AstExprIndexName* index = expr->expr->as()) + { + importRoot = index->expr->as(); + import1 = index; + import2 = expr; + } + else + { + importRoot = expr->expr->as(); + import1 = expr; + } + + if (importRoot && canImportChain(importRoot) && !(FFlag::DebugLuauUserDefinedClasses && classLocals.contains(importRoot->name))) + { + int32_t id0 = bytecode.addConstantString(sref(importRoot->name)); + int32_t id1 = bytecode.addConstantString(sref(import1->index)); + int32_t id2 = import2 ? bytecode.addConstantString(sref(import2->index)) : -1; + + if (id0 < 0 || id1 < 0 || (import2 && id2 < 0)) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + // Note: GETIMPORT encoding is limited to 10 bits per object id component + if (id0 < 1024 && id1 < 1024 && id2 < 1024) + { + uint32_t iid = import2 ? BytecodeBuilder::getImportId(id0, id1, id2) : BytecodeBuilder::getImportId(id0, id1); + int32_t cid = bytecode.addImport(iid); + + if (cid >= 0 && cid < 32768) + { + bytecode.emitAD(LOP_GETIMPORT, target, int16_t(cid)); + bytecode.emitAux(iid); + return; + } + } + } + + RegScope rs(this); + + uint8_t reg = target; + + if (int localReg = getExprLocalReg(expr->expr); localReg >= 0) // Locals can be indexed directly + reg = uint8_t(localReg); + else if (targetTemp && FFlag::LuauCompileMoveElision) // If target is a temp register, we can compute the result directly into it + reg = compileExprAutoTemp(expr->expr, target); + else if (targetTemp && !FFlag::LuauCompileMoveElision) + compileExprTemp(expr->expr, target); + else + reg = compileExprAuto(expr->expr, rs); + + setDebugLine(expr->indexLocation); + + BytecodeBuilder::StringRef iname = sref(expr->index); + int32_t cid = bytecode.addConstantString(iname); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(LOP_GETTABLEKS, target, reg, uint8_t(BytecodeBuilder::getStringHash(iname))); + bytecode.emitAux(cid); + + hintTemporaryExprRegType(expr->expr, reg, LBC_TYPE_TABLE, /* instLength */ 2); + } + + void compileExprIndexExpr(AstExprIndexExpr* expr, uint8_t target) + { + RegScope rs(this); + + Constant cv = getConstant(expr->index); + + if (cv.type == Constant::Type_Number && cv.valueNumber >= 1 && cv.valueNumber <= 256 && double(int(cv.valueNumber)) == cv.valueNumber) + { + uint8_t i = uint8_t(int(cv.valueNumber) - 1); + + uint8_t rt = compileExprAuto(expr->expr, rs); + + setDebugLine(expr->index); + + bytecode.emitABC(LOP_GETTABLEN, target, rt, i); + + hintTemporaryExprRegType(expr->expr, rt, LBC_TYPE_TABLE, /* instLength */ 1); + } + else if (cv.type == Constant::Type_String) + { + BytecodeBuilder::StringRef iname = sref(cv.getString()); + int32_t cid = bytecode.addConstantString(iname); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + uint8_t rt = compileExprAuto(expr->expr, rs); + + setDebugLine(expr->index); + + bytecode.emitABC(LOP_GETTABLEKS, target, rt, uint8_t(BytecodeBuilder::getStringHash(iname))); + bytecode.emitAux(cid); + + hintTemporaryExprRegType(expr->expr, rt, LBC_TYPE_TABLE, /* instLength */ 2); + } + else + { + uint8_t rt = compileExprAuto(expr->expr, rs); + uint8_t ri = compileExprAuto(expr->index, rs); + + bytecode.emitABC(LOP_GETTABLE, target, rt, ri); + + hintTemporaryExprRegType(expr->expr, rt, LBC_TYPE_TABLE, /* instLength */ 1); + hintTemporaryExprRegType(expr->index, ri, LBC_TYPE_NUMBER, /* instLength */ 1); + } + } + + void compileExprGlobal(AstExprGlobal* expr, uint8_t target) + { + if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstLocal** local = classLocals.find(expr->name)) + { + int reg = getLocalReg(*local); + if (reg >= 0) + { + if (target != uint8_t(reg)) + bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0); + } + else + { + uint8_t uid = getUpval(*local); + bytecode.emitABC(LOP_GETUPVAL, target, uid, 0); + } + return; + } + } + + // Optimization: builtin globals can be retrieved using GETIMPORT + if (canImport(expr)) + { + int32_t id0 = bytecode.addConstantString(sref(expr->name)); + if (id0 < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + // Note: GETIMPORT encoding is limited to 10 bits per object id component + if (id0 < 1024) + { + uint32_t iid = BytecodeBuilder::getImportId(id0); + int32_t cid = bytecode.addImport(iid); + + if (cid >= 0 && cid < 32768) + { + bytecode.emitAD(LOP_GETIMPORT, target, int16_t(cid)); + bytecode.emitAux(iid); + return; + } + } + } + + BytecodeBuilder::StringRef gname = sref(expr->name); + int32_t cid = bytecode.addConstantString(gname); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(LOP_GETGLOBAL, target, 0, uint8_t(BytecodeBuilder::getStringHash(gname))); + bytecode.emitAux(cid); + } + + void compileExprConstant(AstExpr* node, const Constant* cv, uint8_t target) + { + switch (cv->type) + { + case Constant::Type_Nil: + bytecode.emitABC(LOP_LOADNIL, target, 0, 0); + break; + + case Constant::Type_Boolean: + bytecode.emitABC(LOP_LOADB, target, cv->valueBoolean, 0); + break; + + case Constant::Type_Number: + { + double d = cv->valueNumber; + + if (d >= std::numeric_limits::min() && d <= std::numeric_limits::max() && double(int16_t(d)) == d && + !(d == 0.0 && signbit(d))) + { + // short number encoding: doesn't require a table entry lookup + bytecode.emitAD(LOP_LOADN, target, int16_t(d)); + } + else + { + // long number encoding: use generic constant path + int32_t cid = bytecode.addConstantNumber(d); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + } + break; + + case Constant::Type_Integer: + { + int64_t l = cv->valueInteger64; + + int32_t cid = bytecode.addConstantInteger(l); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + break; + + case Constant::Type_Vectorf: + { + int32_t cid = bytecode.addConstantVectorf(cv->valueVectorf[0], cv->valueVectorf[1], cv->valueVectorf[2], cv->valueVectorf[3]); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + break; + + case Constant::Type_Vectord: + { + int32_t cid = bytecode.addConstantVectord(cv->valueVectord[0], cv->valueVectord[1], cv->valueVectord[2], cv->valueVectord[3]); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + break; + + case Constant::Type_String: + { + int32_t cid = bytecode.addConstantString(sref(cv->getString())); + if (cid < 0) + CompileError::raise(node->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + break; + + default: + LUAU_ASSERT(!"Unexpected constant type"); + } + } + + void compileExpr(AstExpr* node, uint8_t target, bool targetTemp = false) + { + setDebugLine(node); + + if (options.coverageLevel >= 2 && needsCoverage(node)) + { + bytecode.emitABC(LOP_COVERAGE, 0, 0, 0); + } + + // Optimization: if expression has a constant value, we can emit it directly + if (const Constant* cv = constants.find(node); cv && cv->type != Constant::Type_Unknown) + { + compileExprConstant(node, cv, target); + return; + } + + if (AstExprGroup* expr = node->as()) + { + compileExpr(expr->expr, target, targetTemp); + } + else if (node->is()) + { + bytecode.emitABC(LOP_LOADNIL, target, 0, 0); + } + else if (AstExprConstantBool* expr = node->as()) + { + bytecode.emitABC(LOP_LOADB, target, expr->value, 0); + } + else if (AstExprConstantNumber* expr = node->as()) + { + int32_t cid = bytecode.addConstantNumber(expr->value); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + else if (AstExprConstantInteger* expr = node->as()) + { + int32_t cid = bytecode.addConstantInteger(expr->value); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + else if (AstExprConstantString* expr = node->as()) + { + int32_t cid = bytecode.addConstantString(sref(expr->value)); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + emitLoadK(target, cid); + } + else if (AstExprLocal* expr = node->as()) + { + if (FFlag::LuauExportValueSyntax && expr->local->isExported && !exports.exportedClasses.contains(expr->local) && + (!FFlag::LuauOptimizeExportTable || !exports.exportedFunctions.contains(expr->local))) + + { + BytecodeBuilder::StringRef name = sref(expr->local->name); + int32_t cid = bytecode.addConstantString(name); + if (cid < 0) + CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); + + if (int tableReg = getLocalReg(&exports.exportTableLocal); tableReg >= 0) + { + bytecode.emitABC(LOP_GETTABLEKS, target, tableReg, uint8_t(BytecodeBuilder::getStringHash(name))); + bytecode.emitAux(cid); + } + else + { + // we must reuse the target register for the export table lookup + uint8_t upval = getUpval(&exports.exportTableLocal); + bytecode.emitABC(LOP_GETUPVAL, target, upval, 0); + bytecode.emitABC(LOP_GETTABLEKS, target, target, uint8_t(BytecodeBuilder::getStringHash(name))); + bytecode.emitAux(cid); + } + } + else + { + // note: this can't check expr->upvalue because upvalues may be upgraded to locals during inlining + if (int reg = getExprLocalReg(expr); reg >= 0) + { + // Optimization: we don't need to move if target happens to be in the same register + if (options.optimizationLevel == 0 || target != reg) + bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0); + } + else + { + LUAU_ASSERT(expr->upvalue); + uint8_t uid = getUpval(expr->local); + + bytecode.emitABC(LOP_GETUPVAL, target, uid, 0); + } + } + } + else if (AstExprGlobal* expr = node->as()) + { + compileExprGlobal(expr, target); + } + else if (AstExprVarargs* expr = node->as()) + { + compileExprVarargs(expr, target, /* targetCount= */ 1); + } + else if (AstExprCall* expr = node->as()) + { + // Optimization: when targeting temporary registers, we can compile call in a special mode that doesn't require extra register moves + if (targetTemp && target == regTop - 1) + compileExprCall(expr, target, 1, /* targetTop= */ true); + else + compileExprCall(expr, target, /* targetCount= */ 1); + } + else if (AstExprIndexName* expr = node->as()) + { + compileExprIndexName(expr, target, targetTemp); + } + else if (AstExprIndexExpr* expr = node->as()) + { + compileExprIndexExpr(expr, target); + } + else if (AstExprFunction* expr = node->as()) + { + compileExprFunction(expr, target); + } + else if (AstExprTable* expr = node->as()) + { + compileExprTable(expr, target, targetTemp); + } + else if (AstExprUnary* expr = node->as()) + { + compileExprUnary(expr, target); + } + else if (AstExprBinary* expr = node->as()) + { + compileExprBinary(expr, target, targetTemp); + } + else if (AstExprTypeAssertion* expr = node->as()) + { + compileExpr(expr->expr, target, targetTemp); + } + else if (AstExprIfElse* expr = node->as()) + { + compileExprIfElse(expr, target, targetTemp); + } + else if (AstExprInterpString* interpString = node->as()) + { + compileExprInterpString(interpString, target, targetTemp); + } + else if (AstExprInstantiate* expr = node->as()) + { + compileExpr(expr->expr, target, targetTemp); + } + else + { + LUAU_ASSERT(!"Unknown expression type"); + } + } + + void compileExprTemp(AstExpr* node, uint8_t target) + { + return compileExpr(node, target, /* targetTemp= */ true); + } + + uint8_t compileExprAutoTemp(AstExpr* node, uint8_t target, uint8_t forbiddenSourceReg = kInvalidReg) + { + LUAU_ASSERT(FFlag::LuauCompileMoveElision); + + if (options.optimizationLevel == 0) + { + compileExprTemp(node, target); + return target; + } + + size_t marker = bytecode.emitLabel(); + compileExprTemp(node, target); + + // If only a single instruction was generated, check if it's a move of a register that we can elide + if (bytecode.emitLabel() == marker + 1) + { + unsigned insn = bytecode.lastInstruction(); + + if (LUAU_INSN_OP(insn) == LOP_MOVE && LUAU_INSN_A(insn) == target) + { + uint8_t src = LUAU_INSN_B(insn); + + // Captured registers might get modified while the operation is lowered and rely on the move copy to prevent that + if (!regCaptured[src] && src != forbiddenSourceReg) + { + bytecode.undoEmit(LOP_MOVE); + target = src; + } + } + } + + return target; + } + + uint8_t compileExprAuto(AstExpr* node, RegScope&) + { + // Optimization: directly return locals instead of copying them to a temporary + // This can affect side-effect ordering on the register if it is captured + if (int reg = getExprLocalReg(node); reg >= 0) + return uint8_t(reg); + + // note: the register is owned by the parent scope + uint8_t reg = allocReg(node, 1u); + + if (FFlag::LuauCompileMoveElision) + { + return compileExprAutoTemp(node, reg); + } + else + { + compileExprTemp(node, reg); + + return reg; + } + } + + void compileExprSide(AstExpr* node) + { + // Optimization: some expressions never carry side effects so we don't need to emit any code + if (node->is() || node->is() || node->is() || node->is() || isConstant(node)) + return; + + // note: the remark is omitted for calls as it's fairly noisy due to inlining + if (!node->is()) + bytecode.addDebugRemark("expression only compiled for side effects"); + + RegScope rsi(this); + compileExprAuto(node, rsi); + } + + // initializes target..target+targetCount-1 range using expression + // if expression is a call/vararg, we assume it returns all values, otherwise we fill the rest with nil + // assumes target register range can be clobbered and is at the top of the register space if targetTop = true + void compileExprTempN(AstExpr* node, uint8_t target, uint8_t targetCount, bool targetTop) + { + // we assume that target range is at the top of the register space and can be clobbered + // this is what allows us to compile the last call expression - if it's a call - using targetTop=true + LUAU_ASSERT(!targetTop || unsigned(target + targetCount) == regTop); + + // LOP_CALL/LOP_GETVARARGS encoding uses 255 to signal a multret + if (targetCount == 255) + CompileError::raise(node->location, "Exceeded result count limit; simplify the code to compile"); + + if (AstExprCall* expr = node->as()) + { + compileExprCall(expr, target, targetCount, targetTop); + } + else if (AstExprVarargs* expr = node->as()) + { + compileExprVarargs(expr, target, targetCount); + } + else + { + compileExprTemp(node, target); + + for (size_t i = 1; i < targetCount; ++i) + bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0); + } + } + + // initializes target..target+targetCount-1 range using expressions from the list + // if list has fewer expressions, and last expression is multret, we assume it returns the rest of the values + // if list has fewer expressions, and last expression isn't multret, we fill the rest with nil + // assumes target register range can be clobbered and is at the top of the register space if targetTop = true + void compileExprListTemp(const AstArray& list, uint8_t target, uint8_t targetCount, bool targetTop) + { + // we assume that target range is at the top of the register space and can be clobbered + // this is what allows us to compile the last call expression - if it's a call - using targetTop=true + LUAU_ASSERT(!targetTop || unsigned(target + targetCount) == regTop); + + if (list.size == targetCount) + { + for (size_t i = 0; i < list.size; ++i) + compileExprTemp(list.data[i], uint8_t(target + i)); + } + else if (list.size > targetCount) + { + for (size_t i = 0; i < targetCount; ++i) + compileExprTemp(list.data[i], uint8_t(target + i)); + + // evaluate extra expressions for side effects + for (size_t i = targetCount; i < list.size; ++i) + compileExprSide(list.data[i]); + } + else if (list.size > 0) + { + for (size_t i = 0; i < list.size - 1; ++i) + compileExprTemp(list.data[i], uint8_t(target + i)); + + compileExprTempN(list.data[list.size - 1], uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop); + } + else + { + for (size_t i = 0; i < targetCount; ++i) + bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0); + } + } + + struct LValue + { + enum Kind + { + Kind_Local, + Kind_Upvalue, + Kind_Global, + Kind_IndexName, + Kind_IndexNumber, + Kind_IndexExpr, + }; + + Kind kind; + uint8_t reg; // register for local (Local) or table (Index*) + uint8_t upval; + uint8_t index; // register for index in IndexExpr + uint8_t number; // index-1 (0-255) in IndexNumber + BytecodeBuilder::StringRef name; + Location location; + }; + + LValue compileLValueIndex(uint8_t reg, AstExpr* index, RegScope& rs) + { + Constant cv = getConstant(index); + + if (cv.type == Constant::Type_Number && cv.valueNumber >= 1 && cv.valueNumber <= 256 && double(int(cv.valueNumber)) == cv.valueNumber) + { + LValue result = {LValue::Kind_IndexNumber}; + result.reg = reg; + result.number = uint8_t(int(cv.valueNumber) - 1); + result.location = index->location; + + return result; + } + else if (cv.type == Constant::Type_String) + { + LValue result = {LValue::Kind_IndexName}; + result.reg = reg; + result.name = sref(cv.getString()); + result.location = index->location; + + return result; + } + else + { + LValue result = {LValue::Kind_IndexExpr}; + result.reg = reg; + result.index = compileExprAuto(index, rs); + result.location = index->location; + + return result; + } + } + + LValue compileLValue(AstExpr* node, RegScope& rs) + { + setDebugLine(node); + + if (AstExprLocal* expr = node->as()) + { + if (FFlag::LuauExportValueSyntax && expr->local->isExported && + (!FFlag::LuauOptimizeExportTable || !exports.exportedFunctions.contains(expr->local))) + { + uint8_t tableReg = getExportTableReg(node); + + LValue result = {LValue::Kind_IndexName}; + result.reg = tableReg; + result.name = sref(expr->local->name); + result.location = node->location; + + return result; + } + + // note: this can't check expr->upvalue because upvalues may be upgraded to locals during inlining + if (int reg = getExprLocalReg(expr); reg >= 0) + { + LValue result = {LValue::Kind_Local}; + result.reg = uint8_t(reg); + result.location = node->location; + + return result; + } + else + { + LUAU_ASSERT(expr->upvalue); + + LValue result = {LValue::Kind_Upvalue}; + result.upval = getUpval(expr->local); + result.location = node->location; + + return result; + } + } + else if (AstExprGlobal* expr = node->as()) + { + if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstLocal** classLocal = classLocals.find(expr->name)) + CompileError::raise( + expr->location, + "'%s' refers to a class and cannot be used as a variable name (defined on line %d)", + expr->name.value, + (*classLocal)->location.begin.line + 1 + ); + } + + LValue result = {LValue::Kind_Global}; + result.name = sref(expr->name); + result.location = node->location; + + return result; + } + else if (AstExprIndexName* expr = node->as()) + { + LValue result = {LValue::Kind_IndexName}; + result.reg = compileExprAuto(expr->expr, rs); + result.name = sref(expr->index); + result.location = node->location; + + return result; + } + else if (AstExprIndexExpr* expr = node->as()) + { + uint8_t reg = compileExprAuto(expr->expr, rs); + + return compileLValueIndex(reg, expr->index, rs); + } + else + { + LUAU_ASSERT(!"Unknown assignment expression"); + + return LValue(); + } + } + + void compileLValueUse(const LValue& lv, uint8_t reg, bool set, AstExpr* targetExpr) + { + setDebugLine(lv.location); + + switch (lv.kind) + { + case LValue::Kind_Local: + if (set) + bytecode.emitABC(LOP_MOVE, lv.reg, reg, 0); + else + bytecode.emitABC(LOP_MOVE, reg, lv.reg, 0); + break; + + case LValue::Kind_Upvalue: + bytecode.emitABC(set ? LOP_SETUPVAL : LOP_GETUPVAL, reg, lv.upval, 0); + break; + + case LValue::Kind_Global: + { + int32_t cid = bytecode.addConstantString(lv.name); + if (cid < 0) + CompileError::raise(lv.location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(set ? LOP_SETGLOBAL : LOP_GETGLOBAL, reg, 0, uint8_t(BytecodeBuilder::getStringHash(lv.name))); + bytecode.emitAux(cid); + } + break; + + case LValue::Kind_IndexName: + { + int32_t cid = bytecode.addConstantString(lv.name); + if (cid < 0) + CompileError::raise(lv.location, "Exceeded constant limit; simplify the code to compile"); + + bytecode.emitABC(set ? LOP_SETTABLEKS : LOP_GETTABLEKS, reg, lv.reg, uint8_t(BytecodeBuilder::getStringHash(lv.name))); + bytecode.emitAux(cid); + + if (targetExpr) + { + if (AstExprIndexName* targetExprIndexName = targetExpr->as()) + hintTemporaryExprRegType(targetExprIndexName->expr, lv.reg, LBC_TYPE_TABLE, /* instLength */ 2); + } + } + break; + + case LValue::Kind_IndexNumber: + bytecode.emitABC(set ? LOP_SETTABLEN : LOP_GETTABLEN, reg, lv.reg, lv.number); + + if (targetExpr) + { + if (AstExprIndexExpr* targetExprIndexExpr = targetExpr->as()) + hintTemporaryExprRegType(targetExprIndexExpr->expr, lv.reg, LBC_TYPE_TABLE, /* instLength */ 1); + } + break; + + case LValue::Kind_IndexExpr: + bytecode.emitABC(set ? LOP_SETTABLE : LOP_GETTABLE, reg, lv.reg, lv.index); + + if (targetExpr) + { + if (AstExprIndexExpr* targetExprIndexExpr = targetExpr->as()) + { + hintTemporaryExprRegType(targetExprIndexExpr->expr, lv.reg, LBC_TYPE_TABLE, /* instLength */ 1); + hintTemporaryExprRegType(targetExprIndexExpr->index, lv.index, LBC_TYPE_NUMBER, /* instLength */ 1); + } + } + break; + + default: + LUAU_ASSERT(!"Unknown lvalue kind"); + } + } + + void compileAssign(const LValue& lv, uint8_t source, AstExpr* targetExpr) + { + compileLValueUse(lv, source, /* set= */ true, targetExpr); + } + + AstExprLocal* getExprLocal(AstExpr* node) + { + return unwrapExprOfType(node); + } + + int getExprLocalReg(AstExpr* node) + { + if (AstExprLocal* expr = getExprLocal(node)) + { + // note: this can't check expr->upvalue because upvalues may be upgraded to locals during inlining + Local* l = locals.find(expr->local); + + return l && l->allocated ? l->reg : -1; + } + else if (FFlag::DebugLuauUserDefinedClasses) + { + if (AstExprGlobal* g = node->as()) + { + if (AstLocal** local = classLocals.find(g->name)) + return getLocalReg(*local); + } + } + return -1; + } + + bool isStatBreak(AstStat* node) + { + if (AstStatBlock* stat = node->as()) + return stat->body.size == 1 && stat->body.data[0]->is(); + + return node->is(); + } + + AstStatContinue* extractStatContinue(AstStatBlock* block) + { + if (block->body.size == 1) + return block->body.data[0]->as(); + else + return nullptr; + } + + void compileStatIfLocal(AstStatIf* stat) + { + LUAU_ASSERT(stat->conditionLocal); + + bool thenTerminates = alwaysTerminates(stat->thenbody); + bool skipElse = isConstantTrue(stat->condition); + size_t oldLocals = localStack.size(); + size_t elseJump = 0; + + { + RegScope rs(this); + + uint8_t reg = allocReg(stat, 1u); + uint32_t allocpc = bytecode.getDebugPC(); + + compileExprTemp(stat->condition, reg); + pushLocal(stat->conditionLocal, reg, allocpc); + + if (!skipElse) + { + elseJump = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMPIFNOT, reg, 0); + } + + compileStat(stat->thenbody); + + if (!thenTerminates) + closeLocals(oldLocals); + + popLocals(oldLocals); + } + + if (skipElse) + return; + + if (stat->elsebody) + { + if (thenTerminates) + { + size_t elseLabel = bytecode.emitLabel(); + compileStat(stat->elsebody); + patchJump(stat, elseJump, elseLabel); + } + else + { + size_t thenLabel = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMP, 0, 0); + + size_t elseLabel = bytecode.emitLabel(); + compileStat(stat->elsebody); + + size_t endLabel = bytecode.emitLabel(); + patchJump(stat, elseJump, elseLabel); + patchJump(stat, thenLabel, endLabel); + } + } + else + { + size_t endLabel = bytecode.emitLabel(); + patchJump(stat, elseJump, endLabel); + } + } + + void compileStatIf(AstStatIf* stat) + { + // Optimization: condition is always false => we only need the else body + if (isConstantFalse(stat->condition)) + { + if (stat->elsebody) + compileStat(stat->elsebody); + return; + } + + // Optimization: condition is always false but isn't a constant => we only need the else body and condition's side effects + if (AstExprBinary* cand = stat->condition->as(); cand && cand->op == AstExprBinary::And && isConstantFalse(cand->right)) + { + compileExprSide(cand->left); + if (stat->elsebody) + compileStat(stat->elsebody); + return; + } + + if (FFlag::LuauExperimentalIfLocalSyntax && stat->conditionLocal) + { + compileStatIfLocal(stat); + return; + } + + // Optimization: body is a "break" statement with no "else" => we can directly break out of the loop in "then" case + if (!stat->elsebody && isStatBreak(stat->thenbody) && !areLocalsCaptured(loops.back().localOffset)) + { + // fallthrough = continue with the loop as usual + std::vector elseJump; + compileConditionValue(stat->condition, nullptr, elseJump, true); + + for (size_t jump : elseJump) + loopJumps.push_back({LoopJump::Break, jump}); + return; + } + + AstStatContinue* continueStatement = extractStatContinue(stat->thenbody); + + // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case + if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue)) + { + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = continueStatement; + + // fallthrough = proceed with the loop body as usual + std::vector elseJump; + compileConditionValue(stat->condition, nullptr, elseJump, true); + + for (size_t jump : elseJump) + loopJumps.push_back({LoopJump::Continue, jump}); + return; + } + + std::vector elseJump; + compileConditionValue(stat->condition, nullptr, elseJump, false); + + compileStat(stat->thenbody); + + if (stat->elsebody && elseJump.size() > 0) + { + // we don't need to skip past "else" body if "then" ends with return/break/continue + // this is important because, if "else" also ends with return, we may *not* have any statement to skip to! + if (alwaysTerminates(stat->thenbody)) + { + size_t elseLabel = bytecode.emitLabel(); + + compileStat(stat->elsebody); + + patchJumps(stat, elseJump, elseLabel); + } + else + { + size_t thenLabel = bytecode.emitLabel(); + + bytecode.emitAD(LOP_JUMP, 0, 0); + + size_t elseLabel = bytecode.emitLabel(); + + compileStat(stat->elsebody); + + size_t endLabel = bytecode.emitLabel(); + + patchJumps(stat, elseJump, elseLabel); + patchJump(stat, thenLabel, endLabel); + } + } + else + { + size_t endLabel = bytecode.emitLabel(); + + patchJumps(stat, elseJump, endLabel); + } + } + + void compileStatWhile(AstStatWhile* stat) + { + // Optimization: condition is always false => there's no loop! + if (isConstantFalse(stat->condition)) + return; + + size_t oldJumps = loopJumps.size(); + size_t oldLocals = localStack.size(); + + loops.push_back({oldLocals, oldLocals, nullptr}); + hasLoops = true; + + size_t loopLabel = bytecode.emitLabel(); + + std::vector elseJump; + compileConditionValue(stat->condition, nullptr, elseJump, false); + + compileStat(stat->body); + + size_t contLabel = bytecode.emitLabel(); + + size_t backLabel = bytecode.emitLabel(); + + setDebugLine(stat->condition); + + // Note: this is using JUMPBACK, not JUMP, since JUMPBACK is interruptible and we want all loops to have at least one interruptible + // instruction + bytecode.emitAD(LOP_JUMPBACK, 0, 0); + + size_t endLabel = bytecode.emitLabel(); + + patchJump(stat, backLabel, loopLabel); + patchJumps(stat, elseJump, endLabel); + + patchLoopJumps(stat, oldJumps, endLabel, contLabel); + loopJumps.resize(oldJumps); + + loops.pop_back(); + } + + void compileStatRepeat(AstStatRepeat* stat) + { + size_t oldJumps = loopJumps.size(); + size_t oldLocals = localStack.size(); + + loops.push_back({oldLocals, oldLocals, nullptr}); + hasLoops = true; + + size_t loopLabel = bytecode.emitLabel(); + + // note: we "inline" compileStatBlock here so that we can close/pop locals after evaluating condition + // this is necessary because condition can access locals declared inside the repeat..until body + AstStatBlock* body = stat->body; + + RegScope rs(this); + + bool continueValidated = false; + size_t conditionLocals = 0; + + for (size_t i = 0; i < body->body.size; ++i) + { + if (FFlag::LuauCompileReuseLocalRegs) + popStatLocals(body, body->body.data[i], oldLocals); + + compileStat(body->body.data[i]); + + // continue statement inside the repeat..until loop should not close upvalues defined directly in the loop body before it + // this is because the upvalues defined inside the loop body may be captured by a closure defined in the until + // expression that continue will jump to. + // but any locals defined in nested blocks or after first continue (which performs validateContinueUntil) do have to be closed + if (!loops.back().continueUsed) + loops.back().localOffsetContinue = localStack.size(); + + // if continue was called from this statement, any local defined after this in the loop body should not be accessed by until condition + // it is sufficient to check this condition once, as if this holds for the first continue, it must hold for all subsequent continues. + if (loops.back().continueUsed && !continueValidated) + { + validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1); + continueValidated = true; + conditionLocals = localStack.size(); + } + } + + // if continue was used, some locals might not have had their initialization completed + // the lifetime of these locals has to end before the condition is executed + // because referencing skipped locals is not possible from the condition, this earlier closure doesn't affect upvalues + if (continueValidated) + { + // if continueValidated is set, it means we have visited at least one body node and size > 0 + setDebugLineEnd(body->body.data[body->body.size - 1]); + + closeLocals(conditionLocals); + + popLocals(conditionLocals); + } + + size_t contLabel = bytecode.emitLabel(); + + size_t endLabel; + + setDebugLine(stat->condition); + + if (isConstantTrue(stat->condition)) + { + closeLocals(oldLocals); + + endLabel = bytecode.emitLabel(); + } + else + { + std::vector skipJump; + compileConditionValue(stat->condition, nullptr, skipJump, true); + + // we close locals *after* we compute loop conditionals because during computation of condition it's (in theory) possible that user code + // mutates them + closeLocals(oldLocals); + + size_t backLabel = bytecode.emitLabel(); + + // Note: this is using JUMPBACK, not JUMP, since JUMPBACK is interruptible and we want all loops to have at least one interruptible + // instruction + bytecode.emitAD(LOP_JUMPBACK, 0, 0); + + size_t skipLabel = bytecode.emitLabel(); + + // we need to close locals *again* after the loop ends because the first closeLocals would be jumped over on the last iteration + closeLocals(oldLocals); + + endLabel = bytecode.emitLabel(); + + patchJump(stat, backLabel, loopLabel); + patchJumps(stat, skipJump, skipLabel); + } + + popLocals(oldLocals); + + patchLoopJumps(stat, oldJumps, endLabel, contLabel); + loopJumps.resize(oldJumps); + + loops.pop_back(); + } + + void compileInlineReturn(AstStatReturn* stat, bool fallthrough) + { + setDebugLine(stat); // normally compileStat sets up line info, but compileInlineReturn can be called directly + + InlineFrame frame = inlineFrames.back(); + + compileExprListTemp(stat->list, frame.target, frame.targetCount, /* targetTop= */ false); + + closeLocals(frame.localOffset); + + size_t jumpLabel = bytecode.emitLabel(); + bytecode.emitAD(LOP_JUMP, 0, 0); + + inlineFrames.back().returnJumps.push_back(jumpLabel); + } + + void compileStatReturn(AstStatReturn* stat) + { + // LOP_RETURN encoding uses 255 to signal a multret + if (stat->list.size >= 255) + CompileError::raise(stat->location, "Exceeded return count limit; simplify the code to compile"); + + RegScope rs(this); + + uint8_t temp = 0; + bool consecutive = false; + bool multRet = false; + + // Optimization: return locals directly instead of copying them into a temporary + // this is very important for a single return value and occasionally effective for multiple values + if (int reg = stat->list.size > 0 ? getExprLocalReg(stat->list.data[0]) : -1; reg >= 0) + { + temp = uint8_t(reg); + consecutive = true; + + for (size_t i = 1; i < stat->list.size; ++i) + if (getExprLocalReg(stat->list.data[i]) != int(temp + i)) + { + consecutive = false; + break; + } + } + + // Optimization: try to re-adjust the non-consecutive sequence into a consecutive prefix and extra moves + if (FFlag::LuauCompileReuseLocalRegs && !consecutive && stat->list.size > 1) + { + AstArray& list = stat->list; + bool allLocals = true; + + for (AstExpr* expr : list) + { + if (getExprLocalReg(expr) == -1) + allLocals = false; + } + + if (allLocals) + { + uint8_t base = uint8_t(getExprLocalReg(list.data[0])); + size_t prefix = 1; + + while (prefix < list.size && getExprLocalReg(list.data[prefix]) == int(base + prefix)) + prefix++; + + if (base + prefix == regTop) + { + uint8_t tail = allocReg(stat, unsigned(list.size - prefix)); + LUAU_ASSERT(tail == base + prefix); + + for (size_t i = prefix; i < list.size; i++) + bytecode.emitABC(LOP_MOVE, uint8_t(base + i), uint8_t(getExprLocalReg(list.data[i])), 0); + + temp = base; + consecutive = true; + } + } + } + + if (FFlag::LuauCompileMoveElision && !consecutive && stat->list.size == 1 && !isExprMultRet(stat->list.data[0])) + { + // Single expression return might be able to elide the extra move into the fresh register + temp = allocReg(stat, 1u); + temp = compileExprAutoTemp(stat->list.data[0], temp); + } + else if (!consecutive && stat->list.size > 0) + { + temp = allocReg(stat, unsigned(stat->list.size)); + + // Note: if the last element is a function call or a vararg specifier, then we need to somehow return all values that that call returned + for (size_t i = 0; i < stat->list.size; ++i) + if (i + 1 == stat->list.size) + multRet = compileExprTempMultRet(stat->list.data[i], uint8_t(temp + i)); + else + compileExprTempTop(stat->list.data[i], uint8_t(temp + i)); + } + + closeLocals(0); + + if (multRet) + hasMultiRet = true; + + bytecode.emitABC(LOP_RETURN, uint8_t(temp), multRet ? 0 : uint8_t(stat->list.size + 1), 0); + } + + bool areLocalsRedundant(AstStatLocal* stat) + { + // Extra expressions may have side effects + if (stat->values.size > stat->vars.size) + return false; + + for (AstLocal* local : stat->vars) + { + Variable* v = variables.find(local); + if (!v || !v->constant) + return false; + + if (FFlag::LuauExportValueSyntax && local->isExported && (!FFlag::LuauOptimizeExportTable || exports.exportedTableCid == -1)) + { + // exported locals must be written to the export table + return false; + } + } + + return true; + } + + void compileStatLocal(AstStatLocal* stat) + { + // Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded + if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat)) + return; + + // Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated + if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) + { + if (AstExprLocal* re = getExprLocal(stat->values.data[0])) + { + Variable* lv = variables.find(stat->vars.data[0]); + Variable* rv = variables.find(re->local); + + if (int reg = getExprLocalReg(re); + reg >= 0 && (!lv || !lv->written) && (!rv || !rv->written) && !stat->vars.data[0]->isExported && !re->local->isExported) + { + pushLocal(stat->vars.data[0], uint8_t(reg), kDefaultAllocPc); + return; + } + } + + // Declaration of a local that becomes the inlined function result can use the return target register + if (FFlag::LuauCompileMoveElision && !inlineFrames.empty() && inlineFrames.back().resultLocal == stat->vars.data[0]) + { + compileExprTemp(stat->values.data[0], inlineFrames.back().target); + + // TODO: unreachable + // evaluate extra expressions for side effects + for (size_t i = stat->vars.size; i < stat->values.size; ++i) + compileExprSide(stat->values.data[i]); + + pushLocal(stat->vars.data[0], inlineFrames.back().target, kDefaultAllocPc); + return; + } + + if (FFlag::LuauCompileReuseLocalRegs) + { + uint8_t target = kInvalidReg; + if (tryReuseLocalReg(stat->vars.data[0], target)) + { + uint32_t allocpc = bytecode.getDebugPC(); + compileExprTemp(stat->values.data[0], target); + pushLocal(stat->vars.data[0], target, allocpc); + return; + } + } + } + + // note: allocReg in this case allocates into parent block register - note that we don't have RegScope here + uint8_t vars = allocReg(stat, unsigned(stat->vars.size)); + uint32_t allocpc = bytecode.getDebugPC(); + + compileExprListTemp(stat->values, vars, uint8_t(stat->vars.size), /* targetTop= */ true); + + for (size_t i = 0; i < stat->vars.size; ++i) + { + AstLocal* local = stat->vars.data[i]; + Variable* localVariable = variables.find(local); + if (FFlag::LuauExportValueSyntax && local->isExported && + (!FFlag::LuauOptimizeExportTable || !localVariable->constant || exports.exportedTableCid == -1)) + { + ensureExportTable(stat); + + int32_t cid = bytecode.addConstantString(sref(local->name)); + if (cid < 0) + CompileError::raise(local->location, "Exceeded constant limit; simplify the code to compile"); + + uint8_t tableReg = getExportTableReg(stat); + bytecode.emitABC(LOP_SETTABLEKS, uint8_t(vars + i), tableReg, uint8_t(BytecodeBuilder::getStringHash(sref(local->name)))); + bytecode.emitAux(cid); + } + else + { + pushLocal(local, uint8_t(vars + i), allocpc); + } + } + } + + bool tryCompileUnrolledFor(AstStatFor* stat, int thresholdBase, int thresholdMaxBoost) + { + Constant one = {Constant::Type_Number}; + one.valueNumber = 1.0; + + Constant fromc = getConstant(stat->from); + Constant toc = getConstant(stat->to); + Constant stepc = stat->step ? getConstant(stat->step) : one; + + int tripCount = (fromc.type == Constant::Type_Number && toc.type == Constant::Type_Number && stepc.type == Constant::Type_Number) + ? getTripCount(fromc.valueNumber, toc.valueNumber, stepc.valueNumber) + : -1; + + if (tripCount < 0) + { + bytecode.addDebugRemark("loop unroll failed: invalid iteration count"); + return false; + } + + if (FFlag::LuauCompileLoopUnrollZero && tripCount == 0) + { + bytecode.addDebugRemark("loop unroll succeeded: empty loop"); + return true; + } + + if (tripCount > thresholdBase) + { + bytecode.addDebugRemark("loop unroll failed: too many iterations (%d)", tripCount); + return false; + } + + if (Variable* lv = variables.find(stat->var); lv && lv->written) + { + bytecode.addDebugRemark("loop unroll failed: mutable loop variable"); + return false; + } + + AstLocal* var = stat->var; + uint64_t costModel = modelCost(stat->body, &var, 1, builtins, constants); + + // we use a dynamic cost threshold that's based on the fixed limit boosted by the cost advantage we gain due to unrolling + bool varc = true; + int unrolledCost = computeCost(costModel, &varc, 1) * tripCount; + int baselineCost = (computeCost(costModel, nullptr, 0) + 1) * tripCount; + int unrollProfit = (unrolledCost == 0) ? thresholdMaxBoost : std::min(thresholdMaxBoost, 100 * baselineCost / unrolledCost); + + int threshold = thresholdBase * unrollProfit / 100; + + if (unrolledCost > threshold) + { + bytecode.addDebugRemark( + "loop unroll failed: too expensive (iterations %d, cost %d, profit %.2fx)", tripCount, unrolledCost, double(unrollProfit) / 100 + ); + return false; + } + + bytecode.addDebugRemark("loop unroll succeeded (iterations %d, cost %d, profit %.2fx)", tripCount, unrolledCost, double(unrollProfit) / 100); + + compileUnrolledFor(stat, tripCount, fromc.valueNumber, stepc.valueNumber); + return true; + } + + void compileUnrolledFor(AstStatFor* stat, int tripCount, double from, double step) + { + AstLocal* var = stat->var; + + size_t oldLocals = localStack.size(); + size_t oldJumps = loopJumps.size(); + + loops.push_back({oldLocals, oldLocals, nullptr}); + + // record changes on the first iteration to capture the pre-loop state + exprChanges.clear(); + localChanges.clear(); + + for (int iv = 0; iv < tripCount; ++iv) + { + // we need to re-fold constants in the loop body with the new value; this reuses computed constant values elsewhere in the tree + locstants[var].type = Constant::Type_Number; + locstants[var].valueNumber = from + iv * step; + + if (iv == 0) + foldConstants( + constants, + variables, + locstants, + builtinsFold, + builtinsFoldLibraryK, + options.vectorPrecision == 1, + options.libraryMemberConstantCb, + stat, + names, + tableConstants, + &exprChanges, + &localChanges + ); + else + foldConstants( + constants, + variables, + locstants, + builtinsFold, + builtinsFoldLibraryK, + options.vectorPrecision == 1, + options.libraryMemberConstantCb, + stat, + names, + tableConstants + ); + + + size_t iterJumps = loopJumps.size(); + + compileStat(stat->body); + + // all continue jumps need to go to the next iteration + size_t contLabel = bytecode.emitLabel(); + + for (size_t i = iterJumps; i < loopJumps.size(); ++i) + if (loopJumps[i].type == LoopJump::Continue) + patchJump(stat, loopJumps[i].label, contLabel); + } + + // all break jumps need to go past the loop + size_t endLabel = bytecode.emitLabel(); + + for (size_t i = oldJumps; i < loopJumps.size(); ++i) + if (loopJumps[i].type == LoopJump::Break) + patchJump(stat, loopJumps[i].label, endLabel); + + loopJumps.resize(oldJumps); + + loops.pop_back(); + + // clean up fold state in case we need to recompile - normally we compile the loop body once, but due to inlining we may need to do it again + locstants[var].type = Constant::Type_Unknown; + + Compile::undoChanges(constants, exprChanges); + Compile::undoChanges(locstants, localChanges); + } + + void compileStatFor(AstStatFor* stat) + { + RegScope rs(this); + + // Optimization: small loops can be unrolled when it is profitable + if (options.optimizationLevel >= 2 && isConstant(stat->to) && isConstant(stat->from) && (!stat->step || isConstant(stat->step))) + if (tryCompileUnrolledFor(stat, FInt::LuauCompileLoopUnrollThreshold, FInt::LuauCompileLoopUnrollThresholdMaxBoost)) + return; + + size_t oldLocals = localStack.size(); + size_t oldJumps = loopJumps.size(); + + loops.push_back({oldLocals, oldLocals, nullptr}); + hasLoops = true; + + // register layout: limit, step, index + uint8_t regs = allocReg(stat, 3u); + + // if the iteration index is assigned from within the loop, we need to protect the internal index from the assignment + // to do that, we will copy the index into an actual local variable on each iteration + // this makes sure the code inside the loop can't interfere with the iteration process (other than modifying the table we're iterating + // through) + uint8_t varreg = regs + 2; + uint32_t varregallocpc = bytecode.getDebugPC(); + + if (Variable* il = variables.find(stat->var); il && il->written) + varreg = allocReg(stat, 1u); + + compileExprTemp(stat->from, uint8_t(regs + 2)); + compileExprTemp(stat->to, uint8_t(regs + 0)); + + if (stat->step) + compileExprTemp(stat->step, uint8_t(regs + 1)); + else + bytecode.emitABC(LOP_LOADN, uint8_t(regs + 1), 1, 0); + + size_t forLabel = bytecode.emitLabel(); + + bytecode.emitAD(LOP_FORNPREP, regs, 0); + + size_t loopLabel = bytecode.emitLabel(); + + if (varreg != regs + 2) + bytecode.emitABC(LOP_MOVE, varreg, regs + 2, 0); + + pushLocal(stat->var, varreg, varregallocpc); + + compileStat(stat->body); + + closeLocals(oldLocals); + popLocals(oldLocals); + + setDebugLine(stat); + + size_t contLabel = bytecode.emitLabel(); + + size_t backLabel = bytecode.emitLabel(); + + bytecode.emitAD(LOP_FORNLOOP, regs, 0); + + size_t endLabel = bytecode.emitLabel(); + + patchJump(stat, forLabel, endLabel); + patchJump(stat, backLabel, loopLabel); + + patchLoopJumps(stat, oldJumps, endLabel, contLabel); + loopJumps.resize(oldJumps); + + loops.pop_back(); + } + + void compileStatForIn(AstStatForIn* stat) + { + RegScope rs(this); + + size_t oldLocals = localStack.size(); + size_t oldJumps = loopJumps.size(); + + loops.push_back({oldLocals, oldLocals, nullptr}); + hasLoops = true; + + // register layout: generator, state, index, variables... + uint8_t regs = allocReg(stat, 3u); + + // this puts initial values of (generator, state, index) into the loop registers + compileExprListTemp(stat->values, regs, 3, /* targetTop= */ true); + + // note that we reserve at least 2 variables; this allows our fast path to assume that we need 2 variables instead of 1 or 2 + uint8_t vars = allocReg(stat, std::max(unsigned(stat->vars.size), 2u)); + LUAU_ASSERT(vars == regs + 3); + uint32_t varsallocpc = bytecode.getDebugPC(); + + LuauOpcode skipOp = LOP_FORGPREP; + + // Optimization: when we iterate via pairs/ipairs, we generate special bytecode that optimizes the traversal using internal iteration index + // These instructions dynamically check if generator is equal to next/inext and bail out + // They assume that the generator produces 2 variables, which is why we allocate at least 2 above (see vars assignment) + if (options.optimizationLevel >= 1 && stat->vars.size <= 2) + { + if (stat->values.size == 1 && stat->values.data[0]->is()) + { + Builtin builtin = getBuiltin(stat->values.data[0]->as()->func, globals, variables); + + if (builtin.isGlobal("ipairs")) // for .. in ipairs(t) + skipOp = LOP_FORGPREP_INEXT; + else if (builtin.isGlobal("pairs")) // for .. in pairs(t) + skipOp = LOP_FORGPREP_NEXT; + } + else if (stat->values.size == 2 && (!getfenvUsed && !setfenvUsed)) + { + Builtin builtin = getBuiltin(stat->values.data[0], globals, variables); + + if (builtin.isGlobal("next")) // for .. in next,t + skipOp = LOP_FORGPREP_NEXT; + } + } + + // first iteration jumps into FORGLOOP instruction, but for ipairs/pairs it does extra preparation that makes the cost of an extra instruction + // worthwhile + size_t skipLabel = bytecode.emitLabel(); + + bytecode.emitAD(skipOp, regs, 0); + + size_t loopLabel = bytecode.emitLabel(); + + for (size_t i = 0; i < stat->vars.size; ++i) + pushLocal(stat->vars.data[i], uint8_t(vars + i), varsallocpc); + + compileStat(stat->body); + + closeLocals(oldLocals); + popLocals(oldLocals); + + setDebugLine(stat); + + size_t contLabel = bytecode.emitLabel(); + + size_t backLabel = bytecode.emitLabel(); + + // FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit + bytecode.emitAD(LOP_FORGLOOP, regs, 0); + bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size)); + + size_t endLabel = bytecode.emitLabel(); + + patchJump(stat, skipLabel, backLabel); + patchJump(stat, backLabel, loopLabel); + + patchLoopJumps(stat, oldJumps, endLabel, contLabel); + loopJumps.resize(oldJumps); + + loops.pop_back(); + } + + struct Assignment + { + LValue lvalue; + + uint8_t conflictReg = kInvalidReg; + uint8_t valueReg = kInvalidReg; + }; + + // This function analyzes assignments and marks assignment conflicts: cases when a variable is assigned on lhs + // but subsequently used on the rhs, assuming assignments are performed in order. Note that it's also possible + // for a variable to conflict on the lhs, if it's used in an lvalue expression after it's assigned. + // When conflicts are found, Assignment::conflictReg is allocated and that's where assignment is performed instead, + // until the final fixup in compileStatAssign. Assignment::valueReg is allocated by compileStatAssign as well. + // + // Per Lua manual, section 3.3.3 (Assignments), the proper assignment order is only guaranteed to hold for syntactic access: + // + // Note that this guarantee covers only accesses syntactically inside the assignment statement. If a function or a metamethod called + // during the assignment changes the value of a variable, Lua gives no guarantees about the order of that access. + // + // As such, we currently don't check if an assigned local is captured, which may mean it gets reassigned during a function call. + void resolveAssignConflicts(AstStat* stat, std::vector& vars, const AstArray& values) + { + struct Visitor : AstVisitor + { + Compiler* self; + + std::bitset<256> conflict; + std::bitset<256> assigned; + + Visitor(Compiler* self) + : self(self) + { + } + + bool visit(AstExprLocal* node) override + { + int reg = self->getLocalReg(node->local); + + if (reg >= 0 && assigned[reg]) + conflict[reg] = true; + + return true; + } + }; + + Visitor visitor(this); + + // mark any registers that are used *after* assignment as conflicting + + // first we go through assignments to locals, since they are performed before assignments to other l-values + for (size_t i = 0; i < vars.size(); ++i) + { + const LValue& li = vars[i].lvalue; + + if (li.kind == LValue::Kind_Local) + { + if (i < values.size) + values.data[i]->visit(&visitor); + + visitor.assigned[li.reg] = true; + } + } + + // and now we handle all other l-values + for (size_t i = 0; i < vars.size(); ++i) + { + const LValue& li = vars[i].lvalue; + + if (li.kind != LValue::Kind_Local && i < values.size) + values.data[i]->visit(&visitor); + } + + // mark any registers used in trailing expressions as conflicting as well + for (size_t i = vars.size(); i < values.size; ++i) + values.data[i]->visit(&visitor); + + // mark any registers used on left hand side that are also assigned anywhere as conflicting + // this is order-independent because we evaluate all right hand side arguments into registers before doing table assignments + for (const Assignment& var : vars) + { + const LValue& li = var.lvalue; + + if ((li.kind == LValue::Kind_IndexName || li.kind == LValue::Kind_IndexNumber || li.kind == LValue::Kind_IndexExpr) && + visitor.assigned[li.reg]) + visitor.conflict[li.reg] = true; + + if (li.kind == LValue::Kind_IndexExpr && visitor.assigned[li.index]) + visitor.conflict[li.index] = true; + } + + // for any conflicting var, we need to allocate a temporary register where the assignment is performed, so that we can move the value later + for (Assignment& var : vars) + { + const LValue& li = var.lvalue; + + if (li.kind == LValue::Kind_Local && visitor.conflict[li.reg]) + var.conflictReg = allocReg(stat, 1u); + } + } + + void compileStatAssign(AstStatAssign* stat) + { + RegScope rs(this); + + // Optimization: one to one assignments don't require complex conflict resolution machinery + if (stat->vars.size == 1 && stat->values.size == 1) + { + LValue var = compileLValue(stat->vars.data[0], rs); + + // Optimization: assign to locals directly + if (var.kind == LValue::Kind_Local) + { + compileExpr(stat->values.data[0], var.reg); + } + else + { + uint8_t reg = compileExprAuto(stat->values.data[0], rs); + + setDebugLine(stat->vars.data[0]); + compileAssign(var, reg, stat->vars.data[0]); + } + return; + } + + // compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the + // left hand side - for example, in "a[expr] = foo" expr will get evaluated here + std::vector vars(stat->vars.size); + + for (size_t i = 0; i < stat->vars.size; ++i) + vars[i].lvalue = compileLValue(stat->vars.data[i], rs); + + // perform conflict resolution: if any expression refers to a local that is assigned before evaluating it, we assign to a temporary + // register after this, vars[i].conflictReg is set for locals that need to be assigned in the second pass + resolveAssignConflicts(stat, vars, stat->values); + + // compute rhs into (mostly) fresh registers + // note that when the lhs assignment is a local, we evaluate directly into that register + // this is possible because resolveAssignConflicts renamed conflicting locals into temporaries + // after this, vars[i].valueReg is set to a register with the value for *all* vars, but some have already been assigned + for (size_t i = 0; i < stat->vars.size && i < stat->values.size; ++i) + { + AstExpr* value = stat->values.data[i]; + + if (i + 1 == stat->values.size && stat->vars.size > stat->values.size) + { + // allocate a consecutive range of regs for all remaining vars and compute everything into temps + // note, this also handles trailing nils + unsigned rest = unsigned(stat->vars.size - stat->values.size + 1); + uint8_t temp = allocReg(stat, rest); + + compileExprTempN(value, temp, uint8_t(rest), /* targetTop= */ true); + + for (size_t j = i; j < stat->vars.size; ++j) + vars[j].valueReg = uint8_t(temp + (j - i)); + } + else + { + Assignment& var = vars[i]; + + // if target is a local, use compileExpr directly to target + if (var.lvalue.kind == LValue::Kind_Local) + { + var.valueReg = (var.conflictReg == kInvalidReg) ? var.lvalue.reg : var.conflictReg; + + compileExpr(stat->values.data[i], var.valueReg); + } + else + { + var.valueReg = compileExprAuto(stat->values.data[i], rs); + } + } + } + + // compute expressions with side effects + for (size_t i = stat->vars.size; i < stat->values.size; ++i) + compileExprSide(stat->values.data[i]); + + // almost done... let's assign everything left to right, noting that locals were either written-to directly, or will be written-to in a + // separate pass to avoid conflicts + size_t varPos = 0; + for (const Assignment& var : vars) + { + LUAU_ASSERT(var.valueReg != kInvalidReg); + + if (var.lvalue.kind != LValue::Kind_Local) + { + setDebugLine(var.lvalue.location); + + if (varPos < stat->vars.size) + compileAssign(var.lvalue, var.valueReg, stat->vars.data[varPos]); + else + compileAssign(var.lvalue, var.valueReg, nullptr); + } + + varPos++; + } + + // all regular local writes are done by the prior loops by computing result directly into target, so this just handles conflicts OR + // local copies from temporary registers in multret context, since in that case we have to allocate consecutive temporaries + for (const Assignment& var : vars) + { + if (var.lvalue.kind == LValue::Kind_Local && var.valueReg != var.lvalue.reg) + bytecode.emitABC(LOP_MOVE, var.lvalue.reg, var.valueReg, 0); + } + } + + void compileStatCompoundAssign(AstStatCompoundAssign* stat) + { + RegScope rs(this); + + LValue var = compileLValue(stat->var, rs); + + // Optimization: assign to locals directly + uint8_t target = (var.kind == LValue::Kind_Local) ? var.reg : allocReg(stat, 1u); + + switch (stat->op) + { + case AstExprBinary::Add: + case AstExprBinary::Sub: + case AstExprBinary::Mul: + case AstExprBinary::Div: + case AstExprBinary::FloorDiv: + case AstExprBinary::Mod: + case AstExprBinary::Pow: + { + if (var.kind != LValue::Kind_Local) + compileLValueUse(var, target, /* set= */ false, stat->var); + + int32_t rc = getConstantNumber(stat->value); + + if (rc >= 0 && rc <= 255) + { + bytecode.emitABC(getBinaryOpArith(stat->op, /* k= */ true), target, target, uint8_t(rc)); + } + else + { + uint8_t rr = compileExprAuto(stat->value, rs); + + bytecode.emitABC(getBinaryOpArith(stat->op), target, target, rr); + + if (var.kind != LValue::Kind_Local) + hintTemporaryRegType(stat->var, target, LBC_TYPE_NUMBER, /* instLength */ 1); + + hintTemporaryExprRegType(stat->value, rr, LBC_TYPE_NUMBER, /* instLength */ 1); + } + } + break; + + case AstExprBinary::Concat: + { + std::vector args = {stat->value}; + + // unroll the tree of concats down the right hand side to be able to do multiple ops + unrollConcats(args); + + uint8_t regs = allocReg(stat, unsigned(1 + args.size())); + + compileLValueUse(var, regs, /* set= */ false, stat->var); + + for (size_t i = 0; i < args.size(); ++i) + if (FFlag::LuauCompileConcatTargetTop) + compileExprTempTop(args[i], uint8_t(regs + 1 + i)); + else + compileExprTemp(args[i], uint8_t(regs + 1 + i)); + + bytecode.emitABC(LOP_CONCAT, target, regs, uint8_t(regs + args.size())); + } + break; + + default: + LUAU_ASSERT(!"Unexpected compound assignment operation"); + } + + if (var.kind != LValue::Kind_Local) + compileAssign(var, target, stat->var); + } + + void compileStatFunction(AstStatFunction* stat) + { + // Optimization: compile value expression directly into target local register + if (int reg = getExprLocalReg(stat->name); reg >= 0) + { + compileExpr(stat->func, uint8_t(reg)); + return; + } + + RegScope rs(this); + uint8_t reg = allocReg(stat, 1u); + + compileExprTemp(stat->func, reg); + + LValue var = compileLValue(stat->name, rs); + compileAssign(var, reg, stat->name); + } + + void compileStat(AstStat* node) + { + setDebugLine(node); + + if (options.coverageLevel >= 1 && needsCoverage(node)) + { + bytecode.emitABC(LOP_COVERAGE, 0, 0, 0); + } + + if (AstStatBlock* stat = node->as()) + { + RegScope rs(this); + + size_t oldLocals = localStack.size(); + if (FFlag::LuauExportValueSyntax) + blockDepth++; + + bool terminatesEarly = false; + + for (size_t i = 0; i < stat->body.size; ++i) + { + AstStat* bodyStat = stat->body.data[i]; + + if (FFlag::LuauCompileReuseLocalRegs) + popStatLocals(stat, bodyStat, 0u); + + compileStat(bodyStat); + + if (FFlag::LuauCompileCleanBlockDeadClose) + { + if (alwaysTerminates(bodyStat)) + { + terminatesEarly = true; + break; + } + } + else + { + if (alwaysTerminates(bodyStat)) + break; + } + } + + if (FFlag::LuauExportValueSyntax) + blockDepth--; + + if (FFlag::LuauCompileCleanBlockDeadClose) + { + if (!terminatesEarly) + closeLocals(oldLocals); + } + else + { + closeLocals(oldLocals); + } + + popLocals(oldLocals); + } + else if (AstStatIf* stat = node->as()) + { + compileStatIf(stat); + } + else if (AstStatWhile* stat = node->as()) + { + compileStatWhile(stat); + } + else if (AstStatRepeat* stat = node->as()) + { + compileStatRepeat(stat); + } + else if (node->is()) + { + LUAU_ASSERT(!loops.empty()); + + // before exiting out of the loop, we need to close all local variables that were captured in closures since loop start + // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here + closeLocals(loops.back().localOffset); + + size_t label = bytecode.emitLabel(); + + bytecode.emitAD(LOP_JUMP, 0, 0); + + loopJumps.push_back({LoopJump::Break, label}); + } + else if (AstStatContinue* stat = node->as()) + { + LUAU_ASSERT(!loops.empty()); + + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = stat; + + // before continuing, we need to close all local variables that were captured in closures since loop start + // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here + closeLocals(loops.back().localOffsetContinue); + + size_t label = bytecode.emitLabel(); + + bytecode.emitAD(LOP_JUMP, 0, 0); + + loopJumps.push_back({LoopJump::Continue, label}); + } + else if (AstStatReturn* stat = node->as()) + { + if (options.optimizationLevel >= 2 && !inlineFrames.empty()) + compileInlineReturn(stat, /* fallthrough= */ false); + else + compileStatReturn(stat); + } + else if (AstStatExpr* stat = node->as()) + { + // Optimization: since we don't need to read anything from the stack, we can compile the call to not return anything which saves register + // moves + if (AstExprCall* expr = stat->expr->as()) + { + uint8_t target = uint8_t(regTop); + + compileExprCall(expr, target, /* targetCount= */ 0); + } + else + { + compileExprSide(stat->expr); + } + } + else if (AstStatLocal* stat = node->as()) + { + if (FFlag::LuauExportValueSyntax) + { + for (auto& local : stat->vars) + { + checkExportedLocal(local, stat->location); + } + } + compileStatLocal(stat); + } + else if (AstStatFor* stat = node->as()) + { + compileStatFor(stat); + } + else if (AstStatForIn* stat = node->as()) + { + compileStatForIn(stat); + } + else if (AstStatAssign* stat = node->as()) + { + compileStatAssign(stat); + } + else if (AstStatCompoundAssign* stat = node->as()) + { + compileStatCompoundAssign(stat); + } + else if (AstStatFunction* stat = node->as()) + { + compileStatFunction(stat); + } + else if (AstStatLocalFunction* stat = node->as()) + { + if (FFlag::LuauExportValueSyntax && !FFlag::LuauOptimizeExportTable && stat->name->isExported) + { + checkExportedLocal(stat->name, stat->location); + + ensureExportTable(stat); + + RegScope rs(this); + uint8_t var = allocReg(stat, 1u); + compileExprFunction(stat->func, var); + + int32_t cid = bytecode.addConstantString(sref(stat->name->name)); + if (cid < 0) + CompileError::raise(stat->name->location, "Exceeded constant limit; simplify the code to compile"); + + uint8_t tableReg = getExportTableReg(stat); + bytecode.emitABC(LOP_SETTABLEKS, var, tableReg, uint8_t(BytecodeBuilder::getStringHash(sref(stat->name->name)))); + bytecode.emitAux(cid); + } + else + { + uint8_t var = kInvalidReg; + + if (FFlag::LuauCompileReuseLocalRegs) + { + if (!tryReuseLocalReg(stat->name, var)) + var = allocReg(stat, 1u); + } + else + { + var = allocReg(stat, 1u); + } + + pushLocal(stat->name, var, kDefaultAllocPc); + compileExprFunction(stat->func, var); + + Local& l = locals[stat->name]; + + // we *have* to pushLocal before we compile the function, since the function may refer to the local as an upvalue + // however, this means the debugpc for the local is at an instruction where the local value hasn't been computed yet + // to fix this we just move the debugpc after the local value is established + l.debugpc = bytecode.getDebugPC(); + } + } + else if (AstStatTypeAlias* alias = node->as()) + { + if (FFlag::LuauExportedTypesParticipateInScc && alias->exported && atTopLevel()) + exports.hasTypeExports = true; + } + else if (node->is()) + { + // do nothing + } + else if (FFlag::DebugLuauUserDefinedClasses && node->is()) + { + compileClassDeclaration(node->as()); + } + else + { + LUAU_ASSERT(!"Unknown statement type"); + } + } + + void validateContinueUntil(AstStat* cont, AstExpr* condition, AstStatBlock* body, size_t start) + { + UndefinedLocalVisitor visitor(this); + + for (size_t i = start; i < body->body.size; ++i) + { + if (AstStatLocal* stat = body->body.data[i]->as()) + { + for (AstLocal* local : stat->vars) + visitor.locals.insert(local); + } + else if (AstStatLocalFunction* stat = body->body.data[i]->as()) + { + visitor.locals.insert(stat->name); + } + } + + condition->visit(&visitor); + + if (visitor.undef) + CompileError::raise( + condition->location, + "Local %s used in the repeat..until condition is undefined because continue statement on line %d jumps over it", + visitor.undef->name.value, + cont->location.begin.line + 1 + ); + } + + void preallocateHoistedClasses(AstStatBlock* body) + { + LUAU_ASSERT(FFlag::DebugLuauUserDefinedClasses); + + for (AstStat* stat : body->body) + { + if (AstStatClass* decl = stat->as()) + { + uint8_t reg = allocReg(decl, 1u); + pushLocal(decl->name, reg, kDefaultAllocPc); + bytecode.emitABC(LOP_LOADNIL, reg, 0, 0); + } + } + } + + void gatherConstUpvals(AstExprFunction* func) + { + ConstUpvalueVisitor visitor(this); + func->body->visit(&visitor); + + for (AstLocal* local : visitor.upvals) + getUpval(local); + } + + void pushLocal(AstLocal* local, uint8_t reg, uint32_t allocpc) + { + if (localStack.size() >= kMaxLocalCount) + CompileError::raise( + local->location, "Out of local registers when trying to allocate %s: exceeded limit %d", local->name.value, kMaxLocalCount + ); + + localStack.push_back(local); + + Local& l = locals[local]; + + LUAU_ASSERT(!l.allocated); + + l.reg = reg; + l.allocated = true; + l.debugpc = bytecode.getDebugPC(); + l.allocpc = allocpc == kDefaultAllocPc ? l.debugpc : allocpc; + + if (FFlag::LuauCompileMoveElision) + regCaptured[reg] = l.captured; + + if (FFlag::LuauCompileReuseLocalRegs) + localUses[reg]++; + } + + bool areLocalsCaptured(size_t start) + { + LUAU_ASSERT(start <= localStack.size()); + + for (size_t i = start; i < localStack.size(); ++i) + { + Local* l = locals.find(localStack[i]); + LUAU_ASSERT(l); + + if (l->captured) + return true; + } + + return false; + } + + void closeLocals(size_t start) + { + LUAU_ASSERT(start <= localStack.size()); + + bool captured = false; + uint8_t captureReg = 255; + + for (size_t i = start; i < localStack.size(); ++i) + { + Local* l = locals.find(localStack[i]); + LUAU_ASSERT(l); + + if (l->captured) + { + captured = true; + captureReg = std::min(captureReg, l->reg); + } + } + + if (captured) + { + bytecode.emitABC(LOP_CLOSEUPVALS, captureReg, 0, 0); + } + } + + struct Local + { + uint8_t reg = 0; + bool allocated = false; + bool captured = false; + bool reused = false; + uint32_t debugpc = 0; + uint32_t allocpc = 0; + }; + + void recordLocalDebugInfo(AstLocal* astLocal, Local* l, bool isArgument, uint32_t endpc) + { + if (options.debugLevel >= 2) + { + bytecode.pushDebugLocal(sref(astLocal->name), l->reg, l->debugpc, endpc); + } + + if (options.typeInfoLevel >= 1 && !isArgument) + { + LuauBytecodeType ty = LBC_TYPE_ANY; + + if (LuauBytecodeType* recordedTy = localTypes.find(astLocal)) + ty = *recordedTy; + + bytecode.pushLocalTypeInfo(ty, l->reg, l->allocpc, endpc); + } + } + + // Returns true if the local release has also freed the last use of the register assigned to it + bool releaseLocal(Local* l) + { + LUAU_ASSERT(FFlag::LuauCompileReuseLocalRegs); + + l->allocated = false; + + if (FFlag::LuauCompileMoveElision) + regCaptured[l->reg] = false; + + LUAU_ASSERT(localUses[l->reg] != 0); + localUses[l->reg]--; + return localUses[l->reg] == 0; + } + + void popLocals(size_t start) + { + LUAU_ASSERT(start <= localStack.size()); + + for (size_t i = start; i < localStack.size(); ++i) + { + Local* l = locals.find(localStack[i]); + LUAU_ASSERT(l); + + if (FFlag::LuauCompileReuseLocalRegs) + { + if (l->reused) + { + // Local going out of scope completely, its register can no longer be reused (array is sorted, so binary search here) + auto it = std::lower_bound( + freeLocalRegs.begin(), + freeLocalRegs.end(), + l->reg, + [](const FreeLocal& el, uint8_t pred) + { + return el.reg < pred; + } + ); + if (it != freeLocalRegs.end() && it->reg == l->reg) + { + recordLocalDebugInfo(it->astLocal, &it->local, it->isArgument, bytecode.getDebugPC()); + freeLocalRegs.erase(it); + } + + l->allocated = false; + l->reused = false; + } + else if (l->allocated) + { + releaseLocal(l); + recordLocalDebugInfo(localStack[i], l, i < argCount, bytecode.getDebugPC()); + } + } + else + { + LUAU_ASSERT(l->allocated); + l->allocated = false; + + if (FFlag::LuauCompileMoveElision) + regCaptured[l->reg] = false; + + if (options.debugLevel >= 2) + { + uint32_t debugpc = bytecode.getDebugPC(); + + bytecode.pushDebugLocal(sref(localStack[i]->name), l->reg, l->debugpc, debugpc); + } + + if (options.typeInfoLevel >= 1 && i >= argCount) + { + uint32_t debugpc = bytecode.getDebugPC(); + LuauBytecodeType ty = LBC_TYPE_ANY; + + if (LuauBytecodeType* recordedTy = localTypes.find(localStack[i])) + ty = *recordedTy; + + bytecode.pushLocalTypeInfo(ty, l->reg, l->allocpc, debugpc); + } + } + } + + localStack.resize(start); + } + + void popStatLocals(AstStatBlock* owner, AstStat* beforeStat, size_t start) + { + if (options.optimizationLevel == 0) + return; + + LUAU_ASSERT(FFlag::LuauCompileReuseLocalRegs); + LUAU_ASSERT(start <= localStack.size()); + + for (size_t i = start; i < localStack.size(); ++i) + { + Local* l = locals.find(localStack[i]); + LUAU_ASSERT(l); + + if (!l->allocated || l->reused) + continue; + + if (l->captured) + continue; + + auto var = variables.find(localStack[i]); + + if (!var) + continue; + + if (var->owner != owner || var->nonLexicalUse) + continue; + + Position localEnd = var->lastUsed ? var->lastUsed->location.end : Position::missing(); + Position statStart = beforeStat->location.begin; + + if (!var->lastUsed || statStart.line > localEnd.line || (statStart.line == localEnd.line && statStart.column > localEnd.column)) + { + if (releaseLocal(l)) + { + FreeLocal free; + free.reg = l->reg; + free.local = *l; + free.astLocal = localStack[i]; + free.isArgument = i < argCount; + + // Free registers are ordered from low to high as it is better to give out registers closer to regTop + auto it = std::lower_bound( + freeLocalRegs.begin(), + freeLocalRegs.end(), + l->reg, + [](const FreeLocal& el, uint8_t pred) + { + return el.reg < pred; + } + ); + freeLocalRegs.insert(it, free); + + l->reused = true; + } + else + { + recordLocalDebugInfo(localStack[i], l, i < argCount, bytecode.getDebugPC()); + } + } + } + } + + void patchJump(AstNode* node, size_t label, size_t target) + { + if (!bytecode.patchJumpD(label, target)) + CompileError::raise(node->location, "Exceeded jump distance limit; simplify the code to compile"); + } + + void patchJumps(AstNode* node, std::vector& labels, size_t target) + { + for (size_t l : labels) + patchJump(node, l, target); + } + + void patchLoopJumps(AstNode* node, size_t oldJumps, size_t endLabel, size_t contLabel) + { + LUAU_ASSERT(oldJumps <= loopJumps.size()); + + for (size_t i = oldJumps; i < loopJumps.size(); ++i) + { + const LoopJump& lj = loopJumps[i]; + + switch (lj.type) + { + case LoopJump::Break: + patchJump(node, lj.label, endLabel); + break; + + case LoopJump::Continue: + patchJump(node, lj.label, contLabel); + break; + + default: + LUAU_ASSERT(!"Unknown loop jump type"); + } + } + } + + uint8_t allocReg(AstNode* node, unsigned int count) + { + unsigned int top = regTop; + if (top + count > kMaxRegisterCount) + CompileError::raise(node->location, "Out of registers when trying to allocate %d registers: exceeded limit %d", count, kMaxRegisterCount); + + regTop += count; + stackSize = std::max(stackSize, regTop); + + return uint8_t(top); + } + + template + uint8_t allocReg(AstNode* node, T count) = delete; + + bool tryReuseLocalReg(AstLocal* local, uint8_t& reg) + { + LUAU_ASSERT(FFlag::LuauCompileReuseLocalRegs); + + if (local->isExported || freeLocalRegs.empty()) + return false; + + Variable* lv = variables.find(local); + + if (!lv) + return false; + + Local* l = locals.find(local); + + if (l && l->captured) + return false; + + FreeLocal free = freeLocalRegs.back(); + freeLocalRegs.pop_back(); + + recordLocalDebugInfo(free.astLocal, &free.local, free.isArgument, bytecode.getDebugPC()); + reg = free.reg; + + LUAU_ASSERT(reg < regTop); + return true; + } + + void setDebugLine(AstNode* node) + { + if (options.debugLevel >= 1) + bytecode.setDebugLine(node->location.begin.line + 1); + } + + void setDebugLine(const Location& location) + { + if (options.debugLevel >= 1) + bytecode.setDebugLine(location.begin.line + 1); + } + + void setDebugLineEnd(AstNode* node) + { + if (options.debugLevel >= 1) + bytecode.setDebugLine(node->location.end.line + 1); + } + + bool needsCoverage(AstNode* node) + { + return !node->is() && !node->is(); + } + + void hintTemporaryRegType(AstExpr* expr, int reg, LuauBytecodeType expectedType, int instLength) + { + // If we know the type of a temporary and it's not the type that would be expected by codegen, provide a hint + if (LuauBytecodeType* ty = exprTypes.find(expr)) + { + if (*ty != expectedType) + bytecode.pushLocalTypeInfo(*ty, reg, bytecode.getDebugPC() - instLength, bytecode.getDebugPC()); + } + } + + void hintTemporaryExprRegType(AstExpr* expr, int reg, LuauBytecodeType expectedType, int instLength) + { + // If we allocated a temporary register for the operation argument, try hinting its type + if (!getExprLocal(expr)) + hintTemporaryRegType(expr, reg, expectedType, instLength); + } + + struct FenvVisitor : AstVisitor + { + bool& getfenvUsed; + bool& setfenvUsed; + + FenvVisitor(bool& getfenvUsed, bool& setfenvUsed) + : getfenvUsed(getfenvUsed) + , setfenvUsed(setfenvUsed) + { + } + + bool visit(AstExprGlobal* node) override + { + if (node->name == "getfenv") + getfenvUsed = true; + if (node->name == "setfenv") + setfenvUsed = true; + + return false; + } + }; + + struct FunctionVisitor : AstVisitor + { + std::vector& functions; + bool hasTypes = false; + bool hasNativeFunction = false; + + FunctionVisitor(std::vector& functions) + : functions(functions) + { + // preallocate the result; this works around std::vector's inefficient growth policy for small arrays + functions.reserve(16); + } + + bool visit(AstExprFunction* node) override + { + node->body->visit(this); + + for (AstLocal* arg : node->args) + hasTypes |= arg->annotation != nullptr; + + // this makes sure all functions that are used when compiling this one have been already added to the vector + LUAU_ASSERT(functions.end() == std::find(functions.begin(), functions.end(), node)); + functions.push_back(node); + + if (!hasNativeFunction && node->hasNativeAttribute()) + hasNativeFunction = true; + + return false; + } + + bool visit(AstStatTypeFunction* node) override + { + return false; + } + }; + + struct UndefinedLocalVisitor : AstVisitor + { + UndefinedLocalVisitor(Compiler* self) + : self(self) + , undef(nullptr) + { + } + + void check(AstLocal* local) + { + if (!undef && locals.contains(local)) + undef = local; + } + + bool visit(AstExprLocal* node) override + { + if (!node->upvalue) + check(node->local); + + return false; + } + + bool visit(AstExprFunction* node) override + { + const Function* f = self->functions.find(node); + LUAU_ASSERT(f); + + for (AstLocal* uv : f->upvals) + { + LUAU_ASSERT(uv->functionDepth < node->functionDepth); + + if (uv->functionDepth == node->functionDepth - 1) + check(uv); + } + + return false; + } + + Compiler* self; + AstLocal* undef; + DenseHashSet locals; + }; + + struct ConstUpvalueVisitor : AstVisitor + { + ConstUpvalueVisitor(Compiler* self) + : self(self) + { + } + + bool visit(AstExprLocal* node) override + { + if (node->upvalue && self->isConstant(node)) + { + upvals.push_back(node->local); + } + + return false; + } + + bool visit(AstExprFunction* node) override + { + // short-circuits the traversal to make it faster + return false; + } + + Compiler* self; + std::vector upvals; + }; + + enum class ReturnKind + { + None, // No returns in the function + Expression, // Function returns some expression (could be a constant) + Parameter, // Function returns one of its parameters + Local, // Function returns one of its locals + + Unknown // Function has multiple conflicting return kinds or return targets (for Parameter/Local) + }; + + struct ReturnVisitor : AstVisitor + { + Compiler* self = nullptr; + AstExprFunction* func = nullptr; + + bool returnsOne = true; + ReturnKind returnKind = ReturnKind::None; + AstLocal* returnLocal = nullptr; // For Parameter/Local ReturnKind + + ReturnVisitor(Compiler* self, AstExprFunction* func) + : self(self) + , func(func) + { + } + + bool visit(AstExpr* expr) override + { + return false; + } + + bool visit(AstStatReturn* stat) override + { + if (FFlag::LuauCompileMoveElision) + { + if (stat->list.size == 1 && !self->isExprMultRet(stat->list.data[0])) + { + if (AstExprLocal* exprLocal = unwrapExprOfType(stat->list.data[0])) + { + ReturnKind kind = ReturnKind::Local; + AstLocal* local = exprLocal->local; + + for (auto el : func->args) + { + if (el == local) + { + kind = ReturnKind::Parameter; + break; + } + } + + if (exprLocal->upvalue) + { + kind = ReturnKind::Expression; + local = nullptr; + } + + if (returnKind == ReturnKind::None) + { + returnKind = kind; + returnLocal = local; + } + else if (returnKind != kind || returnLocal != local) + { + returnKind = ReturnKind::Unknown; + returnLocal = nullptr; + } + } + else + { + if (returnKind == ReturnKind::None) + { + returnKind = ReturnKind::Expression; + } + else if (returnKind != ReturnKind::Expression) + { + returnKind = ReturnKind::Unknown; + returnLocal = nullptr; + } + } + } + else + { + returnsOne = false; + returnKind = ReturnKind::Unknown; + returnLocal = nullptr; + } + } + else + { + returnsOne &= stat->list.size == 1 && !self->isExprMultRet(stat->list.data[0]); + } + + return false; + } + }; + + struct RegScope + { + RegScope(Compiler* self) + : self(self) + , oldTop(self->regTop) + { + } + + // This ctor is useful to forcefully adjust the stack frame in case we know that registers after a certain point are scratch and can be + // discarded + RegScope(Compiler* self, unsigned int top) + : self(self) + , oldTop(self->regTop) + { + LUAU_ASSERT(top <= self->regTop); + self->regTop = top; + } + + ~RegScope() + { + self->regTop = oldTop; + } + + Compiler* self; + unsigned int oldTop; + }; + + struct Function + { + uint32_t id; + std::vector upvals; + + uint64_t costModel = 0; + unsigned int stackSize = 0; + bool canInline = false; + bool returnsOne = false; + + ReturnKind returnKind = ReturnKind::None; + AstLocal* returnLocal = nullptr; // For Parameter/Local ReturnKind + bool returnLocalCaptured = false; + }; + + struct LoopJump + { + enum Type + { + Break, + Continue + }; + + Type type; + size_t label; + }; + + struct Loop + { + size_t localOffset; + size_t localOffsetContinue; + + AstStatContinue* continueUsed; + }; + + struct InlineArg + { + AstLocal* local; + + uint8_t reg; + Constant value; + uint32_t allocpc; + + AstExpr* init; + }; + + struct InlineFrame + { + AstExprFunction* func; + + size_t localOffset; + + uint8_t target; + uint8_t targetCount; + + AstLocal* resultLocal = nullptr; + + std::vector returnJumps; + }; + + struct Capture + { + LuauCaptureType type; + uint8_t data; + }; + + BytecodeBuilder& bytecode; + + CompileOptions options; + + + DenseHashMap functions; + DenseHashMap locals; + DenseHashMap globals; + DenseHashMap variables; + DenseHashMap constants; + DenseHashMap locstants; + DenseHashMap tableConstants; + DenseHashMap tableShapes; + DenseHashMap builtins; + DenseHashMap userdataTypes; + DenseHashMap functionTypes; + DenseHashMap localTypes; + DenseHashMap exprTypes; + DenseHashMap classLocals{}; + + DenseHashMap inlineBuiltins; + DenseHashMap inlineBuiltinsBackup; + + Compile::ExprConstantChangeLog exprChanges; + Compile::LocalConstantChangeLog localChanges; + + BuiltinAstTypes builtinTypes; + AstNameTable& names; + + const DenseHashMap* builtinsFold = nullptr; + bool builtinsFoldLibraryK = false; + + // compileFunction state, gets reset for every function + unsigned int regTop = 0; + unsigned int stackSize = 0; + size_t argCount = 0; + bool hasLoops = false; + bool hasMultiRet = false; + AstExprFunction* currentFunction = nullptr; + + size_t blockDepth = 0; + + bool getfenvUsed = false; + bool setfenvUsed = false; + + std::vector localStack; + std::array regCaptured{}; + std::vector upvals; + std::vector loopJumps; + std::vector loops; + std::vector inlineFrames; + std::vector captures; + + struct FreeLocal + { + uint8_t reg = 0; + + Local local; + AstLocal* astLocal = nullptr; + bool isArgument = false; + }; + std::array localUses{}; + std::vector freeLocalRegs; + + struct Exports + { + AstLocal exportTableLocal; + DenseHashMap exportedClasses; + DenseHashSet exportedFunctions; + std::vector exportedVariables; + int32_t exportedTableCid = -1; + bool hasExports = false; + bool hasTypeExports = false; + + explicit Exports(AstLocal tableLocal) + : exportTableLocal(tableLocal) + { + } + + bool isEmpty() const + { + return !hasExports && exportedClasses.empty() && exportedFunctions.empty() && exportedVariables.empty(); + } + }; + + Exports exports; +}; + +static void setCompileOptionsForNativeCompilation(CompileOptions& options) +{ + options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize + options.typeInfoLevel = 1; +} + +void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, AstNameTable& names, const CompileOptions& inputOptions) +{ + LUAU_TIMETRACE_SCOPE("compileOrThrow", "Compiler"); + + LUAU_ASSERT(parseResult.root); + LUAU_ASSERT(parseResult.errors.empty()); + + CompileOptions options = inputOptions; + uint8_t mainFlags = 0; + + for (const HotComment& hc : parseResult.hotcomments) + { + if (hc.header && hc.content.compare(0, 9, "optimize ") == 0) + options.optimizationLevel = std::max(0, std::min(2, atoi(hc.content.c_str() + 9))); + + if (hc.header && hc.content == "native") + { + mainFlags |= LPF_NATIVE_MODULE; + setCompileOptionsForNativeCompilation(options); + } + } + + AstStatBlock* root = parseResult.root; + + // gathers all functions with the invariant that all function references are to functions earlier in the list + // for example, function foo() return function() end end will result in two vector entries, [0] = anonymous and [1] = foo + std::vector functions; + Compiler::FunctionVisitor functionVisitor(functions); + root->visit(&functionVisitor); + + if (functionVisitor.hasNativeFunction) + setCompileOptionsForNativeCompilation(options); + + Compiler compiler(bytecode, options, names); + + // since access to some global objects may result in values that change over time, we block imports from non-readonly tables + assignMutable(compiler.globals, names, options.mutableGlobals); + + // this pass analyzes mutability of locals/globals and associates locals with their initial values + if (FFlag::LuauOptimizeExportTable) + trackValues( + compiler.globals, compiler.variables, compiler.classLocals, compiler.exports.exportedFunctions, compiler.exports.exportedVariables, root + ); + else + trackValues_DEPRECATED(compiler.globals, compiler.variables, compiler.classLocals, root); + + // this visitor tracks calls to getfenv/setfenv and disables some optimizations when they are found + if (options.optimizationLevel >= 1 && (names.get("getfenv").value || names.get("setfenv").value)) + { + Compiler::FenvVisitor fenvVisitor(compiler.getfenvUsed, compiler.setfenvUsed); + root->visit(&fenvVisitor); + } + + // builtin folding is enabled on optimization level 2 since we can't de-optimize folding at runtime + if (options.optimizationLevel >= 2 && (!compiler.getfenvUsed && !compiler.setfenvUsed)) + { + compiler.builtinsFold = &compiler.builtins; + + if (AstName math = names.get("math"); math.value && getGlobalState(compiler.globals, math) == Global::Default) + { + compiler.builtinsFoldLibraryK = true; + } + else if (const char* const* ptr = options.librariesWithKnownMembers) + { + for (; *ptr; ++ptr) + { + if (AstName name = names.get(*ptr); name.value && getGlobalState(compiler.globals, name) == Global::Default) + { + compiler.builtinsFoldLibraryK = true; + break; + } + } + } + } + + if (options.optimizationLevel >= 1) + { + // this pass tracks which calls are builtins and can be compiled more efficiently + analyzeBuiltins(compiler.builtins, compiler.globals, compiler.variables, options, root, names); + + // this pass determines which locals hold constant tables that are never mutated + buildTableConstantMap(compiler.tableConstants, compiler.variables, root); + + // this pass analyzes constantness of expressions + foldConstants( + compiler.constants, + compiler.variables, + compiler.locstants, + compiler.builtinsFold, + compiler.builtinsFoldLibraryK, + options.vectorPrecision == 1, + options.libraryMemberConstantCb, + root, + names, + compiler.tableConstants + ); + + // this pass analyzes table assignments to estimate table shapes for initially empty tables + predictTableShapes(compiler.tableShapes, root); + } + + if (const char* const* ptr = options.userdataTypes) + { + for (; *ptr; ++ptr) + { + // Type will only resolve to an AstName if it is actually mentioned in the source + if (AstName name = names.get(*ptr); name.value) + compiler.userdataTypes[name] = bytecode.addUserdataType(name.value); + } + + if (uintptr_t(ptr - options.userdataTypes) > (LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE)) + CompileError::raise(root->location, "Exceeded userdata type limit in the compilation options"); + } + + // computes type information for all functions based on type annotations + if (options.typeInfoLevel >= 1 || options.optimizationLevel >= 2) + buildTypeMap( + compiler.functionTypes, + compiler.localTypes, + compiler.exprTypes, + root, + options.vectorType, + compiler.userdataTypes, + compiler.builtinTypes, + compiler.builtins, + compiler.globals, + options.libraryMemberTypeCb, + bytecode + ); + + for (AstExprFunction* expr : functions) + { + uint8_t protoflags = 0; + compiler.compileFunction(expr, protoflags); + + // If a function has native attribute and the whole module is not native, we set LPF_NATIVE_FUNCTION flag + // This ensures that LPF_NATIVE_MODULE and LPF_NATIVE_FUNCTION are exclusive. + if ((protoflags & LPF_NATIVE_FUNCTION) && !(mainFlags & LPF_NATIVE_MODULE)) + mainFlags |= LPF_NATIVE_FUNCTION; + } + + AstExprFunction main( + root->location, + /* attributes= */ AstArray({nullptr, 0}), + /* generics= */ AstArray(), + /* genericPacks= */ AstArray(), + /* self= */ nullptr, + AstArray(), + /* vararg= */ true, + /* varargLocation= */ Luau::Location(), + root, + /* functionDepth= */ 0, + /* debugname= */ AstName(), + /* returnAnnotation= */ nullptr + ); + uint32_t mainid = compiler.compileFunction(&main, mainFlags); + + const Compiler::Function* mainf = compiler.functions.find(&main); + LUAU_ASSERT(mainf && mainf->upvals.empty()); + + bytecode.setMainFunction(mainid); + bytecode.finalize(); +} + +void compileOrThrow(BytecodeBuilder& bytecode, const std::string& source, const CompileOptions& options, const ParseOptions& parseOptions) +{ + Allocator allocator; + AstNameTable names(allocator); + ParseResult result = Parser::parse(source.c_str(), source.size(), names, allocator, parseOptions); + + if (!result.errors.empty()) + throw ParseErrors(result.errors); + + compileOrThrow(bytecode, result, names, options); +} + +std::string compile(const std::string& source, const CompileOptions& options, const ParseOptions& parseOptions, BytecodeEncoder* encoder) +{ + LUAU_TIMETRACE_SCOPE("compile", "Compiler"); + + Allocator allocator; + AstNameTable names(allocator); + ParseResult result = Parser::parse(source.c_str(), source.size(), names, allocator, parseOptions); + + if (!result.errors.empty()) + { + // Users of this function expect only a single error message + const Luau::ParseError& parseError = result.errors.front(); + std::string error = format(":%d: %s", parseError.getLocation().begin.line + 1, parseError.what()); + + return BytecodeBuilder::getError(error); + } + + try + { + BytecodeBuilder bcb(encoder); + compileOrThrow(bcb, result, names, options); + + return bcb.getBytecode(); + } + catch (CompileError& e) + { + std::string error = format(":%d: %s", e.getLocation().begin.line + 1, e.what()); + return BytecodeBuilder::getError(error); + } +} + +void setCompileConstantNil(CompileConstant* constant) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Nil; +} + +void setCompileConstantBoolean(CompileConstant* constant, bool b) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Boolean; + target->valueBoolean = b; +} + +void setCompileConstantNumber(CompileConstant* constant, double n) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Number; + target->valueNumber = n; +} + +void setCompileConstantInteger64(CompileConstant* constant, int64_t l) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Integer; + target->valueInteger64 = l; +} + +void setCompileConstantVector(CompileConstant* constant, float x, float y, float z, float w) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Vectorf; + target->valueVectorf[0] = x; + target->valueVectorf[1] = y; + target->valueVectorf[2] = z; + target->valueVectorf[3] = w; +} + +void setCompileConstantVectord(CompileConstant* constant, double x, double y, double z, double w) +{ + Compile::Constant* target = reinterpret_cast(constant); + + target->type = Compile::Constant::Type_Vectord; + target->valueVectord[0] = x; + target->valueVectord[1] = y; + target->valueVectord[2] = z; + target->valueVectord[3] = w; +} + +void setCompileConstantString(CompileConstant* constant, const char* s, size_t l) +{ + Compile::Constant* target = reinterpret_cast(constant); + + if (l > std::numeric_limits::max()) + CompileError::raise({}, "Exceeded custom string constant length limit"); + + target->type = Compile::Constant::Type_String; + target->stringLength = unsigned(l); + target->valueString = s; +} + +} // namespace Luau diff --git a/External/luau/Compiler/src/ConstantFolding.cpp b/External/luau/Compiler/src/ConstantFolding.cpp new file mode 100644 index 00000000..7d9fde5b --- /dev/null +++ b/External/luau/Compiler/src/ConstantFolding.cpp @@ -0,0 +1,1353 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "ConstantFolding.h" + +#include "BuiltinFolding.h" +#include "Utils.h" +#include "Luau/Bytecode.h" +#include "Luau/Lexer.h" + +#include +#include + +LUAU_FASTFLAG(LuauIntegerType2) +LUAU_FASTFLAG(LuauExperimentalIfLocalSyntax) +LUAU_FASTFLAGVARIABLE(LuauCompileNoFoldVectorEqW) + +namespace Luau +{ +namespace Compile +{ + +constexpr size_t kConstantFoldStringLimit = 4096; + +static bool constantsEqual(const Constant& la, const Constant& ra) +{ + LUAU_ASSERT(la.type != Constant::Type_Unknown && ra.type != Constant::Type_Unknown); + + switch (la.type) + { + case Constant::Type_Nil: + return ra.type == Constant::Type_Nil; + + case Constant::Type_Boolean: + return ra.type == Constant::Type_Boolean && la.valueBoolean == ra.valueBoolean; + + case Constant::Type_Number: + return ra.type == Constant::Type_Number && la.valueNumber == ra.valueNumber; + + case Constant::Type_Vectorf: + return ra.type == Constant::Type_Vectorf && la.valueVectorf[0] == ra.valueVectorf[0] && la.valueVectorf[1] == ra.valueVectorf[1] && + la.valueVectorf[2] == ra.valueVectorf[2] && la.valueVectorf[3] == ra.valueVectorf[3]; + + case Constant::Type_Vectord: + return ra.type == Constant::Type_Vectord && la.valueVectord[0] == ra.valueVectord[0] && la.valueVectord[1] == ra.valueVectord[1] && + la.valueVectord[2] == ra.valueVectord[2] && la.valueVectord[3] == ra.valueVectord[3]; + + case Constant::Type_String: + return ra.type == Constant::Type_String && la.stringLength == ra.stringLength && memcmp(la.valueString, ra.valueString, la.stringLength) == 0; + + case Constant::Type_Table: + return ra.type == Constant::Type_Table && la.valueTable == ra.valueTable; + + case Constant::Type_Integer: + if (FFlag::LuauIntegerType2) + return ra.type == Constant::Type_Integer && la.valueInteger64 == ra.valueInteger64; + [[fallthrough]]; + + default: + LUAU_ASSERT(!"Unexpected constant type in comparison"); + return false; + } +} + +// vector component 'w' is not visible to VM runtime configured with LUA_VECTOR_SIZE == 3, so vectors that only differ in 'w' can't be compared +static bool vectorsDifferOnlyInW(const Constant& la, const Constant& ra) +{ + if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + return la.valueVectorf[0] == ra.valueVectorf[0] && la.valueVectorf[1] == ra.valueVectorf[1] && la.valueVectorf[2] == ra.valueVectorf[2] && + la.valueVectorf[3] != ra.valueVectorf[3]; + + if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + return la.valueVectord[0] == ra.valueVectord[0] && la.valueVectord[1] == ra.valueVectord[1] && la.valueVectord[2] == ra.valueVectord[2] && + la.valueVectord[3] != ra.valueVectord[3]; + + return false; +} + +static void foldUnary(Constant& result, AstExprUnary::Op op, const Constant& arg) +{ + switch (op) + { + case AstExprUnary::Op::Not: + if (arg.type != Constant::Type_Unknown) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = !arg.isTruthful(); + } + break; + + case AstExprUnary::Op::Minus: + if (arg.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = -arg.valueNumber; + } + else if (arg.type == Constant::Type_Vectorf) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = -arg.valueVectorf[0]; + result.valueVectorf[1] = -arg.valueVectorf[1]; + result.valueVectorf[2] = -arg.valueVectorf[2]; + result.valueVectorf[3] = -arg.valueVectorf[3]; + } + else if (arg.type == Constant::Type_Vectord) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = -arg.valueVectord[0]; + result.valueVectord[1] = -arg.valueVectord[1]; + result.valueVectord[2] = -arg.valueVectord[2]; + result.valueVectord[3] = -arg.valueVectord[3]; + } + break; + + case AstExprUnary::Op::Len: + if (arg.type == Constant::Type_String) + { + result.type = Constant::Type_Number; + result.valueNumber = double(arg.stringLength); + } + break; + + default: + LUAU_ASSERT(!"Unexpected unary operation"); + } +} + +static void foldBinary(Constant& result, AstExprBinary::Op op, const Constant& la, const Constant& ra, AstNameTable& stringTable) +{ + switch (op) + { + case AstExprBinary::Add: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = la.valueNumber + ra.valueNumber; + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] + ra.valueVectorf[0]; + result.valueVectorf[1] = la.valueVectorf[1] + ra.valueVectorf[1]; + result.valueVectorf[2] = la.valueVectorf[2] + ra.valueVectorf[2]; + result.valueVectorf[3] = la.valueVectorf[3] + ra.valueVectorf[3]; + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] + ra.valueVectord[0]; + result.valueVectord[1] = la.valueVectord[1] + ra.valueVectord[1]; + result.valueVectord[2] = la.valueVectord[2] + ra.valueVectord[2]; + result.valueVectord[3] = la.valueVectord[3] + ra.valueVectord[3]; + } + break; + + case AstExprBinary::Sub: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = la.valueNumber - ra.valueNumber; + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] - ra.valueVectorf[0]; + result.valueVectorf[1] = la.valueVectorf[1] - ra.valueVectorf[1]; + result.valueVectorf[2] = la.valueVectorf[2] - ra.valueVectorf[2]; + result.valueVectorf[3] = la.valueVectorf[3] - ra.valueVectorf[3]; + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] - ra.valueVectord[0]; + result.valueVectord[1] = la.valueVectord[1] - ra.valueVectord[1]; + result.valueVectord[2] = la.valueVectord[2] - ra.valueVectord[2]; + result.valueVectord[3] = la.valueVectord[3] - ra.valueVectord[3]; + } + break; + + case AstExprBinary::Mul: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = la.valueNumber * ra.valueNumber; + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + { + bool hadW = la.valueVectorf[3] != 0.0f || ra.valueVectorf[3] != 0.0f; + float resultW = la.valueVectorf[3] * ra.valueVectorf[3]; + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] * ra.valueVectorf[0]; + result.valueVectorf[1] = la.valueVectorf[1] * ra.valueVectorf[1]; + result.valueVectorf[2] = la.valueVectorf[2] * ra.valueVectorf[2]; + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectorf) + { + bool hadW = ra.valueVectorf[3] != 0.0f; + float resultW = float(la.valueNumber) * ra.valueVectorf[3]; + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = float(la.valueNumber) * ra.valueVectorf[0]; + result.valueVectorf[1] = float(la.valueNumber) * ra.valueVectorf[1]; + result.valueVectorf[2] = float(la.valueNumber) * ra.valueVectorf[2]; + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectorf[3] != 0.0f; + float resultW = la.valueVectorf[3] * float(ra.valueNumber); + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] * float(ra.valueNumber); + result.valueVectorf[1] = la.valueVectorf[1] * float(ra.valueNumber); + result.valueVectorf[2] = la.valueVectorf[2] * float(ra.valueNumber); + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + { + bool hadW = la.valueVectord[3] != 0.0 || ra.valueVectord[3] != 0.0; + double resultW = la.valueVectord[3] * ra.valueVectord[3]; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] * ra.valueVectord[0]; + result.valueVectord[1] = la.valueVectord[1] * ra.valueVectord[1]; + result.valueVectord[2] = la.valueVectord[2] * ra.valueVectord[2]; + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectord) + { + bool hadW = ra.valueVectord[3] != 0.0; + double resultW = la.valueNumber * ra.valueVectord[3]; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueNumber * ra.valueVectord[0]; + result.valueVectord[1] = la.valueNumber * ra.valueVectord[1]; + result.valueVectord[2] = la.valueNumber * ra.valueVectord[2]; + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectord[3] != 0.0; + double resultW = la.valueVectord[3] * ra.valueNumber; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] * ra.valueNumber; + result.valueVectord[1] = la.valueVectord[1] * ra.valueNumber; + result.valueVectord[2] = la.valueVectord[2] * ra.valueNumber; + result.valueVectord[3] = resultW; + } + } + break; + + case AstExprBinary::Div: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = la.valueNumber / ra.valueNumber; + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + { + bool hadW = la.valueVectorf[3] != 0.0f || ra.valueVectorf[3] != 0.0f; + float resultW = la.valueVectorf[3] / ra.valueVectorf[3]; + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] / ra.valueVectorf[0]; + result.valueVectorf[1] = la.valueVectorf[1] / ra.valueVectorf[1]; + result.valueVectorf[2] = la.valueVectorf[2] / ra.valueVectorf[2]; + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectorf) + { + bool hadW = ra.valueVectorf[3] != 0.0f; + float resultW = float(la.valueNumber) / ra.valueVectorf[3]; + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = float(la.valueNumber) / ra.valueVectorf[0]; + result.valueVectorf[1] = float(la.valueNumber) / ra.valueVectorf[1]; + result.valueVectorf[2] = float(la.valueNumber) / ra.valueVectorf[2]; + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectorf[3] != 0.0f; + float resultW = la.valueVectorf[3] / float(ra.valueNumber); + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = la.valueVectorf[0] / float(ra.valueNumber); + result.valueVectorf[1] = la.valueVectorf[1] / float(ra.valueNumber); + result.valueVectorf[2] = la.valueVectorf[2] / float(ra.valueNumber); + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + { + bool hadW = la.valueVectord[3] != 0.0 || ra.valueVectord[3] != 0.0; + double resultW = la.valueVectord[3] / ra.valueVectord[3]; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] / ra.valueVectord[0]; + result.valueVectord[1] = la.valueVectord[1] / ra.valueVectord[1]; + result.valueVectord[2] = la.valueVectord[2] / ra.valueVectord[2]; + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectord) + { + bool hadW = ra.valueVectord[3] != 0.0; + double resultW = la.valueNumber / ra.valueVectord[3]; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueNumber / ra.valueVectord[0]; + result.valueVectord[1] = la.valueNumber / ra.valueVectord[1]; + result.valueVectord[2] = la.valueNumber / ra.valueVectord[2]; + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectord[3] != 0.0; + double resultW = la.valueVectord[3] / ra.valueNumber; + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = la.valueVectord[0] / ra.valueNumber; + result.valueVectord[1] = la.valueVectord[1] / ra.valueNumber; + result.valueVectord[2] = la.valueVectord[2] / ra.valueNumber; + result.valueVectord[3] = resultW; + } + } + break; + + case AstExprBinary::FloorDiv: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = floor(la.valueNumber / ra.valueNumber); + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Vectorf) + { + bool hadW = la.valueVectorf[3] != 0.0f || ra.valueVectorf[3] != 0.0f; + float resultW = floor(la.valueVectorf[3] / ra.valueVectorf[3]); + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = floor(la.valueVectorf[0] / ra.valueVectorf[0]); + result.valueVectorf[1] = floor(la.valueVectorf[1] / ra.valueVectorf[1]); + result.valueVectorf[2] = floor(la.valueVectorf[2] / ra.valueVectorf[2]); + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectorf) + { + bool hadW = ra.valueVectorf[3] != 0.0f; + float resultW = floor(float(la.valueNumber) / ra.valueVectorf[3]); + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = floor(float(la.valueNumber) / ra.valueVectorf[0]); + result.valueVectorf[1] = floor(float(la.valueNumber) / ra.valueVectorf[1]); + result.valueVectorf[2] = floor(float(la.valueNumber) / ra.valueVectorf[2]); + result.valueVectorf[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectorf && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectorf[3] != 0.0f; + float resultW = floor(la.valueVectorf[3] / float(ra.valueNumber)); + + if (resultW == 0.0f || hadW) + { + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = floor(la.valueVectorf[0] / float(ra.valueNumber)); + result.valueVectorf[1] = floor(la.valueVectorf[1] / float(ra.valueNumber)); + result.valueVectorf[2] = floor(la.valueVectorf[2] / float(ra.valueNumber)); + result.valueVectorf[3] = floor(la.valueVectorf[3] / float(ra.valueNumber)); + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Vectord) + { + bool hadW = la.valueVectord[3] != 0.0 || ra.valueVectord[3] != 0.0; + double resultW = floor(la.valueVectord[3] / ra.valueVectord[3]); + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = floor(la.valueVectord[0] / ra.valueVectord[0]); + result.valueVectord[1] = floor(la.valueVectord[1] / ra.valueVectord[1]); + result.valueVectord[2] = floor(la.valueVectord[2] / ra.valueVectord[2]); + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Number && ra.type == Constant::Type_Vectord) + { + bool hadW = ra.valueVectord[3] != 0.0; + double resultW = floor(la.valueNumber / ra.valueVectord[3]); + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = floor(la.valueNumber / ra.valueVectord[0]); + result.valueVectord[1] = floor(la.valueNumber / ra.valueVectord[1]); + result.valueVectord[2] = floor(la.valueNumber / ra.valueVectord[2]); + result.valueVectord[3] = resultW; + } + } + else if (la.type == Constant::Type_Vectord && ra.type == Constant::Type_Number) + { + bool hadW = la.valueVectord[3] != 0.0; + double resultW = floor(la.valueVectord[3] / ra.valueNumber); + + if (resultW == 0.0 || hadW) + { + result.type = Constant::Type_Vectord; + result.valueVectord[0] = floor(la.valueVectord[0] / ra.valueNumber); + result.valueVectord[1] = floor(la.valueVectord[1] / ra.valueNumber); + result.valueVectord[2] = floor(la.valueVectord[2] / ra.valueNumber); + result.valueVectord[3] = floor(la.valueVectord[3] / ra.valueNumber); + } + } + break; + + case AstExprBinary::Mod: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = la.valueNumber - floor(la.valueNumber / ra.valueNumber) * ra.valueNumber; + } + break; + + case AstExprBinary::Pow: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Number; + result.valueNumber = pow(la.valueNumber, ra.valueNumber); + } + break; + + case AstExprBinary::Concat: + if (la.type == Constant::Type_String && ra.type == Constant::Type_String && la.stringLength + ra.stringLength <= kConstantFoldStringLimit) + { + result.type = Constant::Type_String; + result.stringLength = la.stringLength + ra.stringLength; + if (la.stringLength == 0) + result.valueString = ra.valueString; + else if (ra.stringLength == 0) + result.valueString = la.valueString; + else + { + std::string tmp; + tmp.reserve(result.stringLength + 1); + tmp.append(la.valueString, la.stringLength); + tmp.append(ra.valueString, ra.stringLength); + AstName name = stringTable.getOrAdd(tmp.c_str(), result.stringLength); + result.valueString = name.value; + } + } + break; + + case AstExprBinary::CompareNe: + if (la.type != Constant::Type_Unknown && ra.type != Constant::Type_Unknown && + !(FFlag::LuauCompileNoFoldVectorEqW && vectorsDifferOnlyInW(la, ra))) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = !constantsEqual(la, ra); + } + break; + + case AstExprBinary::CompareEq: + if (la.type != Constant::Type_Unknown && ra.type != Constant::Type_Unknown && + !(FFlag::LuauCompileNoFoldVectorEqW && vectorsDifferOnlyInW(la, ra))) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = constantsEqual(la, ra); + } + break; + + case AstExprBinary::CompareLt: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = la.valueNumber < ra.valueNumber; + } + break; + + case AstExprBinary::CompareLe: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = la.valueNumber <= ra.valueNumber; + } + break; + + case AstExprBinary::CompareGt: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = la.valueNumber > ra.valueNumber; + } + break; + + case AstExprBinary::CompareGe: + if (la.type == Constant::Type_Number && ra.type == Constant::Type_Number) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = la.valueNumber >= ra.valueNumber; + } + break; + + case AstExprBinary::And: + if (la.type != Constant::Type_Unknown) + { + result = la.isTruthful() ? ra : la; + } + break; + + case AstExprBinary::Or: + if (la.type != Constant::Type_Unknown) + { + result = la.isTruthful() ? la : ra; + } + break; + + default: + LUAU_ASSERT(!"Unexpected binary operation"); + } +} + +static void foldInterpString(Constant& result, AstExprInterpString* expr, DenseHashMap& constants, AstNameTable& stringTable) +{ + LUAU_ASSERT(expr->strings.size == expr->expressions.size + 1); + size_t resultLength = 0; + for (size_t index = 0; index < expr->strings.size; ++index) + { + resultLength += expr->strings.data[index].size; + if (index < expr->expressions.size) + { + const Constant* c = constants.find(expr->expressions.data[index]); + LUAU_ASSERT(c != nullptr && c->type == Constant::Type::Type_String); + resultLength += c->stringLength; + } + } + + if (resultLength > kConstantFoldStringLimit) + return; + + result.type = Constant::Type_String; + result.stringLength = unsigned(resultLength); + + if (resultLength == 0) + { + result.valueString = ""; + return; + } + + std::string tmp; + tmp.reserve(resultLength); + + for (size_t index = 0; index < expr->strings.size; ++index) + { + AstArray string = expr->strings.data[index]; + tmp.append(string.data, string.size); + if (index < expr->expressions.size) + { + const Constant* c = constants.find(expr->expressions.data[index]); + tmp.append(c->valueString, c->stringLength); + } + } + result.type = Constant::Type_String; + result.stringLength = unsigned(resultLength); + AstName name = stringTable.getOrAdd(tmp.c_str(), resultLength); + result.valueString = name.value; +} + +// Pass to detect which tables are mutated or 'escape' +struct TableMutationTracker : AstVisitor +{ + const DenseHashMap& variables; + + DenseHashSet escaped; + + TableMutationTracker(const DenseHashMap& variables) + : variables(variables) + { + } + + void markEscaped(AstExpr* expr) + { + for (;;) + { + if (AstExprLocal* local = expr->as()) + { + escaped.insert(local->local); + return; + } + else if (AstExprGroup* group = expr->as()) + { + expr = group->expr; + } + else if (AstExprTypeAssertion* assertion = expr->as()) + { + expr = assertion->expr; + } + else if (AstExprInstantiate* inst = expr->as()) + { + expr = inst->expr; + } + else if (AstExprIfElse* ifElse = expr->as()) + { + markEscaped(ifElse->trueExpr); // recurse through true branch + expr = ifElse->falseExpr; // continue loop with false branch + } + else if (AstExprBinary* bin = expr->as()) + { + if (bin->op == AstExprBinary::And || bin->op == AstExprBinary::Or) + { + markEscaped(bin->left); // recurse through lhs + expr = bin->right; // continue loop with rhs + } + else + { + return; + } + } + else + { + return; + } + } + } + + void markEscapedTableIndex(AstExpr* expr, bool isLvalue) + { + if (AstExprIndexName* idx = expr->as()) + { + markEscaped(idx->expr); + } + else if (AstExprIndexExpr* idx = expr->as()) + { + markEscaped(idx->expr); + + if (isLvalue) + markEscaped(idx->index); + } + } + + bool visit(AstExprCall* node) override + { + // Values passed in as arguments can escape + for (AstExpr* arg : node->args) + markEscaped(arg); + + // Table indexed in a self call escapes through 'self' + if (node->self) + markEscapedTableIndex(node->func, false); + + return true; + } + + bool visit(AstExprTable* node) override + { + // Values stored inside a table constructor can escape + for (const AstExprTable::Item& item : node->items) + { + if (item.key) + markEscaped(item.key); + + markEscaped(item.value); + } + + return true; + } + + bool visit(AstStatLocal* node) override + { + // Aliasing a table reference marks the source as escaped + for (size_t i = 0; i < node->values.size && i < node->vars.size; ++i) + markEscaped(node->values.data[i]); + + return true; + } + + bool visit(AstStatAssign* node) override + { + // RHS values that are table locals are being aliased + for (AstExpr* rhs : node->values) + markEscaped(rhs); + + // LHS index expressions mutate the table being indexed + for (AstExpr* lhs : node->vars) + markEscapedTableIndex(lhs, true); + + return true; + } + + bool visit(AstStatCompoundAssign* node) override + { + // LHS index expressions mutate the table + markEscapedTableIndex(node->var, true); + return true; + } + + bool visit(AstStatFunction* node) override + { + // Adding a method on a table mutates it + markEscapedTableIndex(node->name, true); + return true; + } + + bool visit(AstStatForIn* node) override + { + // Iterator state values escape + for (AstExpr* expr : node->values) + markEscaped(expr); + + return true; + } + + bool visit(AstStatReturn* node) override + { + // Returning a table is sometimes safe, but when it's combined with upvalues and local functions, it's very brittle + for (AstExpr* expr : node->list) + markEscaped(expr); + + return true; + } +}; + +struct ConstantVisitor : AstVisitor +{ + DenseHashMap& constants; + DenseHashMap& variables; + DenseHashMap& locals; + + const DenseHashMap* builtins; + bool foldLibraryK = false; + bool vectorDoublePrecision; + LibraryMemberConstantCallback libraryMemberConstantCb; + AstNameTable& stringTable; + std::vector> constantTables; + + bool wasEmpty = false; + + std::vector builtinArgs; + + const DenseHashMap& constantTableLocals; + DenseHashMap tableLocals; + + ExprConstantChangeLog* exprChangeLog = nullptr; + LocalConstantChangeLog* localChangeLog = nullptr; + + ConstantVisitor( + DenseHashMap& constants, + DenseHashMap& variables, + DenseHashMap& locals, + const DenseHashMap* builtins, + bool foldLibraryK, + bool vectorDoublePrecision, + LibraryMemberConstantCallback libraryMemberConstantCb, + AstNameTable& stringTable, + const DenseHashMap& constantTableLocals, + ExprConstantChangeLog* exprChangeLog = nullptr, + LocalConstantChangeLog* localChangeLog = nullptr + ) + : constants(constants) + , variables(variables) + , locals(locals) + , builtins(builtins) + , foldLibraryK(foldLibraryK) + , vectorDoublePrecision(vectorDoublePrecision) + , libraryMemberConstantCb(libraryMemberConstantCb) + , stringTable(stringTable) + , constantTableLocals(constantTableLocals) + , exprChangeLog(exprChangeLog) + , localChangeLog(localChangeLog) + { + // since we do a single pass over the tree, if the initial state was empty we don't need to clear out old entries + wasEmpty = constants.empty() && locals.empty(); + } + + Constant analyze(AstExpr* node) + { + Constant result; + result.type = Constant::Type_Unknown; + + if (AstExprGroup* expr = node->as()) + { + result = analyze(expr->expr); + } + else if (node->is()) + { + result.type = Constant::Type_Nil; + } + else if (AstExprConstantBool* expr = node->as()) + { + result.type = Constant::Type_Boolean; + result.valueBoolean = expr->value; + } + else if (AstExprConstantNumber* expr = node->as()) + { + result.type = Constant::Type_Number; + result.valueNumber = expr->value; + } + else if (AstExprConstantInteger* expr = node->as()) + { + result.type = Constant::Type_Integer; + result.valueInteger64 = expr->value; + } + else if (AstExprConstantString* expr = node->as()) + { + result.type = Constant::Type_String; + result.valueString = expr->value.data; + result.stringLength = unsigned(expr->value.size); + } + else if (AstExprLocal* expr = node->as()) + { + if (const Constant* l = locals.find(expr->local)) + result = *l; + else if (const Constant* l = tableLocals.find(expr->local)) + result = *l; + } + else if (node->is()) + { + // nope + } + else if (node->is()) + { + // nope + } + else if (AstExprCall* expr = node->as()) + { + analyze(expr->func); + + const int* bfid = builtins ? builtins->find(expr) : nullptr; + + if (bfid && *bfid != LBF_NONE) + { + // since recursive calls to analyze() may reuse the vector we need to be careful and preserve existing contents + size_t offset = builtinArgs.size(); + bool canFold = true; + + builtinArgs.reserve(offset + expr->args.size); + + for (size_t i = 0; i < expr->args.size; ++i) + { + Constant ac = analyze(expr->args.data[i]); + + if (ac.type == Constant::Type_Unknown || ac.type == Constant::Type_Table) + canFold = false; + else + builtinArgs.push_back(ac); + } + + if (canFold) + { + LUAU_ASSERT(builtinArgs.size() == offset + expr->args.size); + result = foldBuiltin(stringTable, *bfid, builtinArgs.data() + offset, expr->args.size, vectorDoublePrecision); + } + + builtinArgs.resize(offset); + } + else + { + for (size_t i = 0; i < expr->args.size; ++i) + analyze(expr->args.data[i]); + } + } + else if (AstExprIndexName* expr = node->as()) + { + Constant value = analyze(expr->expr); + if (value.type == Constant::Type_Table) + { + LUAU_ASSERT(value.valueTable < constantTables.size()); + if (value.valueTable < constantTables.size()) + { + const DenseHashMap& props = constantTables[value.valueTable]; + if (const Constant* prop = props.find(expr->index)) + result = *prop; + } + } + else if (value.type == Constant::Type_Vectorf) + { + if (expr->index == "x" || expr->index == "X") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectorf[0]; + } + else if (expr->index == "y" || expr->index == "Y") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectorf[1]; + } + else if (expr->index == "z" || expr->index == "Z") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectorf[2]; + } + + // Do not handle 'w' component because it isn't known if the runtime will be configured in 3-wide or 4-wide mode + // In 3-wide, access to 'w' will call unspecified metamethod or fail + } + else if (value.type == Constant::Type_Vectord) + { + if (expr->index == "x" || expr->index == "X") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectord[0]; + } + else if (expr->index == "y" || expr->index == "Y") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectord[1]; + } + else if (expr->index == "z" || expr->index == "Z") + { + result.type = Constant::Type_Number; + result.valueNumber = value.valueVectord[2]; + } + + // Do not handle 'w' component because it isn't known if the runtime will be configured in 3-wide or 4-wide mode + // In 3-wide, access to 'w' will call unspecified metamethod or fail + } + else if (foldLibraryK) + { + if (AstExprGlobal* eg = expr->expr->as()) + { + if (eg->name == "math") + result = foldBuiltinMath(expr->index); + + // if we have a custom handler and the constant hasn't been resolved + if (libraryMemberConstantCb && result.type == Constant::Type_Unknown) + { + libraryMemberConstantCb(eg->name.value, expr->index.value, reinterpret_cast(&result)); + + if (vectorDoublePrecision && result.type == Constant::Type_Vectorf) + { + Constant copy = result; + result.type = Constant::Type_Vectord; + result.valueVectord[0] = double(copy.valueVectorf[0]); + result.valueVectord[1] = double(copy.valueVectorf[1]); + result.valueVectord[2] = double(copy.valueVectorf[2]); + result.valueVectord[3] = double(copy.valueVectorf[3]); + } + else if (!vectorDoublePrecision && result.type == Constant::Type_Vectord) + { + Constant copy = result; + result.type = Constant::Type_Vectorf; + result.valueVectorf[0] = float(copy.valueVectord[0]); + result.valueVectorf[1] = float(copy.valueVectord[1]); + result.valueVectorf[2] = float(copy.valueVectord[2]); + result.valueVectorf[3] = float(copy.valueVectord[3]); + } + } + } + } + } + else if (AstExprIndexExpr* expr = node->as()) + { + Constant indexVal = analyze(expr->index); + Constant tableVal = analyze(expr->expr); + + if (tableVal.type == Constant::Type_Table && indexVal.type == Constant::Type_String) + { + LUAU_ASSERT(tableVal.valueTable < constantTables.size()); + if (tableVal.valueTable < constantTables.size() && indexVal.stringLength != 0) + { + const DenseHashMap& props = constantTables[tableVal.valueTable]; + AstName indexName = stringTable.getOrAdd(indexVal.valueString, indexVal.stringLength); + if (const Constant* prop = props.find(std::move(indexName))) + result = *prop; + } + } + } + else if (AstExprFunction* expr = node->as()) + { + // this is necessary to propagate constant information in all child functions + expr->body->visit(this); + } + else if (AstExprTable* expr = node->as()) + { + // If expr is a constant table, update result to be a table constant, and insert it into constantTables + DenseHashMap props; + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + + Constant valueVal = analyze(item.value); + + if (item.key) + { + Constant keyVal = analyze(item.key); + + if (keyVal.type == Constant::Type_String && valueVal.type != Constant::Type_Unknown && valueVal.type != Constant::Type_Table && + keyVal.stringLength != 0) + { + AstName constKey = stringTable.getOrAdd(keyVal.valueString, keyVal.stringLength); + + props[std::move(constKey)] = std::move(valueVal); + } + // TODO: Support other types of keys + } + } + + if (props.size() == expr->items.size) + { + result.type = Constant::Type_Table; + result.valueTable = constantTables.size(); + constantTables.push_back(std::move(props)); + } + } + else if (AstExprUnary* expr = node->as()) + { + Constant arg = analyze(expr->expr); + + if (arg.type != Constant::Type_Unknown) + foldUnary(result, expr->op, arg); + } + else if (AstExprBinary* expr = node->as()) + { + Constant la = analyze(expr->left); + Constant ra = analyze(expr->right); + + // note: ra doesn't need to be constant to fold and/or + if (la.type != Constant::Type_Unknown) + foldBinary(result, expr->op, la, ra, stringTable); + } + else if (AstExprTypeAssertion* expr = node->as()) + { + Constant arg = analyze(expr->expr); + + result = arg; + } + else if (AstExprIfElse* expr = node->as()) + { + Constant cond = analyze(expr->condition); + Constant trueExpr = analyze(expr->trueExpr); + Constant falseExpr = analyze(expr->falseExpr); + + if (cond.type != Constant::Type_Unknown) + result = cond.isTruthful() ? trueExpr : falseExpr; + } + else if (AstExprInterpString* expr = node->as()) + { + bool onlyConstantSubExpr = true; + for (AstExpr* expression : expr->expressions) + if (analyze(expression).type != Constant::Type_String) + onlyConstantSubExpr = false; + + if (onlyConstantSubExpr) + foldInterpString(result, expr, constants, stringTable); + } + else if (AstExprInstantiate* expr = node->as()) + { + result = analyze(expr->expr); + } + else + { + LUAU_ASSERT(!"Unknown expression type"); + } + + recordConstant(constants, node, result); + + return result; + } + + template + void recordConstant(DenseHashMap& map, T key, const Constant& value) + { + if (value.type == Constant::Type_Table) + { + // Table constants are recorded in a separate map + } + else if (value.type != Constant::Type_Unknown) + { + logChange(map, key); + map[key] = value; + } + else if (wasEmpty) + { + // No need to clear out entries if we started with empty maps + } + else if (Constant* old = map.find(key)) + { + logChange(map, key, old); + old->type = Constant::Type_Unknown; + } + } + + void logChange(DenseHashMap& map, AstExpr* key, const Constant* existing = nullptr) + { + if (!exprChangeLog) + return; + + const Constant* old = existing ? existing : map.find(key); + exprChangeLog->push_back({key, old ? *old : Constant{}, old == nullptr}); + } + + void logChange(DenseHashMap& map, AstLocal* key, const Constant* existing = nullptr) + { + if (!localChangeLog) + return; + + const Constant* old = existing ? existing : map.find(key); + localChangeLog->push_back({key, old ? *old : Constant{}, old == nullptr}); + } + + void recordValue(AstLocal* local, const Constant& value) + { + // note: we rely on trackValues to have been run before us + Variable* v = variables.find(local); + LUAU_ASSERT(v); + + if (!v->written) + { + if (value.type == Constant::Type_Table) + { + v->constant = false; + tableLocals[local] = value; + } + else + { + v->constant = (value.type != Constant::Type_Unknown); + recordConstant(locals, local, value); + } + } + } + + bool visit(AstExpr* node) override + { + // note: we short-circuit the visitor traversal through any expression trees by returning false + // recursive traversal is happening inside analyze() which makes it easier to get the resulting value of the subexpression + analyze(node); + + return false; + } + + void recordLocal(AstLocal* local, AstExpr* value) + { + Constant arg = analyze(value); + + if (arg.type == Constant::Type_Table) + { + // If this table could be mutated later, record Constant_Unknown instead of Constant_Table + const TableConstantKind* kind = constantTableLocals.find(local); + if (kind && *kind == ConstantTable) + recordValue(local, arg); + else + recordValue(local, {}); + } + else + { + recordValue(local, arg); + } + } + + bool visit(AstStatLocal* node) override + { + // all values that align wrt indexing are simple - we just match them 1-1 + for (size_t i = 0; i < node->vars.size && i < node->values.size; ++i) + { + if (FFlag::LuauExperimentalIfLocalSyntax) + { + recordLocal(node->vars.data[i], node->values.data[i]); + } + else + { + AstExpr* rhs = node->values.data[i]; + Constant arg = analyze(rhs); + + if (arg.type == Constant::Type_Table) + { + AstLocal* local = node->vars.data[i]; + + // If this table could be mutated later, record Constant_Unknown instead of Constant_Table + const TableConstantKind* kind = constantTableLocals.find(local); + if (kind && *kind == ConstantTable) + recordValue(local, arg); + else + recordValue(local, {}); + } + else + recordValue(node->vars.data[i], arg); + } + } + + if (node->vars.size > node->values.size) + { + // if we have trailing variables, then depending on whether the last value is capable of returning multiple values + // (aka call or varargs), we either don't know anything about these vars, or we know they're nil + AstExpr* last = node->values.size ? node->values.data[node->values.size - 1] : nullptr; + bool multRet = last && (last->is() || last->is()); + + if (!multRet) + { + for (size_t i = node->values.size; i < node->vars.size; ++i) + { + Constant nil = {Constant::Type_Nil}; + recordValue(node->vars.data[i], nil); + } + } + } + else + { + // we can have more values than variables; in this case we still need to analyze them to make sure we do constant propagation inside + // them + for (size_t i = node->vars.size; i < node->values.size; ++i) + analyze(node->values.data[i]); + } + + return false; + } + + bool visit(AstStatIf* node) override + { + if (AstLocal* local = node->conditionLocal) + { + recordLocal(local, node->condition); + + node->thenbody->visit(this); + + if (node->elsebody) + node->elsebody->visit(this); + + return false; + } + + return true; + } + + bool visit(AstExprIfElse* node) override + { + if (node->conditionLocal) + recordLocal(node->conditionLocal, node->condition); + + analyze(node); + return false; + } +}; + +void buildTableConstantMap(DenseHashMap& result, const DenseHashMap& variables, AstNode* root) +{ + TableMutationTracker tracker{variables}; + root->visit(&tracker); + + for (auto& [local, var] : variables) + { + if (var.written) + continue; + + if (!var.init || !unwrapExprOfType(var.init)) + continue; + + if (!tracker.escaped.contains(local)) + result[local] = ConstantTable; + } +} + +void undoChanges(DenseHashMap& constants, const ExprConstantChangeLog& changes) +{ + for (auto it = changes.rbegin(); it != changes.rend(); ++it) + { + if (it->wasAbsent) + { + if (Constant* old = constants.find(it->key)) + old->type = Constant::Type_Unknown; + } + else + { + constants[it->key] = it->oldValue; + } + } +} + +void undoChanges(DenseHashMap& locals, const LocalConstantChangeLog& changes) +{ + for (auto it = changes.rbegin(); it != changes.rend(); ++it) + { + if (it->wasAbsent) + { + if (Constant* old = locals.find(it->key)) + old->type = Constant::Type_Unknown; + } + else + { + locals[it->key] = it->oldValue; + } + } +} + +void foldConstants( + DenseHashMap& constants, + DenseHashMap& variables, + DenseHashMap& locals, + const DenseHashMap* builtins, + bool foldLibraryK, + bool vectorDoublePrecision, + LibraryMemberConstantCallback libraryMemberConstantCb, + AstNode* root, + AstNameTable& stringTable, + const DenseHashMap& tableConstants, + ExprConstantChangeLog* exprChangeLog, + LocalConstantChangeLog* localChangeLog +) +{ + ConstantVisitor visitor{ + constants, + variables, + locals, + builtins, + foldLibraryK, + vectorDoublePrecision, + libraryMemberConstantCb, + stringTable, + tableConstants, + exprChangeLog, + localChangeLog + }; + root->visit(&visitor); +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/ConstantFolding.h b/External/luau/Compiler/src/ConstantFolding.h new file mode 100644 index 00000000..2f9199fc --- /dev/null +++ b/External/luau/Compiler/src/ConstantFolding.h @@ -0,0 +1,101 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Compiler.h" + +#include "ValueTracking.h" + +#include + +namespace Luau +{ +namespace Compile +{ + +struct Constant +{ + enum Type + { + Type_Unknown, + Type_Nil, + Type_Boolean, + Type_Number, + Type_Integer, + Type_Vectorf, + Type_Vectord, + Type_String, + Type_Table, + }; + + Type type = Type_Unknown; + unsigned int stringLength = 0; + + union + { + bool valueBoolean; + double valueNumber; + int64_t valueInteger64; + float valueVectorf[4]; + double valueVectord[4]; + size_t valueTable; // index pointing to constant table entry with table's constant properties + const char* valueString = nullptr; // length stored in stringLength + }; + + bool isTruthful() const + { + LUAU_ASSERT(type != Type_Unknown); + return type != Type_Nil && !(type == Type_Boolean && valueBoolean == false); + } + + AstArray getString() const + { + LUAU_ASSERT(type == Type_String); + return {valueString, stringLength}; + } +}; + +enum TableConstantKind +{ + ConstantTable, + NotConstant +}; + +void buildTableConstantMap(DenseHashMap& result, const DenseHashMap& variables, AstNode* root); + +struct ExprConstantChange +{ + AstExpr* key = nullptr; + Constant oldValue; + bool wasAbsent = false; +}; + +struct LocalConstantChange +{ + AstLocal* key = nullptr; + Constant oldValue; + bool wasAbsent = false; +}; + +using ExprConstantChangeLog = std::vector; +using LocalConstantChangeLog = std::vector; + +void undoChanges(DenseHashMap& constants, const ExprConstantChangeLog& changes); +void undoChanges(DenseHashMap& locals, const LocalConstantChangeLog& changes); + +void foldConstants( + DenseHashMap& constants, + DenseHashMap& variables, + DenseHashMap& locals, + const DenseHashMap* builtins, + bool foldLibraryK, + bool vectorDoublePrecision, + LibraryMemberConstantCallback libraryMemberConstantCb, + AstNode* root, + AstNameTable& stringTable, + const DenseHashMap& tableConstants, + ExprConstantChangeLog* exprChangeLog = nullptr, + LocalConstantChangeLog* localChangeLog = nullptr +); + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/CostModel.cpp b/External/luau/Compiler/src/CostModel.cpp new file mode 100644 index 00000000..c8105f52 --- /dev/null +++ b/External/luau/Compiler/src/CostModel.cpp @@ -0,0 +1,481 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "CostModel.h" + +#include "Luau/Bytecode.h" +#include "Luau/Common.h" +#include "Luau/DenseHash.h" + +#include "ConstantFolding.h" +#include "Utils.h" + +#include + +namespace Luau +{ +namespace Compile +{ + +inline uint64_t parallelAddSat(uint64_t x, uint64_t y) +{ + uint64_t r = x + y; + uint64_t s = r & 0x8080808080808080ull; // saturation mask + + return (r ^ s) | (s - (s >> 7)); +} + +static uint64_t parallelMulSat(uint64_t a, int b) +{ + int bs = (b < 127) ? b : 127; + + // multiply every other value by b, yielding 14-bit products + uint64_t l = bs * ((a >> 0) & 0x007f007f007f007full); + uint64_t h = bs * ((a >> 8) & 0x007f007f007f007full); + + // each product is 14-bit, so adding 32768-128 sets high bit iff the sum is 128 or larger without an overflow + uint64_t ls = l + 0x7f807f807f807f80ull; + uint64_t hs = h + 0x7f807f807f807f80ull; + + // we now merge saturation bits as well as low 7-bits of each product into one + uint64_t s = (hs & 0x8000800080008000ull) | ((ls & 0x8000800080008000ull) >> 8); + uint64_t r = ((h & 0x007f007f007f007full) << 8) | (l & 0x007f007f007f007full); + + // the low bits are now correct for values that didn't saturate, and we simply need to mask them if high bit is 1 + return r | (s - (s >> 7)); +} + +struct Cost +{ + static const uint64_t kLiteral = ~0ull; + + // cost model: 8 bytes, where first byte is the baseline cost, and the next 7 bytes are discounts for when variable #i is constant + uint64_t model; + // constant mask: 8-byte 0xff mask; equal to all ff's for literals, for variables only byte #i (1+) is set to align with model + uint64_t constant; + + Cost(int cost = 0, uint64_t constant = 0) + : model(cost < 0x7f ? cost : 0x7f) + , constant(constant) + { + } + + Cost operator+(const Cost& other) const + { + Cost result; + result.model = parallelAddSat(model, other.model); + return result; + } + + Cost& operator+=(const Cost& other) + { + model = parallelAddSat(model, other.model); + constant = 0; + return *this; + } + + Cost operator*(int other) const + { + Cost result; + result.model = parallelMulSat(model, other); + return result; + } + + static Cost fold(const Cost& x, const Cost& y) + { + uint64_t newmodel = parallelAddSat(x.model, y.model); + uint64_t newconstant = x.constant & y.constant; + + // the extra cost for folding is 1; the discount is 1 for the variable that is shared by x&y (or whichever one is used in x/y if the other is + // literal) + uint64_t extra = (newconstant == kLiteral) ? 0 : (1 | (0x0101010101010101ull & newconstant)); + + Cost result; + result.model = parallelAddSat(newmodel, extra); + result.constant = newconstant; + + return result; + } +}; + +struct CostVisitor : AstVisitor +{ + const DenseHashMap& builtins; + const DenseHashMap& constants; + + DenseHashMap vars; + Cost result; + + CostVisitor(const DenseHashMap& builtins, const DenseHashMap& constants) + : builtins(builtins) + , constants(constants) + { + } + + Cost model(AstExpr* node) + { + if (const Constant* c = constants.find(node)) + return Cost(0, Cost::kLiteral); + + if (AstExprGroup* expr = node->as()) + { + return model(expr->expr); + } + else if (node->is() || node->is() || node->is() || + node->is() || node->is()) + { + return Cost(0, Cost::kLiteral); + } + else if (AstExprLocal* expr = node->as()) + { + const uint64_t* i = vars.find(expr->local); + + return Cost(0, i ? *i : 0); // locals typically don't require extra instructions to compute + } + else if (node->is()) + { + return 1; + } + else if (node->is()) + { + return 3; + } + else if (AstExprCall* expr = node->as()) + { + // builtin cost modeling is different from regular calls because we use FASTCALL to compile these + // thus we use a cheaper baseline, don't account for function, and assume constant/local copy is free + const int* bfid = builtins.find(expr); + bool builtin = bfid != nullptr && *bfid != LBF_NONE; + bool builtinShort = builtin && expr->args.size <= 3u; // FASTCALL1/2/3 + + Cost cost = builtin ? 2 : 3; + + if (!builtin) + cost += model(expr->func); + + for (size_t i = 0; i < expr->args.size; ++i) + { + Cost ac = model(expr->args.data[i]); + // for constants/locals we still need to copy them to the argument list + cost += ac.model == 0 && !builtinShort ? Cost(1) : ac; + } + + return cost; + } + else if (AstExprIndexName* expr = node->as()) + { + return model(expr->expr) + 1; + } + else if (AstExprIndexExpr* expr = node->as()) + { + return model(expr->expr) + model(expr->index) + 1; + } + else if (AstExprFunction* expr = node->as()) + { + return 10; // high baseline cost due to allocation + } + else if (AstExprTable* expr = node->as()) + { + Cost cost = 10; // high baseline cost due to allocation + + for (size_t i = 0; i < expr->items.size; ++i) + { + const AstExprTable::Item& item = expr->items.data[i]; + + if (item.key) + cost += model(item.key); + + cost += model(item.value); + cost += 1; + } + + return cost; + } + else if (AstExprUnary* expr = node->as()) + { + return Cost::fold(model(expr->expr), Cost(0, Cost::kLiteral)); + } + else if (AstExprBinary* expr = node->as()) + { + return Cost::fold(model(expr->left), model(expr->right)); + } + else if (AstExprTypeAssertion* expr = node->as()) + { + return model(expr->expr); + } + else if (AstExprIfElse* expr = node->as()) + { + Cost cond = model(expr->condition); + + // propagate constant mask from condition to the local variable if it exists + if (expr->conditionLocal && cond.constant != 0) + vars[expr->conditionLocal] = cond.constant; + + return cond + model(expr->trueExpr) + model(expr->falseExpr) + 2; + } + else if (AstExprInterpString* expr = node->as()) + { + // Baseline cost of string.format + Cost cost = 3; + + for (AstExpr* innerExpression : expr->expressions) + cost += model(innerExpression); + + return cost; + } + else if (AstExprInstantiate* expr = node->as()) + { + return model(expr->expr); + } + else + { + LUAU_ASSERT(!"Unknown expression type"); + return {}; + } + } + + void assign(AstExpr* expr) + { + // variable assignments reset variable mask, so that further uses of this variable aren't discounted + // this doesn't work perfectly with backwards control flow like loops, but is good enough for a single pass + if (AstExprLocal* lv = expr->as()) + if (uint64_t* i = vars.find(lv->local)) + *i = 0; + } + + void loop(AstStatBlock* body, Cost iterCost, int factor = 3) + { + Cost before = result; + + result = Cost(); + body->visit(this); + + result = before + (result + iterCost) * factor; + } + + bool visit(AstExpr* node) override + { + // note: we short-circuit the visitor traversal through any expression trees by returning false + // recursive traversal is happening inside model() which makes it easier to get the resulting value of the subexpression + result += model(node); + + return false; + } + + bool visit(AstStatFor* node) override + { + result += model(node->from); + result += model(node->to); + + if (node->step) + result += model(node->step); + + int tripCount = -1; + double from, to, step = 1; + + if (getNumber(node->from, from) && getNumber(node->to, to) && (!node->step || getNumber(node->step, step))) + tripCount = getTripCount(from, to, step); + + loop(node->body, 1, tripCount < 0 ? 3 : tripCount); + return false; + } + + bool visit(AstStatForIn* node) override + { + for (size_t i = 0; i < node->values.size; ++i) + result += model(node->values.data[i]); + + loop(node->body, 1); + return false; + } + + bool visit(AstStatWhile* node) override + { + Cost condition = model(node->condition); + + loop(node->body, condition); + return false; + } + + bool visit(AstStatRepeat* node) override + { + Cost condition = model(node->condition); + + loop(node->body, condition); + return false; + } + + bool visit(AstStatIf* node) override + { + if (node->conditionLocal) + { + Cost arg = model(node->condition); + + if (arg.constant != 0) + vars[node->conditionLocal] = arg.constant; + } + + if (isConstantFalse(constants, node->condition)) + { + if (node->elsebody) + node->elsebody->visit(this); + return false; + } + + if (isConstantTrue(constants, node->condition)) + { + node->thenbody->visit(this); + return false; + } + + // unconditional 'else' may require a jump after the 'if' body + // note: this ignores cases when 'then' always terminates and also assumes comparison requires an extra instruction which may be false + result += 1 + (node->elsebody && !node->elsebody->is()); + + return true; + } + + bool visit(AstStatLocal* node) override + { + for (size_t i = 0; i < node->values.size; ++i) + { + Cost arg = model(node->values.data[i]); + + // propagate constant mask from expression through variables + if (arg.constant && i < node->vars.size) + vars[node->vars.data[i]] = arg.constant; + + result += arg; + } + + return false; + } + + bool visit(AstStatAssign* node) override + { + for (size_t i = 0; i < node->vars.size; ++i) + assign(node->vars.data[i]); + + for (size_t i = 0; i < node->vars.size || i < node->values.size; ++i) + { + Cost ac; + if (i < node->vars.size) + ac += model(node->vars.data[i]); + if (i < node->values.size) + ac += model(node->values.data[i]); + // local->local or constant->local assignment is not free + result += ac.model == 0 ? Cost(1) : ac; + } + + return false; + } + + bool visit(AstStatCompoundAssign* node) override + { + assign(node->var); + + // if lhs is not a local, setting it requires an extra table operation + result += node->var->is() ? 1 : 2; + + return true; + } + + bool visit(AstStatBreak* node) override + { + result += 1; + + return false; + } + + bool visit(AstStatContinue* node) override + { + result += 1; + + return false; + } + + bool getNumber(AstExpr* node, double& result) + { + if (const Constant* constant = constants.find(node)) + { + if (constant->type == Constant::Type_Number) + { + result = constant->valueNumber; + return true; + } + } + + return false; + } + + bool visit(AstStatBlock* node) override + { + for (size_t i = 0; i < node->body.size; ++i) + { + AstStat* stat = node->body.data[i]; + + stat->visit(this); + + if (alwaysTerminates(constants, stat)) + break; + } + + return false; + } +}; + +uint64_t modelCost( + AstNode* root, + AstLocal* const* vars, + size_t varCount, + const DenseHashMap& builtins, + const DenseHashMap& constants +) +{ + CostVisitor visitor{builtins, constants}; + for (size_t i = 0; i < varCount && i < 7; ++i) + visitor.vars[vars[i]] = 0xffull << (i * 8 + 8); + + root->visit(&visitor); + + return visitor.result.model; +} + +uint64_t modelCost(AstNode* root, AstLocal* const* vars, size_t varCount) +{ + DenseHashMap builtins; + DenseHashMap constants; + + return modelCost(root, vars, varCount, builtins, constants); +} + +int computeCost(uint64_t model, const bool* varsConst, size_t varCount) +{ + int cost = int(model & 0x7f); + + // don't apply discounts to what is likely a saturated sum + if (cost == 0x7f) + return cost; + + for (size_t i = 0; i < varCount && i < 7; ++i) + cost -= int((model >> (i * 8 + 8)) & 0x7f) * varsConst[i]; + + return cost; +} + +int getTripCount(double from, double to, double step) +{ + // we compute trip count in integers because that way we know that the loop math (repeated addition) is precise + int fromi = (from >= -32767 && from <= 32767 && double(int(from)) == from) ? int(from) : INT_MIN; + int toi = (to >= -32767 && to <= 32767 && double(int(to)) == to) ? int(to) : INT_MIN; + int stepi = (step >= -32767 && step <= 32767 && double(int(step)) == step) ? int(step) : INT_MIN; + + if (fromi == INT_MIN || toi == INT_MIN || stepi == INT_MIN || stepi == 0) + return -1; + + if ((stepi < 0 && toi > fromi) || (stepi > 0 && toi < fromi)) + return 0; + + return (toi - fromi) / stepi + 1; +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/CostModel.h b/External/luau/Compiler/src/CostModel.h new file mode 100644 index 00000000..b3770755 --- /dev/null +++ b/External/luau/Compiler/src/CostModel.h @@ -0,0 +1,32 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" + +namespace Luau +{ +namespace Compile +{ + +struct Constant; + +// cost model: 8 bytes, where first byte is the baseline cost, and the next 7 bytes are discounts for when variable #i is constant +uint64_t modelCost( + AstNode* root, + AstLocal* const* vars, + size_t varCount, + const DenseHashMap& builtins, + const DenseHashMap& constants +); +// when additional data is not available, used to test the cost model +uint64_t modelCost(AstNode* root, AstLocal* const* vars, size_t varCount); + +// cost is computed as B - sum(Di * Ci), where B is baseline cost, Di is the discount for each variable and Ci is 1 when variable #i is constant +int computeCost(uint64_t model, const bool* varsConst, size_t varCount); + +// get loop trip count or -1 if we can't compute it precisely +int getTripCount(double from, double to, double step); + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/TableShape.cpp b/External/luau/Compiler/src/TableShape.cpp new file mode 100644 index 00000000..06e95071 --- /dev/null +++ b/External/luau/Compiler/src/TableShape.cpp @@ -0,0 +1,155 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "TableShape.h" + +namespace Luau +{ +namespace Compile +{ + +// conservative limit for the loop bound that establishes table array size +static const int kMaxLoopBound = 16; + +static AstExprTable* getTableHint(AstExpr* expr) +{ + // unadorned table literal + if (AstExprTable* table = expr->as()) + return table; + + // setmetatable(table literal, ...) + if (AstExprCall* call = expr->as(); call && !call->self && call->args.size == 2) + if (AstExprGlobal* func = call->func->as(); func && func->name == "setmetatable") + if (AstExprTable* table = call->args.data[0]->as()) + return table; + + return nullptr; +} + +struct ShapeVisitor : AstVisitor +{ + struct Hasher + { + size_t operator()(const std::pair& p) const + { + return DenseHashPointer()(p.first) ^ std::hash()(p.second); + } + }; + + DenseHashMap& shapes; + + DenseHashMap tables; + DenseHashSet, Hasher> fields; + + DenseHashMap loops; // iterator => upper bound for 1..k + + ShapeVisitor(DenseHashMap& shapes) + : shapes(shapes) + { + } + + void assignField(AstExpr* expr, AstName index) + { + if (AstExprLocal* lv = expr->as()) + { + if (AstExprTable** table = tables.find(lv->local)) + { + std::pair field = {*table, index}; + + if (!fields.contains(field)) + { + fields.insert(field); + shapes[*table].hashSize += 1; + } + } + } + } + + void assignField(AstExpr* expr, AstExpr* index) + { + AstExprLocal* lv = expr->as(); + if (!lv) + return; + + AstExprTable** table = tables.find(lv->local); + if (!table) + return; + + if (AstExprConstantNumber* number = index->as()) + { + TableShape& shape = shapes[*table]; + + if (number->value == double(shape.arraySize + 1)) + shape.arraySize += 1; + } + else if (AstExprLocal* iter = index->as()) + { + if (const unsigned int* bound = loops.find(iter->local)) + { + TableShape& shape = shapes[*table]; + + if (shape.arraySize == 0) + shape.arraySize = *bound; + } + } + } + + void assign(AstExpr* var) + { + if (AstExprIndexName* index = var->as()) + { + assignField(index->expr, index->index); + } + else if (AstExprIndexExpr* index = var->as()) + { + assignField(index->expr, index->index); + } + } + + bool visit(AstStatLocal* node) override + { + // track local -> table association so that we can update table size prediction in assignField + if (node->vars.size == 1 && node->values.size == 1) + if (AstExprTable* table = getTableHint(node->values.data[0]); table && table->items.size == 0) + tables[node->vars.data[0]] = table; + + return true; + } + + bool visit(AstStatAssign* node) override + { + for (size_t i = 0; i < node->vars.size; ++i) + assign(node->vars.data[i]); + + for (size_t i = 0; i < node->values.size; ++i) + node->values.data[i]->visit(this); + + return false; + } + + bool visit(AstStatFunction* node) override + { + assign(node->name); + node->func->visit(this); + + return false; + } + + bool visit(AstStatFor* node) override + { + AstExprConstantNumber* from = node->from->as(); + AstExprConstantNumber* to = node->to->as(); + + if (from && to && from->value == 1.0 && to->value >= 1.0 && to->value <= double(kMaxLoopBound) && !node->step) + loops[node->var] = unsigned(to->value); + + return true; + } +}; + +void predictTableShapes(DenseHashMap& shapes, AstNode* root) +{ + ShapeVisitor visitor{shapes}; + root->visit(&visitor); +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/TableShape.h b/External/luau/Compiler/src/TableShape.h new file mode 100644 index 00000000..b2ecb8ae --- /dev/null +++ b/External/luau/Compiler/src/TableShape.h @@ -0,0 +1,21 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" + +namespace Luau +{ +namespace Compile +{ + +struct TableShape +{ + unsigned int arraySize = 0; + unsigned int hashSize = 0; +}; + +void predictTableShapes(DenseHashMap& shapes, AstNode* root); + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/Types.cpp b/External/luau/Compiler/src/Types.cpp new file mode 100644 index 00000000..d490f61f --- /dev/null +++ b/External/luau/Compiler/src/Types.cpp @@ -0,0 +1,968 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Types.h" + +#include "Luau/BytecodeBuilder.h" + +LUAU_FASTFLAG(LuauIntegerFastcalls) +LUAU_FASTFLAGVARIABLE(LuauCompileRecursiveAliases) + +namespace Luau +{ + +static bool isGeneric(AstName name, const AstArray& generics) +{ + for (const AstGenericType* gt : generics) + if (gt->name == name) + return true; + + return false; +} + +static LuauBytecodeType getPrimitiveType(AstName name) +{ + if (name == "nil") + return LBC_TYPE_NIL; + else if (name == "boolean") + return LBC_TYPE_BOOLEAN; + else if (name == "number") + return LBC_TYPE_NUMBER; + else if (name == "integer") + return LBC_TYPE_INTEGER; + else if (name == "string") + return LBC_TYPE_STRING; + else if (name == "thread") + return LBC_TYPE_THREAD; + else if (name == "buffer") + return LBC_TYPE_BUFFER; + else if (name == "vector") + return LBC_TYPE_VECTOR; + else if (name == "any" || name == "unknown") + return LBC_TYPE_ANY; + else + return LBC_TYPE_INVALID; +} + +static LuauBytecodeType getType( + const AstType* ty, + const AstArray& generics, + const DenseHashMap& typeAliases, + const char* hostVectorType, + const DenseHashMap& userdataTypes, + BytecodeBuilder& bytecode, + DenseHashSet& seenAliases +) +{ + if (const AstTypeReference* ref = ty->as()) + { + if (ref->prefix) + return LBC_TYPE_ANY; + + if (AstStatTypeAlias* const* alias = typeAliases.find(ref->name); alias && *alias) + { + if (seenAliases.contains((*alias)->name)) + { + if (!FFlag::LuauCompileRecursiveAliases) + seenAliases.clear(); + + return LBC_TYPE_ANY; + } + else if (FFlag::LuauCompileRecursiveAliases) + { + seenAliases.insert(ref->name); + LuauBytecodeType type = getType((*alias)->type, (*alias)->generics, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + seenAliases.erase(ref->name); + return type; + } + else + { + seenAliases.insert(ref->name); + return getType((*alias)->type, (*alias)->generics, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + } + } + + if (isGeneric(ref->name, generics)) + return LBC_TYPE_ANY; + + if (hostVectorType && ref->name == hostVectorType) + return LBC_TYPE_VECTOR; + + if (LuauBytecodeType prim = getPrimitiveType(ref->name); prim != LBC_TYPE_INVALID) + return prim; + + if (const uint8_t* userdataIndex = userdataTypes.find(ref->name)) + { + bytecode.useUserdataType(*userdataIndex); + return LuauBytecodeType(LBC_TYPE_TAGGED_USERDATA_BASE + *userdataIndex); + } + + // not primitive or alias or generic => host-provided, we assume userdata for now + return LBC_TYPE_USERDATA; + } + else if (const AstTypeTable* table = ty->as()) + { + return LBC_TYPE_TABLE; + } + else if (const AstTypeFunction* func = ty->as()) + { + return LBC_TYPE_FUNCTION; + } + else if (const AstTypeUnion* un = ty->as()) + { + bool optional = false; + LuauBytecodeType type = LBC_TYPE_INVALID; + + for (AstType* ty : un->types) + { + LuauBytecodeType et = getType(ty, generics, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + + if (et == LBC_TYPE_NIL) + { + optional = true; + continue; + } + + if (type == LBC_TYPE_INVALID) + { + type = et; + continue; + } + + if (type != et) + return LBC_TYPE_ANY; + } + + if (type == LBC_TYPE_INVALID) + return LBC_TYPE_ANY; + + return LuauBytecodeType(type | (optional && (type != LBC_TYPE_ANY) ? LBC_TYPE_OPTIONAL_BIT : 0)); + } + else if (const AstTypeIntersection* inter = ty->as()) + { + return LBC_TYPE_ANY; + } + else if (const AstTypeGroup* group = ty->as()) + { + return getType(group->type, generics, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + } + else if (const AstTypeOptional* optional = ty->as()) + { + return LBC_TYPE_NIL; + } + else if (ty->is()) + { + return LBC_TYPE_BOOLEAN; + } + else if (ty->is()) + { + return LBC_TYPE_STRING; + } + + return LBC_TYPE_ANY; +} + +static std::string getFunctionType( + const AstExprFunction* func, + const DenseHashMap& typeAliases, + const char* hostVectorType, + const DenseHashMap& userdataTypes, + BytecodeBuilder& bytecode +) +{ + bool self = func->self != 0; + + std::string typeInfo; + typeInfo.reserve(func->args.size + self + 2); + + typeInfo.push_back(LBC_TYPE_FUNCTION); + typeInfo.push_back(uint8_t(self + func->args.size)); + + if (self) + typeInfo.push_back(LBC_TYPE_TABLE); + + bool haveNonAnyParam = false; + for (AstLocal* arg : func->args) + { + DenseHashSet seenAliases; + LuauBytecodeType ty = arg->annotation + ? getType(arg->annotation, func->generics, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases) + : LBC_TYPE_ANY; + + if (ty != LBC_TYPE_ANY) + haveNonAnyParam = true; + + typeInfo.push_back(ty); + } + + // If all parameters simplify to any, we can just omit type info for this function + if (!haveNonAnyParam) + return {}; + + return typeInfo; +} + +static bool isMatchingGlobal(const DenseHashMap& globals, AstExpr* node, const char* name) +{ + if (AstExprGlobal* expr = node->as()) + return Compile::getGlobalState(globals, expr->name) == Compile::Global::Default && expr->name == name; + + return false; +} + +static bool isMatchingGlobalMember( + const DenseHashMap& globals, + AstExprIndexName* expr, + const char* library, + const char* member +) +{ + if (AstExprGlobal* object = expr->expr->as()) + return getGlobalState(globals, object->name) == Compile::Global::Default && object->name == library && expr->index == member; + + return false; +} + +struct TypeMapVisitor : AstVisitor +{ + DenseHashMap& functionTypes; + DenseHashMap& localTypes; + DenseHashMap& exprTypes; + const char* hostVectorType = nullptr; + const DenseHashMap& userdataTypes; + const BuiltinAstTypes& builtinTypes; + const DenseHashMap& builtinCalls; + const DenseHashMap& globals; + LibraryMemberTypeCallback libraryMemberTypeCb = nullptr; + BytecodeBuilder& bytecode; + + DenseHashMap typeAliases; + std::vector> typeAliasStack; + DenseHashMap resolvedLocals; + DenseHashMap resolvedExprs; + DenseHashMap functionReturnTypes; + + TypeMapVisitor( + DenseHashMap& functionTypes, + DenseHashMap& localTypes, + DenseHashMap& exprTypes, + const char* hostVectorType, + const DenseHashMap& userdataTypes, + const BuiltinAstTypes& builtinTypes, + const DenseHashMap& builtinCalls, + const DenseHashMap& globals, + LibraryMemberTypeCallback libraryMemberTypeCb, + BytecodeBuilder& bytecode + ) + : functionTypes(functionTypes) + , localTypes(localTypes) + , exprTypes(exprTypes) + , hostVectorType(hostVectorType) + , userdataTypes(userdataTypes) + , builtinTypes(builtinTypes) + , builtinCalls(builtinCalls) + , globals(globals) + , libraryMemberTypeCb(libraryMemberTypeCb) + , bytecode(bytecode) + { + } + + size_t pushTypeAliases(AstStatBlock* block) + { + size_t aliasStackTop = typeAliasStack.size(); + + for (AstStat* stat : block->body) + if (AstStatTypeAlias* alias = stat->as()) + { + AstStatTypeAlias*& prevAlias = typeAliases[alias->name]; + + typeAliasStack.push_back(std::make_pair(alias->name, prevAlias)); + prevAlias = alias; + } + + return aliasStackTop; + } + + void popTypeAliases(size_t aliasStackTop) + { + while (typeAliasStack.size() > aliasStackTop) + { + std::pair& top = typeAliasStack.back(); + + typeAliases[top.first] = top.second; + typeAliasStack.pop_back(); + } + } + + const AstType* resolveAliases_DEPRECATED(const AstType* ty) + { + if (const AstTypeReference* ref = ty->as()) + { + if (ref->prefix) + return ty; + + if (AstStatTypeAlias* const* alias = typeAliases.find(ref->name); alias && *alias) + return (*alias)->type; + } + + return ty; + } + + const AstTableIndexer* tryGetTableIndexer(AstExpr* expr) + { + if (const AstType** typePtr = resolvedExprs.find(expr)) + { + if (const AstTypeTable* tableTy = (*typePtr)->as()) + return tableTy->indexer; + } + + return nullptr; + } + + LuauBytecodeType recordResolvedType(AstExpr* expr, const AstType* ty) + { + ty = resolveAliases_DEPRECATED(ty); + + resolvedExprs[expr] = ty; + + DenseHashSet seenAliases; + LuauBytecodeType bty = getType(ty, {}, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + exprTypes[expr] = bty; + return bty; + } + + LuauBytecodeType recordResolvedType(AstLocal* local, const AstType* ty) + { + ty = resolveAliases_DEPRECATED(ty); + + resolvedLocals[local] = ty; + + DenseHashSet seenAliases; + LuauBytecodeType bty = getType(ty, {}, typeAliases, hostVectorType, userdataTypes, bytecode, seenAliases); + + if (bty != LBC_TYPE_ANY) + localTypes[local] = bty; + + return bty; + } + + bool visit(AstStatBlock* node) override + { + size_t aliasStackTop = pushTypeAliases(node); + + for (AstStat* stat : node->body) + stat->visit(this); + + popTypeAliases(aliasStackTop); + + return false; + } + + // repeat..until scoping rules are such that condition (along with any possible functions declared in it) has aliases from repeat body in scope + bool visit(AstStatRepeat* node) override + { + size_t aliasStackTop = pushTypeAliases(node->body); + + for (AstStat* stat : node->body->body) + stat->visit(this); + + node->condition->visit(this); + + popTypeAliases(aliasStackTop); + + return false; + } + + bool visit(AstStatFor* node) override + { + recordResolvedType(node->var, &builtinTypes.numberType); + + return true; // Let generic visitor step into all expressions + } + + // for...in statement can contain type annotations on locals (we might even infer some for ipairs/pairs/generalized iteration) + bool visit(AstStatForIn* node) override + { + for (AstExpr* expr : node->values) + expr->visit(this); + + // This is similar to how Compiler matches builtin iteration, but we also handle generalized iteration case + if (node->vars.size == 2 && node->values.size == 1) + { + if (AstExprCall* call = node->values.data[0]->as(); call && call->args.size == 1) + { + AstExpr* func = call->func; + AstExpr* arg = call->args.data[0]; + + if (isMatchingGlobal(globals, func, "ipairs")) + { + if (const AstTableIndexer* indexer = tryGetTableIndexer(arg)) + { + recordResolvedType(node->vars.data[0], &builtinTypes.numberType); + recordResolvedType(node->vars.data[1], indexer->resultType); + } + } + else if (isMatchingGlobal(globals, func, "pairs")) + { + if (const AstTableIndexer* indexer = tryGetTableIndexer(arg)) + { + recordResolvedType(node->vars.data[0], indexer->indexType); + recordResolvedType(node->vars.data[1], indexer->resultType); + } + } + } + else if (const AstTableIndexer* indexer = tryGetTableIndexer(node->values.data[0])) + { + recordResolvedType(node->vars.data[0], indexer->indexType); + recordResolvedType(node->vars.data[1], indexer->resultType); + } + } + + for (size_t i = 0; i < node->vars.size; i++) + { + AstLocal* var = node->vars.data[i]; + + if (AstType* annotation = var->annotation) + recordResolvedType(var, annotation); + } + + node->body->visit(this); + + return false; + } + + bool visit(AstStatLocalFunction* node) override + { + if (node->func->returnAnnotation != nullptr) + { + if (AstTypePackExplicit* type = node->func->returnAnnotation->as()) + { + if (type->typeList.types.size >= 1) + functionReturnTypes[node->name] = type->typeList.types.data[0]; + } + } + + return true; // Let generic visitor step into all expressions + } + + bool visit(AstExprFunction* node) override + { + std::string type = getFunctionType(node, typeAliases, hostVectorType, userdataTypes, bytecode); + + if (!type.empty()) + functionTypes[node] = std::move(type); + + return true; // Let generic visitor step into all expressions + } + + bool visit(AstExprLocal* node) override + { + AstLocal* local = node->local; + + if (AstType* annotation = local->annotation) + { + LuauBytecodeType ty = recordResolvedType(node, annotation); + + if (ty != LBC_TYPE_ANY) + localTypes[local] = ty; + } + else if (const AstType** typePtr = resolvedLocals.find(local)) + { + localTypes[local] = recordResolvedType(node, *typePtr); + } + + return false; + } + + bool visit(AstStatLocal* node) override + { + for (AstExpr* expr : node->values) + expr->visit(this); + + for (size_t i = 0; i < node->vars.size; i++) + { + AstLocal* var = node->vars.data[i]; + + // Propagate from the value that's being assigned + // This simple propagation doesn't handle type packs in tail position + if (var->annotation == nullptr) + { + if (i < node->values.size) + { + if (const AstType** typePtr = resolvedExprs.find(node->values.data[i])) + resolvedLocals[var] = *typePtr; + } + } + } + + return false; + } + + bool visit(AstStatIf* node) override + { + if (node->conditionLocal && node->conditionLocal->annotation == nullptr) + { + node->condition->visit(this); + + // Propagate from the value that's being assigned + if (const AstType** typePtr = resolvedExprs.find(node->condition)) + resolvedLocals[node->conditionLocal] = *typePtr; + + node->thenbody->visit(this); + + if (node->elsebody) + node->elsebody->visit(this); + + return false; + } + + return true; + } + + bool visit(AstExprIndexExpr* node) override + { + node->expr->visit(this); + node->index->visit(this); + + if (const AstTableIndexer* indexer = tryGetTableIndexer(node->expr)) + recordResolvedType(node, indexer->resultType); + + return false; + } + + bool visit(AstExprIndexName* node) override + { + node->expr->visit(this); + + if (const AstType** typePtr = resolvedExprs.find(node->expr)) + { + if (const AstTypeTable* tableTy = (*typePtr)->as()) + { + for (const AstTableProp& prop : tableTy->props) + { + if (prop.name == node->index) + { + recordResolvedType(node, prop.type); + return false; + } + } + } + } + + if (LuauBytecodeType* typeBcPtr = exprTypes.find(node->expr)) + { + if (*typeBcPtr == LBC_TYPE_VECTOR) + { + if (node->index == "X" || node->index == "Y" || node->index == "Z") + { + recordResolvedType(node, &builtinTypes.numberType); + return false; + } + else if (node->index == "x" || node->index == "y" || node->index == "z") + { + recordResolvedType(node, &builtinTypes.numberType); + return false; + } + } + } + + if (isMatchingGlobalMember(globals, node, "vector", "zero") || isMatchingGlobalMember(globals, node, "vector", "one")) + { + recordResolvedType(node, &builtinTypes.vectorType); + return false; + } + + if (libraryMemberTypeCb) + { + if (AstExprGlobal* object = node->expr->as()) + { + if (LuauBytecodeType ty = LuauBytecodeType(libraryMemberTypeCb(object->name.value, node->index.value)); ty != LBC_TYPE_ANY) + { + // TODO: 'resolvedExprs' is more limited than 'exprTypes' which limits full inference of more complex types that a user + // callback can return + switch (ty) + { + case LBC_TYPE_BOOLEAN: + resolvedExprs[node] = &builtinTypes.booleanType; + break; + case LBC_TYPE_NUMBER: + resolvedExprs[node] = &builtinTypes.numberType; + break; + case LBC_TYPE_INTEGER: + resolvedExprs[node] = &builtinTypes.integerType; + break; + case LBC_TYPE_STRING: + resolvedExprs[node] = &builtinTypes.stringType; + break; + case LBC_TYPE_VECTOR: + resolvedExprs[node] = &builtinTypes.vectorType; + break; + default: + break; + } + + exprTypes[node] = ty; + return false; + } + } + } + + return false; + } + + bool visit(AstExprUnary* node) override + { + node->expr->visit(this); + + switch (node->op) + { + case AstExprUnary::Op::Not: + recordResolvedType(node, &builtinTypes.booleanType); + break; + case AstExprUnary::Op::Minus: + { + const AstType** typePtr = resolvedExprs.find(node->expr); + LuauBytecodeType* bcTypePtr = exprTypes.find(node->expr); + + if (!typePtr || !bcTypePtr) + return false; + + if (*bcTypePtr == LBC_TYPE_VECTOR) + recordResolvedType(node, *typePtr); + else if (*bcTypePtr == LBC_TYPE_NUMBER) + recordResolvedType(node, *typePtr); + + break; + } + case AstExprUnary::Op::Len: + recordResolvedType(node, &builtinTypes.numberType); + break; + } + + return false; + } + + bool visit(AstExprBinary* node) override + { + node->left->visit(this); + node->right->visit(this); + + // Comparisons result in a boolean + if (node->op == AstExprBinary::CompareNe || node->op == AstExprBinary::CompareEq || node->op == AstExprBinary::CompareLt || + node->op == AstExprBinary::CompareLe || node->op == AstExprBinary::CompareGt || node->op == AstExprBinary::CompareGe) + { + recordResolvedType(node, &builtinTypes.booleanType); + return false; + } + + if (node->op == AstExprBinary::Concat || node->op == AstExprBinary::And || node->op == AstExprBinary::Or) + return false; + + const AstType** leftTypePtr = resolvedExprs.find(node->left); + LuauBytecodeType* leftBcTypePtr = exprTypes.find(node->left); + + if (!leftTypePtr || !leftBcTypePtr) + return false; + + const AstType** rightTypePtr = resolvedExprs.find(node->right); + LuauBytecodeType* rightBcTypePtr = exprTypes.find(node->right); + + if (!rightTypePtr || !rightBcTypePtr) + return false; + + if (*leftBcTypePtr == LBC_TYPE_VECTOR) + recordResolvedType(node, *leftTypePtr); + else if (*rightBcTypePtr == LBC_TYPE_VECTOR) + recordResolvedType(node, *rightTypePtr); + else if (*leftBcTypePtr == LBC_TYPE_NUMBER && *rightBcTypePtr == LBC_TYPE_NUMBER) + recordResolvedType(node, *leftTypePtr); + + return false; + } + + bool visit(AstExprGroup* node) override + { + node->expr->visit(this); + + if (const AstType** typePtr = resolvedExprs.find(node->expr)) + recordResolvedType(node, *typePtr); + + return false; + } + + bool visit(AstExprTypeAssertion* node) override + { + node->expr->visit(this); + + recordResolvedType(node, node->annotation); + + return false; + } + + bool visit(AstExprConstantBool* node) override + { + recordResolvedType(node, &builtinTypes.booleanType); + + return false; + } + + bool visit(AstExprConstantNumber* node) override + { + recordResolvedType(node, &builtinTypes.numberType); + + return false; + } + + bool visit(AstExprConstantInteger* node) override + { + recordResolvedType(node, &builtinTypes.integerType); + + return false; + } + + bool visit(AstExprConstantString* node) override + { + recordResolvedType(node, &builtinTypes.stringType); + + return false; + } + + bool visit(AstExprInterpString* node) override + { + recordResolvedType(node, &builtinTypes.stringType); + + return false; + } + + bool visit(AstExprIfElse* node) override + { + node->condition->visit(this); + + // propagate the condition's resolved type to the binding (unless annotated) + if (node->conditionLocal && node->conditionLocal->annotation == nullptr) + { + if (const AstType** typePtr = resolvedExprs.find(node->condition)) + resolvedLocals[node->conditionLocal] = *typePtr; + } + + node->trueExpr->visit(this); + node->falseExpr->visit(this); + + const AstType** trueTypePtr = resolvedExprs.find(node->trueExpr); + LuauBytecodeType* trueBcTypePtr = exprTypes.find(node->trueExpr); + LuauBytecodeType* falseBcTypePtr = exprTypes.find(node->falseExpr); + + // Optimistic check that both expressions are of the same kind, as AstType* cannot be compared + if (trueTypePtr && trueBcTypePtr && falseBcTypePtr && *trueBcTypePtr == *falseBcTypePtr) + recordResolvedType(node, *trueTypePtr); + + return false; + } + + bool visit(AstExprCall* node) override + { + if (const int* bfid = builtinCalls.find(node)) + { + switch (LuauBuiltinFunction(*bfid)) + { + case LBF_NONE: + case LBF_ASSERT: + case LBF_RAWSET: + case LBF_RAWGET: + case LBF_TABLE_INSERT: + case LBF_TABLE_UNPACK: + case LBF_SELECT_VARARG: + case LBF_GETMETATABLE: + case LBF_SETMETATABLE: + case LBF_BUFFER_WRITEU8: + case LBF_BUFFER_WRITEU16: + case LBF_BUFFER_WRITEU32: + case LBF_BUFFER_WRITEF32: + case LBF_BUFFER_WRITEF64: + case LBF_BUFFER_WRITEINTEGER: + break; + case LBF_MATH_ABS: + case LBF_MATH_ACOS: + case LBF_MATH_ASIN: + case LBF_MATH_ATAN2: + case LBF_MATH_ATAN: + case LBF_MATH_CEIL: + case LBF_MATH_COSH: + case LBF_MATH_COS: + case LBF_MATH_DEG: + case LBF_MATH_EXP: + case LBF_MATH_FLOOR: + case LBF_MATH_FMOD: + case LBF_MATH_FREXP: + case LBF_MATH_LDEXP: + case LBF_MATH_LOG10: + case LBF_MATH_LOG: + case LBF_MATH_MAX: + case LBF_MATH_MIN: + case LBF_MATH_MODF: + case LBF_MATH_POW: + case LBF_MATH_RAD: + case LBF_MATH_SINH: + case LBF_MATH_SIN: + case LBF_MATH_SQRT: + case LBF_MATH_TANH: + case LBF_MATH_TAN: + case LBF_BIT32_ARSHIFT: + case LBF_BIT32_BAND: + case LBF_BIT32_BNOT: + case LBF_BIT32_BOR: + case LBF_BIT32_BXOR: + case LBF_BIT32_BTEST: + case LBF_BIT32_EXTRACT: + case LBF_BIT32_LROTATE: + case LBF_BIT32_LSHIFT: + case LBF_BIT32_REPLACE: + case LBF_BIT32_RROTATE: + case LBF_BIT32_RSHIFT: + case LBF_STRING_BYTE: + case LBF_STRING_LEN: + case LBF_MATH_CLAMP: + case LBF_MATH_SIGN: + case LBF_MATH_ROUND: + case LBF_BIT32_COUNTLZ: + case LBF_BIT32_COUNTRZ: + case LBF_RAWLEN: + case LBF_BIT32_EXTRACTK: + case LBF_TONUMBER: + case LBF_BIT32_BYTESWAP: + case LBF_BUFFER_READI8: + case LBF_BUFFER_READU8: + case LBF_BUFFER_READI16: + case LBF_BUFFER_READU16: + case LBF_BUFFER_READI32: + case LBF_BUFFER_READU32: + case LBF_BUFFER_READF32: + case LBF_BUFFER_READF64: + case LBF_VECTOR_MAGNITUDE: + case LBF_VECTOR_DOT: + case LBF_MATH_LERP: + recordResolvedType(node, &builtinTypes.numberType); + break; + + case LBF_TYPE: + case LBF_STRING_CHAR: + case LBF_TYPEOF: + case LBF_STRING_SUB: + case LBF_TOSTRING: + recordResolvedType(node, &builtinTypes.stringType); + break; + + case LBF_MATH_ISNAN: + case LBF_MATH_ISINF: + case LBF_MATH_ISFINITE: + case LBF_RAWEQUAL: + recordResolvedType(node, &builtinTypes.booleanType); + break; + + case LBF_VECTOR: + case LBF_VECTOR_NORMALIZE: + case LBF_VECTOR_CROSS: + case LBF_VECTOR_FLOOR: + case LBF_VECTOR_CEIL: + case LBF_VECTOR_ABS: + case LBF_VECTOR_SIGN: + case LBF_VECTOR_CLAMP: + case LBF_VECTOR_MIN: + case LBF_VECTOR_MAX: + case LBF_VECTOR_LERP: + recordResolvedType(node, &builtinTypes.vectorType); + break; + + case LBF_INTEGER_ADD: + case LBF_INTEGER_SUB: + case LBF_INTEGER_MOD: + case LBF_INTEGER_MUL: + case LBF_INTEGER_DIV: + case LBF_INTEGER_IDIV: + case LBF_INTEGER_UDIV: + case LBF_INTEGER_REM: + case LBF_INTEGER_UREM: + case LBF_INTEGER_MAX: + case LBF_INTEGER_MIN: + case LBF_INTEGER_BAND: + case LBF_INTEGER_BOR: + case LBF_INTEGER_BNOT: + case LBF_INTEGER_BXOR: + case LBF_INTEGER_LSHIFT: + case LBF_INTEGER_RSHIFT: + case LBF_INTEGER_ARSHIFT: + case LBF_INTEGER_LROTATE: + case LBF_INTEGER_RROTATE: + case LBF_INTEGER_EXTRACT: + case LBF_INTEGER_COUNTLZ: + case LBF_INTEGER_COUNTRZ: + case LBF_INTEGER_BSWAP: + case LBF_INTEGER_CLAMP: + case LBF_INTEGER_NEG: + case LBF_INTEGER_CREATE: + case LBF_BUFFER_READINTEGER: + if (!FFlag::LuauIntegerFastcalls) + return true; + recordResolvedType(node, &builtinTypes.integerType); + break; + + case LBF_INTEGER_TONUMBER: + if (!FFlag::LuauIntegerFastcalls) + return true; + recordResolvedType(node, &builtinTypes.numberType); + break; + + case LBF_INTEGER_LT: + case LBF_INTEGER_LE: + case LBF_INTEGER_GT: + case LBF_INTEGER_GE: + case LBF_INTEGER_ULT: + case LBF_INTEGER_ULE: + case LBF_INTEGER_UGT: + case LBF_INTEGER_UGE: + case LBF_INTEGER_BTEST: + if (!FFlag::LuauIntegerFastcalls) + return true; + recordResolvedType(node, &builtinTypes.booleanType); + break; + } + } + else + { + if (AstExprLocal* local = node->func->as()) + { + if (const AstType** typePtr = functionReturnTypes.find(local->local)) + recordResolvedType(node, *typePtr); + } + } + + return true; // Let generic visitor step into all expressions + } + + // AstExpr classes that are not covered: + // * AstExprConstantNil is not resolved to 'nil' because that doesn't help codegen operations and often used as an initializer before real value + // * AstExprGlobal is not supported as we don't have info on globals + // * AstExprVarargs cannot be resolved to a testable type + // * AstExprTable cannot be reconstructed into a specific AstTypeTable and table annotations don't really help codegen + // * AstExprCall is very complex (especially if builtins and registered globals are included), will be extended in the future +}; + +void buildTypeMap( + DenseHashMap& functionTypes, + DenseHashMap& localTypes, + DenseHashMap& exprTypes, + AstNode* root, + const char* hostVectorType, + const DenseHashMap& userdataTypes, + const BuiltinAstTypes& builtinTypes, + const DenseHashMap& builtinCalls, + const DenseHashMap& globals, + LibraryMemberTypeCallback libraryMemberTypeCb, + BytecodeBuilder& bytecode +) +{ + TypeMapVisitor visitor( + functionTypes, localTypes, exprTypes, hostVectorType, userdataTypes, builtinTypes, builtinCalls, globals, libraryMemberTypeCb, bytecode + ); + root->visit(&visitor); +} + +} // namespace Luau diff --git a/External/luau/Compiler/src/Types.h b/External/luau/Compiler/src/Types.h new file mode 100644 index 00000000..8dc8ea61 --- /dev/null +++ b/External/luau/Compiler/src/Types.h @@ -0,0 +1,47 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/Bytecode.h" +#include "Luau/Compiler.h" +#include "Luau/DenseHash.h" +#include "ValueTracking.h" + +#include + +namespace Luau +{ +class BytecodeBuilder; + +struct BuiltinAstTypes +{ + BuiltinAstTypes(const char* hostVectorType) + : hostVectorType{{}, std::nullopt, AstName{hostVectorType}, std::nullopt, {}} + { + } + + // AstName use here will not match the AstNameTable, but the way we use them here always forces a full string compare + AstTypeReference booleanType{{}, std::nullopt, AstName{"boolean"}, std::nullopt, {}}; + AstTypeReference numberType{{}, std::nullopt, AstName{"number"}, std::nullopt, {}}; + AstTypeReference integerType{{}, std::nullopt, AstName{"integer"}, std::nullopt, {}}; + AstTypeReference stringType{{}, std::nullopt, AstName{"string"}, std::nullopt, {}}; + AstTypeReference vectorType{{}, std::nullopt, AstName{"vector"}, std::nullopt, {}}; + + AstTypeReference hostVectorType; +}; + +void buildTypeMap( + DenseHashMap& functionTypes, + DenseHashMap& localTypes, + DenseHashMap& exprTypes, + AstNode* root, + const char* hostVectorType, + const DenseHashMap& userdataTypes, + const BuiltinAstTypes& builtinTypes, + const DenseHashMap& builtinCalls, + const DenseHashMap& globals, + LibraryMemberTypeCallback libraryMemberTypeCb, + BytecodeBuilder& bytecode +); + +} // namespace Luau diff --git a/External/luau/Compiler/src/Utils.h b/External/luau/Compiler/src/Utils.h new file mode 100644 index 00000000..020592ab --- /dev/null +++ b/External/luau/Compiler/src/Utils.h @@ -0,0 +1,77 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" + +#include "ConstantFolding.h" + +namespace Luau +{ +namespace Compile +{ + +inline bool isConstantTrue(const DenseHashMap& constants, AstExpr* node) +{ + const Constant* cv = constants.find(node); + + return cv && cv->type != Constant::Type_Unknown && cv->isTruthful(); +} + +inline bool isConstantFalse(const DenseHashMap& constants, AstExpr* node) +{ + const Constant* cv = constants.find(node); + + return cv && cv->type != Constant::Type_Unknown && !cv->isTruthful(); +} + +// true iff all execution paths through node subtree result in return/break/continue +// note: because this function doesn't visit loop nodes, it (correctly) only detects break/continue that refer to the outer control flow +inline bool alwaysTerminates(const DenseHashMap& constants, AstStat* node) +{ + if (AstStatBlock* stat = node->as()) + { + for (size_t i = 0; i < stat->body.size; ++i) + { + if (alwaysTerminates(constants, stat->body.data[i])) + return true; + } + + return false; + } + + if (node->is()) + return true; + + if (node->is() || node->is()) + return true; + + if (AstStatIf* stat = node->as()) + { + if (isConstantTrue(constants, stat->condition)) + return alwaysTerminates(constants, stat->thenbody); + + if (isConstantFalse(constants, stat->condition) && stat->elsebody) + return alwaysTerminates(constants, stat->elsebody); + + return stat->elsebody && alwaysTerminates(constants, stat->thenbody) && alwaysTerminates(constants, stat->elsebody); + } + + return false; +} + +template +T* unwrapExprOfType(AstExpr* node) +{ + if (T* expr = node->as()) + return expr; + else if (AstExprGroup* expr = node->as()) + return unwrapExprOfType(expr->expr); + else if (AstExprTypeAssertion* expr = node->as()) + return unwrapExprOfType(expr->expr); + else + return nullptr; +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/ValueTracking.cpp b/External/luau/Compiler/src/ValueTracking.cpp new file mode 100644 index 00000000..26c90489 --- /dev/null +++ b/External/luau/Compiler/src/ValueTracking.cpp @@ -0,0 +1,277 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "ValueTracking.h" + +#include "Luau/Lexer.h" +#include "Luau/SmallVector.h" + +LUAU_FASTFLAG(LuauOptimizeExportTable) +LUAU_FASTFLAG(LuauCompileReuseLocalRegs) + +namespace Luau +{ +namespace Compile +{ + +struct ValueVisitor : AstVisitor +{ + + DenseHashMap& globals; + DenseHashMap& variables; + DenseHashMap& classLocals; + DenseHashSet* exportedFunctions = nullptr; + std::vector* exportedVariables = nullptr; + SmallVector blockOwnerStack; + + // with LuauOptimizeExportTable, remove this constructor + ValueVisitor( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals + ) + : globals(globals) + , variables(variables) + , classLocals(classLocals) + { + } + + ValueVisitor( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals, + DenseHashSet* exportedFunctions, + std::vector* exportedVariables + ) + : globals(globals) + , variables(variables) + , classLocals(classLocals) + , exportedFunctions(exportedFunctions) + , exportedVariables(exportedVariables) + { + } + + void assign(AstExpr* var) + { + if (AstExprLocal* lv = var->as()) + { + if (FFlag::LuauCompileReuseLocalRegs) + { + Variable& variable = variables[lv->local]; + + variable.written = true; + variable.lastUsed = lv; + + if (lv->upvalue || lv->local->isExported) + variable.nonLexicalUse = true; + } + else + { + variables[lv->local].written = true; + } + } + else if (AstExprGlobal* gv = var->as()) + { + globals[gv->name] = Global::Written; + } + else + { + // we need to be able to track assignments in all expressions, including crazy ones like t[function() t = nil end] = 5 + var->visit(this); + } + } + + bool visit(AstExprLocal* node) override + { + if (FFlag::LuauCompileReuseLocalRegs) + { + Variable& variable = variables[node->local]; + + variable.lastUsed = node; + + if (node->upvalue || node->local->isExported) + variable.nonLexicalUse = true; + + return false; + } + else + { + return true; + } + } + + bool visit(AstStatLocal* node) override + { + for (size_t i = 0; i < node->vars.size && i < node->values.size; ++i) + variables[node->vars.data[i]].init = node->values.data[i]; + + for (size_t i = node->values.size; i < node->vars.size; ++i) + variables[node->vars.data[i]].init = nullptr; + + if (FFlag::LuauOptimizeExportTable && exportedVariables) + { + for (size_t i = 0; i < node->vars.size; ++i) + { + AstLocal* local = node->vars.data[i]; + if (local->isExported) + { + exportedVariables->push_back(local); + } + } + } + + if (FFlag::LuauCompileReuseLocalRegs) + { + for (size_t i = 0; i < node->vars.size; ++i) + { + AstLocal* local = node->vars.data[i]; + + LUAU_ASSERT(!blockOwnerStack.empty()); + variables[local].owner = blockOwnerStack.back(); + } + } + + return true; + } + + bool visit(AstStatIf* node) override + { + if (node->conditionLocal) + variables[node->conditionLocal].init = node->condition; + + return true; + } + + bool visit(AstExprIfElse* node) override + { + if (node->conditionLocal) + variables[node->conditionLocal].init = node->condition; + + return true; + } + + bool visit(AstStatAssign* node) override + { + for (size_t i = 0; i < node->vars.size; ++i) + assign(node->vars.data[i]); + + for (size_t i = 0; i < node->values.size; ++i) + node->values.data[i]->visit(this); + + return false; + } + + bool visit(AstStatCompoundAssign* node) override + { + assign(node->var); + node->value->visit(this); + + return false; + } + + bool visit(AstStatLocalFunction* node) override + { + if (FFlag::LuauCompileReuseLocalRegs) + { + Variable& variable = variables[node->name]; + + variable.init = node->func; + + LUAU_ASSERT(!blockOwnerStack.empty()); + variable.owner = blockOwnerStack.back(); + + if (node->name->isExported) + variable.nonLexicalUse = true; + } + else + { + variables[node->name].init = node->func; + } + + if (FFlag::LuauOptimizeExportTable && exportedFunctions && node->name->isExported) + { + exportedFunctions->insert(node->name); + exportedVariables->push_back(node->name); + } + + return true; + } + + bool visit(AstStatFunction* node) override + { + assign(node->name); + node->func->visit(this); + + return false; + } + + bool visit(AstExprFunction* node) override + { + for (AstLocal* arg : node->args) + variables[arg].init = nullptr; + + return true; + } + + bool visit(AstStatClass* decl) override + { + if (!FFlag::DebugLuauUserDefinedClasses) + return false; + + classLocals[decl->name->name] = decl->name; + variables[decl->name].written = true; + + return true; + } + + bool visit(AstStatBlock* block) override + { + if (FFlag::LuauCompileReuseLocalRegs) + { + blockOwnerStack.push_back(block); + for (AstStat* stat : block->body) + stat->visit(this); + blockOwnerStack.pop_back(); + return false; + } + else + { + return true; + } + } +}; + +void assignMutable(DenseHashMap& globals, const AstNameTable& names, const char* const* mutableGlobals) +{ + if (AstName name = names.get("_G"); name.value) + globals[name] = Global::Mutable; + + if (mutableGlobals) + for (const char* const* ptr = mutableGlobals; *ptr; ++ptr) + if (AstName name = names.get(*ptr); name.value) + globals[name] = Global::Mutable; +} + +void trackValues( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals, + DenseHashSet& exportedFunctions, + std::vector& exportedVariables, + AstNode* root +) +{ + ValueVisitor visitor{globals, variables, classLocals, &exportedFunctions, &exportedVariables}; + root->visit(&visitor); +} +void trackValues_DEPRECATED( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals, + AstNode* root +) +{ + ValueVisitor visitor{globals, variables, classLocals}; + root->visit(&visitor); +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/ValueTracking.h b/External/luau/Compiler/src/ValueTracking.h new file mode 100644 index 00000000..7a7565a7 --- /dev/null +++ b/External/luau/Compiler/src/ValueTracking.h @@ -0,0 +1,60 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Ast.h" +#include "Luau/DenseHash.h" + +#include + +namespace Luau +{ +class AstNameTable; +} + +namespace Luau +{ +namespace Compile +{ + +enum class Global +{ + Default = 0, + Mutable, // builtin that has contents unknown at compile time, blocks GETIMPORT for chains + Written, // written in the code which means we can't reason about the value +}; + +struct Variable +{ + AstExpr* init = nullptr; // initial value of the variable; filled by trackValues + AstStatBlock* owner = nullptr; // block statement in which the variable was defined (not necessarily directly) + AstExprLocal* lastUsed = nullptr; // last lexical expression where the variable was read + bool written = false; // is the variable ever assigned to? filled by trackValues + bool constant = false; // is the variable's value a compile-time constant? filled by constantFold + bool nonLexicalUse = false; // variable may be used by expressions which do not lexically mention it (like function calls) +}; + +void assignMutable(DenseHashMap& globals, const AstNameTable& names, const char* const* mutableGlobals); +void trackValues( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals, + DenseHashSet& exportedFunctions, + std::vector& exportedVariables, + AstNode* root +); +void trackValues_DEPRECATED( + DenseHashMap& globals, + DenseHashMap& variables, + DenseHashMap& classLocals, + AstNode* root +); + +inline Global getGlobalState(const DenseHashMap& globals, AstName name) +{ + const Global* it = globals.find(name); + + return it ? *it : Global::Default; +} + +} // namespace Compile +} // namespace Luau diff --git a/External/luau/Compiler/src/lcode.cpp b/External/luau/Compiler/src/lcode.cpp new file mode 100644 index 00000000..d9f7af61 --- /dev/null +++ b/External/luau/Compiler/src/lcode.cpp @@ -0,0 +1,65 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "luacode.h" + +#include "Luau/Compiler.h" + +#include +#include + +char* luau_compile(const char* source, size_t size, lua_CompileOptions* options, size_t* outsize) +{ + LUAU_ASSERT(outsize); + + Luau::CompileOptions opts; + + if (options) + { + static_assert(sizeof(lua_CompileOptions) == sizeof(Luau::CompileOptions), "C and C++ interface must match"); + memcpy(static_cast(&opts), options, sizeof(opts)); + } + + std::string result = compile(std::string(source, size), opts); + + char* copy = static_cast(malloc(result.size())); + if (!copy) + return nullptr; + + memcpy(copy, result.data(), result.size()); + *outsize = result.size(); + return copy; +} + +void luau_set_compile_constant_nil(lua_CompileConstant* constant) +{ + Luau::setCompileConstantNil(constant); +} + +void luau_set_compile_constant_boolean(lua_CompileConstant* constant, int b) +{ + Luau::setCompileConstantBoolean(constant, b != 0); +} + +void luau_set_compile_constant_number(lua_CompileConstant* constant, double n) +{ + Luau::setCompileConstantNumber(constant, n); +} + +void luau_set_compile_constant_integer64(lua_CompileConstant* constant, int64_t l) +{ + Luau::setCompileConstantInteger64(constant, l); +} + +void luau_set_compile_constant_vector(lua_CompileConstant* constant, float x, float y, float z, float w) +{ + Luau::setCompileConstantVector(constant, x, y, z, w); +} + +void luau_set_compile_constant_vectord(lua_CompileConstant* constant, double x, double y, double z, double w) +{ + Luau::setCompileConstantVectord(constant, x, y, z, w); +} + +void luau_set_compile_constant_string(lua_CompileConstant* constant, const char* s, size_t l) +{ + Luau::setCompileConstantString(constant, s, l); +} diff --git a/External/luau/LICENSE.txt b/External/luau/LICENSE.txt new file mode 100644 index 00000000..81f966d1 --- /dev/null +++ b/External/luau/LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019-2025 Roblox Corporation +Copyright (c) 1994–2019 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/External/luau/VM/include/lua.h b/External/luau/VM/include/lua.h new file mode 100644 index 00000000..c1aa2b79 --- /dev/null +++ b/External/luau/VM/include/lua.h @@ -0,0 +1,671 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include +#include +#include + +#include "luaconf.h" + + + +// option for multiple returns in `lua_pcall' and `lua_call' +#define LUA_MULTRET (-1) + +/* +** pseudo-indices +*/ +#define LUA_REGISTRYINDEX (-LUAI_MAXCSTACK - 2000) +#define LUA_ENVIRONINDEX (-LUAI_MAXCSTACK - 2001) +#define LUA_GLOBALSINDEX (-LUAI_MAXCSTACK - 2002) +#define lua_upvalueindex(i) (LUA_GLOBALSINDEX - (i)) +#define lua_ispseudo(i) ((i) <= LUA_REGISTRYINDEX) + +// thread status; 0 is OK +enum lua_Status +{ + LUA_OK = 0, + LUA_YIELD, + LUA_ERRRUN, + LUA_ERRSYNTAX, // legacy error code, preserved for compatibility + LUA_ERRMEM, + LUA_ERRERR, + LUA_BREAK, // yielded for a debug breakpoint +}; + +enum lua_CoStatus +{ + LUA_CORUN = 0, // running + LUA_COSUS, // suspended + LUA_CONOR, // 'normal' (it resumed another coroutine) + LUA_COFIN, // finished + LUA_COERR, // finished with error +}; + +typedef struct lua_State lua_State; + +typedef int (*lua_CFunction)(lua_State* L); +typedef int (*lua_Continuation)(lua_State* L, int status); + +/* +** prototype for memory-allocation functions +*/ + +typedef void* (*lua_Alloc)(void* ud, void* ptr, size_t osize, size_t nsize); + +// `type` is an opaque VM-defined value which identifies the caged heap allocation kind +// separating allocations of different types is recommended +typedef void* (*lua_CageAlloc)(void* ud, void* ptr, size_t osize, size_t nsize, int type); + +// non-return type +#define l_noret void LUA_NORETURN + +/* +** basic types +*/ +#define LUA_TNONE (-1) + +/* + * WARNING: if you change the order of this enumeration, + * grep "ORDER TYPE" + */ +// clang-format off +enum lua_Type +{ + LUA_TNIL = 0, // must be 0 due to lua_isnoneornil + LUA_TBOOLEAN = 1, // must be 1 due to l_isfalse + + LUA_TLIGHTUSERDATA, + LUA_TNUMBER, + LUA_TINTEGER, + + // When vector components are 'float', vector fits into the TValue +#if LUA_VECTOR_DOUBLE == 0 + LUA_TVECTOR, +#endif + + LUA_TSTRING, // all types above this must be value types, all types below this must be GC types - see iscollectable + + LUA_TTABLE, + LUA_TFUNCTION, + LUA_TUSERDATA, + LUA_TTHREAD, + LUA_TBUFFER, + LUA_TCLASS, + LUA_TOBJECT, + + // When vector components are 'double', it is a GCObject with heap-allocated data +#if LUA_VECTOR_DOUBLE == 1 + LUA_TVECTOR, +#endif + + // values below this line are used in GCObject tags but may never show up in TValue type tags + + // LUA_TDEADKEY is used in TKey to identify Luau table entries that have the value set to nil, + // so that we can remove the strong reference to the key. + LUA_TDEADKEY, + + // These values should never show up in TValue tag types. + LUA_TPROTO, + LUA_TUPVAL, + + // the count of all Luau types (including those that are never TValue type tags) + LUA_T_ALL, + + // the count of TValue type tags + LUA_T_COUNT = LUA_TDEADKEY, +}; +// clang-format on + +// type of numbers in Luau +typedef double lua_Number; + +// type for integer functions +typedef int lua_Integer; + +// unsigned integer type +typedef unsigned lua_Unsigned; + +/* +** state manipulation +*/ +LUA_API lua_State* lua_newstate(lua_Alloc allocator, void* ud); +LUA_API void lua_close(lua_State* L); +LUA_API lua_State* lua_newthread(lua_State* L); +LUA_API lua_State* lua_mainthread(lua_State* L); +LUA_API void lua_resetthread(lua_State* L); +LUA_API int lua_isthreadreset(lua_State* L); + +/* +** basic stack manipulation +*/ +LUA_API int lua_absindex(lua_State* L, int idx); +LUA_API int lua_gettop(lua_State* L); +LUA_API void lua_settop(lua_State* L, int idx); +LUA_API void lua_pushvalue(lua_State* L, int idx); +LUA_API void lua_remove(lua_State* L, int idx); +LUA_API void lua_insert(lua_State* L, int idx); +LUA_API void lua_replace(lua_State* L, int idx); +LUA_API int lua_checkstack(lua_State* L, int sz); +LUA_API void lua_rawcheckstack(lua_State* L, int sz); // allows for unlimited stack frames + +LUA_API void lua_xmove(lua_State* from, lua_State* to, int n); +LUA_API void lua_xpush(lua_State* from, lua_State* to, int idx); + +/* +** access functions (stack -> C) +*/ + +LUA_API int lua_isnumber(lua_State* L, int idx); +LUA_API int lua_isstring(lua_State* L, int idx); +LUA_API int lua_isinteger64(lua_State* L, int idx); +LUA_API int lua_iscfunction(lua_State* L, int idx); +LUA_API int lua_isLfunction(lua_State* L, int idx); +LUA_API int lua_isuserdata(lua_State* L, int idx); +LUA_API int lua_type(lua_State* L, int idx); +LUA_API const char* lua_typename(lua_State* L, int tp); + +LUA_API int lua_equal(lua_State* L, int idx1, int idx2); +LUA_API int lua_rawequal(lua_State* L, int idx1, int idx2); +LUA_API int lua_lessthan(lua_State* L, int idx1, int idx2); + +LUA_API double lua_tonumberx(lua_State* L, int idx, int* isnum); +LUA_API int lua_tointegerx(lua_State* L, int idx, int* isnum); +LUA_API unsigned lua_tounsignedx(lua_State* L, int idx, int* isnum); +LUA_API const LUA_VECTOR_TYPE* lua_tovector(lua_State* L, int idx); +LUA_API int lua_toboolean(lua_State* L, int idx); +LUA_API int64_t lua_tointeger64(lua_State* L, int idx, int* isinteger); +LUA_API const char* lua_tolstring(lua_State* L, int idx, size_t* len); +LUA_API const char* lua_tostringatom(lua_State* L, int idx, int* atom); +LUA_API const char* lua_tolstringatom(lua_State* L, int idx, size_t* len, int* atom); +LUA_API const char* lua_namecallatom(lua_State* L, int* atom); +LUA_API int lua_objlen(lua_State* L, int idx); +LUA_API lua_CFunction lua_tocfunction(lua_State* L, int idx); +LUA_API void* lua_tolightuserdata(lua_State* L, int idx); +LUA_API void* lua_tolightuserdatatagged(lua_State* L, int idx, int tag); +LUA_API void* lua_touserdata(lua_State* L, int idx); +LUA_API void* lua_touserdatatagged(lua_State* L, int idx, int tag); +LUA_API int lua_userdatatag(lua_State* L, int idx); +LUA_API int lua_lightuserdatatag(lua_State* L, int idx); +LUA_API lua_State* lua_tothread(lua_State* L, int idx); +LUA_API void* lua_tobuffer(lua_State* L, int idx, size_t* len); +LUA_API const void* lua_topointer(lua_State* L, int idx); + +/* +** push functions (C -> stack) +*/ +LUA_API void lua_pushnil(lua_State* L); +LUA_API void lua_pushnumber(lua_State* L, double n); +LUA_API void lua_pushinteger(lua_State* L, int n); +LUA_API void lua_pushinteger64(lua_State* L, int64_t n); +LUA_API void lua_pushunsigned(lua_State* L, unsigned n); +#if LUA_VECTOR_SIZE == 4 +LUA_API void lua_pushvector(lua_State* L, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z, LUA_VECTOR_TYPE w); +#else +LUA_API void lua_pushvector(lua_State* L, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z); +#endif +LUA_API void lua_pushlstring(lua_State* L, const char* s, size_t l); +LUA_API void lua_pushstring(lua_State* L, const char* s); +LUA_API const char* lua_pushvfstring(lua_State* L, const char* fmt, va_list argp); +LUA_API LUA_PRINTF_ATTR(2, 3) const char* lua_pushfstringL(lua_State* L, const char* fmt, ...); +LUA_API void lua_pushcclosurek(lua_State* L, lua_CFunction fn, const char* debugname, int nup, lua_Continuation cont); +LUA_API void lua_pushboolean(lua_State* L, int b); +LUA_API int lua_pushthread(lua_State* L); + +typedef void (*lua_Destructor)(lua_State* L, void* userdata); + +LUA_API void lua_pushlightuserdatatagged(lua_State* L, void* p, int tag); +LUA_API void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag); +LUA_API void* lua_newuserdatataggedwithmetatable(lua_State* L, size_t sz, int tag); // metatable fetched with lua_getuserdatametatable +LUA_API void* lua_newuserdatadtor(lua_State* L, size_t sz, lua_Destructor dtor); + +LUA_API void* lua_newbuffer(lua_State* L, size_t sz); + +/* +** get functions (Lua -> stack) +*/ +LUA_API int lua_gettable(lua_State* L, int idx); +LUA_API int lua_getfield(lua_State* L, int idx, const char* k); +LUA_API int lua_rawgetfield(lua_State* L, int idx, const char* k); +LUA_API int lua_rawget(lua_State* L, int idx); +LUA_API int lua_rawgeti(lua_State* L, int idx, int n); +LUA_API int lua_rawgetptagged(lua_State* L, int idx, void* p, int tag); +LUA_API void lua_createtable(lua_State* L, int narr, int nrec); + +LUA_API void lua_setreadonly(lua_State* L, int idx, int enabled); +LUA_API int lua_getreadonly(lua_State* L, int idx); +LUA_API void lua_setsafeenv(lua_State* L, int idx, int enabled); + +LUA_API int lua_getmetatable(lua_State* L, int objindex); +LUA_API void lua_getfenv(lua_State* L, int idx); + +/* +** set functions (stack -> Lua) +*/ +LUA_API void lua_settable(lua_State* L, int idx); +LUA_API void lua_setfield(lua_State* L, int idx, const char* k); +LUA_API void lua_rawsetfield(lua_State* L, int idx, const char* k); +LUA_API void lua_rawset(lua_State* L, int idx); +LUA_API void lua_rawseti(lua_State* L, int idx, int n); +LUA_API void lua_rawsetptagged(lua_State* L, int idx, void* p, int tag); +LUA_API int lua_setmetatable(lua_State* L, int objindex); +LUA_API int lua_setfenv(lua_State* L, int idx); + +/* +** `load' and `call' functions (load and run Luau bytecode) +*/ +LUA_API int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size, int env); +LUA_API void lua_call(lua_State* L, int nargs, int nresults); +LUA_API int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc); +LUA_API int lua_cpcall(lua_State* L, lua_CFunction func, void* ud); + +// wrapper for making calls from yieldable C functions +LUA_API int lua_callyieldable(lua_State* L, int nargs, int nresults); +LUA_API int lua_pcallyieldable(lua_State* L, int nargs, int nresults, int errfunc); + +/* +** coroutine functions +*/ +LUA_API int lua_yield(lua_State* L, int nresults); +LUA_API int lua_break(lua_State* L); +LUA_API int lua_resume(lua_State* L, lua_State* from, int narg); +LUA_API int lua_resumeerror(lua_State* L, lua_State* from); +LUA_API int lua_status(lua_State* L); +LUA_API int lua_isyieldable(lua_State* L); +LUA_API void* lua_getthreaddata(lua_State* L); +LUA_API void lua_setthreaddata(lua_State* L, void* data); +LUA_API int lua_costatus(lua_State* L, lua_State* co); + +// NOTE: experimental API, requires a Debug flag and is subject to breaking changes +LUA_API int lua_hasfinalizers(lua_State* L); +LUA_API void lua_pushfinalizerfunction(lua_State* L); +LUA_API void lua_addfinalizer(lua_State* L, lua_State* co, int idx); + +/* +** garbage-collection function and options +*/ + +enum lua_GCOp +{ + // stop and resume incremental garbage collection + LUA_GCSTOP, + LUA_GCRESTART, + + // run a full GC cycle; not recommended for latency sensitive applications + LUA_GCCOLLECT, + + // return the heap size in KB and the remainder in bytes + LUA_GCCOUNT, + LUA_GCCOUNTB, + + // return 1 if GC is active (not stopped); note that GC may not be actively collecting even if it's running + LUA_GCISRUNNING, + + /* + ** perform an explicit GC step, with the step size specified in KB + ** + ** garbage collection is handled by 'assists' that perform some amount of GC work matching pace of allocation + ** explicit GC steps allow to perform some amount of work at custom points to offset the need for GC assists + ** note that GC might also be paused for some duration (until bytes allocated meet the threshold) + ** if an explicit step is performed during this pause, it will trigger the start of the next collection cycle + */ + LUA_GCSTEP, + + /* + ** tune GC parameters G (goal), S (step multiplier) and step size (usually best left ignored) + ** + ** garbage collection is incremental and tries to maintain the heap size to balance memory and performance overhead + ** this overhead is determined by G (goal) which is the ratio between total heap size and the amount of live data in it + ** G is specified in percentages; by default G=200% which means that the heap is allowed to grow to ~2x the size of live data. + ** + ** collector tries to collect S% of allocated bytes by interrupting the application after step size bytes were allocated. + ** when S is too small, collector may not be able to catch up and the effective goal that can be reached will be larger. + ** S is specified in percentages; by default S=200% which means that collector will run at ~2x the pace of allocations. + ** + ** it is recommended to set S in the interval [100 / (G - 100), 100 + 100 / (G - 100))] with a minimum value of 150%; for example: + ** - for G=200%, S should be in the interval [150%, 200%] + ** - for G=150%, S should be in the interval [200%, 300%] + ** - for G=125%, S should be in the interval [400%, 500%] + */ + LUA_GCSETGOAL, + LUA_GCSETSTEPMUL, + LUA_GCSETSTEPSIZE, + + // return 1 if GC is in a paused state; making a GC step in a paused state will unpause the GC + LUA_GCISPAUSED, +}; + +LUA_API int lua_gc(lua_State* L, int what, int data); + +typedef const char* (*lua_CategoryName)(lua_State* L, uint8_t memcat); + +// write a Luau memory dump to a FILE in JSON format +// categoryName callback, when provided, will be called to record a name associated with the category +LUA_API void lua_memorydump(lua_State* L, void* file, lua_CategoryName categoryName); + +/* +** memory statistics +** all allocated bytes are attributed to the memory category of the running thread (0..LUA_MEMORY_CATEGORIES-1) +*/ + +LUA_API void lua_setmemcat(lua_State* L, int category); +LUA_API size_t lua_totalbytes(lua_State* L, int category); + +// measure the allocation rate in bytes/sec +// returns -1 if allocation rate cannot be measured +LUA_API int64_t lua_allocationrate(lua_State* L); + +/* +** miscellaneous functions +*/ + +LUA_API l_noret lua_error(lua_State* L); + +LUA_API int lua_next(lua_State* L, int idx); +LUA_API int lua_rawiter(lua_State* L, int idx, int iter); + +LUA_API void lua_concat(lua_State* L, int n); + +LUA_API void lua_setpointerencodekey(lua_State* L, uint64_t a, uint64_t b, uint64_t c, uint64_t d); +LUA_API uintptr_t lua_encodepointer(lua_State* L, uintptr_t p); + +LUA_API double lua_clock(); + +LUA_API void lua_setuserdatatag(lua_State* L, int idx, int tag); + +LUA_API void lua_setuserdatadtor(lua_State* L, int tag, lua_Destructor dtor); +LUA_API lua_Destructor lua_getuserdatadtor(lua_State* L, int tag); + +// Embedder GC integration APIs +// +// Luau -> embedder (lua_setuserdatamark): when the GC marks a tagged userdata, +// the registered callback fires so the embedder can mark the corresponding +// native object as reachable. +// +// Embedder -> Luau (lua_setembeddergc): the GC calls this each cycle. +// Two cases: +// 1. Cycle reset (markref == NULL): clear accumulated information about marked +// native objects. +// 2. Mark phase (markref != NULL): embedder must call markref for each new ref +// that is reachable from a marked native object. +// +// Together, these are used to implement a fixed-point reachability algorithm +// across both heaps. +// +// Note: because these functions are called during garbage collection, they +// must not perform any reentrant operations on the lua_State. Only truly +// read-only APIs like lua_getthreaddata are safe to call from here. +typedef void (*lua_UserdataMark)(lua_State* L, void* ud); +LUA_API void lua_setuserdatamark(lua_State* L, int tag, lua_UserdataMark markfn); +typedef void (*lua_EmbedderMark)(lua_State* L, int ref); +typedef void (*lua_EmbedderGc)(lua_State* L, lua_EmbedderMark markref); +LUA_API void lua_setembeddergc(lua_State* L, lua_EmbedderGc fn); + +// APIs for interacting with embedder-managed weak references. +// +// Unlike lua_ref, these do not prevent collection on their own; the embedder +// must call markref(L, ref) inside lua_EmbedderGc to keep them alive. If the +// lua_getweakref API returns nil when the embedder expects the object to be +// alive, it indicates a bug in the embedder's logic. +// +// Bugs in the embedder's marking logic can manifest in two ways: +// - the native object owning a ref was not marked (Luau -> embedder), or +// - if the native object was marked, the ref was not marked (embedder -> Luau). +// +// Otherwise, a non-marking-related bug could simply be that the embedder is +// trying to access a ref that was correctly collected. Specifically, if a +// native object that logically owns a ref is destroyed, then the embedder +// should not be trying to access that ref anymore. +// +// In any case, asserting on the return value of lua_getweakref is a good way +// for embedders to catch bugs in their own marking logic. +LUA_API int lua_weakref(lua_State* L, int idx); +LUA_API int lua_weakunref(lua_State* L, int ref); +LUA_API int lua_getweakref(lua_State* L, int ref); // returns the type of the value pushed onto the stack + +// alternative access for userdata metatables +// used by lua_newuserdatataggedwithmetatable to create tagged userdata with the associated metatable assigned +LUA_API void lua_setuserdatametatable(lua_State* L, int tag); +LUA_API void lua_getuserdatametatable(lua_State* L, int tag); + +// Returns the name of a userdata tag - `__type` from the metatable or "userdata" if it is not set +LUA_API const char* lua_getuserdataname(lua_State* L, int tag); + +// NOTE: experimental API and is subject to breaking changes +// registration of callbacks for direct userdata __index, __newindex and __namecall access with string keys assigned with an atom +// cachedslot is initially 0 and can be set to a custom value to help with data lookup inside the userdata +// IMPORTANT: cachedslot values are shared between all userdata, callbacks function of one userdata tag has to correctly handle values set by another +typedef void (*lua_UserdataDirectAccess)(lua_State* L, void* data, int atom, uint16_t* cachedslot, int utag); +typedef int (*lua_UserdataDirectNamecall)(lua_State* L, void* data, int atom, uint16_t* cachedslot, int utag); + +LUA_API int lua_registeruserdatadirectaccess( + lua_State* L, + int tag, + lua_UserdataDirectAccess get, + lua_UserdataDirectAccess set, + lua_UserdataDirectNamecall namecall +); + +/* +** Direct field API +** +** lua_registeruserdatadirectfieldget registers a per-field, per-userdata-type +** handler that is invoked directly without allocating a Luau call frame. +** +** tag: userdata tag (0..LUA_UTAG_LIMIT-1) +** field: field name string (will be interned and pinned) +** fn: handler — receives raw userdata data pointer and result TValue slot +*/ +typedef void (*lua_UserdataDirectFieldGet)(void* ud, void* result); +LUA_API void lua_registeruserdatadirectfieldget(lua_State* L, int tag, const char* field, lua_UserdataDirectFieldGet fn); + +// Helpers for writing result values from a direct field handler. +LUA_API void lua_userdatadirectfield_setnumber(void* result, double n); +#if LUA_VECTOR_SIZE == 4 +LUA_API void lua_userdatadirectfield_setvector(void* result, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z, LUA_VECTOR_TYPE w); +#else +LUA_API void lua_userdatadirectfield_setvector(void* result, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z); +#endif +LUA_API void lua_userdatadirectfield_setboolean(void* result, int b); +LUA_API void lua_userdatadirectfield_setinteger64(void* result, int64_t n); +LUA_API void lua_userdatadirectfield_setnil(void* result); + +LUA_API void lua_setlightuserdataname(lua_State* L, int tag, const char* name); +LUA_API const char* lua_getlightuserdataname(lua_State* L, int tag); + +LUA_API void lua_clonefunction(lua_State* L, int idx); +LUA_API int lua_usesexport(lua_State* L, int idx); + +LUA_API void lua_cleartable(lua_State* L, int idx); +LUA_API void lua_clonetable(lua_State* L, int idx); + +LUA_API lua_Alloc lua_getallocf(lua_State* L, void** ud); + +/* +** reference system, can be used to pin objects +*/ +#define LUA_NOREF -1 +#define LUA_REFNIL 0 + +LUA_API int lua_ref(lua_State* L, int idx); +LUA_API int lua_unref(lua_State* L, int ref); + +#define lua_getref(L, ref) lua_rawgeti(L, LUA_REGISTRYINDEX, (ref)) + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ +#define lua_tonumber(L, i) lua_tonumberx(L, i, NULL) +#define lua_tointeger(L, i) lua_tointegerx(L, i, NULL) +#define lua_tounsigned(L, i) lua_tounsignedx(L, i, NULL) + +#define lua_pop(L, n) lua_settop(L, -(n)-1) + +#define lua_newtable(L) lua_createtable(L, 0, 0) +#define lua_newuserdata(L, s) lua_newuserdatatagged(L, s, 0) + +#define lua_strlen(L, i) lua_objlen(L, (i)) + +#define lua_isfunction(L, n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L, n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L, n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L, n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L, n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isinteger64(L, n) (lua_type(L, (n)) == LUA_TINTEGER) +#define lua_isvector(L, n) (lua_type(L, (n)) == LUA_TVECTOR) +#define lua_isthread(L, n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isbuffer(L, n) (lua_type(L, (n)) == LUA_TBUFFER) +#define lua_isnone(L, n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= LUA_TNIL) +#define lua_isclass(L, n) (lua_type(L, (n)) == LUA_TCLASS) +#define lua_isobject(L, n) (lua_type(L, (n)) == LUA_TOBJECT) + +#define lua_pushliteral(L, s) lua_pushlstring(L, "" s, (sizeof(s) / sizeof(char)) - 1) +#define lua_pushcfunction(L, fn, debugname) lua_pushcclosurek(L, fn, debugname, 0, NULL) +#define lua_pushcclosure(L, fn, debugname, nup) lua_pushcclosurek(L, fn, debugname, nup, NULL) +#define lua_pushlightuserdata(L, p) lua_pushlightuserdatatagged(L, p, 0) + +#define lua_rawgetp(L, idx, p) lua_rawgetptagged(L, idx, p, 0) +#define lua_rawsetp(L, idx, p) lua_rawsetptagged(L, idx, p, 0) + +#define lua_setglobal(L, s) lua_setfield(L, LUA_GLOBALSINDEX, (s)) +#define lua_getglobal(L, s) lua_getfield(L, LUA_GLOBALSINDEX, (s)) + +#define lua_tostring(L, i) lua_tolstring(L, (i), NULL) + +#define lua_pushfstring(L, fmt, ...) lua_pushfstringL(L, fmt, ##__VA_ARGS__) + +/* +** {====================================================================== +** Debug API +** ======================================================================= +*/ + +typedef struct lua_Debug lua_Debug; // activation record + +// Functions to be called by the debugger in specific events +typedef void (*lua_Hook)(lua_State* L, lua_Debug* ar); + +// invoke a debug hook on a thread in break/yield state +// userdata is passed to hook through lua_Debug::userdata field +LUA_API void lua_callhook(lua_State* L, lua_Hook hook, void* userdata); + +LUA_API int lua_stackdepth(lua_State* L); +LUA_API int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar); +LUA_API int lua_getargument(lua_State* L, int level, int n); +LUA_API const char* lua_getlocal(lua_State* L, int level, int n); +LUA_API const char* lua_setlocal(lua_State* L, int level, int n); +LUA_API const char* lua_getupvalue(lua_State* L, int funcindex, int n); +LUA_API const char* lua_setupvalue(lua_State* L, int funcindex, int n); + +LUA_API int lua_hascustomexecution(lua_State* L, int level); // function has custom execution data set +LUA_API int lua_incustomexecution(lua_State* L, int level); // function is running using custom execution + +LUA_API void lua_singlestep(lua_State* L, int enabled); +LUA_API int lua_breakpoint(lua_State* L, int funcindex, int line, int enabled); + +// returns 1 if execution is currently at a breakpoint; should only be called from debug callbacks +LUA_API int lua_atbreakpoint(lua_State* L); + +// Warning: this function is not thread-safe since it stores the result in a shared global array! Only use for debugging. +LUA_API const char* lua_debugtrace(lua_State* L); + +struct lua_Debug +{ + const char* name; // (n) + const char* what; // (s) `Lua', `C', `main', `tail' + const char* source; // (s) + const char* short_src; // (s) + int linedefined; // (s) + int currentline; // (l) + int protoid; // (p) globally unique (within VM) proto id; 0 for C functions + int bytecodeid; // (p) proto index within its bytecode module; -1 for C functions + unsigned char nupvals; // (u) number of upvalues + unsigned char nparams; // (a) number of parameters + char isvararg; // (a) + void* userdata; // only valid in lua_callhook + + char ssbuf[LUA_IDSIZE]; +}; + +typedef void (*lua_Coverage)(void* context, const char* function, int linedefined, int depth, const int* hits, size_t size); + +LUA_API void lua_getcoverage(lua_State* L, int funcindex, void* context, lua_Coverage callback); + +typedef void (*lua_CounterFunction)(void* context, const char* function, int linedefined); +typedef void (*lua_CounterValue)(void* context, int kind, int line, uint64_t hits); + +// Unlike 'lua_getcoverage', counters are customizable in ways which prevent merging them together +// 'lua_getcounters' will visit the specified function and all nested functions +// 'functionvisit' is called first to establish a function, then multiple calls of 'countervisit' are made for each counter in that function +LUA_API void lua_getcounters(lua_State* L, int funcindex, void* context, lua_CounterFunction functionvisit, lua_CounterValue countervisit); + +// }====================================================================== + +/* Callbacks that can be used to reconfigure behavior of the VM dynamically. + * These are shared between all coroutines. + * + * Note: interrupt is safe to set from an arbitrary thread but all other callbacks + * can only be changed when the VM is not running any code */ +struct lua_Callbacks +{ + void* userdata; // arbitrary userdata pointer that is never overwritten by Luau + + void (*interrupt)(lua_State* L, int gc); // gets called at safepoints (loop back edges, call/ret, gc) if set + void (*panic)(lua_State* L, int errcode); // gets called when an unprotected error is raised (if longjmp is used) + + void (*userthread)(lua_State* LP, lua_State* L); // gets called when L is created (LP == parent) or destroyed (LP == NULL) + int16_t (*useratom)(lua_State* L, const char* s, size_t l); // gets called when a string is created to assign an atom id + + // NOTE: experimental API, requires a Debug flag to be called and is subject to breaking changes + void (*userfinalizer)(lua_State* L, lua_State* co); // gets called before a finalizer is attached to 'co' by current thread + + void (*debugbreak)(lua_State* L, lua_Debug* ar); // gets called when BREAK instruction is encountered + void (*debugstep)(lua_State* L, lua_Debug* ar); // gets called after each instruction in single step mode + void (*debuginterrupt)(lua_State* L, lua_Debug* ar); // gets called when thread execution is interrupted by break in another thread + void (*debugprotectederror)(lua_State* L); // gets called when protected call results in an error + + // gets called after a heap object (or array) is allocated + void (*onallocate)(lua_State* L, void* block, size_t osize, size_t nsize, uint8_t memcat, int tt, int tag); + + void (*preresume)(lua_State* L); // gets called before lua_resume runs a (co)routine + void (*postresume)(lua_State* L); // gets called after lua_resume returns (yield, return, or error) + + // gets called before a heap object (or array) is freed + void (*onfree)(lua_State* L, void* block); +}; +typedef struct lua_Callbacks lua_Callbacks; + +LUA_API lua_Callbacks* lua_callbacks(lua_State* L); + +// Must be called after lua_newstate and before the state creates any additional objects +// The VM makes no assumptions about the layout or structure of the caged heap +// The VM does assume that the embedder will free any memory allocated if the main lua_State the cage is associated with is closed +LUA_API void lua_setmemorycage(lua_State* L, lua_CageAlloc alloc, void* ud); + +/****************************************************************************** + * Copyright (c) 2019-2023 Roblox Corporation + * Copyright (C) 1994-2008 Lua.org, PUC-Rio. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ******************************************************************************/ diff --git a/External/luau/VM/include/luaconf.h b/External/luau/VM/include/luaconf.h new file mode 100644 index 00000000..ed0c6fa0 --- /dev/null +++ b/External/luau/VM/include/luaconf.h @@ -0,0 +1,150 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +// When debugging complex issues, consider enabling one of these: +// This will reallocate the stack very aggressively at every opportunity; use this with asan to catch stale stack pointers +// #define HARDSTACKTESTS 1 +// This will call GC validation very aggressively at every incremental GC step; use this with caution as it's SLOW +// #define HARDMEMTESTS 1 +// This will call GC validation very aggressively at every GC opportunity; use this with caution as it's VERY SLOW +// #define HARDMEMTESTS 2 + +// To force MSVC2017+ to generate SSE2 code for some stdlib functions we need to locally enable /fp:fast +// Note that /fp:fast changes the semantics of floating point comparisons so this is only safe to do for functions without ones +#if defined(_MSC_VER) && !defined(__clang__) +#define LUAU_FASTMATH_BEGIN __pragma(float_control(precise, off, push)) +#define LUAU_FASTMATH_END __pragma(float_control(pop)) +#else +#define LUAU_FASTMATH_BEGIN +#define LUAU_FASTMATH_END +#endif + +// Some functions like floor/ceil have SSE4.1 equivalents but we currently support systems without SSE4.1 +// Note that we only need to do this when SSE4.1 support is not guaranteed by compiler settings, as otherwise compiler will optimize these for us. +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(__SSE4_1__) && !defined(__AVX__) +#if defined(_MSC_VER) && !defined(__clang__) +#define LUAU_TARGET_SSE41 +#elif defined(__GNUC__) && defined(__has_attribute) +#if __has_attribute(target) +#define LUAU_TARGET_SSE41 __attribute__((target("sse4.1"))) +#endif +#endif +#endif + +// Used on functions that have a printf-like interface to validate them statically +#if defined(__GNUC__) +#define LUA_PRINTF_ATTR(fmt, arg) __attribute__((format(printf, fmt, arg))) +#else +#define LUA_PRINTF_ATTR(fmt, arg) +#endif + +#ifdef _MSC_VER +#define LUA_NORETURN __declspec(noreturn) +#else +#define LUA_NORETURN __attribute__((__noreturn__)) +#endif + +// Can be used to reconfigure visibility/exports for public APIs +#ifndef LUA_API +#define LUA_API extern +#endif + +#define LUALIB_API LUA_API + +// Can be used to reconfigure visibility for internal APIs +#if defined(__GNUC__) +#define LUAI_FUNC __attribute__((visibility("hidden"))) extern +#define LUAI_DATA LUAI_FUNC +#else +#define LUAI_FUNC extern +#define LUAI_DATA extern +#endif + +// Can be used to reconfigure internal error handling to use longjmp instead of C++ EH +#ifndef LUA_USE_LONGJMP +#define LUA_USE_LONGJMP 0 +#endif + +// LUA_IDSIZE gives the maximum size for the description of the source +#ifndef LUA_IDSIZE +#define LUA_IDSIZE 256 +#endif + +// LUA_MINSTACK is the initial number of reserved stack slots for a C function +#ifndef LUA_MINSTACK +#define LUA_MINSTACK 20 +#endif + +// LUAI_MAXCSTACK limits the number of Lua stack slots that a C function can use +#ifndef LUAI_MAXCSTACK +#define LUAI_MAXCSTACK 8000 +#endif + +// LUAI_MAXCALLS limits the number of nested calls +#ifndef LUAI_MAXCALLS +#define LUAI_MAXCALLS 20000 +#endif + +// LUAI_MAXCCALLS is the maximum depth for nested C calls; this limit depends on native stack size +#ifndef LUAI_MAXCCALLS +#define LUAI_MAXCCALLS 200 +#endif + +// buffer size used for on-stack string operations; this limit depends on native stack size +#ifndef LUA_BUFFERSIZE +#define LUA_BUFFERSIZE 512 +#endif + +// number of valid Lua userdata tags +#ifndef LUA_UTAG_LIMIT +#define LUA_UTAG_LIMIT 128 +#endif + +// number of valid Lua lightuserdata tags +#ifndef LUA_LUTAG_LIMIT +#define LUA_LUTAG_LIMIT 128 +#endif + +// upper bound for number of size classes used by page allocator +#ifndef LUA_SIZECLASSES +#define LUA_SIZECLASSES 40 +#endif + +// available number of separate memory categories +#ifndef LUA_MEMORY_CATEGORIES +#define LUA_MEMORY_CATEGORIES 256 +#endif + +// extra storage for execution callbacks in global state +#ifndef LUA_EXECUTION_CALLBACK_STORAGE +#define LUA_EXECUTION_CALLBACK_STORAGE 512 +#endif + +// minimum size for the string table (must be power of 2) +#ifndef LUA_MINSTRTABSIZE +#define LUA_MINSTRTABSIZE 32 +#endif + +// maximum number of captures supported by pattern matching +#ifndef LUA_MAXCAPTURES +#define LUA_MAXCAPTURES 32 +#endif + +// }================================================================== + +#ifndef LUA_VECTOR_SIZE +#define LUA_VECTOR_SIZE 3 // must be 3 or 4 +#endif + +#ifndef LUA_VECTOR_DOUBLE +#define LUA_VECTOR_DOUBLE 0 +#endif + +#if LUA_VECTOR_DOUBLE == 1 +#define LUA_VECTOR_TYPE double +#else +#define LUA_VECTOR_TYPE float +#endif + +#define LUA_EXTRA_SIZE (LUA_VECTOR_SIZE - 2) diff --git a/External/luau/VM/include/lualib.h b/External/luau/VM/include/lualib.h new file mode 100644 index 00000000..138cbbd3 --- /dev/null +++ b/External/luau/VM/include/lualib.h @@ -0,0 +1,162 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lua.h" + +#define luaL_error(L, fmt, ...) luaL_errorL(L, fmt, ##__VA_ARGS__) +#define luaL_typeerror(L, narg, tname) luaL_typeerrorL(L, narg, tname) +#define luaL_argerror(L, narg, extramsg) luaL_argerrorL(L, narg, extramsg) + +struct luaL_Reg +{ + const char* name; + lua_CFunction func; +}; +typedef struct luaL_Reg luaL_Reg; + +LUALIB_API void luaL_register(lua_State* L, const char* libname, const luaL_Reg* l); +LUALIB_API int luaL_getmetafield(lua_State* L, int obj, const char* e); +LUALIB_API int luaL_callmeta(lua_State* L, int obj, const char* e); +LUALIB_API l_noret luaL_typeerrorL(lua_State* L, int narg, const char* tname); +LUALIB_API l_noret luaL_argerrorL(lua_State* L, int narg, const char* extramsg); +LUALIB_API const char* luaL_checklstring(lua_State* L, int numArg, size_t* l); +LUALIB_API const char* luaL_optlstring(lua_State* L, int numArg, const char* def, size_t* l); +LUALIB_API double luaL_checknumber(lua_State* L, int numArg); +LUALIB_API double luaL_optnumber(lua_State* L, int nArg, double def); + +LUALIB_API int luaL_checkboolean(lua_State* L, int narg); +LUALIB_API int luaL_optboolean(lua_State* L, int narg, int def); + +LUALIB_API int luaL_checkinteger(lua_State* L, int numArg); +LUALIB_API int64_t luaL_checkinteger64(lua_State* L, int numArg); +LUALIB_API int luaL_optinteger(lua_State* L, int nArg, int def); +LUALIB_API int64_t luaL_optinteger64(lua_State* L, int nArg, int64_t def); +LUALIB_API unsigned luaL_checkunsigned(lua_State* L, int numArg); +LUALIB_API unsigned luaL_optunsigned(lua_State* L, int numArg, unsigned def); + +LUALIB_API const LUA_VECTOR_TYPE* luaL_checkvector(lua_State* L, int narg); +LUALIB_API const LUA_VECTOR_TYPE* luaL_optvector(lua_State* L, int narg, const LUA_VECTOR_TYPE* def); + +LUALIB_API void luaL_checkstack(lua_State* L, int sz, const char* msg); +LUALIB_API void luaL_checktype(lua_State* L, int narg, int t); +LUALIB_API void luaL_checkany(lua_State* L, int narg); + +LUALIB_API int luaL_newmetatable(lua_State* L, const char* tname); +LUALIB_API void* luaL_checkudata(lua_State* L, int ud, const char* tname); +LUALIB_API void* luaL_checkudatatagged(lua_State* L, int ud, int tag); + +LUALIB_API void* luaL_checkbuffer(lua_State* L, int narg, size_t* len); + +LUALIB_API void luaL_where(lua_State* L, int lvl); +LUALIB_API LUA_PRINTF_ATTR(2, 3) l_noret luaL_errorL(lua_State* L, const char* fmt, ...); + +LUALIB_API int luaL_checkoption(lua_State* L, int narg, const char* def, const char* const lst[]); + +LUALIB_API const char* luaL_tolstring(lua_State* L, int idx, size_t* len); + +LUALIB_API lua_State* luaL_newstate(void); + +LUALIB_API const char* luaL_findtable(lua_State* L, int idx, const char* fname, int szhint); + +LUALIB_API const char* luaL_typename(lua_State* L, int idx); + +LUALIB_API void luaL_traceback(lua_State* L, lua_State* L1, const char* msg, int level); + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + +#define luaL_argcheck(L, cond, arg, extramsg) ((void)((cond) ? (void)0 : luaL_argerror(L, arg, extramsg))) +#define luaL_argexpected(L, cond, arg, tname) ((void)((cond) ? (void)0 : luaL_typeerror(L, arg, tname))) + +#define luaL_checkstring(L, n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L, n, d) (luaL_optlstring(L, (n), (d), NULL)) + +#define luaL_getmetatable(L, n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) + +#define luaL_opt(L, f, n, d) (lua_isnoneornil(L, (n)) ? (d) : f(L, (n))) + +// backwards compatibility +#define luaL_callyieldable(L, nargs, nresults) lua_callyieldable(L, nargs, nresults) +#define luaL_pcallyieldable(L, nargs, nresults, errfunc) lua_pcallyieldable(L, nargs, nresults, errfunc) + +// generic buffer manipulation + +struct luaL_Strbuf +{ + char* p; // current position in buffer + char* end; // end of the current buffer + lua_State* L; + struct TString* storage; + char buffer[LUA_BUFFERSIZE]; +}; +typedef struct luaL_Strbuf luaL_Strbuf; + +// compatibility typedef: this type is called luaL_Buffer in Lua headers +// renamed to luaL_Strbuf to reduce confusion with internal VM buffer type +typedef struct luaL_Strbuf luaL_Buffer; + +// when internal buffer storage is exhausted, a mutable string value 'storage' will be placed on the stack +// in general, functions expect the mutable string buffer to be placed on top of the stack (top-1) +// with the exception of luaL_addvalue that expects the value at the top and string buffer further away (top-2) + +#define luaL_addchar(B, c) ((void)((B)->p < (B)->end || luaL_prepbuffsize(B, 1)), (*(B)->p++ = (char)(c))) +#define luaL_addstring(B, s) luaL_addlstring(B, s, strlen(s)) + +LUALIB_API void luaL_buffinit(lua_State* L, luaL_Strbuf* B); +LUALIB_API char* luaL_buffinitsize(lua_State* L, luaL_Strbuf* B, size_t size); +LUALIB_API char* luaL_prepbuffsize(luaL_Buffer* B, size_t size); +LUALIB_API void luaL_addlstring(luaL_Strbuf* B, const char* s, size_t l); +LUALIB_API void luaL_addvalue(luaL_Strbuf* B); +LUALIB_API void luaL_addvalueany(luaL_Strbuf* B, int idx); +LUALIB_API void luaL_pushresult(luaL_Strbuf* B); +LUALIB_API void luaL_pushresultsize(luaL_Strbuf* B, size_t size); + +// builtin libraries +LUALIB_API int luaopen_base(lua_State* L); + +#define LUA_COLIBNAME "coroutine" +LUALIB_API int luaopen_coroutine(lua_State* L); + +#define LUA_TABLIBNAME "table" +LUALIB_API int luaopen_table(lua_State* L); + +#define LUA_OSLIBNAME "os" +LUALIB_API int luaopen_os(lua_State* L); + +#define LUA_STRLIBNAME "string" +LUALIB_API int luaopen_string(lua_State* L); + +#define LUA_BITLIBNAME "bit32" +LUALIB_API int luaopen_bit32(lua_State* L); + +#define LUA_BUFFERLIBNAME "buffer" +LUALIB_API int luaopen_buffer(lua_State* L); + +#define LUA_UTF8LIBNAME "utf8" +LUALIB_API int luaopen_utf8(lua_State* L); + +#define LUA_CLASSLIBNAME "class" +LUALIB_API int luaopen_class(lua_State* L); + +#define LUA_MATHLIBNAME "math" +LUALIB_API int luaopen_math(lua_State* L); + +#define LUA_DBLIBNAME "debug" +LUALIB_API int luaopen_debug(lua_State* L); + +#define LUA_VECLIBNAME "vector" +LUALIB_API int luaopen_vector(lua_State* L); + +#define LUA_INTLIBNAME "integer" +LUALIB_API int luaopen_integer(lua_State* L); + +// open all builtin libraries +LUALIB_API void luaL_openlibs(lua_State* L); + +// sandbox libraries and globals +LUALIB_API void luaL_sandbox(lua_State* L); +LUALIB_API void luaL_sandboxthread(lua_State* L); diff --git a/External/luau/VM/src/lapi.cpp b/External/luau/VM/src/lapi.cpp new file mode 100644 index 00000000..fc3d94ef --- /dev/null +++ b/External/luau/VM/src/lapi.cpp @@ -0,0 +1,2261 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lapi.h" + +#include "lbytecode.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lfunc.h" +#include "lgc.h" +#include "ldo.h" +#include "ltm.h" +#include "ludata.h" +#include "lvm.h" +#include "lnumutils.h" +#include "lbuffer.h" +#include "lvector.h" + +#include + +LUAU_FASTFLAG(LuauGcTraceUdata) +LUAU_FASTFLAGVARIABLE(LuauNewPointerEncode) +LUAU_FASTFLAGVARIABLE(DebugLuauCoroutineFinally) +LUAU_FASTFLAGVARIABLE(LuauFrozenMetaButterfly) + +/* + * This file contains most implementations of core Lua APIs from lua.h. + * + * These implementations should use api_check macros to verify that stack and type contracts hold; it's the callers + * responsibility to, for example, pass a valid table index to lua_rawgetfield. Generally errors should only be raised + * for conditions caller can't predict such as an out-of-memory error. + * + * The caller is expected to handle stack reservation (by using less than LUA_MINSTACK slots or by calling lua_checkstack). + * To ensure this is handled correctly, use api_incr_top(L) when pushing values to the stack. + * + * Functions that push any collectable objects to the stack *should* call luaC_threadbarrier. Failure to do this can result + * in stack references that point to dead objects since black threads don't get rescanned. + * + * Functions that push newly created objects to the stack *should* call luaC_checkGC in addition to luaC_threadbarrier. + * Failure to do this can result in OOM since GC may never run. + * + * Note that luaC_checkGC may mark the thread and paint it black; functions that call both before pushing objects must + * therefore call luaC_checkGC before luaC_threadbarrier to guarantee the object is pushed to a gray thread. + */ + +const char* lua_ident = "$Lua: Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio $\n" + "$Authors: R. Ierusalimschy, L. H. de Figueiredo & W. Celes $\n" + "$URL: www.lua.org $\n"; + +const char* luau_ident = "$Luau: Copyright (C) 2019-2024 Roblox Corporation $\n" + "$URL: luau.org $\n"; + +#define api_checknelems(L, n) api_check(L, (n) <= (L->top - L->base)) + +#define api_checkvalidindex(L, i) api_check(L, (i) != luaO_nilobject) + +#define ensure_stack_impl(L, errorL, size) \ + { \ + if (L->top + (size) > L->ci->top && !lua_checkstack(L, (size))) \ + { \ + luaO_pushfstring(errorL, "stack overflow"); \ + lua_error(errorL); \ + } \ + } + +#define ensure_stack(L, size) ensure_stack_impl(L, L, size) + +#define api_incr_top(L) \ + { \ + api_check(L, L->top < L->ci->top); \ + L->top++; \ + } + +#define api_update_top(L, p) \ + { \ + api_check(L, p >= L->base && p <= L->ci->top); \ + L->top = p; \ + } + +LuaTable* getcurrenv(lua_State* L) +{ + if (L->ci == L->base_ci) // no enclosing function? + return L->gt; // use global table as environment + else + return curr_func(L)->env; +} + +static LUAU_NOINLINE TValue* pseudo2addr(lua_State* L, int idx) +{ + api_check(L, lua_ispseudo(idx)); + switch (idx) + { // pseudo-indices + case LUA_REGISTRYINDEX: + return registry(L); + case LUA_ENVIRONINDEX: + { + sethvalue(L, &L->global->pseudotemp, getcurrenv(L)); + return &L->global->pseudotemp; + } + case LUA_GLOBALSINDEX: + { + sethvalue(L, &L->global->pseudotemp, L->gt); + return &L->global->pseudotemp; + } + default: + { + Closure* func = curr_func(L); + idx = LUA_GLOBALSINDEX - idx; + return (idx <= func->nupvalues) ? &func->c.upvals[idx - 1] : cast_to(TValue*, luaO_nilobject); + } + } +} + +static LUAU_FORCEINLINE TValue* index2addr(lua_State* L, int idx) +{ + if (idx > 0) + { + TValue* o = L->base + (idx - 1); + api_check(L, idx <= L->ci->top - L->base); + if (o >= L->top) + return cast_to(TValue*, luaO_nilobject); + else + return o; + } + else if (idx > LUA_REGISTRYINDEX) + { + api_check(L, idx != 0 && -idx <= L->top - L->base); + return L->top + idx; + } + else + { + return pseudo2addr(L, idx); + } +} + +const TValue* luaA_toobject(lua_State* L, int idx) +{ + StkId p = index2addr(L, idx); + return (p == luaO_nilobject) ? NULL : p; +} + +void luaA_pushvalue(lua_State* L, const TValue* o) +{ + ensure_stack(L, 1); + setobj2s(L, L->top, o); + api_incr_top(L); +} + +void luaA_pushclass(lua_State* L, LuauClass* lco) +{ + ensure_stack(L, 1); + api_check(L, lco != nullptr); + setclassvalue(L, L->top, lco); + api_incr_top(L); +} + +int lua_checkstack(lua_State* L, int size) +{ + api_check(L, size >= 0); + + int res = 1; + if (size > LUAI_MAXCSTACK || (L->top - L->base + size) > LUAI_MAXCSTACK) + res = 0; // stack overflow + else if (size > 0) + { + if (stacklimitreached(L, size)) + { + struct CallContext + { + int size; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaD_growstack(L, ctx->size); + } + } ctx = {size}; + + // there could be no memory to extend the stack + if (luaD_rawrunprotected(L, &CallContext::run, &ctx) != LUA_OK) + return 0; + } + else + { + condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK, 0)); + } + + expandstacklimit(L, L->top + size); + } + return res; +} + +void lua_rawcheckstack(lua_State* L, int size) +{ + api_check(L, size >= 0); + + luaD_checkstack(L, size); + expandstacklimit(L, L->top + size); +} + +void lua_xmove(lua_State* from, lua_State* to, int n) +{ + api_check(from, n >= 0); + + if (from == to) + return; + + api_checknelems(from, n); + api_check(from, from->global == to->global); + luaC_threadbarrier(to); + ensure_stack_impl(to, from, n); + + StkId ttop = to->top; + StkId ftop = from->top - n; + for (int i = 0; i < n; i++) + setobj2s(to, ttop + i, ftop + i); + + from->top = ftop; + to->top = ttop + n; +} + +void lua_xpush(lua_State* from, lua_State* to, int idx) +{ + api_check(from, from->global == to->global); + luaC_threadbarrier(to); + ensure_stack_impl(to, from, 1); + setobj2s(to, to->top, index2addr(from, idx)); + api_incr_top(to); +} + +lua_State* lua_newthread(lua_State* L) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + lua_State* L1 = luaE_newthread(L); + setthvalue(L, L->top, L1); + api_incr_top(L); + global_State* g = L->global; + if (g->cb.userthread) + g->cb.userthread(L, L1); + return L1; +} + +lua_State* lua_mainthread(lua_State* L) +{ + return L->global->mainthread; +} + +/* +** basic stack manipulation +*/ + +int lua_absindex(lua_State* L, int idx) +{ + api_check(L, (idx > 0 && idx <= L->top - L->base) || (idx < 0 && -idx <= L->top - L->base) || lua_ispseudo(idx)); + return idx > 0 || lua_ispseudo(idx) ? idx : cast_int(L->top - L->base) + idx + 1; +} + +int lua_gettop(lua_State* L) +{ + return cast_int(L->top - L->base); +} + +void lua_settop(lua_State* L, int idx) +{ + if (idx >= 0) + { + ensure_stack(L, idx - int(L->top - L->base)); + while (L->top < L->base + idx) + setnilvalue(L->top++); + L->top = L->base + idx; + } + else + { + api_check(L, -(idx + 1) <= (L->top - L->base)); + L->top += idx + 1; // `subtract' index (index is negative) + } +} + +void lua_remove(lua_State* L, int idx) +{ + StkId p = index2addr(L, idx); + api_checkvalidindex(L, p); + while (++p < L->top) + setobj2s(L, p - 1, p); + L->top--; +} + +void lua_insert(lua_State* L, int idx) +{ + luaC_threadbarrier(L); + StkId p = index2addr(L, idx); + api_checkvalidindex(L, p); + for (StkId q = L->top; q > p; q--) + setobj2s(L, q, q - 1); + setobj2s(L, p, L->top); +} + +void lua_replace(lua_State* L, int idx) +{ + api_checknelems(L, 1); + luaC_threadbarrier(L); + StkId o = index2addr(L, idx); + api_checkvalidindex(L, o); + if (idx == LUA_ENVIRONINDEX) + { + api_check(L, L->ci != L->base_ci); + Closure* func = curr_func(L); + api_check(L, ttistable(L->top - 1)); + func->env = hvalue(L->top - 1); + luaC_barrier(L, func, L->top - 1); + } + else if (idx == LUA_GLOBALSINDEX) + { + api_check(L, ttistable(L->top - 1)); + L->gt = hvalue(L->top - 1); + } + else + { + setobj(L, o, L->top - 1); + if (idx < LUA_GLOBALSINDEX) // function upvalue? + luaC_barrier(L, curr_func(L), L->top - 1); + } + L->top--; +} + +void lua_pushvalue(lua_State* L, int idx) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId o = index2addr(L, idx); + setobj2s(L, L->top, o); + api_incr_top(L); +} + +/* +** access functions (stack -> C) +*/ + +int lua_type(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return (o == luaO_nilobject) ? LUA_TNONE : ttype(o); +} + +const char* lua_typename(lua_State* L, int t) +{ + api_check(L, t >= LUA_TNONE && t < LUA_T_COUNT); + + return (t == LUA_TNONE) ? "no value" : luaT_typenames[t]; +} + +int lua_iscfunction(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return iscfunction(o); +} + +int lua_isLfunction(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return isLfunction(o); +} + +int lua_isnumber(lua_State* L, int idx) +{ + TValue n; + const TValue* o = index2addr(L, idx); + return tonumber(o, &n); +} + +int lua_isstring(lua_State* L, int idx) +{ + int t = lua_type(L, idx); + return (t == LUA_TSTRING || t == LUA_TNUMBER); +} + +int lua_isuserdata(lua_State* L, int idx) +{ + const TValue* o = index2addr(L, idx); + return (ttisuserdata(o) || ttislightuserdata(o)); +} + +int lua_rawequal(lua_State* L, int index1, int index2) +{ + StkId o1 = index2addr(L, index1); + StkId o2 = index2addr(L, index2); + return (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : luaO_rawequalObj(o1, o2); +} + +int lua_equal(lua_State* L, int index1, int index2) +{ + StkId o1, o2; + int i; + o1 = index2addr(L, index1); + o2 = index2addr(L, index2); + i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : equalobj(L, o1, o2); + return i; +} + +int lua_lessthan(lua_State* L, int index1, int index2) +{ + StkId o1, o2; + int i; + o1 = index2addr(L, index1); + o2 = index2addr(L, index2); + i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : luaV_lessthan(L, o1, o2); + return i; +} + +double lua_tonumberx(lua_State* L, int idx, int* isnum) +{ + TValue n; + const TValue* o = index2addr(L, idx); + if (tonumber(o, &n)) + { + if (isnum) + *isnum = 1; + return nvalue(o); + } + else + { + if (isnum) + *isnum = 0; + return 0; + } +} + +int lua_tointegerx(lua_State* L, int idx, int* isnum) +{ + TValue n; + const TValue* o = index2addr(L, idx); + if (tonumber(o, &n)) + { + int res; + double num = nvalue(o); + luai_num2int(res, num); + if (isnum) + *isnum = 1; + return res; + } + else + { + if (isnum) + *isnum = 0; + return 0; + } +} + +unsigned lua_tounsignedx(lua_State* L, int idx, int* isnum) +{ + TValue n; + const TValue* o = index2addr(L, idx); + if (tonumber(o, &n)) + { + unsigned res; + double num = nvalue(o); + luai_num2unsigned(res, num); + if (isnum) + *isnum = 1; + return res; + } + else + { + if (isnum) + *isnum = 0; + return 0; + } +} + +int lua_toboolean(lua_State* L, int idx) +{ + const TValue* o = index2addr(L, idx); + return !l_isfalse(o); +} + +int64_t lua_tointeger64(lua_State* L, int idx, int* isinteger) +{ + const TValue* o = index2addr(L, idx); + if (ttisinteger(o)) + { + if (isinteger) + *isinteger = 1; + return lvalue(o); + } + else + { + if (isinteger) + *isinteger = 0; + return 0; + } +} + +const char* lua_tolstring(lua_State* L, int idx, size_t* len) +{ + StkId o = index2addr(L, idx); + if (!ttisstring(o)) + { + luaC_threadbarrier(L); + if (!luaV_tostring(L, o)) + { // conversion failed? + if (len != NULL) + *len = 0; + return NULL; + } + luaC_checkGC(L); + o = index2addr(L, idx); // previous call may reallocate the stack + } + if (len != NULL) + *len = tsvalue(o)->len; + return svalue(o); +} + +const char* lua_tostringatom(lua_State* L, int idx, int* atom) +{ + StkId o = index2addr(L, idx); + if (!ttisstring(o)) + return NULL; + TString* s = tsvalue(o); + if (atom) + { + luaS_updateatom(L, s); + *atom = s->atom; + } + return getstr(s); +} + +const char* lua_tolstringatom(lua_State* L, int idx, size_t* len, int* atom) +{ + StkId o = index2addr(L, idx); + + if (!ttisstring(o)) + { + if (len) + *len = 0; + return NULL; + } + + TString* s = tsvalue(o); + if (len) + *len = s->len; + if (atom) + { + luaS_updateatom(L, s); + *atom = s->atom; + } + + return getstr(s); +} + +const char* lua_namecallatom(lua_State* L, int* atom) +{ + TString* s = L->namecall; + if (!s) + return NULL; + if (atom) + { + luaS_updateatom(L, s); + *atom = s->atom; + } + return getstr(s); +} + +const LUA_VECTOR_TYPE* lua_tovector(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + if (!ttisvector(o)) + return NULL; + return vvalue(o); +} + +int lua_objlen(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + switch (ttype(o)) + { + case LUA_TSTRING: + return tsvalue(o)->len; + case LUA_TUSERDATA: + return uvalue(o)->len; + case LUA_TBUFFER: + return bufvalue(o)->len; + case LUA_TTABLE: + return luaH_getn(hvalue(o)); + default: + return 0; + } +} + +lua_CFunction lua_tocfunction(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return (!iscfunction(o)) ? NULL : cast_to(lua_CFunction, clvalue(o)->c.f); +} + +void* lua_tolightuserdata(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return (!ttislightuserdata(o)) ? NULL : pvalue(o); +} + +void* lua_tolightuserdatatagged(lua_State* L, int idx, int tag) +{ + StkId o = index2addr(L, idx); + return (!ttislightuserdata(o) || lightuserdatatag(o) != tag) ? NULL : pvalue(o); +} + +void* lua_touserdata(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + if (ttisuserdata(o)) + return uvalue(o)->data; + else if (ttislightuserdata(o)) + return pvalue(o); + else + return NULL; +} + +void* lua_touserdatatagged(lua_State* L, int idx, int tag) +{ + StkId o = index2addr(L, idx); + return (ttisuserdata(o) && uvalue(o)->tag == tag) ? uvalue(o)->data : NULL; +} + +int lua_userdatatag(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + if (ttisuserdata(o)) + return uvalue(o)->tag; + return -1; +} + +int lua_lightuserdatatag(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + if (ttislightuserdata(o)) + return lightuserdatatag(o); + return -1; +} + +lua_State* lua_tothread(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + return (!ttisthread(o)) ? NULL : thvalue(o); +} + +void* lua_tobuffer(lua_State* L, int idx, size_t* len) +{ + StkId o = index2addr(L, idx); + + if (!ttisbuffer(o)) + return NULL; + + Buffer* b = bufvalue(o); + + if (len) + *len = b->len; + + return b->data; +} + +const void* lua_topointer(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + switch (ttype(o)) + { + case LUA_TUSERDATA: + return uvalue(o)->data; + case LUA_TLIGHTUSERDATA: + return pvalue(o); + default: + return iscollectable(o) ? gcvalue(o) : NULL; + } +} + +/* +** push functions (C -> stack) +*/ + +void lua_pushnil(lua_State* L) +{ + ensure_stack(L, 1); + setnilvalue(L->top); + api_incr_top(L); +} + +void lua_pushnumber(lua_State* L, double n) +{ + ensure_stack(L, 1); + setnvalue(L->top, n); + api_incr_top(L); +} + +void lua_pushinteger(lua_State* L, int n) +{ + ensure_stack(L, 1); + setnvalue(L->top, cast_num(n)); + api_incr_top(L); +} + +void lua_pushinteger64(lua_State* L, int64_t n) +{ + ensure_stack(L, 1); + setlvalue(L->top, n); + api_incr_top(L); +} + +void lua_pushunsigned(lua_State* L, unsigned u) +{ + ensure_stack(L, 1); + setnvalue(L->top, cast_num(u)); + api_incr_top(L); +} + +#if LUA_VECTOR_SIZE == 4 +void lua_pushvector(lua_State* L, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z, LUA_VECTOR_TYPE w) +{ + if (LUA_VECTOR_DOUBLE == 1) + { + luaC_checkGC(L); + luaC_threadbarrier(L); + } + ensure_stack(L, 1); + setvvalue(L, L->top, x, y, z, w); + api_incr_top(L); +} +#else +void lua_pushvector(lua_State* L, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z) +{ + if (LUA_VECTOR_DOUBLE == 1) + { + luaC_checkGC(L); + luaC_threadbarrier(L); + } + ensure_stack(L, 1); + setvvalue(L, L->top, x, y, z, LUA_VECTOR_TYPE(0.0)); + api_incr_top(L); +} +#endif + +void lua_pushlstring(lua_State* L, const char* s, size_t len) +{ + api_check(L, s != nullptr); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + setsvalue(L, L->top, luaS_newlstr(L, s, len)); + api_incr_top(L); +} + +void lua_pushstring(lua_State* L, const char* s) +{ + if (s == NULL) + lua_pushnil(L); + else + lua_pushlstring(L, s, strlen(s)); +} + +const char* lua_pushvfstring(lua_State* L, const char* fmt, va_list argp) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + const char* ret = luaO_pushvfstring(L, fmt, argp); + return ret; +} + +const char* lua_pushfstringL(lua_State* L, const char* fmt, ...) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + va_list argp; + va_start(argp, fmt); + const char* ret = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + return ret; +} + +void lua_pushcclosurek(lua_State* L, lua_CFunction fn, const char* debugname, int nup, lua_Continuation cont) +{ + api_check(L, fn != nullptr); + api_check(L, nup >= 0); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + api_checknelems(L, nup); + Closure* cl = luaF_newCclosure(L, nup, getcurrenv(L)); + cl->c.f = fn; + cl->c.cont = cont; + cl->c.debugname = debugname ? luaS_new(L, debugname) : nullptr; + + L->top -= nup; + while (nup--) + setobj2n(L, &cl->c.upvals[nup], L->top + nup); + setclvalue(L, L->top, cl); + LUAU_ASSERT(iswhite(obj2gco(cl))); + api_incr_top(L); +} + +void lua_pushboolean(lua_State* L, int b) +{ + ensure_stack(L, 1); + setbvalue(L->top, (b != 0)); // ensure that true is 1 + api_incr_top(L); +} + +void lua_pushlightuserdatatagged(lua_State* L, void* p, int tag) +{ + ensure_stack(L, 1); + api_check(L, unsigned(tag) < LUA_LUTAG_LIMIT); + setpvalue(L->top, p, tag); + api_incr_top(L); +} + +int lua_pushthread(lua_State* L) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + setthvalue(L, L->top, L); + api_incr_top(L); + return L->global->mainthread == L; +} + +/* +** get functions (Lua -> stack) +*/ + +int lua_gettable(lua_State* L, int idx) +{ + api_checknelems(L, 1); + luaC_threadbarrier(L); + StkId t = index2addr(L, idx); + api_checkvalidindex(L, t); + luaV_gettable(L, t, L->top - 1, L->top - 1); + return ttype(L->top - 1); +} + +int lua_getfield(lua_State* L, int idx, const char* k) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_checkvalidindex(L, t); + TValue key; + setsvalue(L, &key, luaS_new(L, k)); + luaV_gettable(L, t, &key, L->top); + api_incr_top(L); + return ttype(L->top - 1); +} + +int lua_rawgetfield(lua_State* L, int idx, const char* k) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + TValue key; + setsvalue(L, &key, luaS_new(L, k)); + setobj2s(L, L->top, luaH_getstr(hvalue(t), tsvalue(&key))); + api_incr_top(L); + return ttype(L->top - 1); +} + +int lua_rawget(lua_State* L, int idx) +{ + luaC_threadbarrier(L); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + setobj2s(L, L->top - 1, luaH_get(hvalue(t), L->top - 1)); + return ttype(L->top - 1); +} + +int lua_rawgeti(lua_State* L, int idx, int n) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + setobj2s(L, L->top, luaH_getnum(hvalue(t), n)); + api_incr_top(L); + return ttype(L->top - 1); +} + +int lua_rawgetptagged(lua_State* L, int idx, void* p, int tag) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + setobj2s(L, L->top, luaH_getp(hvalue(t), p, tag)); + api_incr_top(L); + return ttype(L->top - 1); +} + +void lua_createtable(lua_State* L, int narray, int nrec) +{ + api_check(L, narray >= 0 && nrec >= 0); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + sethvalue(L, L->top, luaH_new(L, narray, nrec)); + api_incr_top(L); +} + +void lua_setreadonly(lua_State* L, int objindex, int enabled) +{ + const TValue* o = index2addr(L, objindex); + api_check(L, ttistable(o)); + LuaTable* t = hvalue(o); + api_check(L, t != hvalue(registry(L))); + + if (FFlag::LuauFrozenMetaButterfly) + luaH_setreadonly(L, t, enabled != 0); + else + t->readonly = bool(enabled); +} + +int lua_getreadonly(lua_State* L, int objindex) +{ + const TValue* o = index2addr(L, objindex); + api_check(L, ttistable(o)); + LuaTable* t = hvalue(o); + + if (FFlag::LuauFrozenMetaButterfly) + { + return luaH_getreadonly(t); + } + else + { + int res = t->readonly; + return res; + } +} + +void lua_setsafeenv(lua_State* L, int objindex, int enabled) +{ + const TValue* o = index2addr(L, objindex); + api_check(L, ttistable(o)); + LuaTable* t = hvalue(o); + t->safeenv = bool(enabled); +} + +int lua_getmetatable(lua_State* L, int objindex) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + LuaTable* mt = NULL; + const TValue* obj = index2addr(L, objindex); + switch (ttype(obj)) + { + case LUA_TTABLE: + mt = hvalue(obj)->metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(obj)->metatable; + break; + case LUA_TOBJECT: + mt = objectvalue(obj)->lclass->instancemetatable; + break; + default: + mt = L->global->mt[ttype(obj)]; + break; + } + if (mt) + { + sethvalue(L, L->top, mt); + api_incr_top(L); + } + return mt != NULL; +} + +void lua_getfenv(lua_State* L, int idx) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId o = index2addr(L, idx); + api_checkvalidindex(L, o); + switch (ttype(o)) + { + case LUA_TFUNCTION: + sethvalue(L, L->top, clvalue(o)->env); + break; + case LUA_TTHREAD: + sethvalue(L, L->top, thvalue(o)->gt); + break; + default: + setnilvalue(L->top); + break; + } + api_incr_top(L); +} + +/* +** set functions (stack -> Lua) +*/ + +void lua_settable(lua_State* L, int idx) +{ + api_checknelems(L, 2); + StkId t = index2addr(L, idx); + api_checkvalidindex(L, t); + luaV_settable(L, t, L->top - 2, L->top - 1); + L->top -= 2; // pop index and value +} + +void lua_setfield(lua_State* L, int idx, const char* k) +{ + api_checknelems(L, 1); + StkId t = index2addr(L, idx); + api_checkvalidindex(L, t); + TValue key; + setsvalue(L, &key, luaS_new(L, k)); + luaV_settable(L, t, &key, L->top - 1); + L->top--; +} + +void lua_rawsetfield(lua_State* L, int idx, const char* k) +{ + api_checknelems(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + if (hvalue(t)->readonly) + luaG_readonlyerror(L); + setobj2t(L, luaH_setstr(L, hvalue(t), luaS_new(L, k)), L->top - 1); + luaC_barriert(L, hvalue(t), L->top - 1); + L->top--; +} + +void lua_rawset(lua_State* L, int idx) +{ + api_checknelems(L, 2); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + if (hvalue(t)->readonly) + luaG_readonlyerror(L); + setobj2t(L, luaH_set(L, hvalue(t), L->top - 2), L->top - 1); + luaC_barriert(L, hvalue(t), L->top - 1); + L->top -= 2; +} + +void lua_rawseti(lua_State* L, int idx, int n) +{ + api_checknelems(L, 1); + StkId o = index2addr(L, idx); + api_check(L, ttistable(o)); + if (hvalue(o)->readonly) + luaG_readonlyerror(L); + setobj2t(L, luaH_setnum(L, hvalue(o), n), L->top - 1); + luaC_barriert(L, hvalue(o), L->top - 1); + L->top--; +} + +void lua_rawsetptagged(lua_State* L, int idx, void* p, int tag) +{ + api_checknelems(L, 1); + StkId o = index2addr(L, idx); + api_check(L, ttistable(o)); + if (hvalue(o)->readonly) + luaG_readonlyerror(L); + setobj2t(L, luaH_setp(L, hvalue(o), p, tag), L->top - 1); + luaC_barriert(L, hvalue(o), L->top - 1); + L->top--; +} + +int lua_setmetatable(lua_State* L, int objindex) +{ + api_checknelems(L, 1); + TValue* obj = index2addr(L, objindex); + api_checkvalidindex(L, obj); + LuaTable* mt = NULL; + if (!ttisnil(L->top - 1)) + { + api_check(L, ttistable(L->top - 1)); + mt = hvalue(L->top - 1); + } + switch (ttype(obj)) + { + case LUA_TTABLE: + { + if (hvalue(obj)->readonly) + luaG_readonlyerror(L); + hvalue(obj)->metatable = mt; + if (mt) + luaC_objbarrier(L, hvalue(obj), mt); + break; + } + case LUA_TUSERDATA: + { + uvalue(obj)->metatable = mt; + if (mt) + luaC_objbarrier(L, uvalue(obj), mt); + break; + } + default: + { + L->global->mt[ttype(obj)] = mt; + break; + } + } + L->top--; + return 1; +} + +int lua_setfenv(lua_State* L, int idx) +{ + int res = 1; + api_checknelems(L, 1); + StkId o = index2addr(L, idx); + api_checkvalidindex(L, o); + api_check(L, ttistable(L->top - 1)); + switch (ttype(o)) + { + case LUA_TFUNCTION: + clvalue(o)->env = hvalue(L->top - 1); + break; + case LUA_TTHREAD: + thvalue(o)->gt = hvalue(L->top - 1); + break; + default: + res = 0; + break; + } + if (res) + { + luaC_objbarrier(L, &gcvalue(o)->gch, hvalue(L->top - 1)); + } + L->top--; + return res; +} + +/* +** `load' and `call' functions (run Lua code) +*/ + +#define adjustresults(L, nres) \ + { \ + if (nres == LUA_MULTRET && L->top >= L->ci->top) \ + L->ci->top = L->top; \ + } + +void lua_call(lua_State* L, int nargs, int nresults) +{ + api_check(L, nargs >= 0); + api_check(L, nresults >= LUA_MULTRET); + api_checknelems(L, nargs + 1); + api_check(L, L->status == 0); + + if (nresults > nargs + 1) + ensure_stack(L, nresults - (nargs + 1)); + + StkId func = L->top - (nargs + 1); + + luaD_call(L, func, nresults); + + adjustresults(L, nresults); +} + +/* +** Execute a protected call. +*/ +// data to `f_call' +struct CallS +{ + StkId func; + int nresults; +}; + +static void f_call(lua_State* L, void* ud) +{ + struct CallS* c = cast_to(struct CallS*, ud); + luaD_call(L, c->func, c->nresults); +} + +int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc) +{ + api_check(L, nargs >= 0); + api_check(L, nresults >= LUA_MULTRET); + api_checknelems(L, nargs + 1); + api_check(L, L->status == 0); + + if (nresults > nargs + 1) + ensure_stack(L, nresults - (nargs + 1)); + + ptrdiff_t func = 0; + if (errfunc != 0) + { + StkId o = index2addr(L, errfunc); + api_checkvalidindex(L, o); + func = savestack(L, o); + } + struct CallS c; + c.func = L->top - (nargs + 1); // function to be called + c.nresults = nresults; + + int status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); + + adjustresults(L, nresults); + return status; +} + +/* +** Execute a protected C call. +*/ +// data to `f_Ccall' +struct CCallS +{ + lua_CFunction func; + void* ud; +}; + +static void f_Ccall(lua_State* L, void* ud) +{ + struct CCallS* c = cast_to(struct CCallS*, ud); + + if (!lua_checkstack(L, 2)) + luaG_runerror(L, "stack limit"); + + lua_pushcclosurek(L, c->func, nullptr, 0, nullptr); + lua_pushlightuserdata(L, c->ud); + luaD_call(L, L->top - 2, 0); +} + +int lua_cpcall(lua_State* L, lua_CFunction func, void* ud) +{ + api_check(L, L->status == 0); + api_check(L, func != nullptr); + + struct CCallS c; + c.func = func; + c.ud = ud; + + return luaD_pcall(L, f_Ccall, &c, savestack(L, L->top), 0); +} + +int lua_callyieldable(lua_State* L, int nargs, int nresults) +{ + api_check(L, iscfunction(L->ci->func)); + Closure* cl = clvalue(L->ci->func); + api_check(L, cl->c.cont); + + lua_call(L, nargs, nresults); + + // yielding means we need to propagate yield; resume will call continuation function later + if (isyielded(L)) + return C_CALL_YIELD; + + return cl->c.cont(L, LUA_OK); +} + +int lua_pcallyieldable(lua_State* L, int nargs, int nresults, int errfunc) +{ + api_check(L, iscfunction(L->ci->func)); + Closure* cl = clvalue(L->ci->func); + api_check(L, cl->c.cont); + api_check(L, nargs + 1 <= L->top - L->base); + api_check(L, errfunc >= 0 && errfunc <= L->top - L->base); + + L->ci->errfunc = errfunc; // 0 means no error function + L->ci->flags |= LUA_CALLINFO_HANDLE; + + struct CallContext + { + StkId func; + int nresults; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaD_callint(L, ctx->func, ctx->nresults, lua_isyieldable(L) != 0); + } + } ctx = {L->top - (nargs + 1), nresults}; + + ptrdiff_t savedfunc = savestack(L, ctx.func); + ptrdiff_t savederrfunc = errfunc != 0 ? savestack(L, L->base + (errfunc - 1)) : 0; + + int status = luaD_pcall(L, &CallContext::run, &ctx, savedfunc, savederrfunc); + + // necessary to accommodate functions that return lots of values + expandstacklimit(L, L->top); + + // yielding means we need to propagate yield; resume will call continuation function later + if (status == 0 && isyielded(L)) + return C_CALL_YIELD; + + // the called function has completed synchronously, continuation can use non-protected calls again + L->ci->flags &= ~LUA_CALLINFO_HANDLE; + + return cl->c.cont(L, status); +} + +int lua_status(lua_State* L) +{ + return L->status; +} + +int lua_costatus(lua_State* L, lua_State* co) +{ + api_check(L, co != nullptr); + api_check(L, L->global == co->global); + + if (co == L) + return LUA_CORUN; + if (co->status == LUA_YIELD) + return LUA_COSUS; + if (co->status == LUA_BREAK) + return LUA_CONOR; + if (co->status != 0) // some error occurred + return LUA_COERR; + if (co->ci != co->base_ci) // does it have frames? + return LUA_CONOR; + if (co->top == co->base) + return LUA_COFIN; + return LUA_COSUS; // initial state +} + +int lua_hasfinalizers(lua_State* L) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + return L->finalizers ? 1 : 0; +} + +static int runfinalizery(lua_State* L) +{ + lua_State* co = lua_tothread(L, 1); + api_check(L, co != nullptr && co != L); + api_check(L, L->global == co->global); + api_check(L, co->status != LUA_YIELD && co->status != LUA_BREAK); + api_check(L, co->finalizers != nullptr); + + luaD_preparefinalize(L, co); + return luaD_runfinalizers(L, /* toclose */ false, /* returnstatus */ false); +} + +static int runfinalizercont(lua_State* L, int status) +{ + if (status != LUA_OK) + luaD_throw(L, status); + + return luaD_runfinalizers(L, /* toclose */ false, /* returnstatus */ false); +} + +void lua_pushfinalizerfunction(lua_State* L) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + luaC_threadbarrier(L); + api_check(L, L->status == LUA_OK); + + lua_pushcclosurek(L, runfinalizery, "finalize", 0, runfinalizercont); +} + +void lua_addfinalizer(lua_State* L, lua_State* co, int idx) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + api_check(L, co != nullptr); + api_check(L, !lua_isnoneornil(L, idx)); + + if (co == lua_mainthread(L)) + luaG_runerror(L, "cannot register a finalizer on the main thread"); + + int status = lua_costatus(L, co); + if (status == LUA_COFIN || status == LUA_COERR) + luaG_runerror(L, "cannot register a finalizer on dead coroutine"); + + if (LUAU_UNLIKELY(!!L->global->cb.userfinalizer)) + L->global->cb.userfinalizer(L, co); + + StkId cb = index2addr(L, idx); + + // create a callback table if it's not ready + if (co->finalizers == nullptr) + { + co->finalizers = luaH_new(L, 1, 0); + luaC_objbarrier(L, co, co->finalizers); + } + + // add a new item + TValue* slot = luaH_setnum(L, co->finalizers, luaH_getn(co->finalizers) + 1); + setobj2t(L, slot, cb); + luaC_barriert(L, co->finalizers, cb); +} + +void* lua_getthreaddata(lua_State* L) +{ + return L->userdata; +} + +void lua_setthreaddata(lua_State* L, void* data) +{ + L->userdata = data; +} + +/* +** Garbage-collection function +*/ + +int lua_gc(lua_State* L, int what, int data) +{ + int res = 0; + condhardmemtests(luaC_validate(L), 1); + global_State* g = L->global; + switch (what) + { + case LUA_GCSTOP: + { + g->GCthreshold = SIZE_MAX; + break; + } + case LUA_GCRESTART: + { + g->GCthreshold = g->totalbytes; + break; + } + case LUA_GCCOLLECT: + { + luaC_fullgc(L); + break; + } + case LUA_GCCOUNT: + { + // GC values are expressed in Kbytes: #bytes/2^10 + res = cast_int(g->totalbytes >> 10); + break; + } + case LUA_GCCOUNTB: + { + res = cast_int(g->totalbytes & 1023); + break; + } + case LUA_GCISRUNNING: + { + res = (g->GCthreshold != SIZE_MAX); + break; + } + case LUA_GCSTEP: + { + size_t amount = (cast_to(size_t, data) << 10); + ptrdiff_t oldcredit = g->gcstate == GCSpause ? 0 : g->GCthreshold - g->totalbytes; + + // temporarily adjust the threshold so that we can perform GC work + if (amount <= g->totalbytes) + g->GCthreshold = g->totalbytes - amount; + else + g->GCthreshold = 0; + +#ifdef LUAI_GCMETRICS + double startmarktime = g->gcmetrics.currcycle.marktime; + double startsweeptime = g->gcmetrics.currcycle.sweeptime; +#endif + + // track how much work the loop will actually perform + size_t actualwork = 0; + + while (g->GCthreshold <= g->totalbytes) + { + size_t stepsize = luaC_step(L, false); + + actualwork += stepsize; + + if (g->gcstate == GCSpause) + { // end of cycle? + res = 1; // signal it + break; + } + } + +#ifdef LUAI_GCMETRICS + // record explicit step statistics + GCCycleMetrics* cyclemetrics = g->gcstate == GCSpause ? &g->gcmetrics.lastcycle : &g->gcmetrics.currcycle; + + double totalmarktime = cyclemetrics->marktime - startmarktime; + double totalsweeptime = cyclemetrics->sweeptime - startsweeptime; + + if (totalmarktime > 0.0) + { + cyclemetrics->markexplicitsteps++; + + if (totalmarktime > cyclemetrics->markmaxexplicittime) + cyclemetrics->markmaxexplicittime = totalmarktime; + } + + if (totalsweeptime > 0.0) + { + cyclemetrics->sweepexplicitsteps++; + + if (totalsweeptime > cyclemetrics->sweepmaxexplicittime) + cyclemetrics->sweepmaxexplicittime = totalsweeptime; + } +#endif + + // if cycle hasn't finished, advance threshold forward for the amount of extra work performed + if (g->gcstate != GCSpause) + { + // if a new cycle was triggered by explicit step, old 'credit' of GC work is 0 + ptrdiff_t newthreshold = g->totalbytes + actualwork + oldcredit; + g->GCthreshold = newthreshold < 0 ? 0 : newthreshold; + } + break; + } + case LUA_GCSETGOAL: + { + res = g->gcgoal; + g->gcgoal = data; + break; + } + case LUA_GCSETSTEPMUL: + { + res = g->gcstepmul; + g->gcstepmul = data; + break; + } + case LUA_GCSETSTEPSIZE: + { + // GC values are expressed in Kbytes: #bytes/2^10 + res = g->gcstepsize >> 10; + g->gcstepsize = data << 10; + break; + } + case LUA_GCISPAUSED: + { + res = g->gcstate == GCSpause; + break; + } + default: + res = -1; // invalid option + } + return res; +} + +void lua_memorydump(lua_State* L, void* file, lua_CategoryName categoryName) +{ + api_check(L, file != nullptr); + + luaC_dump(L, file, categoryName); +} + +/* +** miscellaneous functions +*/ + +l_noret lua_error(lua_State* L) +{ + api_checknelems(L, 1); + + luaD_throw(L, LUA_ERRRUN); +} + +int lua_next(lua_State* L, int idx) +{ + api_checknelems(L, 1); + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + int more = luaH_next(L, hvalue(t), L->top - 1); + if (more) + { + api_incr_top(L); + } + else // no more elements + L->top -= 1; // remove key + return more; +} + +int lua_rawiter(lua_State* L, int idx, int iter) +{ + luaC_threadbarrier(L); + ensure_stack(L, 2); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + api_check(L, iter >= 0); + + LuaTable* h = hvalue(t); + int sizearray = h->sizearray; + + // first we advance iter through the array portion + for (; unsigned(iter) < unsigned(sizearray); ++iter) + { + TValue* e = &h->array[iter]; + + if (!ttisnil(e)) + { + StkId top = L->top; + setnvalue(top + 0, double(iter + 1)); + setobj2s(L, top + 1, e); + api_update_top(L, top + 2); + return iter + 1; + } + } + + int sizenode = 1 << h->lsizenode; + + // then we advance iter through the hash portion + for (; unsigned(iter - sizearray) < unsigned(sizenode); ++iter) + { + LuaNode* n = &h->node[iter - sizearray]; + + if (!ttisnil(gval(n))) + { + StkId top = L->top; + getnodekey(L, top + 0, n); + setobj2s(L, top + 1, gval(n)); + api_update_top(L, top + 2); + return iter + 1; + } + } + + // traversal finished + return -1; +} + +void lua_concat(lua_State* L, int n) +{ + api_check(L, n >= 0); + api_checknelems(L, n); + if (n >= 2) + { + luaC_checkGC(L); + luaC_threadbarrier(L); + luaV_concat(L, n, cast_int(L->top - L->base) - 1); + L->top -= (n - 1); + } + else if (n == 0) + { // push empty string + luaC_threadbarrier(L); + ensure_stack(L, 1); + setsvalue(L, L->top, luaS_newlstr(L, "", 0)); + api_incr_top(L); + } + // else n == 1; nothing to do +} + +void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT || tag == UTAG_PROXY); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + Udata* u = luaU_newudata(L, sz, tag); + setuvalue(L, L->top, u); + api_incr_top(L); + return u->data; +} + +void* lua_newuserdatataggedwithmetatable(lua_State* L, size_t sz, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + Udata* u = luaU_newudata(L, sz, tag); + + // currently, we always allocate unmarked objects, so forward barrier can be skipped + LUAU_ASSERT(!isblack(obj2gco(u))); + + LuaTable* h = L->global->udatamt[tag]; + api_check(L, h != nullptr); + + u->metatable = h; + + setuvalue(L, L->top, u); + api_incr_top(L); + return u->data; +} + +void* lua_newuserdatadtor(lua_State* L, size_t sz, lua_Destructor dtor) +{ + api_check(L, dtor != nullptr); + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + // make sure sz + sizeof(dtor) doesn't overflow; luaU_newdata will reject SIZE_MAX correctly + size_t as = sz < SIZE_MAX - sizeof(dtor) ? sz + sizeof(dtor) : SIZE_MAX; + Udata* u = luaU_newudata(L, as, UTAG_IDTOR); + memcpy(&u->data + sz, &dtor, sizeof(dtor)); + setuvalue(L, L->top, u); + api_incr_top(L); + return u->data; +} + +void* lua_newbuffer(lua_State* L, size_t sz) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + Buffer* b = luaB_newbuffer(L, sz); + setbufvalue(L, L->top, b); + api_incr_top(L); + return b->data; +} + +static const char* aux_upvalue(StkId fi, int n, TValue** val) +{ + Closure* f; + if (!ttisfunction(fi)) + return NULL; + f = clvalue(fi); + if (f->isC) + { + if (!(1 <= n && n <= f->nupvalues)) + return NULL; + *val = &f->c.upvals[n - 1]; + return ""; + } + else + { + Proto* p = f->l.p; + if (!(1 <= n && n <= p->nups)) // not a valid upvalue + return NULL; + TValue* r = &f->l.uprefs[n - 1]; + *val = ttisupval(r) ? upvalue(r)->v : r; + if (!(1 <= n && n <= p->sizeupvalues)) // don't have a name for this upvalue + return ""; + return getstr(p->upvalues[n - 1]); + } +} + +const char* lua_getupvalue(lua_State* L, int funcindex, int n) +{ + luaC_threadbarrier(L); + ensure_stack(L, 1); + TValue* val; + const char* name = aux_upvalue(index2addr(L, funcindex), n, &val); + if (name) + { + setobj2s(L, L->top, val); + api_incr_top(L); + } + return name; +} + +const char* lua_setupvalue(lua_State* L, int funcindex, int n) +{ + api_checknelems(L, 1); + StkId fi = index2addr(L, funcindex); + TValue* val; + const char* name = aux_upvalue(fi, n, &val); + if (name) + { + L->top--; + setobj(L, val, L->top); + luaC_barrier(L, clvalue(fi), L->top); + } + return name; +} + + +int lua_hascustomexecution(lua_State* L, int level) +{ + return luaG_hasnative(L, level); +} + +int lua_incustomexecution(lua_State* L, int level) +{ + return luaG_isnative(L, level); +} + +#define siprotate(v, s) v = (((v) << (s)) | ((v) >> (64 - (s)))) + +#define siphashround(v0, v1, v2, v3) \ + (v0) += (v1); \ + siprotate(v1, 13); \ + (v1) ^= (v0); \ + siprotate(v0, 32); \ + (v2) += (v3); \ + siprotate(v3, 16); \ + (v3) ^= (v2); \ + (v2) += (v1); \ + siprotate(v1, 17); \ + (v1) ^= (v2); \ + siprotate(v2, 32); \ + (v0) += (v3); \ + siprotate(v3, 21); \ + (v3) ^= (v0); + +static uint64_t siphash24(uint64_t k0, uint64_t k1, uint32_t input) +{ + uint64_t v0 = k0 ^ 0x736f6d6570736575ull; + uint64_t v1 = k1 ^ 0x646f72616e646f6dull; + uint64_t v2 = k0 ^ 0x6c7967656e657261ull; + uint64_t v3 = k1 ^ 0x7465646279746573ull; + + uint64_t m0 = (4ull << 56ull) | input; // Top byte is the sizeof(input) + + v3 ^= m0; + siphashround(v0, v1, v2, v3); + siphashround(v0, v1, v2, v3); + v0 ^= m0; + + v2 ^= 0xff; + siphashround(v0, v1, v2, v3); + siphashround(v0, v1, v2, v3); + siphashround(v0, v1, v2, v3); + siphashround(v0, v1, v2, v3); + + return v0 ^ v1 ^ v2 ^ v3; +} + +void lua_setpointerencodekey(lua_State* L, uint64_t a, uint64_t b, uint64_t c, uint64_t d) +{ + global_State* g = L->global; + + if (FFlag::LuauNewPointerEncode) + { + // Two keys have historically made the encoding an identity + bool identity = (a == 1 && b == 0 && c == 0 && d == 0) || (a == 0 && b == 1 && c == 0 && d == 0); + + g->ptrencactive = !identity; + + // Derive a 512-bit key from the provided seed + for (int i = 0; i < 4; i++) + { + g->ptrenckeynew[2 * i + 0] = siphash24(a, b, i); + g->ptrenckeynew[2 * i + 1] = siphash24(c, d, i); + } + } + else + { + g->ptrenckey[0] = a & ~1ull; + g->ptrenckey[1] = b | 1ull; + g->ptrenckey[2] = c; + g->ptrenckey[3] = d; + } +} + +uintptr_t lua_encodepointer(lua_State* L, uintptr_t p) +{ + global_State* g = L->global; + + if (FFlag::LuauNewPointerEncode) + { + if (!g->ptrencactive) + return p; + + // Feistel cipher transforms the scrambling SipHash into a permutation + // 64-bit platforms split into two 32-bit pieces, 32-bit platforms into two 16-bit pieces + int halfbits = int(sizeof(p) * 4); + uintptr_t mask = (uintptr_t(1) << halfbits) - 1; + + // Split the block into two pieces + uintptr_t l0 = p & mask; + uintptr_t r0 = p >> halfbits; + + // Perform the minimal required four rounds + for (int i = 0; i < 4; i++) + { + uintptr_t f = siphash24(g->ptrenckeynew[i * 2 + 0], g->ptrenckeynew[i * 2 + 1], uint32_t(r0)) & mask; + + uintptr_t rnext = l0 ^ f; + l0 = r0; + r0 = rnext; + } + + return (r0 << halfbits) | l0; + } + else + { + return uintptr_t((g->ptrenckey[0] * p + g->ptrenckey[2]) ^ (g->ptrenckey[1] * p + g->ptrenckey[3])); + } +} + +int lua_ref(lua_State* L, int idx) +{ + api_check(L, idx != LUA_REGISTRYINDEX); // idx is a stack index for value + int ref = LUA_REFNIL; + global_State* g = L->global; + StkId p = index2addr(L, idx); + if (!ttisnil(p)) + { + LuaTable* reg = hvalue(registry(L)); + + if (g->registryfree != 0) + { // reuse existing slot + ref = g->registryfree; + } + else + { // no free elements + ref = luaH_getn(reg); + ref++; // create new reference + } + + TValue* slot = luaH_setnum(L, reg, ref); + if (g->registryfree != 0) + g->registryfree = int(nvalue(slot)); + setobj2t(L, slot, p); + luaC_barriert(L, reg, p); + } + return ref; +} + +int lua_unref(lua_State* L, int ref) +{ + if (ref <= LUA_REFNIL) + return LUA_NOREF; + + global_State* g = L->global; + LuaTable* reg = hvalue(registry(L)); + + const TValue* slot = luaH_getnum(reg, ref); + api_check(L, slot != luaO_nilobject); + + // similar to how 'luaH_setnum' makes non-nil slot value mutable + TValue* mutableSlot = (TValue*)slot; + + // NB: no barrier needed because value isn't collectable + setnvalue(mutableSlot, g->registryfree); + + g->registryfree = ref; + return LUA_NOREF; +} + +void lua_setuserdatatag(lua_State* L, int idx, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + StkId o = index2addr(L, idx); + api_check(L, ttisuserdata(o)); + uvalue(o)->tag = uint8_t(tag); +} + +void lua_setuserdatadtor(lua_State* L, int tag, lua_Destructor dtor) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + L->global->udatagc[tag] = dtor; +} + +lua_Destructor lua_getuserdatadtor(lua_State* L, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + return L->global->udatagc[tag]; +} + +void lua_setuserdatamark(lua_State* L, int tag, lua_UserdataMark markfn) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + L->global->udatamark[tag] = markfn; +} + +void lua_setembeddergc(lua_State* L, lua_EmbedderGc fn) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + L->global->embeddergc = fn; +} + +int lua_weakref(lua_State* L, int idx) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + api_check(L, idx != LUA_REGISTRYINDEX); // idx is a stack index for value + int ref = LUA_REFNIL; + global_State* g = L->global; + StkId p = index2addr(L, idx); + if (!ttisnil(p)) + { + LuaTable* weakregistry = hvalue(&g->weakregistry); + int& weakregistryfree = g->weakregistryfree; + TValue* slot = nullptr; + + if (weakregistryfree != 0) + { // reuse existing slot + ref = weakregistryfree; + slot = luaH_setnum(L, weakregistry, ref); + weakregistryfree = int(nvalue(slot)); + } + else + { // no free elements + // must use weakregistrytop here: garbage collection may have made + // "holes" that are unsafe to reuse until lua_weakunref is called + ref = ++g->weakregistrytop; + slot = luaH_setnum(L, weakregistry, ref); + } + + setobj2t(L, slot, p); + luaC_barriert(L, weakregistry, p); + } + return ref; +} + +int lua_weakunref(lua_State* L, int ref) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + if (ref <= LUA_REFNIL) + return LUA_NOREF; + + global_State* g = L->global; + api_check(L, ref <= g->weakregistrytop); + LuaTable* weakregistry = hvalue(&g->weakregistry); + + TValue* slot = luaH_setnum(L, weakregistry, ref); + + // NB: no barrier needed because value isn't collectable + setnvalue(slot, g->weakregistryfree); + + g->weakregistryfree = ref; + return LUA_NOREF; +} + +int lua_getweakref(lua_State* L, int ref) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + luaC_threadbarrier(L); + ensure_stack(L, 1); + LuaTable* wr = hvalue(&L->global->weakregistry); + setobj2s(L, L->top, luaH_getnum(wr, ref)); + api_incr_top(L); + return ttype(L->top - 1); +} + +void lua_setuserdatametatable(lua_State* L, int tag) +{ + api_checknelems(L, 1); + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + api_check(L, !L->global->udatamt[tag]); // reassignment not supported + api_check(L, ttistable(L->top - 1)); + L->global->udatamt[tag] = hvalue(L->top - 1); + L->top--; +} + +void lua_getuserdatametatable(lua_State* L, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + luaC_threadbarrier(L); + ensure_stack(L, 1); + + if (LuaTable* h = L->global->udatamt[tag]) + { + sethvalue(L, L->top, h); + } + else + { + setnilvalue(L->top); + } + + api_incr_top(L); +} + +const char* lua_getuserdataname(lua_State* L, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + + const char* tname = "userdata"; + + if (LuaTable* mt = L->global->udatamt[tag]) + { + const TValue* type = luaH_getstr(mt, L->global->tmname[TM_TYPE]); + if (ttisstring(type)) + tname = getstr(tsvalue(type)); + } + + return tname; +} + +int lua_registeruserdatadirectaccess( + lua_State* L, + int tag, + lua_UserdataDirectAccess get, + lua_UserdataDirectAccess set, + lua_UserdataDirectNamecall namecall +) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + luaC_threadbarrier(L); + + if (LuaTable* h = L->global->udatamt[tag]) + { + // only metamehtods that are present will be called directly + lua_UdataDirectAccessData& udatadirect = L->global->udatadirect[tag]; + + if (const TValue* indextm = fasttm(L, h, TM_INDEX)) + { + udatadirect.indextm = *indextm; + udatadirect.index = get; + } + + if (const TValue* newindextm = fasttm(L, h, TM_NEWINDEX)) + { + udatadirect.newindextm = *newindextm; + udatadirect.newindex = set; + } + + if (const TValue* namecalltm = fasttm(L, h, TM_NAMECALL)) + { + udatadirect.namecalltm = *namecalltm; + udatadirect.namecall = namecall; + } + + return 1; + } + + return 0; +} + +void lua_setlightuserdataname(lua_State* L, int tag, const char* name) +{ + api_check(L, unsigned(tag) < LUA_LUTAG_LIMIT); + api_check(L, !L->global->lightuserdataname[tag]); // renaming not supported + if (!L->global->lightuserdataname[tag]) + { + L->global->lightuserdataname[tag] = luaS_new(L, name); + luaS_fix(L->global->lightuserdataname[tag]); // never collect these names + } +} + +const char* lua_getlightuserdataname(lua_State* L, int tag) +{ + api_check(L, unsigned(tag) < LUA_LUTAG_LIMIT); + const TString* name = L->global->lightuserdataname[tag]; + return name ? getstr(name) : nullptr; +} + +void lua_clonefunction(lua_State* L, int idx) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + ensure_stack(L, 1); + StkId p = index2addr(L, idx); + api_check(L, isLfunction(p)); + Closure* cl = clvalue(p); + Closure* newcl = luaF_newLclosure(L, cl->nupvalues, L->gt, cl->l.p); + for (int i = 0; i < cl->nupvalues; ++i) + setobj2n(L, &newcl->l.uprefs[i], &cl->l.uprefs[i]); + setclvalue(L, L->top, newcl); + api_incr_top(L); +} + +int lua_usesexport(lua_State* L, int idx) +{ + StkId o = index2addr(L, idx); + if (!isLfunction(o)) + return 0; + Closure* cl = clvalue(o); + return (cl->l.p->flags & LPF_USES_EXPORT) != 0; +} + +void lua_cleartable(lua_State* L, int idx) +{ + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + LuaTable* tt = hvalue(t); + if (tt->readonly) + luaG_readonlyerror(L); + luaH_clear(tt); +} + +void lua_clonetable(lua_State* L, int idx) +{ + luaC_checkGC(L); + luaC_threadbarrier(L); + + ensure_stack(L, 1); + StkId t = index2addr(L, idx); + api_check(L, ttistable(t)); + + LuaTable* tt = luaH_clone(L, hvalue(t)); + sethvalue(L, L->top, tt); + api_incr_top(L); +} + +lua_Callbacks* lua_callbacks(lua_State* L) +{ + return &L->global->cb; +} + +void lua_setmemcat(lua_State* L, int category) +{ + api_check(L, unsigned(category) < LUA_MEMORY_CATEGORIES); + L->activememcat = uint8_t(category); +} + +size_t lua_totalbytes(lua_State* L, int category) +{ + api_check(L, category < LUA_MEMORY_CATEGORIES); + return category < 0 ? L->global->totalbytes : L->global->memcatbytes[category]; +} + +int64_t lua_allocationrate(lua_State* L) +{ + return luaC_allocationrate(L); +} + +lua_Alloc lua_getallocf(lua_State* L, void** ud) +{ + lua_Alloc f = L->global->frealloc; + if (ud) + *ud = L->global->ud; + return f; +} + +void lua_registeruserdatadirectfieldget(lua_State* L, int tag, const char* field, lua_UserdataDirectFieldGet fn) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + api_check(L, field != nullptr); + api_check(L, fn != nullptr); + + global_State* g = L->global; + + if (g->udatadirectfields[tag] == nullptr) + g->udatadirectfields[tag] = luaH_new(L, 0, 1); + + TString* ts = luaS_new(L, field); + luaS_fix(ts); + + TValue* slot = luaH_setstr(L, g->udatadirectfields[tag], ts); + setpvalue(slot, reinterpret_cast(fn), 0); +} + +void lua_userdatadirectfield_setnumber(void* result, double n) +{ + TValue* slot = LUA_VECTOR_DOUBLE ? static_cast(result)->slot : static_cast(result); + setnvalue(slot, n); +} + +#if LUA_VECTOR_SIZE == 4 +void lua_userdatadirectfield_setvector(void* result, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z, LUA_VECTOR_TYPE w) +{ + if (LUA_VECTOR_DOUBLE == 1) + { + DirectFieldResult* dfr = static_cast(result); + setvvalue(dfr->L, dfr->slot, x, y, z, w); + } + else + { + setvvalue((lua_State*)nullptr, static_cast(result), x, y, z, w); + } +} +#else +void lua_userdatadirectfield_setvector(void* result, LUA_VECTOR_TYPE x, LUA_VECTOR_TYPE y, LUA_VECTOR_TYPE z) +{ + if (LUA_VECTOR_DOUBLE == 1) + { + DirectFieldResult* dfr = static_cast(result); + setvvalue(dfr->L, dfr->slot, x, y, z, 0); + } + else + { + setvvalue((lua_State*)nullptr, static_cast(result), x, y, z, 0); + } +} +#endif + +void lua_userdatadirectfield_setboolean(void* result, int b) +{ + TValue* slot = LUA_VECTOR_DOUBLE ? static_cast(result)->slot : static_cast(result); + setbvalue(slot, b); +} + +void lua_userdatadirectfield_setinteger64(void* result, int64_t n) +{ + TValue* slot = LUA_VECTOR_DOUBLE ? static_cast(result)->slot : static_cast(result); + setlvalue(slot, n); +} + +void lua_userdatadirectfield_setnil(void* result) +{ + TValue* slot = LUA_VECTOR_DOUBLE ? static_cast(result)->slot : static_cast(result); + setnilvalue(slot); +} diff --git a/External/luau/VM/src/lapi.h b/External/luau/VM/src/lapi.h new file mode 100644 index 00000000..8e920540 --- /dev/null +++ b/External/luau/VM/src/lapi.h @@ -0,0 +1,10 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +LUAI_FUNC const TValue* luaA_toobject(lua_State* L, int idx); +LUAI_FUNC void luaA_pushvalue(lua_State* L, const TValue* o); +LUAI_FUNC void luaA_pushclass(lua_State* L, LuauClass* lclass); +LuaTable* getcurrenv(lua_State* L); \ No newline at end of file diff --git a/External/luau/VM/src/laux.cpp b/External/luau/VM/src/laux.cpp new file mode 100644 index 00000000..843e97b6 --- /dev/null +++ b/External/luau/VM/src/laux.cpp @@ -0,0 +1,676 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "lapi.h" +#include "lgc.h" +#include "lnumutils.h" + +#include + +// convert a stack index to positive +#define abs_index(L, i) ((i) > 0 || (i) <= LUA_REGISTRYINDEX ? (i) : lua_gettop(L) + (i) + 1) + +/* +** {====================================================== +** Error-report functions +** ======================================================= +*/ + +static const char* currfuncname(lua_State* L) +{ + Closure* cl = L->ci > L->base_ci ? curr_func(L) : NULL; + + const char* debugname = cl && cl->isC && cl->c.debugname ? getstr(cl->c.debugname) : NULL; + + if (debugname && strcmp(debugname, "__namecall") == 0) + return L->namecall ? getstr(L->namecall) : NULL; + else + return debugname; +} + +l_noret luaL_argerrorL(lua_State* L, int narg, const char* extramsg) +{ + const char* fname = currfuncname(L); + + if (fname) + luaL_error(L, "invalid argument #%d to '%s' (%s)", narg, fname, extramsg); + else + luaL_error(L, "invalid argument #%d (%s)", narg, extramsg); +} + +l_noret luaL_typeerrorL(lua_State* L, int narg, const char* tname) +{ + const char* fname = currfuncname(L); + const TValue* obj = luaA_toobject(L, narg); + + if (obj) + { + if (fname) + luaL_error(L, "invalid argument #%d to '%s' (%s expected, got %s)", narg, fname, tname, luaT_objtypename(L, obj)); + else + luaL_error(L, "invalid argument #%d (%s expected, got %s)", narg, tname, luaT_objtypename(L, obj)); + } + else + { + if (fname) + luaL_error(L, "missing argument #%d to '%s' (%s expected)", narg, fname, tname); + else + luaL_error(L, "missing argument #%d (%s expected)", narg, tname); + } +} + +static l_noret tag_error(lua_State* L, int narg, int tag) +{ + luaL_typeerrorL(L, narg, lua_typename(L, tag)); +} + +// Can be called without stack space reservation +void luaL_where(lua_State* L, int level) +{ + lua_Debug ar; + if (lua_getinfo(L, level, "sl", &ar) && ar.currentline > 0) + { + lua_pushfstring(L, "%s:%d: ", ar.short_src, ar.currentline); + return; + } + + lua_rawcheckstack(L, 1); + lua_pushliteral(L, ""); // else, no information available... +} + +// Can be called without stack space reservation +l_noret luaL_errorL(lua_State* L, const char* fmt, ...) +{ + va_list argp; + va_start(argp, fmt); + luaL_where(L, 1); + lua_pushvfstring(L, fmt, argp); + va_end(argp); + lua_concat(L, 2); + lua_error(L); +} + +// }====================================================== + +int luaL_checkoption(lua_State* L, int narg, const char* def, const char* const lst[]) +{ + const char* name = (def) ? luaL_optstring(L, narg, def) : luaL_checkstring(L, narg); + int i; + for (i = 0; lst[i]; i++) + if (strcmp(lst[i], name) == 0) + return i; + const char* msg = lua_pushfstring(L, "invalid option '%s'", name); + luaL_argerrorL(L, narg, msg); +} + +int luaL_newmetatable(lua_State* L, const char* tname) +{ + lua_getfield(L, LUA_REGISTRYINDEX, tname); // get registry.name + if (!lua_isnil(L, -1)) // name already in use? + return 0; // leave previous value on top, but return 0 + lua_pop(L, 1); + lua_newtable(L); // create metatable + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, tname); // registry.name = metatable + return 1; +} + +void* luaL_checkudata(lua_State* L, int ud, const char* tname) +{ + void* p = lua_touserdata(L, ud); + if (p != NULL) + { // value is a userdata? + if (lua_getmetatable(L, ud)) + { // does it have a metatable? + lua_getfield(L, LUA_REGISTRYINDEX, tname); // get correct metatable + if (lua_rawequal(L, -1, -2)) + { // does it have the correct mt? + lua_pop(L, 2); // remove both metatables + return p; + } + } + } + luaL_typeerrorL(L, ud, tname); // else error +} + +void* luaL_checkudatatagged(lua_State* L, int ud, int tag) +{ + void* p = lua_touserdatatagged(L, ud, tag); + if (p != NULL) + return p; + + const char* tname = lua_getuserdataname(L, tag); + luaL_typeerrorL(L, ud, tname); // else error +} + +void* luaL_checkbuffer(lua_State* L, int narg, size_t* len) +{ + void* b = lua_tobuffer(L, narg, len); + if (!b) + tag_error(L, narg, LUA_TBUFFER); + return b; +} + +void luaL_checkstack(lua_State* L, int space, const char* mes) +{ + if (!lua_checkstack(L, space)) + luaL_error(L, "stack overflow (%s)", mes); +} + +void luaL_checktype(lua_State* L, int narg, int t) +{ + if (lua_type(L, narg) != t) + tag_error(L, narg, t); +} + +void luaL_checkany(lua_State* L, int narg) +{ + if (lua_type(L, narg) == LUA_TNONE) + luaL_error(L, "missing argument #%d", narg); +} + +const char* luaL_checklstring(lua_State* L, int narg, size_t* len) +{ + const char* s = lua_tolstring(L, narg, len); + if (!s) + tag_error(L, narg, LUA_TSTRING); + return s; +} + +const char* luaL_optlstring(lua_State* L, int narg, const char* def, size_t* len) +{ + if (lua_isnoneornil(L, narg)) + { + if (len) + *len = (def ? strlen(def) : 0); + return def; + } + else + return luaL_checklstring(L, narg, len); +} + +double luaL_checknumber(lua_State* L, int narg) +{ + int isnum; + double d = lua_tonumberx(L, narg, &isnum); + if (!isnum) + tag_error(L, narg, LUA_TNUMBER); + return d; +} + +double luaL_optnumber(lua_State* L, int narg, double def) +{ + return luaL_opt(L, luaL_checknumber, narg, def); +} + +int luaL_checkboolean(lua_State* L, int narg) +{ + // This checks specifically for boolean values, ignoring + // all other truthy/falsy values. If the desired result + // is true if value is present then lua_toboolean should + // directly be used instead. + if (!lua_isboolean(L, narg)) + tag_error(L, narg, LUA_TBOOLEAN); + return lua_toboolean(L, narg); +} + +int luaL_optboolean(lua_State* L, int narg, int def) +{ + return luaL_opt(L, luaL_checkboolean, narg, def); +} + +int luaL_checkinteger(lua_State* L, int narg) +{ + int isnum; + int d = lua_tointegerx(L, narg, &isnum); + if (!isnum) + tag_error(L, narg, LUA_TNUMBER); + return d; +} + +int64_t luaL_checkinteger64(lua_State* L, int narg) +{ + if (!lua_isinteger64(L, narg)) + tag_error(L, narg, LUA_TINTEGER); + return lua_tointeger64(L, narg, nullptr); +} + +int luaL_optinteger(lua_State* L, int narg, int def) +{ + return luaL_opt(L, luaL_checkinteger, narg, def); +} + +int64_t luaL_optinteger64(lua_State* L, int narg, int64_t def) +{ + return luaL_opt(L, luaL_checkinteger64, narg, def); +} + +unsigned luaL_checkunsigned(lua_State* L, int narg) +{ + int isnum; + unsigned d = lua_tounsignedx(L, narg, &isnum); + if (!isnum) + tag_error(L, narg, LUA_TNUMBER); + return d; +} + +unsigned luaL_optunsigned(lua_State* L, int narg, unsigned def) +{ + return luaL_opt(L, luaL_checkunsigned, narg, def); +} + +const LUA_VECTOR_TYPE* luaL_checkvector(lua_State* L, int narg) +{ + const LUA_VECTOR_TYPE* v = lua_tovector(L, narg); + if (!v) + tag_error(L, narg, LUA_TVECTOR); + return v; +} + +const LUA_VECTOR_TYPE* luaL_optvector(lua_State* L, int narg, const LUA_VECTOR_TYPE* def) +{ + return luaL_opt(L, luaL_checkvector, narg, def); +} + +int luaL_getmetafield(lua_State* L, int obj, const char* event) +{ + if (!lua_getmetatable(L, obj)) // no metatable? + return 0; + lua_pushstring(L, event); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) + { + lua_pop(L, 2); // remove metatable and metafield + return 0; + } + else + { + lua_remove(L, -2); // remove only metatable + return 1; + } +} + +int luaL_callmeta(lua_State* L, int obj, const char* event) +{ + obj = abs_index(L, obj); + if (!luaL_getmetafield(L, obj, event)) // no metafield? + return 0; + lua_pushvalue(L, obj); + lua_call(L, 1, 1); + return 1; +} + +static int libsize(const luaL_Reg* l) +{ + int size = 0; + for (; l->name; l++) + size++; + return size; +} + +void luaL_register(lua_State* L, const char* libname, const luaL_Reg* l) +{ + if (libname) + { + int size = libsize(l); + // check whether lib already exists + luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 1); + lua_getfield(L, -1, libname); // get _LOADED[libname] + if (!lua_istable(L, -1)) + { // not found? + lua_pop(L, 1); // remove previous result + // try global variable (and create one if it does not exist) + if (luaL_findtable(L, LUA_GLOBALSINDEX, libname, size) != NULL) + luaL_error(L, "name conflict for module '%s'", libname); + lua_pushvalue(L, -1); + lua_setfield(L, -3, libname); // _LOADED[libname] = new table + } + lua_remove(L, -2); // remove _LOADED table + } + for (; l->name; l++) + { + lua_pushcfunction(L, l->func, l->name); + lua_setfield(L, -2, l->name); + } +} + +const char* luaL_findtable(lua_State* L, int idx, const char* fname, int szhint) +{ + const char* e; + lua_pushvalue(L, idx); + do + { + e = strchr(fname, '.'); + if (e == NULL) + e = fname + strlen(fname); + lua_pushlstring(L, fname, e - fname); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) + { // no such field? + lua_pop(L, 1); // remove this nil + lua_createtable(L, 0, (*e == '.' ? 1 : szhint)); // new table for field + lua_pushlstring(L, fname, e - fname); + lua_pushvalue(L, -2); + lua_settable(L, -4); // set new table into field + } + else if (!lua_istable(L, -1)) + { // field has a non-table value? + lua_pop(L, 2); // remove table and value + return fname; // return problematic part of the name + } + lua_remove(L, -2); // remove previous table + fname = e + 1; + } while (*e == '.'); + return NULL; +} + +const char* luaL_typename(lua_State* L, int idx) +{ + const TValue* obj = luaA_toobject(L, idx); + return obj ? luaT_objtypename(L, obj) : "no value"; +} + +void luaL_traceback(lua_State* L, lua_State* L1, const char* msg, int level) +{ + api_check(L, level >= 0); + + luaL_Strbuf buf; + luaL_buffinit(L, &buf); + + if (msg) + { + luaL_addstring(&buf, msg); + luaL_addstring(&buf, "\n"); + } + + lua_Debug ar; + for (int i = level; lua_getinfo(L1, i, "sln", &ar); ++i) + { + if (strcmp(ar.what, "C") == 0) + continue; + + if (ar.source) + luaL_addstring(&buf, ar.short_src); + + if (ar.currentline > 0) + { + char line[32]; // manual conversion for performance + char* lineend = line + sizeof(line); + char* lineptr = lineend; + for (unsigned int r = ar.currentline; r > 0; r /= 10) + *--lineptr = '0' + (r % 10); + + luaL_addchar(&buf, ':'); + luaL_addlstring(&buf, lineptr, lineend - lineptr); + } + + if (ar.name) + { + luaL_addstring(&buf, " function "); + luaL_addstring(&buf, ar.name); + } + + luaL_addchar(&buf, '\n'); + } + + luaL_pushresult(&buf); +} + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +static size_t getnextbuffersize(lua_State* L, size_t currentsize, size_t desiredsize) +{ + size_t newsize = currentsize + currentsize / 2; + + // check for size overflow + if (SIZE_MAX - desiredsize < currentsize) + luaL_error(L, "buffer too large"); + + // growth factor might not be enough to satisfy the desired size + if (newsize < desiredsize) + newsize = desiredsize; + + return newsize; +} + +static char* extendstrbuf(luaL_Strbuf* B, size_t additionalsize, int boxloc) +{ + lua_State* L = B->L; + + if (B->storage) + LUAU_ASSERT(B->storage == tsvalue(L->top + boxloc)); + + char* base = B->storage ? B->storage->data : B->buffer; + + size_t capacity = B->end - base; + size_t nextsize = getnextbuffersize(B->L, capacity, capacity + additionalsize); + + TString* newStorage = luaS_bufstart(L, nextsize); + + memcpy(newStorage->data, base, B->p - base); + + // place the string storage at the expected position in the stack + if (base == B->buffer) + { + lua_pushnil(L); + lua_insert(L, boxloc); + } + + setsvalue(L, L->top + boxloc, newStorage); + B->p = newStorage->data + (B->p - base); + B->end = newStorage->data + nextsize; + B->storage = newStorage; + + return B->p; +} + +void luaL_buffinit(lua_State* L, luaL_Strbuf* B) +{ + // start with an internal buffer + B->p = B->buffer; + B->end = B->p + LUA_BUFFERSIZE; + + B->L = L; + B->storage = nullptr; +} + +char* luaL_buffinitsize(lua_State* L, luaL_Strbuf* B, size_t size) +{ + luaL_buffinit(L, B); + return luaL_prepbuffsize(B, size); +} + +char* luaL_prepbuffsize(luaL_Strbuf* B, size_t size) +{ + if (size_t(B->end - B->p) < size) + return extendstrbuf(B, size - (B->end - B->p), -1); + return B->p; +} + +void luaL_addlstring(luaL_Strbuf* B, const char* s, size_t len) +{ + if (size_t(B->end - B->p) < len) + extendstrbuf(B, len - (B->end - B->p), -1); + + memcpy(B->p, s, len); + B->p += len; +} + +void luaL_addvalue(luaL_Strbuf* B) +{ + lua_State* L = B->L; + + size_t vl; + if (const char* s = lua_tolstring(L, -1, &vl)) + { + if (size_t(B->end - B->p) < vl) + extendstrbuf(B, vl - (B->end - B->p), -2); + + memcpy(B->p, s, vl); + B->p += vl; + + lua_pop(L, 1); + } +} + +void luaL_addvalueany(luaL_Strbuf* B, int idx) +{ + lua_State* L = B->L; + + switch (lua_type(L, idx)) + { + case LUA_TNONE: + { + LUAU_ASSERT(!"expected value"); + break; + } + case LUA_TNIL: + luaL_addstring(B, "nil"); + break; + case LUA_TBOOLEAN: + if (lua_toboolean(L, idx)) + luaL_addstring(B, "true"); + else + luaL_addstring(B, "false"); + break; + case LUA_TNUMBER: + { + double n = lua_tonumber(L, idx); + char s[LUAI_MAXNUM2STR]; + char* e = luai_num2str(s, n); + luaL_addlstring(B, s, e - s); + break; + } + case LUA_TSTRING: + { + size_t len; + const char* s = lua_tolstring(L, idx, &len); + luaL_addlstring(B, s, len); + break; + } + case LUA_TINTEGER: + { + int64_t n = lua_tointeger64(L, idx, nullptr); + char s[LUAI_MAXINT2STR]; + char* e = luai_int2str(s, n); + luaL_addlstring(B, s, e - s); + break; + } + default: + { + size_t len; + luaL_tolstring(L, idx, &len); + + // note: luaL_addlstring assumes box is stored at top of stack, so we can't call it here + // instead we use luaL_addvalue which will take the string from the top of the stack and add that + luaL_addvalue(B); + } + } +} + +void luaL_pushresult(luaL_Strbuf* B) +{ + lua_State* L = B->L; + + if (TString* storage = B->storage) + { + luaC_checkGC(L); + + // if we finished just at the end of the string buffer, we can convert it to a mutable string without a copy + if (B->p == B->end) + { + setsvalue(L, L->top - 1, luaS_buffinish(L, storage)); + } + else + { + setsvalue(L, L->top - 1, luaS_newlstr(L, storage->data, B->p - storage->data)); + } + } + else + { + lua_pushlstring(L, B->buffer, B->p - B->buffer); + } +} + +void luaL_pushresultsize(luaL_Strbuf* B, size_t size) +{ + B->p += size; + luaL_pushresult(B); +} + +// }====================================================== + +const char* luaL_tolstring(lua_State* L, int idx, size_t* len) +{ + if (luaL_callmeta(L, idx, "__tostring")) // is there a metafield? + { + const char* s = lua_tolstring(L, -1, len); + if (!s) + luaL_error(L, "'__tostring' must return a string"); + return s; + } + + switch (lua_type(L, idx)) + { + case LUA_TNIL: + lua_pushliteral(L, "nil"); + break; + case LUA_TBOOLEAN: + lua_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false")); + break; + case LUA_TNUMBER: + { + double n = lua_tonumber(L, idx); + char s[LUAI_MAXNUM2STR]; + char* e = luai_num2str(s, n); + lua_pushlstring(L, s, e - s); + break; + } + case LUA_TVECTOR: + { + const LUA_VECTOR_TYPE* v = lua_tovector(L, idx); + + char s[LUAI_MAXNUM2STR * LUA_VECTOR_SIZE]; + char* e = s; + for (int i = 0; i < LUA_VECTOR_SIZE; ++i) + { + if (i != 0) + { + *e++ = ','; + *e++ = ' '; + } + e = luai_num2str(e, v[i]); + } + lua_pushlstring(L, s, e - s); + break; + } + case LUA_TSTRING: + lua_pushvalue(L, idx); + break; + case LUA_TINTEGER: + { + int64_t l = lua_tointeger64(L, idx, nullptr); + char s[LUAI_MAXINT2STR]; + char* e = luai_int2str(s, l); + lua_pushlstring(L, s, e - s); + break; + } + default: + { + const void* ptr = lua_topointer(L, idx); + unsigned long long enc = lua_encodepointer(L, uintptr_t(ptr)); + lua_pushfstring(L, "%s: 0x%016llx", luaL_typename(L, idx), enc); + break; + } + } + return lua_tolstring(L, -1, len); +} diff --git a/External/luau/VM/src/lbaselib.cpp b/External/luau/VM/src/lbaselib.cpp new file mode 100644 index 00000000..11cd5018 --- /dev/null +++ b/External/luau/VM/src/lbaselib.cpp @@ -0,0 +1,490 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lstate.h" +#include "lapi.h" +#include "ldo.h" +#include "lgc.h" +#include "ludata.h" + +#include +#include +#include + +LUAU_FASTFLAGVARIABLE(LuauFastpcall) +LUAU_FASTFLAGVARIABLE(LuauPcallOptimize) + +static void writestring(const char* s, size_t l) +{ + fwrite(s, 1, l, stdout); +} + +static int luaB_print(lua_State* L) +{ + int n = lua_gettop(L); // number of arguments + for (int i = 1; i <= n; i++) + { + size_t l; + const char* s = luaL_tolstring(L, i, &l); // convert to string using __tostring et al + if (i > 1) + writestring("\t", 1); + writestring(s, l); + lua_pop(L, 1); // pop result + } + writestring("\n", 1); + return 0; +} + +static int luaB_tonumber(lua_State* L) +{ + int base = luaL_optinteger(L, 2, 10); + if (base == 10) + { // standard conversion + int isnum = 0; + double n = lua_tonumberx(L, 1, &isnum); + if (isnum) + { + lua_pushnumber(L, n); + return 1; + } + luaL_checkany(L, 1); // error if we don't have any argument + } + else + { + const char* s1 = luaL_checkstring(L, 1); + luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range"); + char* s2; + unsigned long long n; + n = strtoull(s1, &s2, base); + if (s1 != s2) + { // at least one valid digit? + while (isspace((unsigned char)(*s2))) + s2++; // skip trailing spaces + if (*s2 == '\0') + { // no invalid trailing characters? + lua_pushnumber(L, (double)n); + return 1; + } + } + } + lua_pushnil(L); // else not a number + return 1; +} + +static int luaB_error(lua_State* L) +{ + int level = luaL_optinteger(L, 2, 1); + lua_settop(L, 1); + if (lua_isstring(L, 1) && level > 0) + { // add extra information? + luaL_where(L, level); + lua_pushvalue(L, 1); + lua_concat(L, 2); + } + lua_error(L); +} + +static int luaB_getmetatable(lua_State* L) +{ + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) + { + lua_pushnil(L); + return 1; // no metatable + } + luaL_getmetafield(L, 1, "__metatable"); + return 1; // returns either __metatable field (if present) or metatable +} + +static int luaB_setmetatable(lua_State* L) +{ + int t = lua_type(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table"); + if (luaL_getmetafield(L, 1, "__metatable")) + luaL_error(L, "cannot change a protected metatable"); + lua_settop(L, 2); + lua_setmetatable(L, 1); + return 1; +} + +static void getfunc(lua_State* L, int opt) +{ + if (lua_isfunction(L, 1)) + lua_pushvalue(L, 1); + else + { + lua_Debug ar; + int level = opt ? luaL_optinteger(L, 1, 1) : luaL_checkinteger(L, 1); + luaL_argcheck(L, level >= 0, 1, "level must be non-negative"); + if (lua_getinfo(L, level, "f", &ar) == 0) + luaL_argerror(L, 1, "invalid level"); + if (lua_isnil(L, -1)) + luaL_error(L, "no function environment for tail call at level %d", level); + } +} + +static int luaB_getfenv(lua_State* L) +{ + getfunc(L, 1); + if (lua_iscfunction(L, -1)) // is a C function? + lua_pushvalue(L, LUA_GLOBALSINDEX); // return the thread's global env. + else + lua_getfenv(L, -1); + lua_setsafeenv(L, -1, false); + return 1; +} + +static int luaB_setfenv(lua_State* L) +{ + luaL_checktype(L, 2, LUA_TTABLE); + getfunc(L, 0); + lua_pushvalue(L, 2); + lua_setsafeenv(L, -1, false); + if (lua_isnumber(L, 1) && lua_tonumber(L, 1) == 0) + { + // change environment of current thread + lua_pushthread(L); + lua_insert(L, -2); + lua_setfenv(L, -2); + return 0; + } + else if (lua_iscfunction(L, -2) || lua_setfenv(L, -2) == 0) + luaL_error(L, "'setfenv' cannot change environment of given object"); + return 1; +} + +static int luaB_rawequal(lua_State* L) +{ + luaL_checkany(L, 1); + luaL_checkany(L, 2); + lua_pushboolean(L, lua_rawequal(L, 1, 2)); + return 1; +} + +static int luaB_rawget(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_rawget(L, 1); + return 1; +} + +static int luaB_rawset(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + luaL_checkany(L, 3); + lua_settop(L, 3); + lua_rawset(L, 1); + return 1; +} + +static int luaB_rawlen(lua_State* L) +{ + int tt = lua_type(L, 1); + luaL_argcheck(L, tt == LUA_TTABLE || tt == LUA_TSTRING, 1, "table or string expected"); + int len = lua_objlen(L, 1); + lua_pushinteger(L, len); + return 1; +} + +static int luaB_gcinfo(lua_State* L) +{ + lua_pushinteger(L, lua_gc(L, LUA_GCCOUNT, 0)); + return 1; +} + +static int luaB_type(lua_State* L) +{ + luaL_checkany(L, 1); + // resulting name doesn't differentiate between userdata types + lua_pushstring(L, lua_typename(L, lua_type(L, 1))); + return 1; +} + +static int luaB_typeof(lua_State* L) +{ + luaL_checkany(L, 1); + // resulting name returns __type if specified unless the input is a newproxy-created userdata + lua_pushstring(L, luaL_typename(L, 1)); + return 1; +} + +int luaB_next(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 2); // create a 2nd argument if there isn't one + if (lua_next(L, 1)) + return 2; + else + { + lua_pushnil(L); + return 1; + } +} + +static int luaB_pairs(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushvalue(L, lua_upvalueindex(1)); // return generator, + lua_pushvalue(L, 1); // state, + lua_pushnil(L); // and initial value + return 3; +} + +int luaB_inext(lua_State* L) +{ + int i = luaL_checkinteger(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + i++; // next value + lua_pushinteger(L, i); + lua_rawgeti(L, 1, i); + return (lua_isnil(L, -1)) ? 0 : 2; +} + +static int luaB_ipairs(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushvalue(L, lua_upvalueindex(1)); // return generator, + lua_pushvalue(L, 1); // state, + lua_pushinteger(L, 0); // and initial value + return 3; +} + +static int luaB_assert(lua_State* L) +{ + luaL_checkany(L, 1); + if (!lua_toboolean(L, 1)) + luaL_error(L, "%s", luaL_optstring(L, 2, "assertion failed!")); + return lua_gettop(L); +} + +static int luaB_select(lua_State* L) +{ + int n = lua_gettop(L); + if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') + { + lua_pushinteger(L, n - 1); + return 1; + } + else + { + int i = luaL_checkinteger(L, 1); + if (i < 0) + i = n + i; + else if (i > n) + i = n; + luaL_argcheck(L, 1 <= i, 1, "index out of range"); + return n - i; + } +} + +static int luaB_pcally(lua_State* L) +{ + luaL_checkany(L, 1); + + return lua_pcallyieldable(L, lua_gettop(L) - 1, LUA_MULTRET, 0); +} + +static int luaB_pcallcont(lua_State* L, int status) +{ + if (FFlag::LuauPcallOptimize) + { + luaD_checkstack(L, 1); + + if (status == 0) + { + StkId base = L->base; + StkId top = L->top; + + // Move results 1 right to make space for the 'true' status + for (StkId q = top; q > base; q--) + setobj2s(L, q, q - 1); + + setbvalue(base, 1); + L->top = top + 1; + + return cast_int(L->top - base); + } + else + { + StkId top = L->top; + + // Move error 1 right to make space for the 'false' status + setobj2s(L, top, top - 1); + + setbvalue(top - 1, 0); + L->top = top + 1; + + return 2; + } + } + else + { + if (status == 0) + { + lua_rawcheckstack(L, 1); + lua_pushboolean(L, true); + lua_insert(L, 1); // insert status before all results + return lua_gettop(L); + } + else + { + lua_rawcheckstack(L, 1); + lua_pushboolean(L, false); + lua_insert(L, -2); // insert status before error object + return 2; + } + } +} + +static int luaB_xpcally(lua_State* L) +{ + luaL_checktype(L, 2, LUA_TFUNCTION); + + // swap function & error function + lua_pushvalue(L, 1); + lua_pushvalue(L, 2); + lua_replace(L, 1); + lua_replace(L, 2); + // at this point the stack looks like err, f, args + + return lua_pcallyieldable(L, lua_gettop(L) - 2, LUA_MULTRET, 1); +} + +static int luaB_xpcallcont(lua_State* L, int status) +{ + if (status == 0) + { + if (FFlag::LuauPcallOptimize) + { + StkId base = L->base; + + // xpcall had an 'errfunc' before the results, so we just replace it with 'true' status + setbvalue(base, 1); + + return cast_int(L->top - base); + } + else + { + lua_rawcheckstack(L, 1); + lua_pushboolean(L, true); + lua_replace(L, 1); // replace error function with status + return lua_gettop(L); // return status + all results + } + } + else + { + if (FFlag::LuauPcallOptimize) + { + luaD_checkstack(L, 1); + + StkId top = L->top; + + // Move error 1 right to make space for the 'false' status + setobj2s(L, top, top - 1); + + setbvalue(top - 1, 0); + L->top = top + 1; + + return 2; + } + else + { + lua_rawcheckstack(L, 1); + lua_pushboolean(L, false); + lua_insert(L, -2); // place status before the error that was on top of the stack + return 2; + } + } +} + +static int luaB_tostring(lua_State* L) +{ + luaL_checkany(L, 1); + luaL_tolstring(L, 1, NULL); + return 1; +} + +static int luaB_newproxy(lua_State* L) +{ + int t = lua_type(L, 1); + luaL_argexpected(L, t == LUA_TNONE || t == LUA_TNIL || t == LUA_TBOOLEAN, 1, "nil or boolean"); + + bool needsmt = lua_toboolean(L, 1); + + lua_newuserdatatagged(L, 0, UTAG_PROXY); + + if (needsmt) + { + lua_newtable(L); + lua_setmetatable(L, -2); + } + + return 1; +} + +static const luaL_Reg base_funcs[] = { + {"assert", luaB_assert}, + {"error", luaB_error}, + {"gcinfo", luaB_gcinfo}, + {"getfenv", luaB_getfenv}, + {"getmetatable", luaB_getmetatable}, + {"next", luaB_next}, + {"newproxy", luaB_newproxy}, + {"print", luaB_print}, + {"rawequal", luaB_rawequal}, + {"rawget", luaB_rawget}, + {"rawset", luaB_rawset}, + {"rawlen", luaB_rawlen}, + {"select", luaB_select}, + {"setfenv", luaB_setfenv}, + {"setmetatable", luaB_setmetatable}, + {"tonumber", luaB_tonumber}, + {"tostring", luaB_tostring}, + {"type", luaB_type}, + {"typeof", luaB_typeof}, + {NULL, NULL}, +}; + +static void auxopen(lua_State* L, const char* name, lua_CFunction f, lua_CFunction u) +{ + lua_pushcfunction(L, u, NULL); + lua_pushcclosure(L, f, name, 1); + lua_setfield(L, -2, name); +} + +int luaopen_base(lua_State* L) +{ + // set global _G + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setglobal(L, "_G"); + + // open lib into global table + luaL_register(L, "_G", base_funcs); + lua_pushliteral(L, "Luau"); + lua_setglobal(L, "_VERSION"); // set global _VERSION + + // `ipairs' and `pairs' need auxiliary functions as upvalues + auxopen(L, "ipairs", luaB_ipairs, luaB_inext); + auxopen(L, "pairs", luaB_pairs, luaB_next); + + lua_pushcclosurek(L, luaB_pcally, "pcall", 0, luaB_pcallcont); + + if (FFlag::LuauFastpcall) + L->global->builtinPcall = clvalue(luaA_toobject(L, -1)); + + lua_setfield(L, -2, "pcall"); + + lua_pushcclosurek(L, luaB_xpcally, "xpcall", 0, luaB_xpcallcont); + + if (FFlag::LuauFastpcall) + L->global->builtinXpcall = clvalue(luaA_toobject(L, -1)); + + lua_setfield(L, -2, "xpcall"); + + return 1; +} diff --git a/External/luau/VM/src/lbitlib.cpp b/External/luau/VM/src/lbitlib.cpp new file mode 100644 index 00000000..4f8247db --- /dev/null +++ b/External/luau/VM/src/lbitlib.cpp @@ -0,0 +1,246 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" +#include "lnumutils.h" + +#define ALLONES ~0u +#define NBITS int(8 * sizeof(unsigned)) + +// macro to trim extra bits +#define trim(x) ((x) & ALLONES) + +// builds a number with 'n' ones (1 <= n <= NBITS) +#define mask(n) (~((ALLONES << 1) << ((n)-1))) + +typedef unsigned b_uint; + +static b_uint andaux(lua_State* L) +{ + int i, n = lua_gettop(L); + b_uint r = ~(b_uint)0; + for (i = 1; i <= n; i++) + r &= luaL_checkunsigned(L, i); + return trim(r); +} + +static int b_and(lua_State* L) +{ + b_uint r = andaux(L); + lua_pushunsigned(L, r); + return 1; +} + +static int b_test(lua_State* L) +{ + b_uint r = andaux(L); + lua_pushboolean(L, r != 0); + return 1; +} + +static int b_or(lua_State* L) +{ + int i, n = lua_gettop(L); + b_uint r = 0; + for (i = 1; i <= n; i++) + r |= luaL_checkunsigned(L, i); + lua_pushunsigned(L, trim(r)); + return 1; +} + +static int b_xor(lua_State* L) +{ + int i, n = lua_gettop(L); + b_uint r = 0; + for (i = 1; i <= n; i++) + r ^= luaL_checkunsigned(L, i); + lua_pushunsigned(L, trim(r)); + return 1; +} + +static int b_not(lua_State* L) +{ + b_uint r = ~luaL_checkunsigned(L, 1); + lua_pushunsigned(L, trim(r)); + return 1; +} + +static int b_shift(lua_State* L, b_uint r, int i) +{ + if (i < 0) + { // shift right? + i = -i; + r = trim(r); + if (i >= NBITS) + r = 0; + else + r >>= i; + } + else + { // shift left + if (i >= NBITS) + r = 0; + else + r <<= i; + r = trim(r); + } + lua_pushunsigned(L, r); + return 1; +} + +static int b_lshift(lua_State* L) +{ + return b_shift(L, luaL_checkunsigned(L, 1), luaL_checkinteger(L, 2)); +} + +static int b_rshift(lua_State* L) +{ + return b_shift(L, luaL_checkunsigned(L, 1), -luaL_checkinteger(L, 2)); +} + +static int b_arshift(lua_State* L) +{ + b_uint r = luaL_checkunsigned(L, 1); + int i = luaL_checkinteger(L, 2); + if (i < 0 || !(r & ((b_uint)1 << (NBITS - 1)))) + return b_shift(L, r, -i); + else + { // arithmetic shift for 'negative' number + if (i >= NBITS) + r = ALLONES; + else + r = trim((r >> i) | ~(~(b_uint)0 >> i)); // add signal bit + lua_pushunsigned(L, r); + return 1; + } +} + +static int b_rot(lua_State* L, int i) +{ + b_uint r = luaL_checkunsigned(L, 1); + i &= (NBITS - 1); // i = i % NBITS + r = trim(r); + if (i != 0) // avoid undefined shift of NBITS when i == 0 + r = (r << i) | (r >> (NBITS - i)); + lua_pushunsigned(L, trim(r)); + return 1; +} + +static int b_lrot(lua_State* L) +{ + return b_rot(L, luaL_checkinteger(L, 2)); +} + +static int b_rrot(lua_State* L) +{ + return b_rot(L, -luaL_checkinteger(L, 2)); +} + +/* +** get field and width arguments for field-manipulation functions, +** checking whether they are valid. +** ('luaL_error' called without 'return' to avoid later warnings about +** 'width' being used uninitialized.) +*/ +static int fieldargs(lua_State* L, int farg, int* width) +{ + int f = luaL_checkinteger(L, farg); + int w = luaL_optinteger(L, farg + 1, 1); + luaL_argcheck(L, 0 <= f, farg, "field cannot be negative"); + luaL_argcheck(L, 0 < w, farg + 1, "width must be positive"); + if (f + w > NBITS) + luaL_error(L, "trying to access non-existent bits"); + *width = w; + return f; +} + +static int b_extract(lua_State* L) +{ + int w; + b_uint r = luaL_checkunsigned(L, 1); + int f = fieldargs(L, 2, &w); + r = (r >> f) & mask(w); + lua_pushunsigned(L, r); + return 1; +} + +static int b_replace(lua_State* L) +{ + int w; + b_uint r = luaL_checkunsigned(L, 1); + b_uint v = luaL_checkunsigned(L, 2); + int f = fieldargs(L, 3, &w); + int m = mask(w); + v &= m; // erase bits outside given width + r = (r & ~(m << f)) | (v << f); + lua_pushunsigned(L, r); + return 1; +} + +static int b_countlz(lua_State* L) +{ + b_uint v = luaL_checkunsigned(L, 1); + + b_uint r = NBITS; + for (int i = 0; i < NBITS; ++i) + if (v & (1u << (NBITS - 1 - i))) + { + r = i; + break; + } + + lua_pushunsigned(L, r); + return 1; +} + +static int b_countrz(lua_State* L) +{ + b_uint v = luaL_checkunsigned(L, 1); + + b_uint r = NBITS; + for (int i = 0; i < NBITS; ++i) + if (v & (1u << i)) + { + r = i; + break; + } + + lua_pushunsigned(L, r); + return 1; +} + +static int b_swap(lua_State* L) +{ + b_uint n = luaL_checkunsigned(L, 1); + n = (n << 24) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | (n >> 24); + + lua_pushunsigned(L, n); + return 1; +} + +static const luaL_Reg bitlib[] = { + {"arshift", b_arshift}, + {"band", b_and}, + {"bnot", b_not}, + {"bor", b_or}, + {"bxor", b_xor}, + {"btest", b_test}, + {"extract", b_extract}, + {"lrotate", b_lrot}, + {"lshift", b_lshift}, + {"replace", b_replace}, + {"rrotate", b_rrot}, + {"rshift", b_rshift}, + {"countlz", b_countlz}, + {"countrz", b_countrz}, + {"byteswap", b_swap}, + {NULL, NULL}, +}; + +int luaopen_bit32(lua_State* L) +{ + luaL_register(L, LUA_BITLIBNAME, bitlib); + + return 1; +} diff --git a/External/luau/VM/src/lbuffer.cpp b/External/luau/VM/src/lbuffer.cpp new file mode 100644 index 00000000..c67d7609 --- /dev/null +++ b/External/luau/VM/src/lbuffer.cpp @@ -0,0 +1,33 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lbuffer.h" + +#include "lgc.h" +#include "lmem.h" + +#include + +LUAU_FASTFLAG(LuauBufferCage) + +Buffer* luaB_newbuffer(lua_State* L, size_t s) +{ + if (s > MAX_BUFFER_SIZE) + luaM_toobig(L); + + global_State* g = L->global; + Buffer* b = (FFlag::LuauBufferCage && g->cagealloc) ? luaM_newgcocaged(L, Buffer, sizebuffer(s), L->activememcat, LUA_TBUFFER) + : luaM_newgco(L, Buffer, sizebuffer(s), L->activememcat, LUA_TBUFFER); + + luaC_init(L, b, LUA_TBUFFER); + b->len = unsigned(s); + memset(b->data, 0, b->len); + return b; +} + +void luaB_freebuffer(lua_State* L, Buffer* b, lua_Page* page) +{ + global_State* g = L->global; + if (FFlag::LuauBufferCage && g->cagealloc) + luaM_freegcocaged(L, b, sizebuffer(b->len), b->memcat, page); + else + luaM_freegco(L, b, sizebuffer(b->len), b->memcat, page); +} diff --git a/External/luau/VM/src/lbuffer.h b/External/luau/VM/src/lbuffer.h new file mode 100644 index 00000000..bc857b85 --- /dev/null +++ b/External/luau/VM/src/lbuffer.h @@ -0,0 +1,13 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "lobject.h" + +// buffer size limit +#define MAX_BUFFER_SIZE (1 << 30) + +// GCObject size has to be at least 16 bytes, so a minimum of 8 bytes is always reserved +#define sizebuffer(len) (offsetof(Buffer, data) + ((len) < 8 ? 8 : (len))) + +LUAI_FUNC Buffer* luaB_newbuffer(lua_State* L, size_t s); +LUAI_FUNC void luaB_freebuffer(lua_State* L, Buffer* u, struct lua_Page* page); diff --git a/External/luau/VM/src/lbuflib.cpp b/External/luau/VM/src/lbuflib.cpp new file mode 100644 index 00000000..70d9aecd --- /dev/null +++ b/External/luau/VM/src/lbuflib.cpp @@ -0,0 +1,441 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" +#include "lbuffer.h" + +#if defined(LUAU_BIG_ENDIAN) +#include +#endif + +LUAU_FASTFLAG(LuauIntegerLibrary) + +#include + +// while C API returns 'size_t' for binary compatibility in case of future extensions, +// in the current implementation, length and offset are limited to 31 bits +// because offset is limited to an integer, a single 64bit comparison can be used and will not overflow +#define isoutofbounds(offset, len, accessize) (uint64_t(unsigned(offset)) + (accessize) > uint64_t(len)) + +static_assert(MAX_BUFFER_SIZE <= INT_MAX, "current implementation can't handle a larger limit"); + +#if defined(LUAU_BIG_ENDIAN) +template +inline T buffer_swapbe(T v) +{ + if (sizeof(T) == 8) + return htole64(v); + else if (sizeof(T) == 4) + return htole32(v); + else if (sizeof(T) == 2) + return htole16(v); + else + return v; +} +#endif + +static int buffer_create(lua_State* L) +{ + int size = luaL_checkinteger(L, 1); + + luaL_argcheck(L, size >= 0, 1, "size"); + + lua_newbuffer(L, size); + return 1; +} + +static int buffer_fromstring(lua_State* L) +{ + size_t len = 0; + const char* val = luaL_checklstring(L, 1, &len); + + void* data = lua_newbuffer(L, len); + memcpy(data, val, len); + return 1; +} + +static int buffer_tostring(lua_State* L) +{ + size_t len = 0; + void* data = luaL_checkbuffer(L, 1, &len); + + lua_pushlstring(L, (char*)data, len); + return 1; +} + +template +static int buffer_readinteger(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + + if (isoutofbounds(offset, len, sizeof(T))) + luaL_error(L, "buffer access out of bounds"); + + T val; + memcpy(&val, (char*)buf + offset, sizeof(T)); + +#if defined(LUAU_BIG_ENDIAN) + val = buffer_swapbe(val); +#endif + + lua_pushnumber(L, double(val)); + return 1; +} + +template +static int buffer_writeinteger(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + int value = luaL_checkunsigned(L, 3); + + if (isoutofbounds(offset, len, sizeof(T))) + luaL_error(L, "buffer access out of bounds"); + + T val = T(value); + +#if defined(LUAU_BIG_ENDIAN) + val = buffer_swapbe(val); +#endif + + memcpy((char*)buf + offset, &val, sizeof(T)); + return 0; +} + +static int buffer_readlong(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + + if (isoutofbounds(offset, len, sizeof(uint64_t))) + luaL_error(L, "buffer access out of bounds"); + + int64_t val; + memcpy(&val, (char*)buf + offset, sizeof(int64_t)); + +#if defined(LUAU_BIG_ENDIAN) + val = buffer_swapbe(val); +#endif + + lua_pushinteger64(L, val); + return 1; +} + +static int buffer_writelong(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + int64_t value = luaL_checkinteger64(L, 3); + + if (isoutofbounds(offset, len, sizeof(int64_t))) + luaL_error(L, "buffer access out of bounds"); + +#if defined(LUAU_BIG_ENDIAN) + value = buffer_swapbe(value); +#endif + + memcpy((char*)buf + offset, &value, sizeof(int64_t)); + return 0; +} + +template +static int buffer_readfp(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + + if (isoutofbounds(offset, len, sizeof(T))) + luaL_error(L, "buffer access out of bounds"); + + T val; + +#if defined(LUAU_BIG_ENDIAN) + static_assert(sizeof(T) == sizeof(StorageType), "type size must match to reinterpret data"); + StorageType tmp; + memcpy(&tmp, (char*)buf + offset, sizeof(tmp)); + tmp = buffer_swapbe(tmp); + + memcpy(&val, &tmp, sizeof(tmp)); +#else + memcpy(&val, (char*)buf + offset, sizeof(T)); +#endif + + lua_pushnumber(L, double(val)); + return 1; +} + +template +static int buffer_writefp(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + double value = luaL_checknumber(L, 3); + + if (isoutofbounds(offset, len, sizeof(T))) + luaL_error(L, "buffer access out of bounds"); + + T val = T(value); + +#if defined(LUAU_BIG_ENDIAN) + static_assert(sizeof(T) == sizeof(StorageType), "type size must match to reinterpret data"); + StorageType tmp; + memcpy(&tmp, &val, sizeof(tmp)); + tmp = buffer_swapbe(tmp); + + memcpy((char*)buf + offset, &tmp, sizeof(tmp)); +#else + memcpy((char*)buf + offset, &val, sizeof(T)); +#endif + + return 0; +} + +static int buffer_readstring(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + int size = luaL_checkinteger(L, 3); + + luaL_argcheck(L, size >= 0, 3, "size"); + + if (isoutofbounds(offset, len, unsigned(size))) + luaL_error(L, "buffer access out of bounds"); + + lua_pushlstring(L, (char*)buf + offset, size); + return 1; +} + +static int buffer_writestring(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + size_t size = 0; + const char* val = luaL_checklstring(L, 3, &size); + int count = luaL_optinteger(L, 4, int(size)); + + luaL_argcheck(L, count >= 0, 4, "count"); + + if (size_t(count) > size) + luaL_error(L, "string length overflow"); + + // string size can't exceed INT_MAX at this point + if (isoutofbounds(offset, len, unsigned(count))) + luaL_error(L, "buffer access out of bounds"); + + memcpy((char*)buf + offset, val, count); + return 0; +} + +static int buffer_len(lua_State* L) +{ + size_t len = 0; + luaL_checkbuffer(L, 1, &len); + + lua_pushnumber(L, double(unsigned(len))); + return 1; +} + +static int buffer_copy(lua_State* L) +{ + size_t tlen = 0; + void* tbuf = luaL_checkbuffer(L, 1, &tlen); + int toffset = luaL_checkinteger(L, 2); + + size_t slen = 0; + void* sbuf = luaL_checkbuffer(L, 3, &slen); + int soffset = luaL_optinteger(L, 4, 0); + + int size = luaL_optinteger(L, 5, int(slen) - soffset); + + if (size < 0) + luaL_error(L, "buffer access out of bounds"); + + if (isoutofbounds(soffset, slen, unsigned(size))) + luaL_error(L, "buffer access out of bounds"); + + if (isoutofbounds(toffset, tlen, unsigned(size))) + luaL_error(L, "buffer access out of bounds"); + + memmove((char*)tbuf + toffset, (char*)sbuf + soffset, size); + return 0; +} + +static int buffer_fill(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int offset = luaL_checkinteger(L, 2); + unsigned value = luaL_checkunsigned(L, 3); + int size = luaL_optinteger(L, 4, int(len) - offset); + + if (size < 0) + luaL_error(L, "buffer access out of bounds"); + + if (isoutofbounds(offset, len, unsigned(size))) + luaL_error(L, "buffer access out of bounds"); + + memset((char*)buf + offset, value & 0xff, size); + return 0; +} + +static int buffer_readbits(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int64_t bitoffset = (int64_t)luaL_checknumber(L, 2); + int bitcount = luaL_checkinteger(L, 3); + + if (bitoffset < 0) + luaL_error(L, "buffer access out of bounds"); + + if (unsigned(bitcount) > 32) + luaL_error(L, "bit count is out of range of [0; 32]"); + + if (uint64_t(bitoffset + bitcount) > uint64_t(len) * 8) + luaL_error(L, "buffer access out of bounds"); + + unsigned startbyte = unsigned(bitoffset / 8); + unsigned endbyte = unsigned((bitoffset + bitcount + 7) / 8); + + uint64_t data = 0; + +#if defined(LUAU_BIG_ENDIAN) + for (int i = int(endbyte) - 1; i >= int(startbyte); i--) + data = (data << 8) + uint8_t(((char*)buf)[i]); +#else + memcpy(&data, (char*)buf + startbyte, endbyte - startbyte); +#endif + + uint64_t subbyteoffset = bitoffset & 0x7; + uint64_t mask = (1ull << bitcount) - 1; + + lua_pushunsigned(L, unsigned((data >> subbyteoffset) & mask)); + return 1; +} + +static int buffer_writebits(lua_State* L) +{ + size_t len = 0; + void* buf = luaL_checkbuffer(L, 1, &len); + int64_t bitoffset = (int64_t)luaL_checknumber(L, 2); + int bitcount = luaL_checkinteger(L, 3); + unsigned value = luaL_checkunsigned(L, 4); + + if (bitoffset < 0) + luaL_error(L, "buffer access out of bounds"); + + if (unsigned(bitcount) > 32) + luaL_error(L, "bit count is out of range of [0; 32]"); + + if (uint64_t(bitoffset + bitcount) > uint64_t(len) * 8) + luaL_error(L, "buffer access out of bounds"); + + unsigned startbyte = unsigned(bitoffset / 8); + unsigned endbyte = unsigned((bitoffset + bitcount + 7) / 8); + + uint64_t data = 0; + +#if defined(LUAU_BIG_ENDIAN) + for (int i = int(endbyte) - 1; i >= int(startbyte); i--) + data = data * 256 + uint8_t(((char*)buf)[i]); +#else + memcpy(&data, (char*)buf + startbyte, endbyte - startbyte); +#endif + + uint64_t subbyteoffset = bitoffset & 0x7; + uint64_t mask = ((1ull << bitcount) - 1) << subbyteoffset; + + data = (data & ~mask) | ((uint64_t(value) << subbyteoffset) & mask); + +#if defined(LUAU_BIG_ENDIAN) + for (int i = int(startbyte); i < int(endbyte); i++) + { + ((char*)buf)[i] = data & 0xff; + data >>= 8; + } +#else + memcpy((char*)buf + startbyte, &data, endbyte - startbyte); +#endif + return 0; +} + +static const luaL_Reg bufferlib[] = { + {"create", buffer_create}, + {"fromstring", buffer_fromstring}, + {"tostring", buffer_tostring}, + {"readi8", buffer_readinteger}, + {"readu8", buffer_readinteger}, + {"readi16", buffer_readinteger}, + {"readu16", buffer_readinteger}, + {"readi32", buffer_readinteger}, + {"readu32", buffer_readinteger}, + {"readf32", buffer_readfp}, + {"readf64", buffer_readfp}, + {"writei8", buffer_writeinteger}, + {"writeu8", buffer_writeinteger}, + {"writei16", buffer_writeinteger}, + {"writeu16", buffer_writeinteger}, + {"writei32", buffer_writeinteger}, + {"writeu32", buffer_writeinteger}, + {"writef32", buffer_writefp}, + {"writef64", buffer_writefp}, + {"readstring", buffer_readstring}, + {"writestring", buffer_writestring}, + {"len", buffer_len}, + {"copy", buffer_copy}, + {"fill", buffer_fill}, + {"readbits", buffer_readbits}, + {"writebits", buffer_writebits}, + {"readinteger", buffer_readlong}, + {"writeinteger", buffer_writelong}, + {NULL, NULL}, +}; + +static const luaL_Reg bufferlib_NOINTEGER[] = { + {"create", buffer_create}, + {"fromstring", buffer_fromstring}, + {"tostring", buffer_tostring}, + {"readi8", buffer_readinteger}, + {"readu8", buffer_readinteger}, + {"readi16", buffer_readinteger}, + {"readu16", buffer_readinteger}, + {"readi32", buffer_readinteger}, + {"readu32", buffer_readinteger}, + {"readf32", buffer_readfp}, + {"readf64", buffer_readfp}, + {"writei8", buffer_writeinteger}, + {"writeu8", buffer_writeinteger}, + {"writei16", buffer_writeinteger}, + {"writeu16", buffer_writeinteger}, + {"writei32", buffer_writeinteger}, + {"writeu32", buffer_writeinteger}, + {"writef32", buffer_writefp}, + {"writef64", buffer_writefp}, + {"readstring", buffer_readstring}, + {"writestring", buffer_writestring}, + {"len", buffer_len}, + {"copy", buffer_copy}, + {"fill", buffer_fill}, + {"readbits", buffer_readbits}, + {"writebits", buffer_writebits}, + {NULL, NULL}, +}; + +int luaopen_buffer(lua_State* L) +{ + if (FFlag::LuauIntegerLibrary) + luaL_register(L, LUA_BUFFERLIBNAME, bufferlib); + else + luaL_register(L, LUA_BUFFERLIBNAME, bufferlib_NOINTEGER); + + return 1; +} diff --git a/External/luau/VM/src/lbuiltins.cpp b/External/luau/VM/src/lbuiltins.cpp new file mode 100644 index 00000000..66485319 --- /dev/null +++ b/External/luau/VM/src/lbuiltins.cpp @@ -0,0 +1,2787 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lbuiltins.h" + +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lgc.h" +#include "lnumutils.h" +#include "ldo.h" +#include "lbuffer.h" +#include "lvector.h" + +#include +#include + +#ifdef _MSC_VER +#include +#endif + +#ifdef LUAU_TARGET_SSE41 +#include + +#ifndef _MSC_VER +#include // on MSVC this comes from intrin.h +#endif +#endif + +// luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM +// The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack. +// If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path +// If luauF_* succeeds, it needs to return *all* requested arguments, filling results with nil as appropriate. +// On input, nparams refers to the actual number of arguments (0+), whereas nresults contains LUA_MULTRET for arbitrary returns or 0+ for a +// fixed-length return +// Because of this, and the fact that "extra" returned values will be ignored, implementations below typically check that nresults is <= expected +// number, which covers the LUA_MULTRET case. + +static int luauF_assert(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults == 0 && !l_isfalse(arg0)) + { + return 0; + } + + return -1; +} + +static int luauF_abs(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, fabs(a1)); + return 1; + } + + return -1; +} + +static int luauF_acos(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, acos(a1)); + return 1; + } + + return -1; +} + +static int luauF_asin(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, asin(a1)); + return 1; + } + + return -1; +} + +static int luauF_atan2(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + setnvalue(res, atan2(a1, a2)); + return 1; + } + + return -1; +} + +static int luauF_atan(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, atan(a1)); + return 1; + } + + return -1; +} + +LUAU_FASTMATH_BEGIN +static int luauF_ceil(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, ceil(a1)); + return 1; + } + + return -1; +} +LUAU_FASTMATH_END + +static int luauF_cosh(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, cosh(a1)); + return 1; + } + + return -1; +} + +static int luauF_cos(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, cos(a1)); + return 1; + } + + return -1; +} + +static int luauF_deg(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + const double rpd = (3.14159265358979323846 / 180.0); + setnvalue(res, a1 / rpd); + return 1; + } + + return -1; +} + +static int luauF_exp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, exp(a1)); + return 1; + } + + return -1; +} + +LUAU_FASTMATH_BEGIN +static int luauF_floor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, floor(a1)); + return 1; + } + + return -1; +} +LUAU_FASTMATH_END + +static int luauF_fmod(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + setnvalue(res, fmod(a1, a2)); + return 1; + } + + return -1; +} + +static int luauF_frexp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 2 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + int e; + double f = frexp(a1, &e); + setnvalue(res, f); + setnvalue(res + 1, double(e)); + return 2; + } + + return -1; +} + +static int luauF_ldexp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + setnvalue(res, ldexp(a1, int(a2))); + return 1; + } + + return -1; +} + +static int luauF_log10(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, log10(a1)); + return 1; + } + + return -1; +} + +static int luauF_log(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + + if (nparams == 1) + { + setnvalue(res, log(a1)); + return 1; + } + else if (ttisnumber(args)) + { + double a2 = nvalue(args); + + if (a2 == 2.0) + { + setnvalue(res, log2(a1)); + return 1; + } + else if (a2 == 10.0) + { + setnvalue(res, log10(a1)); + return 1; + } + else + { + setnvalue(res, log(a1) / log(a2)); + return 1; + } + } + } + + return -1; +} + +static int luauF_max(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + double r = (a2 > a1) ? a2 : a1; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + + r = (a > r) ? a : r; + } + + setnvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_min(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + double r = (a2 < a1) ? a2 : a1; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + + r = (a < r) ? a : r; + } + + setnvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_modf(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 2 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + double ip; + double fp = modf(a1, &ip); + setnvalue(res, ip); + setnvalue(res + 1, fp); + return 2; + } + + return -1; +} + +static int luauF_pow(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + setnvalue(res, pow(a1, a2)); + return 1; + } + + return -1; +} + +static int luauF_rad(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + const double rpd = (3.14159265358979323846 / 180.0); + setnvalue(res, a1 * rpd); + return 1; + } + + return -1; +} + +static int luauF_sinh(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, sinh(a1)); + return 1; + } + + return -1; +} + +static int luauF_sin(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, sin(a1)); + return 1; + } + + return -1; +} + +LUAU_FASTMATH_BEGIN +static int luauF_sqrt(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, sqrt(a1)); + return 1; + } + + return -1; +} +LUAU_FASTMATH_END + +static int luauF_tanh(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, tanh(a1)); + return 1; + } + + return -1; +} + +static int luauF_tan(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, tan(a1)); + return 1; + } + + return -1; +} + +static int luauF_arshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u; + luai_num2unsigned(u, a1); + int s = int(a2); + + // note: we only specialize fast-path that doesn't require further conditionals (negative shifts and shifts greater or equal to bit width can + // be handled generically) + if (unsigned(s) < 32) + { + // note: technically right shift of negative values is UB, but this behavior is getting defined in C++20 and all compilers do the right + // (shift) thing. + uint32_t r = int32_t(u) >> s; + + setnvalue(res, double(r)); + return 1; + } + } + + return -1; +} + +static int luauF_band(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u1, u2; + luai_num2unsigned(u1, a1); + luai_num2unsigned(u2, a2); + + uint32_t r = u1 & u2; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + unsigned u; + luai_num2unsigned(u, a); + + r &= u; + } + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_bnot(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + unsigned u; + luai_num2unsigned(u, a1); + + uint32_t r = ~u; + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_bor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u1, u2; + luai_num2unsigned(u1, a1); + luai_num2unsigned(u2, a2); + + uint32_t r = u1 | u2; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + unsigned u; + luai_num2unsigned(u, a); + + r |= u; + } + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_bxor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u1, u2; + luai_num2unsigned(u1, a1); + luai_num2unsigned(u2, a2); + + uint32_t r = u1 ^ u2; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + unsigned u; + luai_num2unsigned(u, a); + + r ^= u; + } + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_btest(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u1, u2; + luai_num2unsigned(u1, a1); + luai_num2unsigned(u2, a2); + + uint32_t r = u1 & u2; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + double a = nvalue(args + (i - 2)); + unsigned u; + luai_num2unsigned(u, a); + + r &= u; + } + + setbvalue(res, r != 0); + return 1; + } + + return -1; +} + +static int luauF_extract(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned n; + luai_num2unsigned(n, a1); + int f = int(a2); + + if (nparams == 2) + { + if (unsigned(f) < 32) + { + uint32_t m = 1; + uint32_t r = (n >> f) & m; + + setnvalue(res, double(r)); + return 1; + } + } + else if (ttisnumber(args + 1)) + { + double a3 = nvalue(args + 1); + int w = int(a3); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + uint32_t r = (n >> f) & m; + + setnvalue(res, double(r)); + return 1; + } + } + } + + return -1; +} + +static int luauF_lrotate(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u; + luai_num2unsigned(u, a1); + int s = int(a2); + + // MSVC doesn't recognize the rotate form that is UB-safe +#ifdef _MSC_VER + uint32_t r = _rotl(u, s); +#else + uint32_t r = (u << (s & 31)) | (u >> ((32 - s) & 31)); +#endif + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_lshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u; + luai_num2unsigned(u, a1); + int s = int(a2); + + // note: we only specialize fast-path that doesn't require further conditionals (negative shifts and shifts greater or equal to bit width can + // be handled generically) + if (unsigned(s) < 32) + { + uint32_t r = u << s; + + setnvalue(res, double(r)); + return 1; + } + } + + return -1; +} + +static int luauF_replace(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + double a3 = nvalue(args + 1); + + unsigned n, v; + luai_num2unsigned(n, a1); + luai_num2unsigned(v, a2); + int f = int(a3); + + if (nparams == 3) + { + if (unsigned(f) < 32) + { + uint32_t m = 1; + uint32_t r = (n & ~(m << f)) | ((v & m) << f); + + setnvalue(res, double(r)); + return 1; + } + } + else if (ttisnumber(args + 2)) + { + double a4 = nvalue(args + 2); + int w = int(a4); + + if (f >= 0 && w > 0 && f + w <= 32) + { + uint32_t m = ~(0xfffffffeu << (w - 1)); + uint32_t r = (n & ~(m << f)) | ((v & m) << f); + + setnvalue(res, double(r)); + return 1; + } + } + } + + return -1; +} + +static int luauF_rrotate(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u; + luai_num2unsigned(u, a1); + int s = int(a2); + + // MSVC doesn't recognize the rotate form that is UB-safe +#ifdef _MSC_VER + uint32_t r = _rotr(u, s); +#else + uint32_t r = (u >> (s & 31)) | (u << ((32 - s) & 31)); +#endif + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_rshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned u; + luai_num2unsigned(u, a1); + int s = int(a2); + + // note: we only specialize fast-path that doesn't require further conditionals (negative shifts and shifts greater or equal to bit width can + // be handled generically) + if (unsigned(s) < 32) + { + uint32_t r = u >> s; + + setnvalue(res, double(r)); + return 1; + } + } + + return -1; +} + +static int luauF_type(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + int tt = ttype(arg0); + TString* ttname = L->global->ttname[tt]; + + setsvalue(L, res, ttname); + return 1; + } + + return -1; +} + +static int luauF_byte(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && ttisstring(arg0) && ttisnumber(args)) + { + TString* ts = tsvalue(arg0); + int i = int(nvalue(args)); + int j = (nparams >= 3) ? (ttisnumber(args + 1) ? int(nvalue(args + 1)) : 0) : i; + + if (i >= 1 && j >= i && j <= int(ts->len)) + { + int c = j - i + 1; + const char* s = getstr(ts); + + // for vararg returns, we only support a single result + // this is because this frees us from concerns about stack space + if (c == (nresults < 0 ? 1 : nresults)) + { + for (int k = 0; k < c; ++k) + { + setnvalue(res + k, uint8_t(s[i + k - 1])); + } + + return c; + } + } + } + + return -1; +} + +static int luauF_char(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + char buffer[8]; + + if (nparams < int(sizeof(buffer)) && nresults <= 1) + { + if (luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + + if (nparams >= 1) + { + if (!ttisnumber(arg0)) + return -1; + + int ch = int(nvalue(arg0)); + + if ((unsigned char)(ch) != ch) + return -1; + + buffer[0] = ch; + } + + for (int i = 2; i <= nparams; ++i) + { + if (!ttisnumber(args + (i - 2))) + return -1; + + int ch = int(nvalue(args + (i - 2))); + + if ((unsigned char)(ch) != ch) + return -1; + + buffer[i - 1] = ch; + } + + buffer[nparams] = 0; + + setsvalue(L, res, luaS_newlstr(L, buffer, nparams)); + return 1; + } + + return -1; +} + +static int luauF_len(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisstring(arg0)) + { + TString* ts = tsvalue(arg0); + + setnvalue(res, int(ts->len)); + return 1; + } + + return -1; +} + +static int luauF_typeof(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + const TString* ttname = luaT_objtypenamestr(L, arg0); + + setsvalue(L, res, ttname); + return 1; + } + + return -1; +} + +static int luauF_sub(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisstring(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + TString* ts = tsvalue(arg0); + int i = int(nvalue(args)); + int j = int(nvalue(args + 1)); + + if (luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + + if (i >= 1 && j >= i && unsigned(j - 1) < unsigned(ts->len)) + { + setsvalue(L, res, luaS_newlstr(L, getstr(ts) + (i - 1), j - i + 1)); + return 1; + } + } + + return -1; +} + +static int luauF_clamp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + double v = nvalue(arg0); + double min = nvalue(args); + double max = nvalue(args + 1); + + if (min <= max) + { + double r = v < min ? min : v; + r = r > max ? max : r; + + setnvalue(res, r); + return 1; + } + } + + return -1; +} + +static int luauF_sign(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double v = nvalue(arg0); + setnvalue(res, v > 0.0 ? 1.0 : v < 0.0 ? -1.0 : 0.0); + return 1; + } + + return -1; +} + +LUAU_FASTMATH_BEGIN +static int luauF_round(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double v = nvalue(arg0); + setnvalue(res, round(v)); + return 1; + } + + return -1; +} +LUAU_FASTMATH_END + +static int luauF_rawequal(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1) + { + setbvalue(res, luaO_rawequalObj(arg0, args)); + return 1; + } + + return -1; +} + +static int luauF_rawget(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttistable(arg0)) + { + setobj2s(L, res, luaH_get(hvalue(arg0), args)); + return 1; + } + + return -1; +} + +static int luauF_rawset(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttistable(arg0)) + { + const TValue* key = args; + if (ttisnil(key)) + return -1; + else if (ttisnumber(key) && luai_numisnan(nvalue(key))) + return -1; + else if (ttisvector(key) && luai_vecisnan(vvalue(key))) + return -1; + + LuaTable* t = hvalue(arg0); + if (t->readonly) + return -1; + + setobj2s(L, res, arg0); + setobj2t(L, luaH_set(L, t, args), args + 1); + luaC_barriert(L, t, args + 1); + return 1; + } + + return -1; +} + +static int luauF_tinsert(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams == 2 && nresults <= 0 && ttistable(arg0)) + { + LuaTable* t = hvalue(arg0); + if (t->readonly) + return -1; + + int pos = luaH_getn(t) + 1; + setobj2t(L, luaH_setnum(L, t, pos), args); + luaC_barriert(L, t, args); + return 0; + } + + return -1; +} + +static int luauF_tunpack(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults < 0 && ttistable(arg0)) + { + LuaTable* t = hvalue(arg0); + int n = -1; + + if (nparams == 1) + n = luaH_getn(t); + else if (nparams == 3 && ttisnumber(args) && ttisnumber(args + 1) && nvalue(args) == 1.0) + n = int(nvalue(args + 1)); + + if (n >= 0 && n <= t->sizearray && cast_int(L->stack_last - res) >= n && n + nparams <= LUAI_MAXCSTACK) + { + TValue* array = t->array; + for (int i = 0; i < n; ++i) + setobj2s(L, res + i, array + i); + expandstacklimit(L, res + n); + return n; + } + } + + return -1; +} + +static int luauF_vector(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args)) + { + LUA_VECTOR_TYPE x = LUA_VECTOR_TYPE(nvalue(arg0)); + LUA_VECTOR_TYPE y = LUA_VECTOR_TYPE(nvalue(args)); + LUA_VECTOR_TYPE z = LUA_VECTOR_TYPE(0.0); + + if (nparams >= 3) + { + if (!ttisnumber(args + 1)) + return -1; + z = LUA_VECTOR_TYPE(nvalue(args + 1)); + } + +#if LUA_VECTOR_SIZE == 4 + LUA_VECTOR_TYPE w = LUA_VECTOR_TYPE(0.0); + if (nparams >= 4) + { + if (!ttisnumber(args + 2)) + return -1; + w = LUA_VECTOR_TYPE(nvalue(args + 2)); + } + setvvalue(L, res, x, y, z, w); +#else + setvvalue(L, res, x, y, z, 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_countlz(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + + unsigned n; + luai_num2unsigned(n, a1); + +#ifdef _MSC_VER + unsigned long rl; + int r = _BitScanReverse(&rl, n) ? 31 - int(rl) : 32; +#else + int r = n == 0 ? 32 : __builtin_clz(n); +#endif + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_countrz(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + + unsigned n; + luai_num2unsigned(n, a1); + +#ifdef _MSC_VER + unsigned long rl; + int r = _BitScanForward(&rl, n) ? int(rl) : 32; +#else + int r = n == 0 ? 32 : __builtin_ctz(n); +#endif + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_select(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams == 1 && nresults == 1) + { + int n = cast_int(L->base - L->ci->func) - L->ci->p->numparams - 1; + + if (ttisnumber(arg0)) + { + int i = int(nvalue(arg0)); + + // i >= 1 && i <= n + if (unsigned(i - 1) < unsigned(n)) + { + setobj2s(L, res, L->base - n + (i - 1)); + return 1; + } + // note: for now we don't handle negative case (wrap around) and defer to fallback + } + else if (ttisstring(arg0) && *svalue(arg0) == '#') + { + setnvalue(res, double(n)); + return 1; + } + } + + return -1; +} + +static int luauF_rawlen(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + if (ttistable(arg0)) + { + LuaTable* h = hvalue(arg0); + setnvalue(res, double(luaH_getn(h))); + return 1; + } + else if (ttisstring(arg0)) + { + TString* ts = tsvalue(arg0); + setnvalue(res, double(ts->len)); + return 1; + } + } + + return -1; +} + +static int luauF_extractk(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + // args is known to contain a number constant with packed in-range f/w + if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + double a2 = nvalue(args); + + unsigned n; + luai_num2unsigned(n, a1); + int fw = int(a2); + + int f = fw & 31; + int w1 = fw >> 5; + + uint32_t m = ~(0xfffffffeu << w1); + uint32_t r = (n >> f) & m; + + setnvalue(res, double(r)); + return 1; + } + + return -1; +} + +static int luauF_getmetatable(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + LuaTable* mt = NULL; + if (ttistable(arg0)) + mt = hvalue(arg0)->metatable; + else if (ttisuserdata(arg0)) + mt = uvalue(arg0)->metatable; + else + mt = L->global->mt[ttype(arg0)]; + + const TValue* mtv = mt ? luaH_getstr(mt, L->global->tmname[TM_METATABLE]) : luaO_nilobject; + if (!ttisnil(mtv)) + { + setobj2s(L, res, mtv); + return 1; + } + + if (mt) + { + sethvalue(L, res, mt); + return 1; + } + else + { + setnilvalue(res); + return 1; + } + } + + return -1; +} + +static int luauF_setmetatable(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + // note: setmetatable(_, nil) is rare so we use fallback for it to optimize the fast path + if (nparams >= 2 && nresults <= 1 && ttistable(arg0) && ttistable(args)) + { + LuaTable* t = hvalue(arg0); + if (t->readonly || t->metatable != NULL) + return -1; // note: overwriting non-null metatable is very rare but it requires __metatable check + + LuaTable* mt = hvalue(args); + t->metatable = mt; + luaC_objbarrier(L, t, mt); + + sethvalue(L, res, t); + return 1; + } + + return -1; +} + +static int luauF_tonumber(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams == 1 && nresults <= 1) + { + double num; + + if (ttisnumber(arg0)) + { + setnvalue(res, nvalue(arg0)); + return 1; + } + else if (ttisstring(arg0) && luaO_str2d(svalue(arg0), &num)) + { + setnvalue(res, num); + return 1; + } + else + { + setnilvalue(res); + return 1; + } + } + + return -1; +} + +static int luauF_tostring(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + switch (ttype(arg0)) + { + case LUA_TNIL: + { + TString* s = L->global->ttname[LUA_TNIL]; + setsvalue(L, res, s); + return 1; + } + case LUA_TBOOLEAN: + { + TString* s = bvalue(arg0) ? luaS_newliteral(L, "true") : luaS_newliteral(L, "false"); + setsvalue(L, res, s); + return 1; + } + case LUA_TNUMBER: + { + if (luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + + char s[LUAI_MAXNUM2STR]; + char* e = luai_num2str(s, nvalue(arg0)); + setsvalue(L, res, luaS_newlstr(L, s, e - s)); + return 1; + } + case LUA_TSTRING: + { + setsvalue(L, res, tsvalue(arg0)); + return 1; + } + case LUA_TINTEGER: + { + if (luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + + char s[LUAI_MAXINT2STR]; + char* e = luai_int2str(s, lvalue(arg0)); + setsvalue(L, res, luaS_newlstr(L, s, e - s)); + return 1; + } + } + + // fall back to generic C implementation + } + + return -1; +} + +static int luauF_byteswap(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + unsigned n; + luai_num2unsigned(n, a1); + + n = (n << 24) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | (n >> 24); + + setnvalue(res, double(n)); + return 1; + } + + return -1; +} + +// because offset is limited to an integer, a single 64bit comparison can be used and will not overflow +#define checkoutofbounds(offset, len, accessize) (uint64_t(unsigned(offset)) + (accessize - 1) >= uint64_t(len)) + +template +static int luauF_readinteger(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 2 && nresults <= 1 && ttisbuffer(arg0) && ttisnumber(args)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(T))) + return -1; + + T val; + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); + setnvalue(res, double(val)); + return 1; + } +#endif + + return -1; +} + +template +static int luauF_writeinteger(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 3 && nresults <= 0 && ttisbuffer(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(T))) + return -1; + + unsigned value; + double incoming = nvalue(args + 1); + luai_num2unsigned(value, incoming); + + T val = T(value); + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); + return 0; + } +#endif + + return -1; +} + +template +static int luauF_readfp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 2 && nresults <= 1 && ttisbuffer(arg0) && ttisnumber(args)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(T))) + return -1; + + T val; +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + val = *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)); +#else + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); +#endif + setnvalue(res, double(val)); + return 1; + } +#endif + + return -1; +} + +template +static int luauF_writefp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 3 && nresults <= 0 && ttisbuffer(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(T))) + return -1; + + T val = T(nvalue(args + 1)); +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)) = val; +#else + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); +#endif + return 0; + } +#endif + + return -1; +} + +static int luauF_vectormagnitude(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + setnvalue(res, luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3])); +#else + setnvalue(res, luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectornormalize(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + LUA_VECTOR_TYPE invSqrt = LUA_VECTOR_TYPE(1.0) / luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]); + + setvvalue(L, res, v[0] * invSqrt, v[1] * invSqrt, v[2] * invSqrt, v[3] * invSqrt); +#else + LUA_VECTOR_TYPE invSqrt = LUA_VECTOR_TYPE(1.0) / luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + + setvvalue(L, res, v[0] * invSqrt, v[1] * invSqrt, v[2] * invSqrt, 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorcross(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisvector(arg0) && ttisvector(args)) + { + const LUA_VECTOR_TYPE* a = vvalue(arg0); + const LUA_VECTOR_TYPE* b = vvalue(args); + + // same for 3- and 4- wide vectors + setvvalue(L, res, a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], 0.0); + return 1; + } + + return -1; +} + +static int luauF_vectordot(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisvector(arg0) && ttisvector(args)) + { + const LUA_VECTOR_TYPE* a = vvalue(arg0); + const LUA_VECTOR_TYPE* b = vvalue(args); + +#if LUA_VECTOR_SIZE == 4 + setnvalue(res, a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); +#else + setnvalue(res, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorfloor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + setvvalue(L, res, luai_floor(v[0]), luai_floor(v[1]), luai_floor(v[2]), luai_floor(v[3])); +#else + setvvalue(L, res, luai_floor(v[0]), luai_floor(v[1]), luai_floor(v[2]), 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorceil(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + setvvalue(L, res, luai_ceil(v[0]), luai_ceil(v[1]), luai_ceil(v[2]), luai_ceil(v[3])); +#else + setvvalue(L, res, luai_ceil(v[0]), luai_ceil(v[1]), luai_ceil(v[2]), 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorabs(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + setvvalue(L, res, luai_fabs(v[0]), luai_fabs(v[1]), luai_fabs(v[2]), luai_fabs(v[3])); +#else + setvvalue(L, res, luai_fabs(v[0]), luai_fabs(v[1]), luai_fabs(v[2]), 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorsign(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisvector(arg0)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + +#if LUA_VECTOR_SIZE == 4 + setvvalue(L, res, luai_sign(v[0]), luai_sign(v[1]), luai_sign(v[2]), luai_sign(v[3])); +#else + setvvalue(L, res, luai_sign(v[0]), luai_sign(v[1]), luai_sign(v[2]), 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_vectorclamp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisvector(arg0) && ttisvector(args) && ttisvector(args + 1)) + { + const LUA_VECTOR_TYPE* v = vvalue(arg0); + const LUA_VECTOR_TYPE* min = vvalue(args); + const LUA_VECTOR_TYPE* max = vvalue(args + 1); + + if (min[0] <= max[0] && min[1] <= max[1] && min[2] <= max[2]) + { +#if LUA_VECTOR_SIZE == 4 + setvvalue( + L, + res, + luai_clamp(v[0], min[0], max[0]), + luai_clamp(v[1], min[1], max[1]), + luai_clamp(v[2], min[2], max[2]), + luai_clamp(v[3], min[3], max[3]) + ); +#else + setvvalue(L, res, luai_clamp(v[0], min[0], max[0]), luai_clamp(v[1], min[1], max[1]), luai_clamp(v[2], min[2], max[2]), 0.0); +#endif + + return 1; + } + } + + return -1; +} + +static int luauF_vectormin(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisvector(arg0) && ttisvector(args)) + { + const LUA_VECTOR_TYPE* a = vvalue(arg0); + const LUA_VECTOR_TYPE* b = vvalue(args); + + LUA_VECTOR_TYPE result[4]; + + result[0] = (b[0] < a[0]) ? b[0] : a[0]; + result[1] = (b[1] < a[1]) ? b[1] : a[1]; + result[2] = (b[2] < a[2]) ? b[2] : a[2]; + +#if LUA_VECTOR_SIZE == 4 + result[3] = (b[3] < a[3]) ? b[3] : a[3]; +#else + result[3] = 0.0; +#endif + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisvector(args + (i - 2))) + return -1; + + const LUA_VECTOR_TYPE* c = vvalue(args + (i - 2)); + + result[0] = (c[0] < result[0]) ? c[0] : result[0]; + result[1] = (c[1] < result[1]) ? c[1] : result[1]; + result[2] = (c[2] < result[2]) ? c[2] : result[2]; +#if LUA_VECTOR_SIZE == 4 + result[3] = (c[3] < result[3]) ? c[3] : result[3]; +#endif + } + + setvvalue(L, res, result[0], result[1], result[2], result[3]); + return 1; + } + + return -1; +} + +static int luauF_vectormax(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisvector(arg0) && ttisvector(args)) + { + const LUA_VECTOR_TYPE* a = vvalue(arg0); + const LUA_VECTOR_TYPE* b = vvalue(args); + + LUA_VECTOR_TYPE result[4]; + + result[0] = (b[0] > a[0]) ? b[0] : a[0]; + result[1] = (b[1] > a[1]) ? b[1] : a[1]; + result[2] = (b[2] > a[2]) ? b[2] : a[2]; + +#if LUA_VECTOR_SIZE == 4 + result[3] = (b[3] > a[3]) ? b[3] : a[3]; +#else + result[3] = 0.0; +#endif + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisvector(args + (i - 2))) + return -1; + + const LUA_VECTOR_TYPE* c = vvalue(args + (i - 2)); + + result[0] = (c[0] > result[0]) ? c[0] : result[0]; + result[1] = (c[1] > result[1]) ? c[1] : result[1]; + result[2] = (c[2] > result[2]) ? c[2] : result[2]; +#if LUA_VECTOR_SIZE == 4 + result[3] = (c[3] > result[3]) ? c[3] : result[3]; +#endif + } + + setvvalue(L, res, result[0], result[1], result[2], result[3]); + return 1; + } + + return -1; +} + +static int luauF_vectorlerp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisvector(arg0) && ttisvector(args) && ttisnumber(args + 1)) + { + const LUA_VECTOR_TYPE* a = vvalue(arg0); + const LUA_VECTOR_TYPE* b = vvalue(args); + const LUA_VECTOR_TYPE t = LUA_VECTOR_TYPE(nvalue(args + 1)); + +#if LUA_VECTOR_SIZE == 4 + setvvalue(L, res, luai_lerp(a[0], b[0], t), luai_lerp(a[1], b[1], t), luai_lerp(a[2], b[2], t), luai_lerp(a[3], b[3], t)); +#else + setvvalue(L, res, luai_lerp(a[0], b[0], t), luai_lerp(a[1], b[1], t), luai_lerp(a[2], b[2], t), 0.0); +#endif + + return 1; + } + + return -1; +} + +static int luauF_lerp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1)) + { + double a = nvalue(arg0); + double b = nvalue(args); + double t = nvalue(args + 1); + + double r = (t == 1.0) ? b : a + (b - a) * t; + + setnvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_isnan(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double x = nvalue(arg0); + + setbvalue(res, isnan(x)); + return 1; + } + + return -1; +} + +static int luauF_isinf(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double x = nvalue(arg0); + + setbvalue(res, isinf(x)); + return 1; + } + + return -1; +} + +static int luauF_isfinite(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double x = nvalue(arg0); + + setbvalue(res, isfinite(x)); + return 1; + } + + return -1; +} + +static int luauF_integertonumber(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + setnvalue(res, cast_num(lvalue(arg0))); + return 1; + } + + return -1; +} + +static int luauF_integeradd(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + setlvalue(res, (int64_t)((uint64_t)a1 + (uint64_t)a2)); + return 1; + } + + return -1; +} + +static int luauF_integersub(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + setlvalue(res, (int64_t)((uint64_t)a1 - (uint64_t)a2)); + return 1; + } + + return -1; +} + +static int luauF_integerneg(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + setlvalue(res, (int64_t)(~(uint64_t)lvalue(arg0) + 1)); + return 1; + } + + return -1; +} + +static int luauF_integerdiv(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + if ((b == 0) || ((a == LLONG_MIN) && (b == -1))) + return -1; + + setlvalue(res, a / b); + return 1; + } + + return -1; +} + +static int luauF_integerudiv(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + if (b == 0) + return -1; + + setlvalue(res, a / b); + return 1; + } + + return -1; +} + +static int luauF_integerband(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t r = (uint64_t)lvalue(arg0); + + for (int i = 2; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + r &= (uint64_t)lvalue(args + (i - 2)); + } + + setlvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_integerbor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t r = (uint64_t)lvalue(arg0); + + for (int i = 2; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + r |= (uint64_t)lvalue(args + (i - 2)); + } + + setlvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_integerbxor(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t r = (uint64_t)lvalue(arg0); + + for (int i = 2; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + r ^= (uint64_t)lvalue(args + (i - 2)); + } + + setlvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_integerbnot(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + setlvalue(res, ~(uint64_t)lvalue(arg0)); + return 1; + } + + return -1; +} + +static int luauF_integerbswap(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t a = (uint64_t)lvalue(arg0); + + setlvalue( + res, + (a >> 56) | ((a & 0xFF000000000000) >> 40) | ((a & 0xFF0000000000) >> 24) | ((a & 0xFF00000000) >> 8) | ((a & 0xFF000000) << 8) | + ((a & 0xFF0000) << 24) | ((a & 0xFF00) << 40) | ((a & 0xFF) << 56) + ); + + return 1; + } + + return -1; +} + +static int luauF_integerbtest(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t r = (uint64_t)lvalue(arg0); + + for (int i = 2; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + r &= (uint64_t)lvalue(args + (i - 2)); + } + + setbvalue(res, (r != 0)); + return 1; + } + + return -1; +} + +static int luauF_integerlt(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + setbvalue(res, a < b); + return 1; + } + + return -1; +} + +static int luauF_integerle(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + setbvalue(res, a <= b); + return 1; + } + + return -1; +} + +static int luauF_integergt(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + setbvalue(res, a > b); + return 1; + } + + return -1; +} + +static int luauF_integerge(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + setbvalue(res, a >= b); + return 1; + } + + return -1; +} + +static int luauF_integerult(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + setbvalue(res, a < b); + return 1; + } + + return -1; +} + +static int luauF_integerule(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + setbvalue(res, a <= b); + return 1; + } + + return -1; +} + +static int luauF_integerugt(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + setbvalue(res, a > b); + return 1; + } + + return -1; +} + +static int luauF_integeruge(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + setbvalue(res, a >= b); + return 1; + } + + return -1; +} + +static int luauF_integerurem(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t a = (uint64_t)lvalue(arg0); + uint64_t b = (uint64_t)lvalue(args); + + if (b == 0) + return -1; + + setlvalue(res, a % b); + return 1; + } + + return -1; +} + +static int luauF_integerrem(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a = lvalue(arg0); + int64_t b = lvalue(args); + + if (b == 0) + return -1; + + setlvalue(res, ((a == LLONG_MIN) && (b == -1)) ? 0 : (a % b)); + + return 1; + } + + return -1; +} + +static int luauF_integercountlz(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t n = (uint64_t)lvalue(arg0); + int result; +#ifdef _MSC_VER +#ifdef _WIN64 + unsigned long rl; + result = _BitScanReverse64(&rl, n) ? 63 - int(rl) : 64; +#else + unsigned long rl; + if (_BitScanReverse(&rl, uint32_t(n >> 32))) + result = 31 - int(rl); + else + result = _BitScanReverse(&rl, uint32_t(n)) ? 63 - int(rl) : 64; +#endif +#else + result = (n == 0) ? 64 : __builtin_clzll(n); +#endif + + setlvalue(res, result); + + return 1; + } + + return -1; +} + +static int luauF_integercountrz(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisinteger(arg0)) + { + uint64_t n = (uint64_t)lvalue(arg0); + int result; +#ifdef _MSC_VER +#ifdef _WIN64 + unsigned long rl; + result = _BitScanForward64(&rl, n) ? int(rl) : 64; +#else + unsigned long rl; + if (_BitScanForward(&rl, uint32_t(n))) + result = int(rl); + else + result = _BitScanForward(&rl, uint32_t(n >> 32)) ? int(rl) + 32 : 64; +#endif +#else + result = (n == 0) ? 64 : __builtin_ctzll(n); +#endif + + setlvalue(res, result); + + return 1; + } + + return -1; +} + +static int luauF_integerextract(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if ((nparams >= 3) && !ttisinteger(args + 1)) + return -1; + + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t n = lvalue(arg0); + int64_t f = lvalue(args); + int64_t w = (nparams >= 3) ? lvalue(args + 1) : 1; + + if ((f < 0) || (f > 63) || (w < 1) || (w > 64) || ((f + w) > 64)) + return -1; + + setlvalue(res, (((uint64_t)n) >> f) & ((0xFFFFFFFFFFFFFFFFULL) >> (64 - w))); + return 1; + } + + return -1; +} + +static int luauF_integerclamp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 3 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args) && ttisinteger(args + 1)) + { + int64_t a = lvalue(arg0); + int64_t rmin = lvalue(args); + int64_t rmax = lvalue(args + 1); + + if (rmin > rmax) + return -1; + + setlvalue(res, (a < rmin) ? rmin : ((a > rmax) ? rmax : a)); + return 1; + } + + return -1; +} + +static int luauF_integerlrotate(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t n = (uint64_t)lvalue(arg0); + unsigned s = (unsigned)((uint64_t)lvalue(args) % 64); + + setlvalue(res, s != 0 ? (n << s) | (n >> (64 - s)) : n); + + return 1; + } + + return -1; +} + +static int luauF_integerrrotate(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t n = (uint64_t)lvalue(arg0); + unsigned s = (unsigned)((uint64_t)lvalue(args) % 64); + + setlvalue(res, s != 0 ? (n >> s) | (n << (64 - s)) : n); + + return 1; + } + + return -1; +} + +static int luauF_integerlshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t n = (uint64_t)lvalue(arg0); + int64_t i = lvalue(args); + + setlvalue(res, ((i >= -63) && (i <= 63)) ? ((i < 0) ? (n >> (-i)) : (n << i)) : 0); + + return 1; + } + + return -1; +} + +static int luauF_integerarshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t n = lvalue(arg0); + int64_t i = lvalue(args); + + if ((i >= -63) && (i <= 63)) + { + setlvalue(res, (i < 0) ? (int64_t)((uint64_t)n << (-i)) : (n >> i)); + } + else if (i < -63) + { + setlvalue(res, 0); + } + else + { + setlvalue(res, (n < 0) ? -1 : 0); + } + + return 1; + } + + return -1; +} + +static int luauF_integerrshift(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + uint64_t n = (uint64_t)lvalue(arg0); + int64_t i = lvalue(args); + + setlvalue(res, ((i >= -63) && (i <= 63)) ? ((i < 0) ? (n << (-i)) : (n >> i)) : 0); + + return 1; + } + + return -1; +} + +static int luauF_integermin(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + + int64_t r = (a2 < a1) ? a2 : a1; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + int64_t a = lvalue(args + (i - 2)); + + r = (a < r) ? a : r; + } + + setlvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_integermax(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + + int64_t r = (a2 < a1) ? a1 : a2; + + for (int i = 3; i <= nparams; ++i) + { + if (!ttisinteger(args + (i - 2))) + return -1; + + int64_t a = lvalue(args + (i - 2)); + + r = (a > r) ? a : r; + } + + setlvalue(res, r); + return 1; + } + + return -1; +} + +static int luauF_integermul(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + setlvalue(res, (int64_t)((uint64_t)a1 * (uint64_t)a2)); + return 1; + } + + return -1; +} + +static int luauF_integermod(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + + if (a2 == 0) + return -1; + + if ((a1 == LLONG_MIN) && (a2 == -1)) + { + setlvalue(res, 0); + return 1; + } + + int64_t remainder = a1 % a2; + if (remainder && ((a1 < 0) != (a2 < 0))) + remainder += a2; + + setlvalue(res, remainder); + return 1; + } + + return -1; +} + +static int luauF_integeridiv(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 2 && nresults <= 1 && ttisinteger(arg0) && ttisinteger(args)) + { + int64_t a1 = lvalue(arg0); + int64_t a2 = lvalue(args); + if (a2 == 0) + return -1; + if ((a1 == LLONG_MIN) && (a2 == -1)) + return -1; + + int64_t result = a1 / a2; + // Floored division rounds toward -inf: adjust the truncated quotient down by 1 + // when the operands have opposite signs and the division is inexact. + if (((a1 ^ a2) < 0) && (a1 % a2)) + result -= 1; + setlvalue(res, result); + return 1; + } + + return -1; +} + +static int luauF_integercreate(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + + if (a1 >= -9223372036854775808.0 && a1 < 9223372036854775808.0) + { + int64_t x = (int64_t)a1; + if ((double)x == a1) + { + setlvalue(res, x); + return 1; + } + } + + setnilvalue(res); + return 1; + } + + return -1; +} + +static int luauF_bufferreadlong(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 2 && nresults <= 1 && ttisbuffer(arg0) && ttisnumber(args)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(int64_t))) + return -1; + + int64_t val; + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(int64_t)); + setlvalue(res, val); + return 1; + } +#endif + + return -1; +} + +static int luauF_bufferwritelong(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ +#if !defined(LUAU_BIG_ENDIAN) + if (nparams >= 3 && nresults <= 0 && ttisbuffer(arg0) && ttisnumber(args) && ttisinteger(args + 1)) + { + int offset; + luai_num2int(offset, nvalue(args)); + if (checkoutofbounds(offset, bufvalue(arg0)->len, sizeof(int64_t))) + return -1; + + int64_t val = lvalue(args + 1); + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(int64_t)); + return 0; + } +#endif + + return -1; +} + +static int luauF_missing(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + return -1; +} + +#ifdef LUAU_TARGET_SSE41 +template +LUAU_TARGET_SSE41 inline double roundsd_sse41(double v) +{ + __m128d av = _mm_set_sd(v); + __m128d rv = _mm_round_sd(av, av, Rounding | _MM_FROUND_NO_EXC); + return _mm_cvtsd_f64(rv); +} + +LUAU_TARGET_SSE41 static int luauF_floor_sse41(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, roundsd_sse41<_MM_FROUND_TO_NEG_INF>(a1)); + return 1; + } + + return -1; +} + +LUAU_TARGET_SSE41 static int luauF_ceil_sse41(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + setnvalue(res, roundsd_sse41<_MM_FROUND_TO_POS_INF>(a1)); + return 1; + } + + return -1; +} + +LUAU_TARGET_SSE41 static int luauF_round_sse41(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1 && ttisnumber(arg0)) + { + double a1 = nvalue(arg0); + // roundsd only supports bankers rounding natively, so we need to emulate rounding by using truncation + // offset is prevfloat(0.5), which is important so that we round prevfloat(0.5) to 0. + const double offset = 0.49999999999999994; + + __m128d va1 = _mm_set_sd(a1); + __m128d sign = _mm_and_pd(va1, _mm_set_sd(-0.0)); + __m128d off = _mm_or_pd(_mm_set_sd(offset), sign); + __m128d sum = _mm_add_sd(va1, off); + __m128d result = _mm_round_sd(sum, sum, _MM_FROUND_TO_ZERO | _MM_FROUND_NO_EXC); + setnvalue(res, _mm_cvtsd_f64(result)); + return 1; + } + + return -1; +} + +static bool luau_hassse41() +{ + int cpuinfo[4] = {}; +#ifdef _MSC_VER + __cpuid(cpuinfo, 1); +#else + __cpuid(1, cpuinfo[0], cpuinfo[1], cpuinfo[2], cpuinfo[3]); +#endif + + // We require SSE4.1 support for ROUNDSD + // https://en.wikipedia.org/wiki/CPUID#EAX=1:_Processor_Info_and_Feature_Bits + return (cpuinfo[2] & (1 << 19)) != 0; +} +#endif + +const luau_FastFunction luauF_table[256] = { + NULL, + luauF_assert, + + luauF_abs, + luauF_acos, + luauF_asin, + luauF_atan2, + luauF_atan, + +#ifdef LUAU_TARGET_SSE41 + luau_hassse41() ? luauF_ceil_sse41 : luauF_ceil, +#else + luauF_ceil, +#endif + + luauF_cosh, + luauF_cos, + luauF_deg, + luauF_exp, + +#ifdef LUAU_TARGET_SSE41 + luau_hassse41() ? luauF_floor_sse41 : luauF_floor, +#else + luauF_floor, +#endif + + luauF_fmod, + luauF_frexp, + luauF_ldexp, + luauF_log10, + luauF_log, + luauF_max, + luauF_min, + luauF_modf, + luauF_pow, + luauF_rad, + luauF_sinh, + luauF_sin, + luauF_sqrt, + luauF_tanh, + luauF_tan, + + luauF_arshift, + luauF_band, + luauF_bnot, + luauF_bor, + luauF_bxor, + luauF_btest, + luauF_extract, + luauF_lrotate, + luauF_lshift, + luauF_replace, + luauF_rrotate, + luauF_rshift, + + luauF_type, + + luauF_byte, + luauF_char, + luauF_len, + + luauF_typeof, + + luauF_sub, + + luauF_clamp, + luauF_sign, + +#ifdef LUAU_TARGET_SSE41 + luau_hassse41() ? luauF_round_sse41 : luauF_round, +#else + luauF_round, +#endif + + luauF_rawset, + luauF_rawget, + luauF_rawequal, + + luauF_tinsert, + luauF_tunpack, + + luauF_vector, + + luauF_countlz, + luauF_countrz, + + luauF_select, + + luauF_rawlen, + + luauF_extractk, + + luauF_getmetatable, + luauF_setmetatable, + + luauF_tonumber, + luauF_tostring, + + luauF_byteswap, + + luauF_readinteger, + luauF_readinteger, + luauF_writeinteger, + luauF_readinteger, + luauF_readinteger, + luauF_writeinteger, + luauF_readinteger, + luauF_readinteger, + luauF_writeinteger, + luauF_readfp, + luauF_writefp, + luauF_readfp, + luauF_writefp, + + luauF_vectormagnitude, + luauF_vectornormalize, + luauF_vectorcross, + luauF_vectordot, + luauF_vectorfloor, + luauF_vectorceil, + luauF_vectorabs, + luauF_vectorsign, + luauF_vectorclamp, + luauF_vectormin, + luauF_vectormax, + + luauF_lerp, + + luauF_vectorlerp, + + luauF_isnan, + luauF_isinf, + luauF_isfinite, + + luauF_integercreate, + luauF_integertonumber, + luauF_integerneg, + luauF_integeradd, + luauF_integersub, + luauF_integermul, + luauF_integerdiv, + luauF_integermin, + luauF_integermax, + luauF_integerrem, + luauF_integeridiv, + luauF_integerudiv, + luauF_integerurem, + luauF_integermod, + luauF_integerclamp, + luauF_integerband, + luauF_integerbor, + luauF_integerbnot, + luauF_integerbxor, + luauF_integerlt, + luauF_integerle, + luauF_integerult, + luauF_integerule, + luauF_integergt, + luauF_integerge, + luauF_integerugt, + luauF_integeruge, + luauF_integerlshift, + luauF_integerrshift, + luauF_integerarshift, + luauF_integerlrotate, + luauF_integerrrotate, + luauF_integerextract, + luauF_integerbtest, + luauF_integercountrz, + luauF_integercountlz, + luauF_integerbswap, + + luauF_bufferreadlong, + luauF_bufferwritelong, + +// When adding builtins, add them above this line; what follows is 64 "dummy" entries with luauF_missing fallback. +// This is important so that older versions of the runtime that don't support newer builtins automatically fall back via luauF_missing. +// Given the builtin addition velocity this should always provide a larger compatibility window than bytecode versions suggest. +#define MISSING8 luauF_missing, luauF_missing, luauF_missing, luauF_missing, luauF_missing, luauF_missing, luauF_missing, luauF_missing + + MISSING8, + MISSING8, + MISSING8, + MISSING8, + MISSING8, + MISSING8, + MISSING8, + MISSING8, + +#undef MISSING8 +}; + +static int luauPF_pcall(lua_State* L, StkId ra, int nparams) +{ + if (nparams < 1 || !ttisfunction(ra + 1) || L->global->builtinPcall == nullptr) + return -1; + + setclvalue(L, ra, L->global->builtinPcall); + + return 0; +} + +static int luauPF_xpcall(lua_State* L, StkId ra, int nparams) +{ + if (nparams < 2 || !ttisfunction(ra + 1) || !ttisfunction(ra + 2) || L->global->builtinXpcall == nullptr) + return -1; + + setclvalue(L, ra, L->global->builtinXpcall); + + // swap 'f' and 'errf' so that we get 'errf, f, arguments' prepared for calling 'f' + TValue tmp; + setobj(L, &tmp, ra + 1); + setobj2s(L, ra + 1, ra + 2); + setobj2s(L, ra + 2, &tmp); + + return 1; +} + +const luau_ProtectedFastFunction luauPF_table[2] = {luauPF_pcall, luauPF_xpcall}; diff --git a/External/luau/VM/src/lbuiltins.h b/External/luau/VM/src/lbuiltins.h new file mode 100644 index 00000000..ee588d97 --- /dev/null +++ b/External/luau/VM/src/lbuiltins.h @@ -0,0 +1,13 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +typedef int (*luau_FastFunction)(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams); + +extern const luau_FastFunction luauF_table[256]; + +typedef int (*luau_ProtectedFastFunction)(lua_State* L, StkId ra, int nparams); + +extern const luau_ProtectedFastFunction luauPF_table[2]; diff --git a/External/luau/VM/src/lbytecode.h b/External/luau/VM/src/lbytecode.h new file mode 100644 index 00000000..96e79bb9 --- /dev/null +++ b/External/luau/VM/src/lbytecode.h @@ -0,0 +1,8 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +// This is a forwarding header for Luau bytecode definition +#include "Luau/Bytecode.h" + +#include "Luau/BytecodeUtils.h" diff --git a/External/luau/VM/src/lclass.cpp b/External/luau/VM/src/lclass.cpp new file mode 100644 index 00000000..0d66bddf --- /dev/null +++ b/External/luau/VM/src/lclass.cpp @@ -0,0 +1,502 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details + +#include "lclass.h" + +#include "lapi.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lualib.h" +#include "lvm.h" + +LuauClass* luaR_newblankclass(lua_State* L, TString* name, bool isopen) +{ + LuauClass* classobject = luaM_newgco(L, LuauClass, sizeof(LuauClass), L->activememcat, LUA_TCLASS); + luaC_init(L, classobject, LUA_TCLASS); + classobject->name = name; + classobject->super = NULL; + classobject->staticmembers = NULL; + classobject->memberstooffset = NULL; + classobject->offsettomember = NULL; + classobject->metatable = NULL; + classobject->instancemetatable = NULL; + classobject->numberofinstancemembers = 0; + classobject->numberofallmembers = 0; + classobject->isopen = isopen; + classobject->hasuserinitinchain = false; + + return classobject; +} + +/* + * We rewrite both the `__call` metamethod and the `__init` method because in the inheritance scenario, a LuauClass is cloned from the original and + * flattened out. This flattened-out LuauClass's constructors need to have their closures updated. Otherwise they point at the old un-flattened + * LuauClass. + */ +static void luaR_setupconstructor(lua_State* L, LuauClass* classobject, LuaTable* env) +{ + classobject->metatable = luaH_new(L, 0, 1); + luaC_objbarrier(L, classobject, classobject->metatable); + + Closure* constructor = luaF_newCclosure(L, 0, env); + constructor->c.f = luaR_constructobject; + constructor->c.debugname = luaS_new(L, "luaR_constructobject"); + + LUAU_ASSERT(iswhite(obj2gco(constructor))); + + constructor->c.cont = NULL; + + TValue* callSlot = luaH_setstr(L, classobject->metatable, L->global->tmname[TM_CALL]); + LUAU_ASSERT(ttisnil(callSlot)); + setclvalue(L, callSlot, constructor); + luaC_barrier(L, classobject->metatable, callSlot); + + classobject->metatable->readonly = true; + + // Add the default constructor. + // + // If the code defines an explicit __init method, LOP_NEWCLASSMEMBER will + // overwrite this. + Closure* defaultCtor = luaF_newCclosure(L, 1, env); + defaultCtor->c.f = luaR_defaultcreateobject; + defaultCtor->c.debugname = luaS_new(L, "luaR_defaultcreateobject"); + + setclassvalue(L, &defaultCtor->c.upvals[0], classobject); + LUAU_ASSERT(iswhite(obj2gco(defaultCtor))); + + defaultCtor->c.cont = NULL; + + TString* initKey = luaS_new(L, "__init"); + const TValue* initIndex = luaH_getstr(classobject->memberstooffset, initKey); + const double initDouble = nvalue(initIndex); + LUAU_ASSERT(initDouble >= classobject->numberofinstancemembers && initDouble < classobject->numberofallmembers); + const uint32_t initOffset = uint32_t(initDouble) - classobject->numberofinstancemembers; + + setclvalue(L, &classobject->staticmembers[initOffset], defaultCtor); + luaC_barrier(L, classobject, &classobject->staticmembers[initOffset]); +} + +LuauClass* luaR_newclass( + lua_State* L, + TString* name, + LuaTable* memberstooffset, + TString** offsettomember, + uint32_t numberofinstancemembers, + uint32_t numberofstaticmembers, + LuaTable* envt +) +{ + LUAU_ASSERT(L->global->GCthreshold == SIZE_MAX && "GC must be paused"); + LuauClass* classobject = luaR_newblankclass(L, name, false); + + classobject->staticmembers = luaM_newarray(L, numberofstaticmembers, TValue, classobject->memcat); + // Initialize static members to nil, otherwise we may read uninitialized memory. + for (uint32_t i = 0; i < numberofstaticmembers; i++) + setnilvalue(&classobject->staticmembers[i]); + + classobject->memberstooffset = memberstooffset; + classobject->offsettomember = offsettomember; + + classobject->numberofinstancemembers = numberofinstancemembers; + classobject->numberofallmembers = numberofinstancemembers + numberofstaticmembers; + + luaR_setupconstructor(L, classobject, envt); + + return classobject; +} + +/** + * Allocates and returns a new class object with the same members as `classobject`. + * @param classobject The class object to clone. + */ +LUAI_FUNC LuauClass* luaR_cloneclass(lua_State* L, LuauClass* classobject) +{ + LuauClass* newclass = luaR_newblankclass(L, classobject->name, classobject->isopen); + + // newclass was just allocated, so it is white and none of the writes below need a write barrier. + LUAU_ASSERT(iswhite(obj2gco(newclass))); + + newclass->super = classobject->super; + newclass->hasuserinitinchain = classobject->hasuserinitinchain; + + const uint32_t numallmembers = classobject->numberofallmembers; + const uint32_t numstaticmembers = numallmembers - classobject->numberofinstancemembers; + + // The name->offset mapping is fixed when the class shape is built and is never mutated afterwards (shapes in a Proto's constant table are + // additionally marked readonly), so the clone shares it rather than paying for a table copy on every class definition that executes. + newclass->memberstooffset = luaH_clone(L, classobject->memberstooffset); + + newclass->offsettomember = luaM_newarray(L, numallmembers, TString*, newclass->memcat); + memcpy(newclass->offsettomember, classobject->offsettomember, numallmembers * sizeof(TString*)); + + newclass->numberofallmembers = numallmembers; + + newclass->staticmembers = luaM_newarray(L, numstaticmembers, TValue, newclass->memcat); + memcpy(newclass->staticmembers, classobject->staticmembers, numstaticmembers * sizeof(TValue)); + + newclass->numberofinstancemembers = classobject->numberofinstancemembers; + + if (classobject->instancemetatable) + newclass->instancemetatable = luaH_clone(L, classobject->instancemetatable); + + luaR_setupconstructor(L, newclass, getcurrenv(L)); + + return newclass; +} + +// Registers val as a static member of classObject with name memberName at static offset staticMemberOffset and overall offset offset. +void luaR_registerstaticmember( + lua_State* L, + LuauClass* classObject, + TString* memberName, + const TValue* val, + uint32_t offset, + uint32_t staticMemberOffset +) +{ + setobj2class(L, &classObject->staticmembers[staticMemberOffset], val); + luaC_barrier(L, classObject, &classObject->staticmembers[staticMemberOffset]); + + classObject->offsettomember[offset] = memberName; + + TValue* offsetVal = luaH_setstr(L, classObject->memberstooffset, memberName); + setnvalue(offsetVal, offset); + luaC_barrier(L, classObject->memberstooffset, offsetVal); +} + +/** + * Updates child with parent's instance members and non-overridden static members in the following steps: +- Check that parent is open. +- Check for illegal instance member overrides in child. +- Point child's super to parent. +- Set staticmembers and offsettomember to NULL so GC doesn't try to free them in a weird state. +- Bump member offsets in child->memberstooffset by parent->numberofinstancemembers since instances of child will have parent's instance members +come first in their memory layouts. +- Add entries for each of parent's instance members to child->memberstooffset. +- Count how many static members we need to copy over from parent to child (ie non-overridden ones). +- Resize childOffsettomember appropriately. The child class's instance and static members are moved up by parent->numberofinstancemembers, the +parent's instance members copied over at the beginning. +- Resize childStaticMembers. +- Copy each non-overridden static member from parent->staticmembers to child->staticmembers, add an entry for it to child->memberstooffset, and add +it to child->offsettomember. +- Copy parent's instance metatable if it exists. We don't need to worry about overwriting the child's instance metatable because it's only created +by NEWCLASSMEMBER instructions, which are only ever emitted after NEWCLASS. (TODO: This isn't true if we inherit lazily) + */ +void luaR_inheritclass(lua_State* L, LuauClass* child, LuauClass* parent) +{ + // First check if parent is open + if (!parent->isopen) + luaG_runerror(L, "Non-open class '%s' cannot be extended", getstr(parent->name)); + + // Next, check for illegal instance member overrides + if (parent->numberofinstancemembers > 0) + { + for (uint32_t idx = 0; idx < parent->numberofinstancemembers; idx++) + { + TString* memberName = parent->offsettomember[idx]; + const TValue* existing = luaH_getstr(child->memberstooffset, memberName); + if (!ttisnil(existing)) + { + luaG_runerror( + L, + "Cannot override instance member '%s' of parent class '%s' in child class '%s'", + getstr(memberName), + getstr(parent->name), + getstr(child->name) + ); + } + } + } + + child->super = parent; + luaC_objbarrier(L, child, parent); + + // TODO: might need updating for lazy inheritance + child->hasuserinitinchain = parent->hasuserinitinchain; + + uint32_t childDeclaredStaticMembers = child->numberofallmembers - child->numberofinstancemembers; + TValue* childStaticMembers = child->staticmembers; + TString** childOffsetToMember = child->offsettomember; + + child->staticmembers = NULL; + child->offsettomember = NULL; + + if (parent->numberofinstancemembers > 0) + { + // Bump every member offset in child->memberstooffset up by parent->numberofinstancemembers (even static members are shifted up), and then add + // the parent's instance members to child->memberstooffset. + for (uint32_t idx = 0; idx < child->numberofallmembers; idx++) + { + TString* memberName = childOffsetToMember[idx]; + TValue* offsetInChild = luaH_setstr(L, child->memberstooffset, memberName); + setnvalue(offsetInChild, nvalue(offsetInChild) + parent->numberofinstancemembers); + } + + for (uint32_t idx = 0; idx < parent->numberofinstancemembers; idx++) + { + TString* memberName = parent->offsettomember[idx]; + TValue* offsetInChild = luaH_setstr(L, child->memberstooffset, memberName); + setnvalue(offsetInChild, idx); + luaC_barrier(L, child->memberstooffset, offsetInChild); + } + } + + // Count how many static members we'll actually need to copy from parent, ie non-overridden ones + uint32_t numStaticMembersToCopy = 0; + + // We start at numberofinstancemembers so we only look at static members + for (uint32_t idx = parent->numberofinstancemembers; idx < parent->numberofallmembers; idx++) + { + TString* memberName = parent->offsettomember[idx]; + const TValue* existing = luaH_getstr(child->memberstooffset, memberName); + if (ttisnil(existing)) + numStaticMembersToCopy++; + // TODO: Throw an error if we overwrite a static member with an instance member? + } + + uint32_t originalChildAllMembers = child->numberofallmembers; + uint32_t newNumberOfAllMembers = originalChildAllMembers + parent->numberofinstancemembers + numStaticMembersToCopy; + + // Resize childOffsetToMember appropriately + if (newNumberOfAllMembers > originalChildAllMembers) + { + if (originalChildAllMembers == 0) + childOffsetToMember = luaM_newarray(L, newNumberOfAllMembers, TString*, child->memcat); + else + luaM_reallocarray(L, childOffsetToMember, originalChildAllMembers, newNumberOfAllMembers, TString*, child->memcat); + } + + // Make room for parent instance members + memmove(childOffsetToMember + parent->numberofinstancemembers, childOffsetToMember, sizeof(TString*) * originalChildAllMembers); + + // Copy parent instance members to the beginning of childOffsetToMember + memcpy(childOffsetToMember, parent->offsettomember, sizeof(TString*) * parent->numberofinstancemembers); + + child->offsettomember = childOffsetToMember; + child->numberofallmembers = newNumberOfAllMembers; + + // Resize child->staticmembers appropriately + if (numStaticMembersToCopy > 0) + { + if (childDeclaredStaticMembers == 0) + childStaticMembers = luaM_newarray(L, numStaticMembersToCopy, TValue, child->memcat); + else + { + luaM_reallocarray( + L, childStaticMembers, childDeclaredStaticMembers, childDeclaredStaticMembers + numStaticMembersToCopy, TValue, child->memcat + ); + } + } + + child->staticmembers = childStaticMembers; + child->numberofinstancemembers += parent->numberofinstancemembers; + + // Copy static members from parent that aren't overridden in child. + uint32_t numStaticMembersCopied = 0; + for (uint32_t idx = parent->numberofinstancemembers; idx < parent->numberofallmembers; idx++) + { + TString* memberName = parent->offsettomember[idx]; + // This lookup duplicates the one we did earlier, when we counted how many static members we needed to copy. We could optimize by caching the + // indices with static members to copy. + const TValue* existing = luaH_getstr(child->memberstooffset, memberName); + if (ttisnil(existing)) + { + // This static member isn't declared in the child, so we need to copy it over from the parent + uint32_t staticMemberOffsetInChild = childDeclaredStaticMembers + numStaticMembersCopied; + + const TValue* parentVal = &parent->staticmembers[idx - parent->numberofinstancemembers]; + + setobj2class(L, &child->staticmembers[staticMemberOffsetInChild], parentVal); + luaC_barrier(L, child, &child->staticmembers[staticMemberOffsetInChild]); + + // We also need to add an entry to the memberstooffset table for this member + uint32_t offsetInChildInt = child->numberofinstancemembers + staticMemberOffsetInChild; + TValue* offsetInChild = luaH_setstr(L, child->memberstooffset, memberName); + setnvalue(offsetInChild, static_cast(offsetInChildInt)); + luaC_barrier(L, child->memberstooffset, offsetInChild); + + // And add it to offsettomember + child->offsettomember[offsetInChildInt] = memberName; + + numStaticMembersCopied++; + } + } + + LUAU_ASSERT(numStaticMembersToCopy == numStaticMembersCopied); + + // Copy instance metatable + if (parent->instancemetatable) + { + LUAU_ASSERT(!child->instancemetatable); + child->instancemetatable = luaH_clone(L, parent->instancemetatable); + luaC_objbarrier(L, child, child->instancemetatable); + } +} + +void luaR_addclassmember(lua_State* L, LuauClass* classobject, TString* name, TValue* value) +{ + LUAU_ASSERT(classobject->staticmembers != nullptr); + const TValue* offset = luaH_getstr(classobject->memberstooffset, name); + const uint32_t offsetint = uint32_t(nvalue(offset)); + LUAU_ASSERT(offsetint >= classobject->numberofinstancemembers && offsetint < classobject->numberofallmembers); + LUAU_ASSERT(ttisfunction(value) && value->value.gc->gch.tt == LUA_TFUNCTION); + setobj2class(L, &classobject->staticmembers[offsetint - classobject->numberofinstancemembers], value); + luaC_barrier(L, classobject, value); + + classobject->hasuserinitinchain |= (name == luaS_newlstr(L, "__init", 6)); + + // Only metamethods in the parser's allowlist are supported (see ALLOWED_METAMETHODS in Parser.cpp) + bool isMetamethod = (name == luaS_newlstr(L, "__tostring", 10)); + for (int i = 0; i < TM_N && !isMetamethod; i++) + isMetamethod = (name == L->global->tmname[i]); + + if (isMetamethod) + { + if (!classobject->instancemetatable) + { + classobject->instancemetatable = luaH_new(L, 0, 1); + luaC_objbarrier(L, classobject, classobject->instancemetatable); + } + else + { + // Check if we're overriding a comparison metamethod + global_State* g = L->global; + bool isComparisonMetamethod = + (name == g->tmname[TM_EQ]) || (name == g->tmname[TM_LT]) || (name == g->tmname[TM_LE]); + + if (isComparisonMetamethod) + { + const TValue* existing = luaH_getstr(classobject->instancemetatable, name); + if (!ttisnil(existing)) + luaG_runerror( + L, "Overriding comparison metamethods is not allowed ('%s' in class '%s')", getstr(name), getstr(classobject->name) + ); + } + } + TValue* dest = luaH_setstr(L, classobject->instancemetatable, name); + setobj2t(L, dest, value); + luaC_barrier(L, classobject->instancemetatable, value); + } +} + +int luaR_constructobject(lua_State* L) +{ + // This runs as the class's `__call` metamethod, so luaV_tryfuncTM has inserted the class being called ahead of the call arguments. + LuauClass* classobject = classvalue(L->base); + + LuauObject* self = luaM_newgco(L, LuauObject, sizeof(LuauObject), L->activememcat, LUA_TOBJECT); + memset(self, 0, sizeof(LuauObject)); + luaC_init(L, self, LUA_TOBJECT); + self->lclass = classobject; + self->members = luaM_newarray(L, classobject->numberofinstancemembers, TValue, L->activememcat); + self->numberofmembers = classobject->numberofinstancemembers; + + for (uint32_t idx = 0; idx < classobject->numberofinstancemembers; idx++) + setnilvalue(&self->members[idx]); + + TString* initKey = luaS_new(L, "__init"); + const TValue* initIndex = luaH_getstr(classobject->memberstooffset, initKey); + const uint32_t initOffset = uint32_t(nvalue(initIndex)) - classobject->numberofinstancemembers; + + const TValue* initFunction = &classobject->staticmembers[initOffset]; + + // Discount the class object + int numargs = int(L->top - L->base) - 1; + + // Put self onto the stack to ensure that it unconditionally survives GC during execution of __init. + // The reference via the `self` argument to __init is insufficient to guarantee survival because `__init` may do `self = nil` and trigger GC. + setobjectvalue(L, L->top, self); + L->top++; + + luaD_checkstack(L, 2 + numargs); + + StkId argsBase = L->top; + // __init itself. + setobj2s(L, L->top++, initFunction); + + // self + setobjectvalue(L, L->top++, self); + + // Forward arguments + // Skip the class object placed on the stack by luaV_tryfuncTM + for (int i = 1; i < numargs + 1; i++) + setobj2s(L, L->top++, L->base + i); + + luaD_call(L, argsBase, 0); + + // self is still at L->top - 1 + return 1; +} + +int luaR_defaultcreateobject(lua_State* L) +{ + Closure* cl = clvalue(L->ci->func); + LuauClass* classobject = classvalue(&cl->c.upvals[0]); + + if (classobject->hasuserinitinchain) + luaL_error(L, "Class %s must define a constructor because it is derived from a class that defines one", getstr(classobject->name)); + + int numargs = lua_gettop(L); + if (numargs != 2) + luaL_error(L, "The constructor of %s must be called with 2 arguments. Got %d", getstr(classobject->name), numargs); + + // L->base + 0 = self + // L->base + 1 = props (if numargs == 2) + + if (!ttisobject(L->base)) + luaL_error(L, "%s.__init must be called with an instance of the class as its first argument", getstr(classobject->name)); + + LuauObject* classinst = objectvalue(L->base); + LUAU_ASSERT(classinst); + + if (classinst->lclass != classobject) + luaL_errorL(L, "Cannot call %s.__init on an instance of class %s", getstr(classobject->name), getstr(classinst->lclass->name)); + + constexpr int propSlot = 1; + + // L->top - 1 = Temp storage for the table lookup result. + setnilvalue(L->top); + L->top++; + + // Use the second argument to initialize all class members. + for (uint32_t idx = 0; idx < classobject->numberofinstancemembers; idx++) + { + TValue key; + setsvalue(L, &key, classobject->offsettomember[idx]); + luaV_gettable(L, L->base + propSlot, &key, L->top - 1); + setobj(L, &classinst->members[idx], L->top - 1); + luaC_barrier(L, classinst, &classinst->members[idx]); + } + + L->top--; + + return 0; +} + + +void luaR_freeclass(lua_State* L, LuauClass* classobject, lua_Page* page) +{ + if (classobject->staticmembers) + { + luaM_freearray( + L, classobject->staticmembers, classobject->numberofallmembers - classobject->numberofinstancemembers, TValue, classobject->memcat + ); + } + + if (classobject->offsettomember) + luaM_freearray(L, classobject->offsettomember, classobject->numberofallmembers, TString*, classobject->memcat); + + luaM_freegco(L, classobject, sizeof(LuauClass), classobject->memcat, page); +} + +void luaR_freeobject(lua_State* L, LuauObject* classinstance, lua_Page* page) +{ + luaM_freearray(L, classinstance->members, classinstance->numberofmembers, TValue, classinstance->memcat); + luaM_freegco(L, classinstance, sizeof(LuauObject), classinstance->memcat, page); +} diff --git a/External/luau/VM/src/lclass.h b/External/luau/VM/src/lclass.h new file mode 100644 index 00000000..9c090556 --- /dev/null +++ b/External/luau/VM/src/lclass.h @@ -0,0 +1,70 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lmem.h" +#include "lobject.h" + +/** + * Allocate and return a new class object. + * @param name The name of this class. This does not have to be unique within a program. + * @param memberstooffset A table mapping member names to their offset within the class + * @param offsettomember An array of length `numberofinstancemembers + numberofstaticmembers` where + * each entry is the name of the member at the specified offset. + * @param numberofinstancemembers The number of instance members (fields) this class has. + * @param numberofstaticmembers The number of static members (only methods today) this class has. + */ +LUAI_FUNC LuauClass* luaR_newclass( + lua_State* L, + TString* name, + LuaTable* memberstooffset, + TString** offsettomember, + uint32_t numberofinstancemembers, + uint32_t numberofstaticmembers, + LuaTable* envt +); + +/** + * Allocates and returns a new class object with the same members as `classobject`. + * @param classobject The class object to clone. + */ +LUAI_FUNC LuauClass* luaR_cloneclass(lua_State* L, LuauClass* classobject); + +/** + * Returns a new LuauClass object containing `child`'s members extended with `parent`'s. + */ +LUAI_FUNC void luaR_inheritclass(lua_State* L, LuauClass* child, LuauClass* parent); + +/** + * Add a new class member to `classobject` named `name` and with value `value`. + */ +LUAI_FUNC void luaR_addclassmember(lua_State* L, LuauClass* classobject, TString* name, TValue* value); + +LUAI_FUNC void luaR_freeclass(lua_State* L, LuauClass* classobject, lua_Page* page); + +/** + * The default constructor for class instances. This is written as a Lua API + * function and expects the stack to have a single optional indexable value. + * + * This function will allocate a new class instance, iterate over the instance + * members of the class object, initialize each class instance member with the + * result of indexing into the parameter, and then assign the value to the top + * of the stack. Must be called with two arguments: the class instance to + * populate and the table to populate it from. + */ +LUAI_FUNC int luaR_defaultcreateobject(lua_State* L); + +/** + * Construct an object, pass any stack arguments on to its __init() method, and + * assign the new object to the top of the stack. + */ +LUAI_FUNC int luaR_constructobject(lua_State* L); + +LUAI_FUNC void luaR_freeobject(lua_State* L, LuauObject* classinstance, lua_Page* page); + +#define luaR_checkoffsetinbounds(inst, offset) (offset < (inst)->lclass->numberofallmembers) + +#define luaR_lookupmemberatoffset(inst, offset) \ + (LUAU_ASSERT(luaR_checkoffsetinbounds(inst, offset)), \ + offset < (inst)->lclass->numberofinstancemembers ? &(inst)->members[offset] \ + : &(inst)->lclass->staticmembers[offset - inst->lclass->numberofinstancemembers]) diff --git a/External/luau/VM/src/lclasslib.cpp b/External/luau/VM/src/lclasslib.cpp new file mode 100644 index 00000000..80a9b43d --- /dev/null +++ b/External/luau/VM/src/lclasslib.cpp @@ -0,0 +1,67 @@ +#include "lapi.h" +#include "lobject.h" +#include "lua.h" +#include "lualib.h" +#include "lstate.h" + +/** + * class.isinstance(obj: any, class: class): boolean + */ +static int class_isinstance(lua_State* L) +{ + luaL_checkany(L, 1); + luaL_checktype(L, 2, LUA_TCLASS); + + const TValue* obj = luaA_toobject(L, 1); + + if (!ttisobject(obj)) + { + lua_pushboolean(L, false); + return 1; + } + + const TValue* cls = luaA_toobject(L, 2); + + const LuauClass* lclass = classvalue(cls); + + for (LuauClass* objClass = objectvalue(obj)->lclass; objClass != NULL; objClass = objClass->super) + { + if (objClass == lclass) + { + lua_pushboolean(L, true); + return 1; + } + } + + lua_pushboolean(L, false); + return 1; +} + +static int class_classof(lua_State* L) +{ + luaL_checkany(L, 1); + if (!lua_isobject(L, 1)) + { + lua_pushnil(L); + return 1; + } + const TValue* inst = luaA_toobject(L, 1); + const LuauObject* ci = objectvalue(inst); + luaA_pushclass(L, ci->lclass); + return 1; +} + +static const luaL_Reg classlib[] = { + {"isinstance", class_isinstance}, + {"classof", class_classof}, + {nullptr, nullptr}, +}; + +/* +** Open class library +*/ +int luaopen_class(lua_State* L) +{ + luaL_register(L, LUA_CLASSLIBNAME, classlib); + return 1; +} \ No newline at end of file diff --git a/External/luau/VM/src/lcommon.h b/External/luau/VM/src/lcommon.h new file mode 100644 index 00000000..e7f923d1 --- /dev/null +++ b/External/luau/VM/src/lcommon.h @@ -0,0 +1,46 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include +#include + +#include "luaconf.h" + +#include "Luau/Common.h" + +// internal assertions for in-house debugging +#define check_exp(c, e) (LUAU_ASSERT(c), (e)) +#define api_check(l, e) LUAU_ASSERT(e) + +#ifndef cast_to +#define cast_to(t, exp) ((t)(exp)) +#endif + +#define cast_byte(i) cast_to(uint8_t, (i)) +#define cast_num(i) cast_to(double, (i)) +#define cast_int(i) cast_to(int, (i)) + +/* +** type for virtual-machine instructions +** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h) +*/ +typedef uint32_t Instruction; + +/* +** macro to control inclusion of some hard tests on stack reallocation +*/ +#if defined(HARDSTACKTESTS) && HARDSTACKTESTS +#define condhardstacktests(x) (x) +#else +#define condhardstacktests(x) ((void)0) +#endif + +/* +** macro to control inclusion of some hard tests on garbage collection +*/ +#if defined(HARDMEMTESTS) && HARDMEMTESTS +#define condhardmemtests(x, l) (HARDMEMTESTS >= l ? (x) : (void)0) +#else +#define condhardmemtests(x, l) ((void)0) +#endif diff --git a/External/luau/VM/src/lcorolib.cpp b/External/luau/VM/src/lcorolib.cpp new file mode 100644 index 00000000..49b1d147 --- /dev/null +++ b/External/luau/VM/src/lcorolib.cpp @@ -0,0 +1,509 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lstate.h" +#include "lvm.h" + +LUAU_FASTFLAG(DebugLuauCoroutineFinally) + +// TODO: Remove with FFlagDebugLuauCoroutineFinally +#define CO_STATUS_ERROR -1 +#define CO_STATUS_BREAK -2 + +static const char* const statnames[] = {"running", "suspended", "normal", "dead", "dead"}; // dead appears twice for LUA_COERR and LUA_COFIN + +static int costatus(lua_State* L) +{ + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + lua_pushstring(L, statnames[lua_costatus(L, co)]); + return 1; +} + +static int interruptThread(lua_State* L, lua_State* co) +{ + // notify the debugger that the thread was suspended + if (L->global->cb.debuginterrupt) + luau_callhook(L, L->global->cb.debuginterrupt, co); + + return lua_break(L); +} + +template +static int auxresumefinisherror(lua_State* L, lua_State* co) +{ + if (Wrap) + { + if (lua_isstring(L, -1)) + { // error object is a string? + luaL_where(L, 1); // add extra info + lua_insert(L, -2); + lua_concat(L, 2); + } + lua_error(L); // propagate error + } + else + { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; // return false + error message + } +} + +template +static LUAU_FORCEINLINE int auxresumefinishsuccess(lua_State* L, lua_State* co) +{ + int nres = cast_int(co->top - co->base); + + if (nres + 1 > LUA_MINSTACK && !lua_checkstack(L, nres + 1)) + luaL_error(L, "too many results to resume"); + + if (!Wrap) + lua_pushboolean(L, 1); + + if (nres != 0) + { + // move yielded values + luaC_threadbarrier(L); + + StkId totop = L->top; + StkId fromtop = co->top - nres; + + for (int i = 0; i < nres; i++) + setobj2s(L, totop + i, fromtop + i); + + co->top = fromtop; + L->top = totop + nres; + } + + return nres + (Wrap ? 0 : 1); // non-wrap version returns additional status +} + +template +static LUAU_FORCEINLINE int auxresumefinish(lua_State* L, lua_State* co, int status) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + if (status == LUA_YIELD) + return auxresumefinishsuccess(L, co); + + // we might have reached another breakpoint in the target coroutine + if (LUAU_UNLIKELY(status == LUA_BREAK)) + return interruptThread(L, co); + + // complete finalizers, but only if it wasn't a failure of the resume itself + if (status == co->status && co->finalizers) + { + if (Wrap) + luaL_error(L, "cannot run finalizers from a wrapped coroutine"); + + luaD_preparefinalize(L, co); + return luaD_runfinalizers(L, /* toclose */ false, /* returnstatus */ true); + } + + if (status == LUA_OK) + return auxresumefinishsuccess(L, co); + + lua_xmove(co, L, 1); + return auxresumefinisherror(L, co); +} + +template +static int auxresume(lua_State* L, lua_State* co, int narg) +{ + // error handling for edge cases + if (co->status != LUA_YIELD) + { + int status = lua_costatus(L, co); + if (status != LUA_COSUS) + { + lua_pushfstring(L, "cannot resume %s coroutine", statnames[status]); + + if (FFlag::DebugLuauCoroutineFinally) + return auxresumefinisherror(L, co); + else + return CO_STATUS_ERROR; + } + } + + if (narg) + { + if (!lua_checkstack(co, narg)) + luaL_error(L, "too many arguments to resume"); + lua_xmove(L, co, narg); + } + else + { + // coroutine might be completely full already + if ((co->top - co->base) > LUAI_MAXCSTACK) + luaL_error(L, "too many arguments to resume"); + } + + co->singlestep = L->singlestep; + + int status = lua_resume(co, L, narg); + + if (FFlag::DebugLuauCoroutineFinally) + { + return auxresumefinish(L, co, status); + } + else + { + if (status == 0 || status == LUA_YIELD) + { + int nres = cast_int(co->top - co->base); + if (nres) + { + // +1 accounts for true/false status in resumefinish + if (nres + 1 > LUA_MINSTACK && !lua_checkstack(L, nres + 1)) + luaL_error(L, "too many results to resume"); + lua_xmove(co, L, nres); // move yielded values + } + return nres; + } + else if (status == LUA_BREAK) + { + return CO_STATUS_BREAK; + } + else + { + lua_xmove(co, L, 1); // move error message + return CO_STATUS_ERROR; + } + } +} + +static int auxresumecont(lua_State* L, lua_State* co) +{ + LUAU_ASSERT(!FFlag::DebugLuauCoroutineFinally); + + if (co->status == 0 || co->status == LUA_YIELD) + { + int nres = cast_int(co->top - co->base); + if (!lua_checkstack(L, nres + 1)) + luaL_error(L, "too many results to resume"); + lua_xmove(co, L, nres); // move yielded values + return nres; + } + else + { + lua_rawcheckstack(L, 2); + lua_xmove(co, L, 1); // move error message + return CO_STATUS_ERROR; + } +} + +static int coresumefinish(lua_State* L, int r) +{ + LUAU_ASSERT(!FFlag::DebugLuauCoroutineFinally); + + if (r < 0) + { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; // return false + error message + } + else + { + lua_pushboolean(L, 1); + lua_insert(L, -(r + 1)); + return r + 1; // return true + `resume' returns + } +} + +static int coresumey(lua_State* L) +{ + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + int narg = cast_int(L->top - L->base) - 1; + + if (FFlag::DebugLuauCoroutineFinally) + { + return auxresume(L, co, narg); + } + else + { + int r = auxresume(L, co, narg); + + if (r == CO_STATUS_BREAK) + return interruptThread(L, co); + + return coresumefinish(L, r); + } +} + +static int coresumecont(lua_State* L, int status) +{ + if (FFlag::DebugLuauCoroutineFinally) + { + // finalizer errored, return false status and the error + if (status != LUA_OK) + { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; + } + + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + + // if we haven't completed auxresume yet + if (lua_gettop(L) == 1) + return auxresumefinish(L, co, co->status); + + return luaD_runfinalizers(L, /* toclose */ false, /* returnstatus */ true); + } + else + { + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + + // if coroutine still hasn't yielded after the break, break current thread again + if (co->status == LUA_BREAK) + return interruptThread(L, co); + + int r = auxresumecont(L, co); + + return coresumefinish(L, r); + } +} + +static int auxwrapfinish(lua_State* L, int r) +{ + LUAU_ASSERT(!FFlag::DebugLuauCoroutineFinally); + + if (r < 0) + { + if (lua_isstring(L, -1)) + { // error object is a string? + luaL_where(L, 1); // add extra info + lua_insert(L, -2); + lua_concat(L, 2); + } + lua_error(L); // propagate error + } + return r; +} + +static int auxwrapy(lua_State* L) +{ + lua_State* co = lua_tothread(L, lua_upvalueindex(1)); + int narg = cast_int(L->top - L->base); + + if (FFlag::DebugLuauCoroutineFinally) + { + return auxresume(L, co, narg); + } + else + { + int r = auxresume(L, co, narg); + + if (r == CO_STATUS_BREAK) + return interruptThread(L, co); + + return auxwrapfinish(L, r); + } +} + +static int auxwrapcont(lua_State* L, int status) +{ + lua_State* co = lua_tothread(L, lua_upvalueindex(1)); + + if (FFlag::DebugLuauCoroutineFinally) + { + return auxresumefinish(L, co, co->status); + } + else + { + // we might have reached another breakpoint in the target coroutine + if (co->status == LUA_BREAK) + return interruptThread(L, co); + + int r = auxresumecont(L, co); + + return auxwrapfinish(L, r); + } +} + +static int cocreate(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TFUNCTION); + lua_State* NL = lua_newthread(L); + lua_xpush(L, NL, 1); // push function on top of NL + return 1; +} + +static int cowrap(lua_State* L) +{ + cocreate(L); + + lua_pushcclosurek(L, auxwrapy, NULL, 1, auxwrapcont); + return 1; +} + +static int coyield(lua_State* L) +{ + int nres = cast_int(L->top - L->base); + return lua_yield(L, nres); +} + +static int corunning(lua_State* L) +{ + if (lua_pushthread(L)) + lua_pushnil(L); // main thread is not a coroutine + return 1; +} + +static int coyieldable(lua_State* L) +{ + lua_pushboolean(L, lua_isyieldable(L)); + return 1; +} + +static int coclose(lua_State* L) +{ + LUAU_ASSERT(!FFlag::DebugLuauCoroutineFinally); + + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + + int status = lua_costatus(L, co); + if (status != LUA_COFIN && status != LUA_COERR && status != LUA_COSUS) + luaL_error(L, "cannot close %s coroutine", statnames[status]); + + if (co->status == LUA_OK || co->status == LUA_YIELD) + { + lua_pushboolean(L, true); + lua_resetthread(co); + return 1; + } + else + { + lua_pushboolean(L, false); + + if (co->status == LUA_ERRMEM) + lua_pushstring(L, LUA_MEMERRMSG); + else if (co->status == LUA_ERRERR) + lua_pushstring(L, LUA_ERRERRMSG); + else if (lua_gettop(co)) + lua_xmove(co, L, 1); // move error message + + lua_resetthread(co); + return 2; + } +} + +static int coclosey(lua_State* L) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + + int status = lua_costatus(L, co); + if (status != LUA_COFIN && status != LUA_COERR && status != LUA_COSUS) + luaL_error(L, "cannot close %s coroutine", statnames[status]); + + if (co->status == LUA_OK || co->status == LUA_YIELD) + { + if (co->finalizers) + { + luaD_preparefinalizestate(L, co, true); + lua_resetthread(co); // can be reused immediately + return luaD_runfinalizers(L, /* toclose */ true, /* returnstatus */ true); + } + + lua_pushboolean(L, true); + lua_resetthread(co); + return 1; + } + else + { + LUAU_ASSERT(!co->finalizers); // finalizers should have already been executed + + lua_pushboolean(L, false); + + if (co->status == LUA_ERRMEM) + lua_pushstring(L, LUA_MEMERRMSG); + else if (co->status == LUA_ERRERR) + lua_pushstring(L, LUA_ERRERRMSG); + else if (lua_gettop(co)) + lua_xmove(co, L, 1); // move error message + + lua_resetthread(co); + return 2; + } +} + +static int coclosecont(lua_State* L, int status) +{ + LUAU_ASSERT(FFlag::DebugLuauCoroutineFinally); + + if (status != LUA_OK) + { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; + } + + return luaD_runfinalizers(L, /* toclose */ true, /* returnstatus */ true); +} + +// coroutine.finally(co, callback) +static int cofinally(lua_State* L) +{ + lua_State* co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + + // callback can be any callable object + if (lua_isnoneornil(L, 2)) + luaL_error(L, "missing argument #%d", 2); + + lua_addfinalizer(L, co, 2); + return 0; +} + +static const luaL_Reg co_funcs_DEPRECATED[] = { + {"create", cocreate}, + {"running", corunning}, + {"status", costatus}, + {"wrap", cowrap}, + {"yield", coyield}, + {"isyieldable", coyieldable}, + {"close", coclose}, + {NULL, NULL}, +}; + +static const luaL_Reg co_funcs[] = { + {"create", cocreate}, + {"running", corunning}, + {"status", costatus}, + {"wrap", cowrap}, + {"yield", coyield}, + {"isyieldable", coyieldable}, + {"finally", cofinally}, + {NULL, NULL}, +}; + +int luaopen_coroutine(lua_State* L) +{ + if (FFlag::DebugLuauCoroutineFinally) + luaL_register(L, LUA_COLIBNAME, co_funcs); + else + luaL_register(L, LUA_COLIBNAME, co_funcs_DEPRECATED); + + lua_pushcclosurek(L, coresumey, "resume", 0, coresumecont); + lua_setfield(L, -2, "resume"); + + if (FFlag::DebugLuauCoroutineFinally) + { + lua_pushcclosurek(L, coclosey, "close", 0, coclosecont); + lua_setfield(L, -2, "close"); + } + + return 1; +} diff --git a/External/luau/VM/src/ldblib.cpp b/External/luau/VM/src/ldblib.cpp new file mode 100644 index 00000000..6dfc6914 --- /dev/null +++ b/External/luau/VM/src/ldblib.cpp @@ -0,0 +1,144 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lvm.h" + +#include +#include +#include + +static lua_State* getthread(lua_State* L, int* arg) +{ + if (lua_isthread(L, 1)) + { + *arg = 1; + return lua_tothread(L, 1); + } + else + { + *arg = 0; + return L; + } +} + +static int db_info(lua_State* L) +{ + int arg; + lua_State* L1 = getthread(L, &arg); + int l1top = 0; + + // if L1 != L, L1 can be in any state, and therefore there are no guarantees about its stack space + if (L != L1) + { + // for 'f' option, we reserve one slot and we also record the stack top + lua_rawcheckstack(L1, 1); + + l1top = lua_gettop(L1); + } + + int level; + if (lua_isnumber(L, arg + 1)) + { + level = (int)lua_tointeger(L, arg + 1); + luaL_argcheck(L, level >= 0, arg + 1, "level can't be negative"); + } + else if (arg == 0 && lua_isfunction(L, 1)) + { + // convert absolute index to relative index + level = -lua_gettop(L); + } + else + luaL_argerror(L, arg + 1, "function or level expected"); + + const char* options = luaL_checkstring(L, arg + 2); + + lua_Debug ar; + if (!lua_getinfo(L1, level, options, &ar)) + return 0; + + int results = 0; + bool occurs[26] = {}; + + for (const char* it = options; *it; ++it) + { + if (unsigned(*it - 'a') < 26) + { + if (occurs[*it - 'a']) + { + // restore stack state of another thread as 'f' option might not have been visited yet + if (L != L1) + lua_settop(L1, l1top); + + luaL_argerror(L, arg + 2, "duplicate option"); + } + occurs[*it - 'a'] = true; + } + + switch (*it) + { + case 's': + lua_pushstring(L, ar.short_src); + results++; + break; + + case 'l': + lua_pushinteger(L, ar.currentline); + results++; + break; + + case 'n': + lua_pushstring(L, ar.name ? ar.name : ""); + results++; + break; + + case 'f': + if (L1 == L) + lua_pushvalue(L, -1 - results); // function is right before results + else + lua_xmove(L1, L, 1); // function is at top of L1 + results++; + break; + + case 'a': + lua_pushinteger(L, ar.nparams); + lua_pushboolean(L, ar.isvararg); + results += 2; + break; + + default: + // restore stack state of another thread as 'f' option might not have been visited yet + if (L != L1) + lua_settop(L1, l1top); + + luaL_argerror(L, arg + 2, "invalid option"); + } + } + + return results; +} + +static int db_traceback(lua_State* L) +{ + int arg; + lua_State* L1 = getthread(L, &arg); + const char* msg = luaL_optstring(L, arg + 1, NULL); + int level = luaL_optinteger(L, arg + 2, (L == L1) ? 1 : 0); + luaL_argcheck(L, level >= 0, arg + 2, "level can't be negative"); + + luaL_traceback(L, L1, msg, level); + + return 1; +} + +static const luaL_Reg dblib[] = { + {"info", db_info}, + {"traceback", db_traceback}, + {NULL, NULL}, +}; + +int luaopen_debug(lua_State* L) +{ + luaL_register(L, LUA_DBLIBNAME, dblib); + return 1; +} diff --git a/External/luau/VM/src/ldebug.cpp b/External/luau/VM/src/ldebug.cpp new file mode 100644 index 00000000..3849b4b3 --- /dev/null +++ b/External/luau/VM/src/ldebug.cpp @@ -0,0 +1,704 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "ldebug.h" + +#include "lapi.h" +#include "lfunc.h" +#include "lmem.h" +#include "lgc.h" +#include "ldo.h" +#include "lbytecode.h" +#include "lvm.h" + +#include +#include + +static const char* getfuncname(Closure* cl); + +static int currentpc(lua_State* L, CallInfo* ci) +{ + return pcRel(ci->savedpc, ci->p); +} + +static int currentline(lua_State* L, CallInfo* ci) +{ + return luaG_getline(ci->p, currentpc(L, ci)); +} + +static Proto* getluaproto(CallInfo* ci) +{ + return cast_to(Proto*, ci->p); +} + +int lua_getargument(lua_State* L, int level, int n) +{ + if (unsigned(level) >= unsigned(L->ci - L->base_ci)) + return 0; + + CallInfo* ci = L->ci - level; + // changing tables in native functions externally may invalidate safety contracts wrt table state (metatable/size/readonly) + if (ci->flags & LUA_CALLINFO_NATIVE) + return 0; + + Proto* fp = getluaproto(ci); + int res = 0; + + if (fp && n > 0) + { + if (n <= fp->numparams) + { + luaC_threadbarrier(L); + luaA_pushvalue(L, ci->base + (n - 1)); + res = 1; + } + else if (fp->is_vararg && n < ci->base - ci->func) + { + luaC_threadbarrier(L); + luaA_pushvalue(L, ci->func + n); + res = 1; + } + } + + return res; +} + +const char* lua_getlocal(lua_State* L, int level, int n) +{ + if (unsigned(level) >= unsigned(L->ci - L->base_ci)) + return NULL; + + CallInfo* ci = L->ci - level; + // changing tables in native functions externally may invalidate safety contracts wrt table state (metatable/size/readonly) + if (ci->flags & LUA_CALLINFO_NATIVE) + return NULL; + + Proto* fp = getluaproto(ci); + const LocVar* var = fp ? luaF_getlocal(fp, n, currentpc(L, ci)) : NULL; + if (var) + { + luaC_threadbarrier(L); + luaA_pushvalue(L, ci->base + var->reg); + } + const char* name = var ? getstr(var->varname) : NULL; + return name; +} + +const char* lua_setlocal(lua_State* L, int level, int n) +{ + api_check(L, L->top - L->base >= 1); + + if (unsigned(level) >= unsigned(L->ci - L->base_ci)) + return NULL; + + CallInfo* ci = L->ci - level; + // changing registers in native functions externally may invalidate safety contracts wrt register type tags + if (ci->flags & LUA_CALLINFO_NATIVE) + return NULL; + + Proto* fp = getluaproto(ci); + const LocVar* var = fp ? luaF_getlocal(fp, n, currentpc(L, ci)) : NULL; + if (var) + setobj2s(L, ci->base + var->reg, L->top - 1); + L->top--; // pop value + const char* name = var ? getstr(var->varname) : NULL; + return name; +} + +static Closure* auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f, CallInfo* ci) +{ + Closure* cl = NULL; + for (; *what; what++) + { + switch (*what) + { + case 's': + { + if (f->isC) + { + ar->source = "=[C]"; + ar->what = "C"; + ar->linedefined = -1; + ar->short_src = "[C]"; + } + else + { + TString* source = (ci != nullptr ? ci->p : f->l.p)->source; + ar->source = getstr(source); + ar->what = "Lua"; + ar->linedefined = (ci != nullptr ? ci->p : f->l.p)->linedefined; + ar->short_src = luaO_chunkid(ar->ssbuf, sizeof(ar->ssbuf), getstr(source), source->len); + } + break; + } + case 'l': + { + if (ci) + { + ar->currentline = isLua(ci) ? currentline(L, ci) : -1; + } + else + { + ar->currentline = f->isC ? -1 : f->l.p->linedefined; + } + + break; + } + case 'u': + { + ar->nupvals = f->nupvalues; + break; + } + case 'a': + { + if (f->isC) + { + ar->isvararg = 1; + ar->nparams = 0; + } + else + { + ar->isvararg = (ci != nullptr ? ci->p : f->l.p)->is_vararg; + ar->nparams = (ci != nullptr ? ci->p : f->l.p)->numparams; + } + break; + } + case 'p': + { + if (f->isC) + { + ar->protoid = 0; + ar->bytecodeid = -1; + } + else + { + Proto* p = ci != nullptr ? ci->p : f->l.p; + ar->protoid = int(p->funid); + ar->bytecodeid = p->bytecodeid; + } + break; + } + case 'n': + { + ar->name = ci ? getfuncname(ci_func(ci)) : getfuncname(f); + break; + } + case 'f': + { + cl = f; + break; + } + default:; + } + } + return cl; +} + +void lua_callhook(lua_State* L, lua_Hook hook, void* userdata) +{ + api_check(L, hook != nullptr); + api_check(L, L->ci != L->base_ci); + + return luau_callhook(L, hook, userdata); +} + +int lua_stackdepth(lua_State* L) +{ + return int(L->ci - L->base_ci); +} + +int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar) +{ + Closure* f = NULL; + CallInfo* ci = NULL; + if (level < 0) + { + // element has to be within stack + if (-level > L->top - L->base) + return 0; + + StkId func = L->top + level; + + // and it has to be a function + if (!ttisfunction(func)) + return 0; + + f = clvalue(func); + } + else if (unsigned(level) < unsigned(L->ci - L->base_ci)) + { + ci = L->ci - level; + LUAU_ASSERT(ttisfunction(ci->func)); + f = clvalue(ci->func); + } + if (f) + { + // auxgetinfo fills ar and optionally requests to put closure on stack + if (Closure* fcl = auxgetinfo(L, what, ar, f, ci)) + { + luaC_threadbarrier(L); + setclvalue(L, L->top, fcl); + incr_top(L); + } + } + return f ? 1 : 0; +} + +static const char* getfuncname(Closure* cl) +{ + if (cl->isC) + { + if (TString* str = cl->c.debugname) + return getstr(str); + } + else + { + Proto* p = cl->l.p; + + if (p->debugname) + { + return getstr(p->debugname); + } + } + return nullptr; +} + +l_noret luaG_typeerrorL(lua_State* L, const TValue* o, const char* op) +{ + const char* t = luaT_objtypename(L, o); + + luaG_runerror(L, "attempt to %s a %s value", op, t); +} + +l_noret luaG_forerrorL(lua_State* L, const TValue* o, const char* what) +{ + const char* t = luaT_objtypename(L, o); + + luaG_runerror(L, "invalid 'for' %s (number expected, got %s)", what, t); +} + +l_noret luaG_concaterror(lua_State* L, StkId p1, StkId p2) +{ + const char* t1 = luaT_objtypename(L, p1); + const char* t2 = luaT_objtypename(L, p2); + + luaG_runerror(L, "attempt to concatenate %s with %s", t1, t2); +} + +l_noret luaG_aritherror(lua_State* L, const TValue* p1, const TValue* p2, TMS op) +{ + const char* t1 = luaT_objtypename(L, p1); + const char* t2 = luaT_objtypename(L, p2); + const char* opname = luaT_eventname[op] + 2; // skip __ from metamethod name + + if (t1 == t2) + luaG_runerror(L, "attempt to perform arithmetic (%s) on %s", opname, t1); + else + luaG_runerror(L, "attempt to perform arithmetic (%s) on %s and %s", opname, t1, t2); +} + +l_noret luaG_ordererror(lua_State* L, const TValue* p1, const TValue* p2, TMS op) +{ + const char* t1 = luaT_objtypename(L, p1); + const char* t2 = luaT_objtypename(L, p2); + const char* opname = (op == TM_LT) ? "<" : (op == TM_LE) ? "<=" : "=="; + + luaG_runerror(L, "attempt to compare %s %s %s", t1, opname, t2); +} + +l_noret luaG_indexerror(lua_State* L, const TValue* p1, const TValue* p2) +{ + const char* t1 = luaT_objtypename(L, p1); + const char* t2 = luaT_objtypename(L, p2); + const TString* key = ttisstring(p2) ? tsvalue(p2) : 0; + + if (key && key->len <= 64) // limit length to make sure we don't generate very long error messages for very long keys + luaG_runerror(L, "attempt to index %s with '%s'", t1, getstr(key)); + else + luaG_runerror(L, "attempt to index %s with %s", t1, t2); +} + +l_noret luaG_missingmembererror(lua_State* L, const TValue* p1, const TValue* p2) +{ + if (!ttisstring(p2)) + luaG_runerrorL(L, "cannot index %s with a %s", luaT_objtypename(L, p1), luaT_objtypename(L, p2)); + else + luaG_runerrorL(L, "this %s does not have a key named '%s'", luaT_objtypename(L, p1), getstr(tsvalue(p2))); +} + +l_noret luaG_methoderror(lua_State* L, const TValue* p1, const TString* p2) +{ + const char* t1 = luaT_objtypename(L, p1); + + luaG_runerror(L, "attempt to call missing method '%s' of %s", getstr(p2), t1); +} + +l_noret luaG_readonlyerror(lua_State* L) +{ + luaG_runerror(L, "attempt to modify a readonly table"); +} + +static void pusherror(lua_State* L, const char* msg) +{ + CallInfo* ci = L->ci; + if (isLua(ci)) + { + TString* source = getluaproto(ci)->source; + char chunkbuf[LUA_IDSIZE]; // add file:line information + const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), getstr(source), source->len); + int line = currentline(L, ci); + luaO_pushfstring(L, "%s:%d: %s", chunkid, line, msg); + } + else + { + lua_pushstring(L, msg); + } +} + +l_noret luaG_runerrorL(lua_State* L, const char* fmt, ...) +{ + va_list argp; + va_start(argp, fmt); + char result[LUA_BUFFERSIZE]; + vsnprintf(result, sizeof(result), fmt, argp); + va_end(argp); + + lua_rawcheckstack(L, 1); + + pusherror(L, result); + luaD_throw(L, LUA_ERRRUN); +} + +void luaG_pusherror(lua_State* L, const char* error) +{ + lua_rawcheckstack(L, 1); + + pusherror(L, error); +} + +void luaG_breakpoint(lua_State* L, Proto* p, int line, bool enable) +{ + void (*ondisable)(lua_State*, Proto*) = L->global->ecb.disable; + + // since native code doesn't support breakpoints, we would need to update all call frames with LUAU_CALLINFO_NATIVE that refer to p + if (p->lineinfo && (ondisable || !p->execdata)) + { + for (int i = 0; i < p->sizecode; ++i) + { + // note: we keep prologue as is, instead opting to break at the first meaningful instruction + if (LUAU_INSN_OP(p->code[i]) == LOP_PREPVARARGS) + continue; + + if (luaG_getline(p, i) != line) + continue; + + // lazy copy of the original opcode array; done when the first breakpoint is set + if (!p->debuginsn) + { + p->debuginsn = luaM_newarray(L, p->sizecode, uint8_t, p->memcat); + for (int j = 0; j < p->sizecode; ++j) + p->debuginsn[j] = LUAU_INSN_OP(p->code[j]); + } + + uint8_t op = enable ? LOP_BREAK : LUAU_INSN_OP(p->debuginsn[i]); + + // patch just the opcode byte, leave arguments alone + p->code[i] &= ~0xff; + p->code[i] |= op; + LUAU_ASSERT(LUAU_INSN_OP(p->code[i]) == op); + + // currently we don't restore native code when breakpoint is disabled. + // this will be addressed in the future. + if (enable && p->execdata && ondisable) + ondisable(L, p); + + // note: this is important! + // we only patch the *first* instruction in each proto that's attributed to a given line + // this can be changed, but if requires making patching a bit more nuanced so that we don't patch AUX words + break; + } + } + + for (int i = 0; i < p->sizep; ++i) + { + luaG_breakpoint(L, p->p[i], line, enable); + } +} + +bool luaG_onbreak(lua_State* L) +{ + if (L->ci == L->base_ci) + return false; + + if (!isLua(L->ci)) + return false; + + return LUAU_INSN_OP(*L->ci->savedpc) == LOP_BREAK; +} + +int luaG_getline(Proto* p, int pc) +{ + LUAU_ASSERT(pc >= 0 && pc < p->sizecode); + + if (!p->lineinfo) + return 0; + + return p->abslineinfo[pc >> p->linegaplog2] + p->lineinfo[pc]; +} + +int luaG_isnative(lua_State* L, int level) +{ + if (unsigned(level) >= unsigned(L->ci - L->base_ci)) + return 0; + + CallInfo* ci = L->ci - level; + return (ci->flags & LUA_CALLINFO_NATIVE) != 0 ? 1 : 0; +} + +int luaG_hasnative(lua_State* L, int level) +{ + if (unsigned(level) >= unsigned(L->ci - L->base_ci)) + return 0; + + CallInfo* ci = L->ci - level; + + Proto* proto = getluaproto(ci); + if (proto == nullptr) + return 0; + + return (proto->execdata != nullptr); +} + +void lua_singlestep(lua_State* L, int enabled) +{ + L->singlestep = bool(enabled); +} + +static int getmaxline(Proto* p) +{ + int result = -1; + + for (int i = 0; i < p->sizecode; ++i) + { + int line = luaG_getline(p, i); + result = result < line ? line : result; + } + + for (int i = 0; i < p->sizep; ++i) + { + int psize = getmaxline(p->p[i]); + result = result < psize ? psize : result; + } + + return result; +} + +// Find the line number with instructions. If the provided line doesn't have any instruction, it should return the next valid line number. +static int getnextline(Proto* p, int line) +{ + int closest = -1; + + if (p->lineinfo) + { + for (int i = 0; i < p->sizecode; ++i) + { + // note: we keep prologue as is, instead opting to break at the first meaningful instruction + if (LUAU_INSN_OP(p->code[i]) == LOP_PREPVARARGS) + continue; + + int candidate = luaG_getline(p, i); + + if (candidate == line) + return line; + + if (candidate > line && (closest == -1 || candidate < closest)) + closest = candidate; + } + } + + for (int i = 0; i < p->sizep; ++i) + { + int candidate = getnextline(p->p[i], line); + + if (candidate == line) + return line; + + if (candidate > line && (closest == -1 || candidate < closest)) + closest = candidate; + } + + return closest; +} + +int lua_breakpoint(lua_State* L, int funcindex, int line, int enabled) +{ + const TValue* func = luaA_toobject(L, funcindex); + api_check(L, ttisfunction(func) && !clvalue(func)->isC); + + Proto* p = clvalue(func)->l.p; + + // set the breakpoint to the next closest line with valid instructions + int target = getnextline(p, line); + + if (target != -1) + luaG_breakpoint(L, p, target, bool(enabled)); + + return target; +} + +int lua_atbreakpoint(lua_State* L) +{ + return luaG_onbreak(L) ? 1 : 0; +} + +static void getcoverage(Proto* p, int depth, int* buffer, size_t size, void* context, lua_Coverage callback) +{ + memset(buffer, -1, size * sizeof(int)); + + for (int i = 0; i < p->sizecode; ++i) + { + Instruction insn = p->code[i]; + if (LUAU_INSN_OP(insn) != LOP_COVERAGE) + continue; + + int line = luaG_getline(p, i); + int hits = LUAU_INSN_E(insn); + + LUAU_ASSERT(size_t(line) < size); + buffer[line] = buffer[line] < hits ? hits : buffer[line]; + } + + const char* debugname = p->debugname ? getstr(p->debugname) : NULL; + int linedefined = p->linedefined; + + callback(context, debugname, linedefined, depth, buffer, size); + + for (int i = 0; i < p->sizep; ++i) + getcoverage(p->p[i], depth + 1, buffer, size, context, callback); +} + +void lua_getcoverage(lua_State* L, int funcindex, void* context, lua_Coverage callback) +{ + const TValue* func = luaA_toobject(L, funcindex); + api_check(L, ttisfunction(func) && !clvalue(func)->isC); + + Proto* p = clvalue(func)->l.p; + + size_t size = getmaxline(p) + 1; + if (size == 0) + return; + + int* buffer = luaM_newarray(L, size, int, 0); + + getcoverage(p, 0, buffer, size, context, callback); + + luaM_freearray(L, buffer, size, int, 0); +} + +static void getcounters(lua_State* L, Proto* p, void* context, lua_CounterFunction functionvisit, lua_CounterValue countervisit) +{ + if (p->execdata != nullptr && L->global->ecb.getcounterdata != nullptr) + { + size_t count = 0; + char* data = L->global->ecb.getcounterdata(L, p, &count); + + if (data != nullptr && count != 0) + { + const char* debugname = p->debugname ? getstr(p->debugname) : nullptr; + int linedefined = p->linedefined; + + functionvisit(context, debugname, linedefined); + + for (size_t i = 0; i < count; i++) + { + uint32_t kind = 0; + memcpy(&kind, data + 0, sizeof(kind)); + data += sizeof(kind); + + uint32_t pcpos = 0; + memcpy(&pcpos, data + 0, sizeof(pcpos)); + data += sizeof(pcpos); + + uint64_t hits = 0; + memcpy(&hits, data + 0, sizeof(hits)); + data += sizeof(hits); + + int line = pcpos == ~0u ? p->linedefined : luaG_getline(p, pcpos); + + countervisit(context, kind, line, hits); + } + } + } + + for (int i = 0; i < p->sizep; ++i) + getcounters(L, p->p[i], context, functionvisit, countervisit); +} + +void lua_getcounters(lua_State* L, int funcindex, void* context, lua_CounterFunction functionvisit, lua_CounterValue countervisit) +{ + const TValue* func = luaA_toobject(L, funcindex); + api_check(L, ttisfunction(func) && !clvalue(func)->isC); + + if (L->global->ecb.getcounterdata == nullptr) + return; + + Proto* p = clvalue(func)->l.p; + + getcounters(L, p, context, functionvisit, countervisit); +} + +static size_t append(char* buf, size_t bufsize, size_t offset, const char* data) +{ + size_t size = strlen(data); + size_t copy = offset + size >= bufsize ? bufsize - offset - 1 : size; + memcpy(buf + offset, data, copy); + return offset + copy; +} + +const char* lua_debugtrace(lua_State* L) +{ + static char buf[4096]; + + const int limit1 = 10; + const int limit2 = 10; + + int depth = int(L->ci - L->base_ci); + size_t offset = 0; + + lua_Debug ar; + for (int level = 0; lua_getinfo(L, level, "sln", &ar); ++level) + { + if (ar.source) + offset = append(buf, sizeof(buf), offset, ar.short_src); + + if (ar.currentline > 0) + { + char line[32]; + snprintf(line, sizeof(line), ":%d", ar.currentline); + + offset = append(buf, sizeof(buf), offset, line); + } + + if (ar.name) + { + offset = append(buf, sizeof(buf), offset, " function "); + offset = append(buf, sizeof(buf), offset, ar.name); + } + + offset = append(buf, sizeof(buf), offset, "\n"); + + if (depth > limit1 + limit2 && level == limit1 - 1) + { + char skip[32]; + snprintf(skip, sizeof(skip), "... (+%d frames)\n", int(depth - limit1 - limit2)); + + offset = append(buf, sizeof(buf), offset, skip); + + level = depth - limit2 - 1; + } + } + + LUAU_ASSERT(offset < sizeof(buf)); + buf[offset] = '\0'; + + return buf; +} diff --git a/External/luau/VM/src/ldebug.h b/External/luau/VM/src/ldebug.h new file mode 100644 index 00000000..2c26ff5c --- /dev/null +++ b/External/luau/VM/src/ldebug.h @@ -0,0 +1,35 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lstate.h" + +#define pcRel(pc, p) ((pc) && (pc) != (p)->code ? cast_to(int, (pc) - (p)->code) - 1 : 0) + +#define luaG_typeerror(L, o, opname) luaG_typeerrorL(L, o, opname) +#define luaG_forerror(L, o, what) luaG_forerrorL(L, o, what) +#define luaG_runerror(L, fmt, ...) luaG_runerrorL(L, fmt, ##__VA_ARGS__) + +#define LUA_MEMERRMSG "not enough memory" +#define LUA_ERRERRMSG "error in error handling" + +LUAI_FUNC l_noret luaG_typeerrorL(lua_State* L, const TValue* o, const char* opname); +LUAI_FUNC l_noret luaG_forerrorL(lua_State* L, const TValue* o, const char* what); +LUAI_FUNC l_noret luaG_concaterror(lua_State* L, StkId p1, StkId p2); +LUAI_FUNC l_noret luaG_aritherror(lua_State* L, const TValue* p1, const TValue* p2, TMS op); +LUAI_FUNC l_noret luaG_ordererror(lua_State* L, const TValue* p1, const TValue* p2, TMS op); +LUAI_FUNC l_noret luaG_indexerror(lua_State* L, const TValue* p1, const TValue* p2); +LUAI_FUNC l_noret luaG_methoderror(lua_State* L, const TValue* p1, const TString* p2); +LUAI_FUNC l_noret luaG_missingmembererror(lua_State* L, const TValue* p1, const TValue* p2); +LUAI_FUNC l_noret luaG_readonlyerror(lua_State* L); + +LUAI_FUNC LUA_PRINTF_ATTR(2, 3) l_noret luaG_runerrorL(lua_State* L, const char* fmt, ...); +LUAI_FUNC void luaG_pusherror(lua_State* L, const char* error); + +LUAI_FUNC void luaG_breakpoint(lua_State* L, Proto* p, int line, bool enable); +LUAI_FUNC bool luaG_onbreak(lua_State* L); + +LUAI_FUNC int luaG_getline(Proto* p, int pc); + +LUAI_FUNC int luaG_isnative(lua_State* L, int level); +LUAI_FUNC int luaG_hasnative(lua_State* L, int level); diff --git a/External/luau/VM/src/ldo.cpp b/External/luau/VM/src/ldo.cpp new file mode 100644 index 00000000..9c5761df --- /dev/null +++ b/External/luau/VM/src/ldo.cpp @@ -0,0 +1,933 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "ldo.h" + +#include "lstring.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "ltable.h" +#include "lvm.h" + +#if LUA_USE_LONGJMP +#include +#include +#else +#include +#endif + +#include + +LUAU_FASTFLAG(LuauFastpcall) + +// keep max stack allocation request under 1GB +#define MAX_STACK_SIZE (int(1024 / sizeof(TValue)) * 1024 * 1024) + +/* +** {====================================================== +** Error-recovery functions +** ======================================================= +*/ + +#if LUA_USE_LONGJMP +struct lua_jmpbuf +{ + lua_jmpbuf* volatile prev; + volatile int status; + jmp_buf buf; +}; + +// use POSIX versions of setjmp/longjmp if possible: they don't save/restore signal mask and are therefore faster +#if defined(__linux__) || defined(__APPLE__) +#define LUAU_SETJMP(buf) _setjmp(buf) +#define LUAU_LONGJMP(buf, code) _longjmp(buf, code) +#else +#define LUAU_SETJMP(buf) setjmp(buf) +#define LUAU_LONGJMP(buf, code) longjmp(buf, code) +#endif + +int luaD_rawrunprotected(lua_State* L, Pfunc f, void* ud) +{ + lua_jmpbuf jb; + jb.prev = L->global->errorjmp; + jb.status = 0; + L->global->errorjmp = &jb; + + if (LUAU_SETJMP(jb.buf) == 0) + f(L, ud); + + L->global->errorjmp = jb.prev; + return jb.status; +} + +l_noret luaD_throw(lua_State* L, int errcode) +{ + if (lua_jmpbuf* jb = L->global->errorjmp) + { + jb->status = errcode; + LUAU_LONGJMP(jb->buf, 1); + } + + if (L->global->cb.panic) + L->global->cb.panic(L, errcode); + + abort(); +} +#else +class lua_exception : public std::exception +{ +public: + lua_exception(lua_State* L, int status) + : L(L) + , status(status) + { + } + + const char* what() const throw() override + { + // LUA_ERRRUN passes error object on the stack + if (status == LUA_ERRRUN) + if (const char* str = lua_tostring(L, -1)) + return str; + + switch (status) + { + case LUA_ERRRUN: + return "lua_exception: runtime error"; + case LUA_ERRSYNTAX: + return "lua_exception: syntax error"; + case LUA_ERRMEM: + return "lua_exception: " LUA_MEMERRMSG; + case LUA_ERRERR: + return "lua_exception: " LUA_ERRERRMSG; + default: + return "lua_exception: unexpected exception status"; + } + } + + int getStatus() const + { + return status; + } + + const lua_State* getThread() const + { + return L; + } + +private: + lua_State* L; + int status; +}; + +int luaD_rawrunprotected(lua_State* L, Pfunc f, void* ud) +{ + int status = 0; + + try + { + f(L, ud); + return 0; + } + catch (lua_exception& e) + { + // It is assumed/required that the exception caught here was thrown from the same Luau state. + // If this assert fires, it indicates a lua_exception was not properly caught and propagated + // to the exception handler for a different Luau state. Report this issue to the Luau team if + // you need more information or assistance resolving this assert. + LUAU_ASSERT(e.getThread() == L); + + status = e.getStatus(); + } + catch (std::exception& e) + { + // Luau will never throw this, but this can catch exceptions that escape from C++ implementations of external functions + try + { + // there's no exception object on stack; let's push the error on stack so that error handling below can proceed + luaG_pusherror(L, e.what()); + status = LUA_ERRRUN; + } + catch (std::exception&) + { + // out of memory while allocating error string + status = LUA_ERRMEM; + } + } + + return status; +} + +l_noret luaD_throw(lua_State* L, int errcode) +{ + throw lua_exception(L, errcode); +} +#endif + +// }====================================================== + +static void correctstack(lua_State* L, TValue* oldstack) +{ + L->top = (L->top - oldstack) + L->stack; + for (UpVal* up = L->openupval; up != NULL; up = up->u.open.threadnext) + up->v = (up->v - oldstack) + L->stack; + for (CallInfo* ci = L->base_ci; ci <= L->ci; ci++) + { + ci->top = (ci->top - oldstack) + L->stack; + ci->base = (ci->base - oldstack) + L->stack; + ci->func = (ci->func - oldstack) + L->stack; + } + L->base = (L->base - oldstack) + L->stack; +} + +void luaD_reallocstack(lua_State* L, int newsize, int fornewci) +{ + // throw 'out of memory' error because space for a custom error message cannot be guaranteed here + if (newsize > MAX_STACK_SIZE) + { + // reallocation was performed to setup a new CallInfo frame, which we have to remove + if (fornewci) + { + CallInfo* cip = L->ci - 1; + + L->ci = cip; + L->base = cip->base; + L->top = cip->top; + } + + luaD_throw(L, LUA_ERRMEM); + } + + TValue* oldstack = L->stack; + int realsize = newsize + EXTRA_STACK; + LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - EXTRA_STACK); + luaM_reallocarray(L, L->stack, L->stacksize, realsize, TValue, L->memcat); + TValue* newstack = L->stack; + for (int i = L->stacksize; i < realsize; i++) + setnilvalue(newstack + i); // erase new segment + L->stacksize = realsize; + L->stack_last = newstack + newsize; + correctstack(L, oldstack); +} + +void luaD_reallocCI(lua_State* L, int newsize) +{ + CallInfo* oldci = L->base_ci; + luaM_reallocarray(L, L->base_ci, L->size_ci, newsize, CallInfo, L->memcat); + L->size_ci = newsize; + L->ci = (L->ci - oldci) + L->base_ci; + L->end_ci = L->base_ci + L->size_ci - 1; +} + +void luaD_growstack(lua_State* L, int n) +{ + luaD_reallocstack(L, getgrownstacksize(L, n), 0); +} + +CallInfo* luaD_growCI(lua_State* L) +{ + // allow extra stack space to handle stack overflow in xpcall + const int hardlimit = LUAI_MAXCALLS + (LUAI_MAXCALLS >> 3); + + if (L->size_ci >= hardlimit) + luaD_throw(L, LUA_ERRERR); // error while handling stack error + + int request = L->size_ci * 2; + luaD_reallocCI(L, L->size_ci >= LUAI_MAXCALLS ? hardlimit : request < LUAI_MAXCALLS ? request : LUAI_MAXCALLS); + + if (L->size_ci > LUAI_MAXCALLS) + luaG_runerror(L, "stack overflow"); + + return ++L->ci; +} + +void luaD_checkCstack(lua_State* L) +{ + // allow extra stack space to handle stack overflow in xpcall + const int hardlimit = LUAI_MAXCCALLS + (LUAI_MAXCCALLS >> 3); + + if (L->nCcalls == LUAI_MAXCCALLS) + luaG_runerror(L, "C stack overflow"); + else if (L->nCcalls >= hardlimit) + luaD_throw(L, LUA_ERRERR); // error while handling stack error +} + +static void resume_continue(lua_State* L, ptrdiff_t basecioffset); + +static void performcall(lua_State* L, StkId func, int nresults, bool preparereentry) +{ + if (luau_precall(L, func, nresults) == PCRLUA) + { // is a Lua function? + L->ci->flags |= LUA_CALLINFO_RETURN; // luau_execute will stop after returning from the stack frame + + bool oldactive = L->isactive; + L->isactive = true; + luaC_threadbarrier(L); + + if (preparereentry) + { + L->status = SCHEDULED_REENTRY; + } + else + { + ptrdiff_t basecioffset = FFlag::LuauFastpcall ? saveci(L, L->ci - 1) : 0; + + luau_execute(L); + + // process continuations up to the caller frame + // this is needed because even when 'preparereentry' is false, current thread might be yieldable + // in that case, luau_execute might exit with unfinished C continuation frames (e.g. pcall) + if (FFlag::LuauFastpcall) + resume_continue(L, basecioffset); + } + + if (!oldactive) + L->isactive = false; + } +} + +// Used to perform yieldable calls from non-call opcodes like FORGLOOP +bool luaD_performcally(lua_State* L, StkId func, int nresults) +{ + if (++L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + L->baseCcalls++; // Allow yielding across this C call + + if (FFlag::LuauFastpcall) + { + L->ci->flags |= LUA_CALLINFO_OPYIELD; + + performcall(L, func, nresults, /* preparereentry */ false); + + if (L->status != LUA_OK) + return true; + + L->ci->flags &= ~LUA_CALLINFO_OPYIELD; + } + else + { + ptrdiff_t cioffset = saveci(L, L->ci); + + performcall(L, func, nresults, /* preparereentry */ false); + + if (L->status != LUA_OK) + { + CallInfo* caller = restoreci(L, cioffset); + + caller->flags |= LUA_CALLINFO_OPYIELD; + return true; + } + } + + L->baseCcalls--; + L->nCcalls--; + luaC_checkGC(L); + return false; +} + +/* +** Call a function (C or Lua). The function to be called is at *func. +** The arguments are on the stack, right after the function. +** When returns, all the results are on the stack, starting at the original +** function position. +*/ +void luaD_callint(lua_State* L, StkId func, int nresults, bool preparereentry) +{ + if (++L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + // when called from a yieldable C function, maintain yieldable invariant (baseCcalls <= nCcalls) + bool fromyieldableccall = false; + + if (L->ci != L->base_ci) + { + Closure* ccl = clvalue(L->ci->func); + + if (ccl->isC && ccl->c.cont) + { + fromyieldableccall = true; + L->baseCcalls++; + } + } + + ptrdiff_t funcoffset = savestack(L, func); + ptrdiff_t cioffset = saveci(L, L->ci); + + performcall(L, func, nresults, preparereentry); + + bool yielded = isyielded(L); + + if (fromyieldableccall) + { + // restore original yieldable invariant + // in case of an error, this would either be restored by luaD_pcall or the thread would no longer be resumable + L->baseCcalls--; + + // on yield, we have to set the CallInfo top of the C function including slots for expected results, to restore later + if (yielded) + { + CallInfo* callerci = restoreci(L, cioffset); + callerci->top = restorestack(L, funcoffset) + (nresults != LUA_MULTRET ? nresults : 0); + } + } + + if (nresults != LUA_MULTRET && !yielded) + L->top = restorestack(L, funcoffset) + nresults; + + L->nCcalls--; + luaC_checkGC(L); +} + +void luaD_call(lua_State* L, StkId func, int nresults) +{ + luaD_callint(L, func, nresults, /* preparereentry */ false); +} + +// Non-yieldable version of luaD_call, used primarily to call an error handler which cannot yield +void luaD_callny(lua_State* L, StkId func, int nresults) +{ + if (++L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + LUAU_ASSERT(L->nCcalls > L->baseCcalls); + + ptrdiff_t funcoffset = savestack(L, func); + + performcall(L, func, nresults, /* preparereentry */ false); + + LUAU_ASSERT(!isyielded(L)); + + if (nresults != LUA_MULTRET) + L->top = restorestack(L, funcoffset) + nresults; + + L->nCcalls--; + luaC_checkGC(L); +} + +void luaD_seterrorobj(lua_State* L, int errcode, StkId oldtop) +{ + switch (errcode) + { + case LUA_ERRMEM: + { + setsvalue(L, oldtop, luaS_newliteral(L, LUA_MEMERRMSG)); // can not fail because string is pinned in luaopen + break; + } + case LUA_ERRERR: + { + setsvalue(L, oldtop, luaS_newliteral(L, LUA_ERRERRMSG)); // can not fail because string is pinned in luaopen + break; + } + case LUA_ERRSYNTAX: + case LUA_ERRRUN: + { + setobj2s(L, oldtop, L->top - 1); // error message on current top + break; + } + } + L->top = oldtop + 1; +} + +void luaD_preparefinalizestate(lua_State* L, lua_State* co, bool resulttrue) +{ + LUAU_ASSERT(co->finalizers); + + sethvalue(L, L->top, co->finalizers); + L->top++; + setnvalue(L->top, double(luaH_getn(co->finalizers))); + L->top++; + setbvalue(L->top, resulttrue ? 1 : 0); + L->top++; + + co->finalizers = nullptr; // we have taken the list for processing +} + +void luaD_preparefinalize(lua_State* L, lua_State* co) +{ + bool resulttrue = co->status == LUA_OK; + int nres = resulttrue ? cast_int(co->top - co->base) : 1; + + if (!lua_checkstack(L, nres + 3)) + luaG_runerror(L, "too many results to invoke finalizer"); + + luaD_preparefinalizestate(L, co, resulttrue); + lua_xmove(co, L, nres); +} + +int luaD_runfinalizers(lua_State* L, bool toclose, bool returnstatus) +{ + int results = lua_gettop(L) - 4; + int position = lua_tointeger(L, 3); + int status = lua_toboolean(L, 4); + + if (position != 0) + { + if (!lua_checkstack(L, results + 2)) + { + lua_pushstring(L, "too many results to invoke finalizer"); + lua_error(L); + } + + // decrement the position + lua_pushinteger(L, position - 1); + lua_replace(L, 3); + + // take the finalizer from the end of the table + lua_rawgeti(L, 2, position); + + // push status string + lua_pushstring(L, toclose ? "cancelled" : (status == 1 ? "finished" : "error")); + + // copy results over + for (int i = 0; i < results; i++) + lua_pushvalue(L, 5 + i); + + // pcall with no errfunc makes ourselves the handler for any errors + return lua_pcallyieldable(L, results + 1, 0, 0); + } + + if (returnstatus) + return results + 1; + + if (status == 0) + lua_error(L); + + return results; +} + +static void resume_continue(lua_State* L, ptrdiff_t basecioffset) +{ + // unroll Luau/C combined stack, processing continuations + while ((L->status == LUA_OK || L->status == SCHEDULED_REENTRY) && L->ci > (FFlag::LuauFastpcall ? restoreci(L, basecioffset) : L->base_ci)) + { + LUAU_ASSERT(L->baseCcalls == L->nCcalls); + + L->status = LUA_OK; + + Closure* cl = curr_func(L); + + if (cl->isC) + { + LUAU_ASSERT(cl->c.cont); + + // fast continuation path for the synthetic pcall/xpcall frame + if (FFlag::LuauFastpcall && (L->ci->flags & LUA_CALLINFO_PCALL) != 0) + { + luau_pospcallsuccess(L); + continue; + } + + // continuation can use non-protected calls again + L->ci->flags &= ~LUA_CALLINFO_HANDLE; + + // C continuation; we expect this to be followed by Lua continuations + int n = cl->c.cont(L, 0); + + // continuation can break or yield again + if (L->status == LUA_BREAK || L->status == LUA_YIELD) + break; + + if (L->status == SCHEDULED_REENTRY) + continue; + + luau_poscall(L, L->top - n); + } + else + { + if (L->ci->flags & LUA_CALLINFO_OPYIELD) + luau_finishop(L); + + // Luau continuation; it terminates at the end of the stack or at another C continuation + luau_execute(L); + } + } +} + +static void resume(lua_State* L, void* ud) +{ + StkId firstArg = cast_to(StkId, ud); + + if (L->status == LUA_OK) + { + // start coroutine + LUAU_ASSERT(L->ci == L->base_ci && firstArg >= L->base); + if (firstArg == L->base) + luaG_runerror(L, "cannot resume dead coroutine"); + + int precallresult = luau_precall(L, firstArg - 1, LUA_MULTRET); + + // on scheduled reentry, we will continue into the yield-continue block below + if (L->status == SCHEDULED_REENTRY) + { + firstArg = L->base; + } + else + { + // C function is either completed or yielded, exit + if (precallresult != PCRLUA) + return; + + // mark to not return past the current Luau function frame + L->ci->flags |= LUA_CALLINFO_RETURN; + } + } + + // restore from yield or reentry + if (L->status != LUA_OK) + { + // resume from previous yield or break + LUAU_ASSERT(firstArg >= L->base); + LUAU_ASSERT(isyielded(L)); + L->status = LUA_OK; + + Closure* cl = curr_func(L); + + if (cl->isC) + { + // if the top stack frame is a C call continuation, resume_continue will handle that case + if (!cl->c.cont) + { + // finish interrupted execution of `OP_CALL' + luau_poscall(L, firstArg); + } + else + { + // restore arguments we have protected for C continuation + L->base = L->ci->base; + } + } + else + { + // yielded inside a hook: just continue its execution + L->base = L->ci->base; + } + } + + // run continuations from the stack; typically resumes Luau code and pcalls + resume_continue(L, /* basecioffset */ 0); +} + +static CallInfo* resume_findhandler(lua_State* L) +{ + CallInfo* ci = L->ci; + + while (ci > L->base_ci) + { + if (ci->flags & LUA_CALLINFO_HANDLE) + return ci; + + ci--; + } + + return NULL; +} + +static void restore_stack_limit(lua_State* L) +{ + LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - EXTRA_STACK); + if (L->size_ci > LUAI_MAXCALLS) + { // there was an overflow? + int inuse = cast_int(L->ci - L->base_ci); + if (inuse + 1 < LUAI_MAXCALLS) // can `undo' overflow? + luaD_reallocCI(L, LUAI_MAXCALLS); + } + else + { + condhardstacktests(luaD_reallocCI(L, L->size_ci)); + } +} + +static void callerrfunc(lua_State* L, void* ud) +{ + StkId errfunc = cast_to(StkId, ud); + + setobj2s(L, L->top, L->top - 1); + setobj2s(L, L->top - 1, errfunc); + incr_top(L); + + luaD_callny(L, L->top - 2, 1); +} + +static void resume_handle(lua_State* L, void* ud) +{ + CallInfo* ci = (CallInfo*)ud; + Closure* cl = ci_func(ci); + + LUAU_ASSERT(ci->flags & LUA_CALLINFO_HANDLE); + LUAU_ASSERT(cl->isC && cl->c.cont); + LUAU_ASSERT(L->status != 0); + + // make sure we don't run the handler the second time + ci->flags &= ~LUA_CALLINFO_HANDLE; + + // restore thread status to LUA_OK since we're handling the error + int status = L->status; + + L->status = LUA_OK; + + // push error object to stack top if it's not already there + if (status != LUA_ERRRUN) + luaD_seterrorobj(L, status, L->top); + + // call user-defined error function + if (ci->errfunc != 0) + { + // save ci pointer - it will be invalidated by callerrfunc call + ptrdiff_t old_ci = saveci(L, ci); + + // if errfunc fails, we fail with "error in error handling" or "not enough memory" + int err = luaD_rawrunprotected(L, callerrfunc, ci->base + (ci->errfunc - 1)); + + // in general we preserve the status, except for cases when the error handler fails + // out of memory is treated specially because it's common for it to be cascading, in which case we preserve the code + if (err == 0) + status = LUA_ERRRUN; + else if (status == LUA_ERRMEM && err == LUA_ERRMEM) + status = LUA_ERRMEM; + else + status = LUA_ERRERR; + + luaD_seterrorobj(L, status, L->top - 1); + + ci = restoreci(L, old_ci); + ci->errfunc = 0; + } + + // restore nCcalls to base for the continuation + L->nCcalls = L->baseCcalls; + + // restore the stack frame to the frame with continuation + L->ci = ci; + + // close eventual pending closures; this means it's now safe to restore stack + luaF_close(L, L->ci->base); + + // adjust the stack frame for ci to prepare for cont call + L->base = ci->base; + ci->top = L->top; + + restore_stack_limit(L); + + int n = cl->c.cont(L, status); + + if (L->status != LUA_OK) + return; + + // finish cont call and restore stack to previous ci top + luau_poscall(L, L->top - n); + + // run remaining continuations from the stack; typically resumes pcalls + resume_continue(L, /* basecioffset */ 0); +} + +static int resume_error(lua_State* L, const char* msg, int narg) +{ + L->top -= narg; + setsvalue(L, L->top, luaS_new(L, msg)); + incr_top(L); + return LUA_ERRRUN; +} + +static int resume_start(lua_State* L, lua_State* from, int nargs) +{ + api_check(L, nargs >= 0); + api_check(L, L->top - L->base >= nargs); + + if (L->status != LUA_YIELD && L->status != LUA_BREAK && (L->status != 0 || L->ci != L->base_ci)) + return resume_error(L, "cannot resume non-suspended coroutine", nargs); + + L->nCcalls = from ? from->nCcalls : 0; + if (L->nCcalls >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow", nargs); + + L->baseCcalls = ++L->nCcalls; + L->isactive = true; + + luaC_threadbarrier(L); + + return LUA_OK; +} + +static int resume_finish(lua_State* L, int status, int oldnCcalls) +{ + CallInfo* ch = NULL; + while (status != LUA_OK && (ch = resume_findhandler(L)) != NULL) + { + if (lua_isyieldable(L) != 0 && L->global->cb.debugprotectederror) + { + L->global->cb.debugprotectederror(L); + + // debug hook is only allowed to break + if (L->status == LUA_BREAK) + { + status = LUA_OK; + break; + } + } + + // restore the baseline we established in resume_start + L->baseCcalls = oldnCcalls; + + L->status = cast_byte(status); + status = luaD_rawrunprotected(L, resume_handle, ch); + } + + // C call count base was set to an incremented value of C call count in resume, so we decrement here + L->nCcalls = oldnCcalls - 1; + + // make execution context non-yieldable as we are leaving the resume + L->baseCcalls = L->nCcalls; + + L->isactive = false; + + if (status != LUA_OK) + { + L->status = cast_byte(status); + luaD_seterrorobj(L, status, L->top); + L->ci->top = L->top; + } + else if (L->status == LUA_OK) + { + expandstacklimit(L, L->top); + } + + return L->status; +} + +// Some profilers can use preresume/postresume to push one Luau execution frame +// covering the entire resume and pop it on exit. lua_resume never longjmps past its own +// frame: the resume body runs under luaD_rawrunprotected (which catches and returns a +// status) and resume_finish only runs continuations under luaD_rawrunprotected, so the +// pop always executes. +int lua_resume(lua_State* L, lua_State* from, int nargs) +{ + if (int starterror = resume_start(L, from, nargs)) + return starterror; + + if (LUAU_UNLIKELY(!!L->global->cb.preresume)) + L->global->cb.preresume(L); + + int oldnCcalls = L->nCcalls; + + int status = luaD_rawrunprotected(L, resume, L->top - nargs); + + int result = resume_finish(L, status, oldnCcalls); + + if (LUAU_UNLIKELY(!!L->global->cb.postresume)) + L->global->cb.postresume(L); + + return result; +} + +int lua_resumeerror(lua_State* L, lua_State* from) +{ + if (int starterror = resume_start(L, from, 1)) + return starterror; + + if (LUAU_UNLIKELY(!!L->global->cb.preresume)) + L->global->cb.preresume(L); + + int oldnCcalls = L->nCcalls; + + int status = LUA_ERRRUN; + + if (CallInfo* ci = resume_findhandler(L)) + { + L->status = cast_byte(status); + status = luaD_rawrunprotected(L, resume_handle, ci); + } + + int result = resume_finish(L, status, oldnCcalls); + + if (LUAU_UNLIKELY(!!L->global->cb.postresume)) + L->global->cb.postresume(L); + + return result; +} + +int lua_yield(lua_State* L, int nresults) +{ + api_check(L, nresults >= 0); + api_check(L, nresults <= L->top - L->base); + + if (L->nCcalls > L->baseCcalls) + luaG_runerror(L, "attempt to yield across metamethod/C-call boundary"); + L->base = L->top - nresults; // protect stack slots below + L->status = LUA_YIELD; + return -1; +} + +int lua_break(lua_State* L) +{ + if (L->nCcalls > L->baseCcalls) + luaG_runerror(L, "attempt to break across metamethod/C-call boundary"); + L->status = LUA_BREAK; + return -1; +} + +int lua_isyieldable(lua_State* L) +{ + return (L->nCcalls <= L->baseCcalls); +} + +int luaD_pcall(lua_State* L, Pfunc func, void* u, ptrdiff_t old_top, ptrdiff_t ef) +{ + unsigned short oldnCcalls = L->nCcalls; + unsigned short oldbaseCcalls = L->baseCcalls; + ptrdiff_t old_ci = saveci(L, L->ci); + bool oldactive = L->isactive; + int status = luaD_rawrunprotected(L, func, u); + if (status != 0) + { + int errstatus = status; + + // call user-defined error function (used in xpcall) + if (ef) + { + // push error object to stack top if it's not already there + if (status != LUA_ERRRUN) + luaD_seterrorobj(L, status, L->top); + + // if errfunc fails, we fail with "error in error handling" or "not enough memory" + int err = luaD_rawrunprotected(L, callerrfunc, restorestack(L, ef)); + + // in general we preserve the status, except for cases when the error handler fails + // out of memory is treated specially because it's common for it to be cascading, in which case we preserve the code + if (err == 0) + errstatus = LUA_ERRRUN; + else if (status == LUA_ERRMEM && err == LUA_ERRMEM) + errstatus = LUA_ERRMEM; + else + errstatus = status = LUA_ERRERR; + } + + // since the call failed with an error, we might have to reset the 'active' thread state + if (!oldactive) + L->isactive = false; + + bool yieldable = L->nCcalls <= L->baseCcalls; // Inlined logic from 'lua_isyieldable' to avoid potential for an out of line call. + + // restore nCcalls and baseCcalls before calling the debugprotectederror callback which may rely on the proper value to have been restored. + L->nCcalls = oldnCcalls; + L->baseCcalls = oldbaseCcalls; + + // an error occurred, check if we have a protected error callback + if (yieldable && L->global->cb.debugprotectederror) + { + L->global->cb.debugprotectederror(L); + + // debug hook is only allowed to break + if (L->status == LUA_BREAK) + return 0; + } + + StkId oldtop = restorestack(L, old_top); + luaF_close(L, oldtop); // close eventual pending closures + luaD_seterrorobj(L, errstatus, oldtop); + L->ci = restoreci(L, old_ci); + L->base = L->ci->base; + restore_stack_limit(L); + } + return status; +} diff --git a/External/luau/VM/src/ldo.h b/External/luau/VM/src/ldo.h new file mode 100644 index 00000000..33f6ee84 --- /dev/null +++ b/External/luau/VM/src/ldo.h @@ -0,0 +1,83 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" +#include "lstate.h" +#include "luaconf.h" +#include "ldebug.h" + +// returns target stack for 'n' extra elements to reallocate +// if possible, stack size growth factor is 2x +#define getgrownstacksize(L, n) ((n) <= L->stacksize ? 2 * L->stacksize : L->stacksize + (n)) +#define stacklimitreached(L, n) ((char*)L->stack_last - (char*)L->top <= (n) * (int)sizeof(TValue)) + +#define luaD_checkstackfornewci(L, n) \ + if (stacklimitreached(L, (n))) \ + luaD_reallocstack(L, getgrownstacksize(L, (n)), 1); \ + else \ + condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK, 1)); + +#define luaD_checkstack(L, n) \ + if (stacklimitreached(L, (n))) \ + luaD_growstack(L, n); \ + else \ + condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK, 0)); + +#define incr_top(L) \ + { \ + luaD_checkstack(L, 1); \ + L->top++; \ + } + +#define savestack(L, p) check_exp((p) >= L->stack && (p) <= L->stack + L->stacksize, (char*)(p) - (char*)L->stack) +#define restorestack(L, n) check_exp((n) >= 0 && (size_t)(n) <= sizeof(TValue) * L->stacksize, (TValue*)((char*)L->stack + (n))) + +#define expandstacklimit(L, p) \ + { \ + LUAU_ASSERT((p) <= (L)->stack_last); \ + if ((L)->ci->top < (p)) \ + (L)->ci->top = (p); \ + } + +#define incr_ci(L) ((L->ci == L->end_ci) ? luaD_growCI(L) : (condhardstacktests(luaD_reallocCI(L, L->size_ci)), ++L->ci)) + +#define saveci(L, p) check_exp((p) >= L->base_ci && (p) <= L->base_ci + L->size_ci, (char*)(p) - (char*)L->base_ci) +#define restoreci(L, n) check_exp((n) >= 0 && (size_t)(n) <= sizeof(CallInfo) * L->size_ci, (CallInfo*)((char*)L->base_ci + (n))) + +#define isyielded(L) ((L)->status == LUA_YIELD || (L)->status == LUA_BREAK || (L)->status == SCHEDULED_REENTRY) + +// results from luaD_precall +#define PCRLUA 0 // initiated a call to a Lua function +#define PCRC 1 // did a call to a C function +#define PCRYIELD 2 // C function yielded + +// return value for a yielded C call +#define C_CALL_YIELD -1 + +// luaD_call can 'yield' into an immediate reentry +// reentry will remove extra call frames from C call stack and continue execution +// this lua_State::status code is internal and should not be used by users +#define SCHEDULED_REENTRY 0x7f + +// type of protected functions, to be ran by `runprotected' +typedef void (*Pfunc)(lua_State* L, void* ud); + +LUAI_FUNC CallInfo* luaD_growCI(lua_State* L); + +LUAI_FUNC bool luaD_performcally(lua_State* L, StkId func, int nresults); +LUAI_FUNC void luaD_callint(lua_State* L, StkId func, int nresults, bool forreentry); +LUAI_FUNC void luaD_call(lua_State* L, StkId func, int nresults); +LUAI_FUNC void luaD_callny(lua_State* L, StkId func, int nresults); +LUAI_FUNC int luaD_pcall(lua_State* L, Pfunc func, void* u, ptrdiff_t oldtop, ptrdiff_t ef); +LUAI_FUNC void luaD_reallocCI(lua_State* L, int newsize); +LUAI_FUNC void luaD_reallocstack(lua_State* L, int newsize, int fornewci); +LUAI_FUNC void luaD_growstack(lua_State* L, int n); +LUAI_FUNC void luaD_checkCstack(lua_State* L); +LUAI_FUNC void luaD_seterrorobj(lua_State* L, int errcode, StkId oldtop); +LUAI_FUNC void luaD_preparefinalizestate(lua_State* L, lua_State* co, bool resulttrue); +LUAI_FUNC void luaD_preparefinalize(lua_State* L, lua_State* co); +LUAI_FUNC int luaD_runfinalizers(lua_State* L, bool toclose, bool returnstatus); + +LUAI_FUNC l_noret luaD_throw(lua_State* L, int errcode); +LUAI_FUNC int luaD_rawrunprotected(lua_State* L, Pfunc f, void* ud); diff --git a/External/luau/VM/src/lfunc.cpp b/External/luau/VM/src/lfunc.cpp new file mode 100644 index 00000000..32b39f1d --- /dev/null +++ b/External/luau/VM/src/lfunc.cpp @@ -0,0 +1,255 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lfunc.h" + +#include "lstate.h" +#include "lmem.h" +#include "lgc.h" + +LUAU_FASTINTVARIABLE(LuauInlineHitsThreshold, 32) + +Proto* luaF_newproto(lua_State* L) +{ + Proto* f = luaM_newgco(L, Proto, sizeof(Proto), L->activememcat, LUA_TPROTO); + + luaC_init(L, f, LUA_TPROTO); + + f->nups = 0; + f->numparams = 0; + f->is_vararg = 0; + f->maxstacksize = 0; + f->flags = 0; + + f->k = NULL; + f->code = NULL; + f->p = NULL; + f->codeentry = NULL; + + f->execdata = NULL; + f->exectarget = 0; + + f->lineinfo = NULL; + f->abslineinfo = NULL; + f->locvars = NULL; + f->upvalues = NULL; + f->source = NULL; + + f->debugname = NULL; + f->debuginsn = NULL; + + f->typeinfo = NULL; + + f->userdata = NULL; + + f->gclist = NULL; + + f->sizecode = 0; + f->sizep = 0; + f->sizelocvars = 0; + f->sizeupvalues = 0; + f->sizek = 0; + f->sizelineinfo = 0; + f->linegaplog2 = 0; + f->linedefined = 0; + f->bytecodeid = 0; + f->sizetypeinfo = 0; + + f->feedbackvec = NULL; + f->feedbackvecsize = 0; + f->funid = 0; + f->optimized = nullptr; + f->deoptimized = nullptr; + f->cost = 0; + + return f; +} + +Closure* luaF_newLclosure(lua_State* L, int nelems, LuaTable* e, Proto* p) +{ + Closure* c = luaM_newgco(L, Closure, sizeLclosure(nelems), L->activememcat, LUA_TFUNCTION); + luaC_init(L, c, LUA_TFUNCTION); + c->isC = 0; + c->env = e; + c->nupvalues = cast_byte(nelems); + c->stacksize = p->maxstacksize; + c->preload = 0; + c->l.p = p; + for (int i = 0; i < nelems; ++i) + setnilvalue(&c->l.uprefs[i]); + return c; +} + +Closure* luaF_newCclosure(lua_State* L, int nelems, LuaTable* e) +{ + Closure* c = luaM_newgco(L, Closure, sizeCclosure(nelems), L->activememcat, LUA_TFUNCTION); + luaC_init(L, c, LUA_TFUNCTION); + c->isC = 1; + c->env = e; + c->nupvalues = cast_byte(nelems); + c->stacksize = LUA_MINSTACK; + c->preload = 0; + c->c.f = NULL; + c->c.cont = NULL; + c->c.debugname = NULL; + + return c; +} + +UpVal* luaF_findupval(lua_State* L, StkId level) +{ + global_State* g = L->global; + UpVal** pp = &L->openupval; + UpVal* p; + while (*pp != NULL && (p = *pp)->v >= level) + { + LUAU_ASSERT(!isdead(g, obj2gco(p))); + LUAU_ASSERT(upisopen(p)); + if (p->v == level) + return p; + + pp = &p->u.open.threadnext; + } + + LUAU_ASSERT(L->isactive); + LUAU_ASSERT(!isblack(obj2gco(L))); // we don't use luaC_threadbarrier because active threads never turn black + + UpVal* uv = luaM_newgco(L, UpVal, sizeof(UpVal), L->activememcat, LUA_TUPVAL); // not found: create a new one + luaC_init(L, uv, LUA_TUPVAL); + uv->markedopen = 0; + uv->v = level; // current value lives in the stack + + // chain the upvalue in the threads open upvalue list at the proper position + uv->u.open.threadnext = *pp; + *pp = uv; + + // double link the upvalue in the global open upvalue list + uv->u.open.prev = &g->uvhead; + uv->u.open.next = g->uvhead.u.open.next; + uv->u.open.next->u.open.prev = uv; + g->uvhead.u.open.next = uv; + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + + return uv; +} + +void luaF_freeupval(lua_State* L, UpVal* uv, lua_Page* page) +{ + luaM_freegco(L, uv, sizeof(UpVal), uv->memcat, page); // free upvalue +} + +void luaF_close(lua_State* L, StkId level) +{ + global_State* g = L->global; + UpVal* uv; + while (L->openupval != NULL && (uv = L->openupval)->v >= level) + { + GCObject* o = obj2gco(uv); + LUAU_ASSERT(!isblack(o) && upisopen(uv)); + LUAU_ASSERT(!isdead(g, o)); + + // unlink value *before* closing it since value storage overlaps + L->openupval = uv->u.open.threadnext; + + luaF_closeupval(L, uv, /* dead= */ false); + } +} + +void luaF_closeupval(lua_State* L, UpVal* uv, bool dead) +{ + // unlink value from all lists *before* closing it since value storage overlaps + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + uv->u.open.next->u.open.prev = uv->u.open.prev; + uv->u.open.prev->u.open.next = uv->u.open.next; + + if (dead) + return; + + setobj(L, &uv->u.value, uv->v); + uv->v = &uv->u.value; + luaC_upvalclosed(L, uv); +} + +void luaF_freeproto(lua_State* L, Proto* f, lua_Page* page) +{ + luaM_freearray(L, f->code, f->sizecode, Instruction, f->memcat); + luaM_freearray(L, f->p, f->sizep, Proto*, f->memcat); + luaM_freearray(L, f->k, f->sizek, TValue, f->memcat); + if (f->lineinfo) + luaM_freearray(L, f->lineinfo, f->sizelineinfo, uint8_t, f->memcat); + luaM_freearray(L, f->locvars, f->sizelocvars, struct LocVar, f->memcat); + luaM_freearray(L, f->upvalues, f->sizeupvalues, TString*, f->memcat); + if (f->debuginsn) + luaM_freearray(L, f->debuginsn, f->sizecode, uint8_t, f->memcat); + + if (f->execdata) + L->global->ecb.destroy(L, f); + + if (f->typeinfo) + luaM_freearray(L, f->typeinfo, f->sizetypeinfo, uint8_t, f->memcat); + + if (f->feedbackvec) + luaM_freearray(L, f->feedbackvec, f->feedbackvecsize, FeedbackVectorSlot, f->memcat); + + luaM_freegco(L, f, sizeof(Proto), f->memcat, page); +} + +void luaF_freeclosure(lua_State* L, Closure* c, lua_Page* page) +{ + int size = c->isC ? sizeCclosure(c->nupvalues) : sizeLclosure(c->nupvalues); + luaM_freegco(L, c, size, c->memcat, page); +} + +const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc) +{ + for (int i = 0; i < f->sizelocvars; i++) + { + if (pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc) + { // is variable active? + local_number--; + if (local_number == 0) + return &f->locvars[i]; + } + } + + return NULL; // not found +} + +const LocVar* luaF_findlocal(const Proto* f, int local_reg, int pc) +{ + for (int i = 0; i < f->sizelocvars; i++) + if (local_reg == f->locvars[i].reg && pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc) + return &f->locvars[i]; + + return NULL; // not found +} + +bool luaF_recordhit(lua_State* L, Closure* caller, Closure* target, uint32_t slotid) +{ + if (L->global->ecb.inlinefunction == nullptr) + return false; + + LUAU_ASSERT(!caller->isC); + Proto* callerp = caller->l.p; + if (target->isC) + return false; + Proto* targetp = target->l.p; + LUAU_ASSERT(slotid < callerp->feedbackvecsize); + FeedbackVectorSlot& slot = callerp->feedbackvec[slotid]; + LUAU_ASSERT(slot.kind == FeedbackVectorSlotKind::CALL_TARGET); + + if (slot.call_target.proto == 0) + slot.call_target.proto = targetp->funid; + + if (slot.call_target.proto != targetp->funid) + return false; + + slot.call_target.hits++; + + if (static_cast(slot.call_target.hits) >= FInt::LuauInlineHitsThreshold) + { + L->global->ecb.inlinefunction(L, caller, target, slot.call_target.pc); + return false; + } + + return true; +} diff --git a/External/luau/VM/src/lfunc.h b/External/luau/VM/src/lfunc.h new file mode 100644 index 00000000..c7fc33cb --- /dev/null +++ b/External/luau/VM/src/lfunc.h @@ -0,0 +1,34 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +#define sizeCclosure(n) (offsetof(Closure, c.upvals) + sizeof(TValue) * (n)) +#define sizeLclosure(n) (offsetof(Closure, l.uprefs) + sizeof(TValue) * (n)) +#define getproto(cl) ((cl)->isC ? nullptr : (FFlag::LuauPromoteProto && cl->l.p->optimized ? luaF_promoteproto(cl) : (cl)->l.p)) + +LUAI_FUNC Proto* luaF_newproto(lua_State* L); +LUAI_FUNC Closure* luaF_newLclosure(lua_State* L, int nelems, LuaTable* e, Proto* p); +LUAI_FUNC Closure* luaF_newCclosure(lua_State* L, int nelems, LuaTable* e); +LUAI_FUNC UpVal* luaF_findupval(lua_State* L, StkId level); +LUAI_FUNC void luaF_close(lua_State* L, StkId level); +LUAI_FUNC void luaF_closeupval(lua_State* L, UpVal* uv, bool dead); +LUAI_FUNC void luaF_freeproto(lua_State* L, Proto* f, struct lua_Page* page); +LUAI_FUNC void luaF_freeclosure(lua_State* L, Closure* c, struct lua_Page* page); +LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv, struct lua_Page* page); +LUAI_FUNC const LocVar* luaF_getlocal(const Proto* func, int local_number, int pc); +LUAI_FUNC const LocVar* luaF_findlocal(const Proto* func, int local_reg, int pc); +// A feedback slot is sealed when luaF_recordhit returns false. +LUAI_FUNC bool luaF_recordhit(lua_State* L, Closure* func, Closure* target, uint32_t slotid); +// Define it in header to force inlining +LUAI_FUNC inline Proto* luaF_promoteproto(Closure* cl) +{ + LUAU_ASSERT(!cl->isC); + while (cl->l.p->optimized != nullptr) + { + cl->l.p = cl->l.p->optimized; + cl->stacksize = cl->l.p->maxstacksize; + } + return cl->l.p; +} diff --git a/External/luau/VM/src/lgc.cpp b/External/luau/VM/src/lgc.cpp new file mode 100644 index 00000000..7f66931e --- /dev/null +++ b/External/luau/VM/src/lgc.cpp @@ -0,0 +1,1580 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lgc.h" + +#include "lobject.h" +#include "lstate.h" +#include "ltable.h" +#include "lfunc.h" +#include "lstring.h" +#include "ldo.h" +#include "lmem.h" +#include "ludata.h" +#include "lbuffer.h" +#include "lclass.h" +#include "lvector.h" + +#include + +#include + +LUAU_FASTFLAGVARIABLE(LuauGcTraceUdata) +LUAU_FLAGVERSION(LuauGcTraceUdata, 3) +LUAU_FASTFLAG(LuauBackedgeHeapCheck) +LUAU_FASTFLAG(LuauFastpcall) +LUAU_FASTFLAG(DebugLuauCoroutineFinally) +LUAU_FASTFLAG(LuauFrozenMetaButterfly) +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauGcHeapShrinkFix, false) + +/* + * Luau uses an incremental non-generational non-moving mark&sweep garbage collector. + * + * The collector runs in three stages: mark, atomic and sweep. Mark and sweep are incremental and try to do a limited amount + * of work every GC step; atomic is ran once per the GC cycle and is indivisible. In either case, the work happens during GC + * steps that are "scheduled" by the GC pacing algorithm - the steps happen either from explicit calls to lua_gc, or after + * the mutator (aka application) allocates some amount of memory, which is known as "GC assist". In either case, GC steps + * can't happen concurrently with other access to VM state. + * + * Current GC stage is stored in global_State::gcstate, and has two additional stages for pause and second-phase mark, explained below. + * + * GC pacer is an algorithm that tries to ensure that GC can always catch up to the application allocating garbage, but do this + * with minimal amount of effort. To configure the pacer Luau provides control over three variables: GC goal, defined as the + * target heap size during atomic phase in relation to live heap size (e.g. 200% goal means the heap's worst case size is double + * the total size of alive objects), step size (how many kilobytes should the application allocate for GC step to trigger), and + * GC multiplier (how much should the GC try to mark relative to how much the application allocated). It's critical that step + * multiplier is significantly above 1, as this is what allows the GC to catch up to the application's allocation rate, and + * GC goal and GC multiplier are linked in subtle ways, described in lua.h comments for LUA_GCSETGOAL. + * + * During mark, GC tries to identify all reachable objects and mark them as reachable, while keeping unreachable objects unmarked. + * During sweep, GC tries to sweep all objects that were not reachable at the end of mark. The atomic phase is needed to ensure + * that all pending marking has completed and all objects that are still marked as unreachable are, in fact, unreachable. + * + * Notably, during mark GC doesn't free any objects, and so the heap size constantly grows; during sweep, GC doesn't do any marking + * work, so it can't immediately free objects that became unreachable after sweeping started. + * + * Every collectable object has one of three colors at any given point in time: white, gray or black. This coloring scheme + * is necessary to implement incremental marking: white objects have not been marked and may be unreachable, black objects + * have been marked and will not be marked again if they stay black, and gray objects have been marked but may contain unmarked + * references. + * + * Objects are allocated as white; however, during sweep, we need to differentiate between objects that remained white in the mark + * phase (these are not reachable and can be freed) and objects that were allocated after the mark phase ended. Because of this, the + * colors are encoded using three bits inside GCheader::marked: white0, white1 and black (so technically we use a four-color scheme: + * any object can be white0, white1, gray or black). All bits are exclusive, and gray objects have all three bits unset. This allows + * us to have the "current" white bit, which is flipped during atomic stage - during sweeping, objects that have the white color from + * the previous mark may be deleted, and all other objects may or may not be reachable, and will be changed to the current white color, + * so that the next mark can start coloring objects from scratch again. + * + * Crucially, the coloring scheme comes with what's known as a tri-color invariant: a black object may never point to a white object. + * + * At the end of atomic stage, the expectation is that there are no gray objects anymore, which means all objects are either black + * (reachable) or white (unreachable = dead). Tri-color invariant is maintained throughout mark and atomic phase. To uphold this + * invariant, every modification of an object needs to check if the object is black and the new referent is white; if so, we + * need to either mark the referent, making it non-white (known as a forward barrier), or mark the object as gray and queue it + * for additional marking (known as a backward barrier). + * + * Luau uses both types of barriers. Forward barriers advance GC progress, since they don't create new outstanding work for GC, + * but they may be expensive when an object is modified many times in succession. Backward barriers are cheaper, as they defer + * most of the work until "later", but they require queueing the object for a rescan which isn't always possible. Table writes usually + * use backward barriers (but switch to forward barriers during second-phase mark), whereas upvalue writes and setmetatable use forward + * barriers. + * + * Since marking is incremental, it needs a way to track progress, which is implemented as a gray set: at any point, objects that + * are gray need to mark their white references, objects that are black have no pending work, and objects that are white have not yet + * been reached. Once the gray set is empty, the work completes; as such, incremental marking is as simple as removing an object from + * the gray set, and turning it to black (which requires turning all its white references to gray). The gray set is implemented as + * an intrusive singly linked list, using `gclist` field in multiple objects (functions, tables, threads and protos). When an object + * doesn't have gclist field, the marking of that object needs to be "immediate", changing the colors of all references in one go. + * + * When a black object is modified, it needs to become gray again. Objects like this are placed on a separate `grayagain` list by a + * barrier - this is important because it allows us to have a mark stage that terminates when the gray set is empty even if the mutator + * is constantly changing existing objects to gray. After mark stage finishes traversing `gray` list, we copy `grayagain` list to `gray` + * once and incrementally mark it again. During this phase of marking, we may get more objects marked as `grayagain`, so after we finish + * emptying out the `gray` list the second time, we finish the mark stage and do final marking of `grayagain` during atomic phase. + * GC works correctly without this second-phase mark (called GCSpropagateagain), but it reduces the time spent during atomic phase. + * + * Sweeping is also incremental, but instead of working at a granularity of an object, it works at a granularity of a page: all GC + * objects are allocated in special pages (see lmem.cpp for details), and sweeper traverses all objects in one page in one incremental + * step, freeing objects that aren't reachable (old white), and recoloring all other objects with the new white to prepare them for next + * mark. During sweeping we don't need to maintain the GC invariant, because our goal is to paint all objects with current white - + * however, some barriers will still trigger (because some reachable objects are still black as sweeping didn't get to them yet), and + * some barriers will proactively mark black objects as white to avoid extra barriers from triggering excessively. + * + * Most references that GC deals with are strong, and as such they fit neatly into the incremental marking scheme. Some, however, are + * weak - notably, tables can be marked as having weak keys/values (using __mode metafield). During incremental marking, we don't know + * for certain if a given object is alive - if it's marked as black, it definitely was reachable during marking, but if it's marked as + * white, we don't know if it's actually unreachable. Because of this, we need to defer weak table handling to the atomic phase; after + * all objects are marked, we traverse all weak tables (that are linked into special weak table lists using `gclist` during marking), + * and remove all entries that have white keys or values. If keys or values are strong, they are marked normally. + * + * The simplified scheme described above isn't fully accurate because of threads and upvalues. + * + * Threads are hard to deal with because for them to fit into the white-gray-black scheme, writes to thread stacks need to have barriers + * that turn the thread from black (already scanned) to gray - but this is very expensive because stack writes are very common. To + * get around this problem, threads have an "active" state which means that a thread is actively executing code. When GC reaches an active + * thread, it keeps it as gray, and rescans it during atomic phase. When a thread is inactive, GC instead paints the thread black. All + * API calls that can write to thread stacks outside of execution (which implies active) uses a thread barrier that checks if the thread is + * black, and if it is it marks it as gray and puts it on a gray list to be rescanned during atomic phase. + * + * Upvalues are special objects that can be closed, in which case they contain the value (acting as a reference cell) and can be dealt + * with using the regular algorithm, or open, in which case they refer to a stack slot in some other thread. These are difficult to deal + * with because the stack writes are not monitored. Because of this open upvalues are treated in a somewhat special way: they are never marked + * as black (doing so would violate the GC invariant), and they are kept in a special global list (global_State::uvhead) which is traversed + * during atomic phase. This is needed because an open upvalue might point to a stack location in a dead thread that never marked the stack + * slot - upvalues like this are identified since they don't have `markedopen` bit set during thread traversal and closed in `clearupvals`. + */ + +#define GC_SWEEPPAGESTEPCOST 16 + +#define GC_INTERRUPT(state) \ + { \ + void (*interrupt)(lua_State*, int) = g->cb.interrupt; \ + if (LUAU_UNLIKELY(!!interrupt)) \ + interrupt(L, state); \ + } + +#define maskmarks cast_byte(~(bitmask(BLACKBIT) | WHITEBITS)) + +#define makewhite(g, x) ((x)->gch.marked = cast_byte(((x)->gch.marked & maskmarks) | luaC_white(g))) + +#define white2gray(x) reset2bits((x)->gch.marked, WHITE0BIT, WHITE1BIT) +#define black2gray(x) resetbit((x)->gch.marked, BLACKBIT) + +#define stringmark(s) (reset2bits((s)->marked, WHITE0BIT, WHITE1BIT), l_setbit((s)->marked, BLACKBIT)) + +#define markvalue(g, o) \ + { \ + checkconsistency(o); \ + if (iscollectable(o) && iswhite(gcvalue(o))) \ + reallymarkobject(g, gcvalue(o)); \ + } + +#define markobject(g, t) \ + { \ + if (iswhite(obj2gco(t))) \ + reallymarkobject(g, obj2gco(t)); \ + } + +#ifdef LUAI_GCMETRICS +static void recordGcStateStep(global_State* g, int startgcstate, double seconds, bool assist, size_t work) +{ + switch (startgcstate) + { + case GCSpause: + // record root mark time if we have switched to next state + if (g->gcstate == GCSpropagate) + { + g->gcmetrics.currcycle.marktime += seconds; + + if (assist) + g->gcmetrics.currcycle.markassisttime += seconds; + } + break; + case GCSpropagate: + case GCSpropagateagain: + g->gcmetrics.currcycle.marktime += seconds; + g->gcmetrics.currcycle.markwork += work; + + if (assist) + g->gcmetrics.currcycle.markassisttime += seconds; + break; + case GCSatomic: + g->gcmetrics.currcycle.atomictime += seconds; + break; + case GCSsweep: + g->gcmetrics.currcycle.sweeptime += seconds; + g->gcmetrics.currcycle.sweepwork += work; + + if (assist) + g->gcmetrics.currcycle.sweepassisttime += seconds; + break; + default: + LUAU_ASSERT(!"Unexpected GC state"); + } + + if (assist) + { + g->gcmetrics.stepassisttimeacc += seconds; + g->gcmetrics.currcycle.assistwork += work; + } + else + { + g->gcmetrics.stepexplicittimeacc += seconds; + g->gcmetrics.currcycle.explicitwork += work; + } +} + +static double recordGcDeltaTime(double& timer) +{ + double now = lua_clock(); + double delta = now - timer; + timer = now; + return delta; +} + +static void startGcCycleMetrics(global_State* g) +{ + g->gcmetrics.currcycle.starttimestamp = lua_clock(); + g->gcmetrics.currcycle.pausetime = g->gcmetrics.currcycle.starttimestamp - g->gcmetrics.lastcycle.endtimestamp; +} + +static void finishGcCycleMetrics(global_State* g) +{ + g->gcmetrics.currcycle.endtimestamp = lua_clock(); + g->gcmetrics.currcycle.endtotalsizebytes = g->totalbytes; + + g->gcmetrics.completedcycles++; + g->gcmetrics.lastcycle = g->gcmetrics.currcycle; + g->gcmetrics.currcycle = GCCycleMetrics(); + + g->gcmetrics.currcycle.starttotalsizebytes = g->totalbytes; + g->gcmetrics.currcycle.heaptriggersizebytes = g->GCthreshold; +} +#endif + +static void removeentry(LuaNode* n) +{ + LUAU_ASSERT(ttisnil(gval(n))); + if (iscollectable(gkey(n))) + setttype(gkey(n), LUA_TDEADKEY); // dead key; remove it +} + +static void reallymarkobject(global_State* g, GCObject* o) +{ + LUAU_ASSERT(iswhite(o) && !isdead(g, o)); + white2gray(o); + switch (o->gch.tt) + { + case LUA_TSTRING: + { + gray2black(o); // strings are never gray + return; + } + case LUA_TUSERDATA: + { + if (FFlag::LuauGcTraceUdata) + { + gray2black(o); // udata are never gray + Udata* u = gco2u(o); + if (u->tag < LUA_UTAG_LIMIT) + if (lua_UserdataMark markfn = g->udatamark[u->tag]) + markfn(g->mainthread, u->data); + if (LuaTable* mt = u->metatable) + markobject(g, mt); + return; + } + else + { + LuaTable* mt = gco2u(o)->metatable; + gray2black(o); // udata are never gray + if (mt) + markobject(g, mt); + return; + } + } + case LUA_TUPVAL: + { + UpVal* uv = gco2uv(o); + markvalue(g, uv->v); + if (!upisopen(uv)) // closed? + gray2black(o); // open upvalues are never black + return; + } + case LUA_TFUNCTION: + { + gco2cl(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TTABLE: + { + gco2h(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TTHREAD: + { + gco2th(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TVECTOR: + { + gray2black(o); // vectors are never gray + return; + } + case LUA_TBUFFER: + { + gray2black(o); // buffers are never gray + return; + } + case LUA_TPROTO: + { + gco2p(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TCLASS: + { + gco2class(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TOBJECT: + { + gco2object(o)->gclist = g->gray; + g->gray = o; + break; + } + default: + LUAU_ASSERT(0); + } +} + +static const char* gettablemode(global_State* g, LuaTable* h) +{ + const TValue* mode = gfasttm(g, h->metatable, TM_MODE); + + if (mode && ttisstring(mode)) + return svalue(mode); + + return NULL; +} + +static int traversetable(global_State* g, LuaTable* h) +{ + int i; + int weakkey = 0; + int weakvalue = 0; + if (h->metatable) + markobject(g, cast_to(LuaTable*, h->metatable)); + + // is there a weak mode? + if (const char* modev = gettablemode(g, h)) + { + weakkey = (strchr(modev, 'k') != NULL); + weakvalue = (strchr(modev, 'v') != NULL); + if (weakkey || weakvalue) + { // is really weak? + h->gclist = g->weak; // must be cleared after GC, ... + g->weak = obj2gco(h); // ... so put in the appropriate list + } + } + + if (weakkey && weakvalue) + return 1; + if (!weakvalue) + { + i = h->sizearray; + while (i--) + markvalue(g, &h->array[i]); + } + i = sizenode(h); + while (i--) + { + LuaNode* n = gnode(h, i); + LUAU_ASSERT(ttype(gkey(n)) != LUA_TDEADKEY || ttisnil(gval(n))); + if (ttisnil(gval(n))) + removeentry(n); // remove empty entries + else + { + LUAU_ASSERT(!ttisnil(gkey(n))); + if (!weakkey) + markvalue(g, gkey(n)); + if (!weakvalue) + markvalue(g, gval(n)); + } + } + return weakkey || weakvalue; +} + +/* +** All marks are conditional because a GC may happen while the +** prototype is still being created +*/ +static void traverseproto(global_State* g, Proto* f) +{ + int i; + if (f->source) + stringmark(f->source); + if (f->debugname) + stringmark(f->debugname); + for (i = 0; i < f->sizek; i++) // mark literals + markvalue(g, &f->k[i]); + for (i = 0; i < f->sizeupvalues; i++) + { // mark upvalue names + if (f->upvalues[i]) + stringmark(f->upvalues[i]); + } + for (i = 0; i < f->sizep; i++) + { // mark nested protos + if (f->p[i]) + markobject(g, f->p[i]); + } + for (i = 0; i < f->sizelocvars; i++) + { // mark local-variable names + if (f->locvars[i].varname) + stringmark(f->locvars[i].varname); + } + if (f->optimized) + markobject(g, f->optimized); + + if (f->deoptimized) + markobject(g, f->deoptimized); +} + +static void traverseclosure(global_State* g, Closure* cl) +{ + markobject(g, cl->env); + if (cl->isC) + { + if (TString* str = cl->c.debugname) + stringmark(str); + + int i; + for (i = 0; i < cl->nupvalues; i++) // mark its upvalues + markvalue(g, &cl->c.upvals[i]); + } + else + { + int i; + LUAU_ASSERT(cl->nupvalues == cl->l.p->nups); + markobject(g, cast_to(Proto*, cl->l.p)); + for (i = 0; i < cl->nupvalues; i++) // mark its upvalues + markvalue(g, &cl->l.uprefs[i]); + } +} + +static void traversestack(global_State* g, lua_State* l) +{ + markobject(g, l->gt); + + if (l->namecall) + stringmark(l->namecall); + + if (FFlag::DebugLuauCoroutineFinally && l->finalizers) + markobject(g, l->finalizers); + + for (StkId o = l->stack; o < l->top; o++) + markvalue(g, o); + + for (UpVal* uv = l->openupval; uv; uv = uv->u.open.threadnext) + { + LUAU_ASSERT(upisopen(uv)); + uv->markedopen = 1; + markobject(g, uv); + } +} + +static void traverseclass(global_State* g, LuauClass* classobject) +{ + markobject(g, classobject->name); + if (classobject->super) + markobject(g, classobject->super); + markobject(g, classobject->memberstooffset); + for (uint32_t i = 0; i < classobject->numberofallmembers; i++) + markobject(g, classobject->offsettomember[i]); + for (uint32_t i = 0; i < classobject->numberofallmembers - classobject->numberofinstancemembers; i++) + markvalue(g, &classobject->staticmembers[i]); + if (classobject->metatable) + markobject(g, classobject->metatable); + if (classobject->instancemetatable) + markobject(g, classobject->instancemetatable); +} + +static void traverseobject(global_State* g, LuauObject* classinst) +{ + markobject(g, classinst->lclass); + for (uint32_t i = 0; i < classinst->numberofmembers; i++) + markvalue(g, &classinst->members[i]); +} + +static void clearstack(lua_State* l) +{ + StkId stack_end = l->stack + l->stacksize; + for (StkId o = l->top; o < stack_end; o++) // clear not-marked stack slice + setnilvalue(o); +} + +static void shrinkstack(lua_State* L) +{ + // compute used stack - note that we can't use th->top if we're in the middle of vararg call + StkId lim = L->top; + for (CallInfo* ci = L->base_ci; ci <= L->ci; ci++) + { + LUAU_ASSERT(ci->top <= L->stack_last); + if (lim < ci->top) + lim = ci->top; + } + + // shrink stack and callinfo arrays if we aren't using most of the space + int ci_used = cast_int(L->ci - L->base_ci); // number of `ci' in use + int s_used = cast_int(lim - L->stack); // part of stack in use + if (L->size_ci > LUAI_MAXCALLS) // handling overflow? + return; // do not touch the stacks + + if (3 * size_t(ci_used) < size_t(L->size_ci) && 2 * BASIC_CI_SIZE < L->size_ci) + luaD_reallocCI(L, L->size_ci / 2); // still big enough... + condhardstacktests(luaD_reallocCI(L, ci_used + 1)); + + if (3 * size_t(s_used) < size_t(L->stacksize) && 2 * (BASIC_STACK_SIZE + EXTRA_STACK) < L->stacksize) + luaD_reallocstack(L, L->stacksize / 2, 0); // still big enough... + condhardstacktests(luaD_reallocstack(L, s_used, 0)); +} + +static void shrinkstackprotected(lua_State* L) +{ + struct CallContext + { + static void run(lua_State* L, void* ud) + { + shrinkstack(L); + } + } ctx = {}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + +/* +** traverse one gray object, turning it to black. +** Returns `quantity' traversed. +*/ +static size_t propagatemark(global_State* g) +{ + GCObject* o = g->gray; + LUAU_ASSERT(isgray(o)); + gray2black(o); + switch (o->gch.tt) + { + case LUA_TTABLE: + { + LuaTable* h = gco2h(o); + g->gray = h->gclist; + if (traversetable(g, h)) // table is weak? + black2gray(o); // keep it gray + + return sizeof(LuaTable) + sizeof(TValue) * h->sizearray + sizeof(LuaNode) * (h->node == &luaH_dummynode ? 0 : sizenode(h)); + } + case LUA_TFUNCTION: + { + Closure* cl = gco2cl(o); + g->gray = cl->gclist; + traverseclosure(g, cl); + return cl->isC ? sizeCclosure(cl->nupvalues) : sizeLclosure(cl->nupvalues); + } + case LUA_TTHREAD: + { + lua_State* th = gco2th(o); + g->gray = th->gclist; + + bool active = th->isactive || th == th->global->mainthread; + + traversestack(g, th); + + // active threads will need to be rescanned later to mark new stack writes so we mark them gray again + if (active) + { + th->gclist = g->grayagain; + g->grayagain = o; + + black2gray(o); + } + + // the stack needs to be cleared after the last modification of the thread state before sweep begins + // if the thread is inactive, we might not see the thread in this cycle so we must clear it now + if (!active || g->gcstate == GCSatomic) + clearstack(th); + + // we could shrink stack at any time but we opt to do it during initial mark to do that just once per cycle + if (g->gcstate == GCSpropagate) + shrinkstackprotected(th); + + return sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci; + } + case LUA_TPROTO: + { + Proto* p = gco2p(o); + g->gray = p->gclist; + traverseproto(g, p); + + return sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo + + sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues + p->sizetypeinfo; + } + case LUA_TCLASS: + { + LuauClass* classobject = gco2class(o); + g->gray = classobject->gclist; + traverseclass(g, classobject); + // We've traversed the "object" itself ... + return sizeof(LuauClass) + + // ... plus the method closures, each a `TValue` wide ... + ((classobject->numberofallmembers - classobject->numberofinstancemembers) * sizeof(TValue)) + + // ... plus a string pointer for each method or property, each a pointer wide. + (classobject->numberofallmembers * sizeof(TString*)); + } + case LUA_TOBJECT: + { + LuauObject* classinst = gco2object(o); + g->gray = classinst->gclist; + traverseobject(g, classinst); + // We've traversed the instance ... + return sizeof(LuauObject) + + // ... plus all of the instance fields. + classinst->numberofmembers * sizeof(TValue); + } + default: + LUAU_ASSERT(0); + return 0; + } +} + +static void embeddermarkref(lua_State* L, int ref) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + if (ref <= LUA_REFNIL) + return; + LuaTable* wt = hvalue(&L->global->weakregistry); + const TValue* slot = luaH_getnum(wt, ref); + if (iscollectable(slot) && iswhite(gcvalue(slot))) + reallymarkobject(L->global, gcvalue(slot)); +} + +static size_t propagateall(global_State* g) +{ + size_t work = 0; + while (g->gray) + { + work += propagatemark(g); + } + return work; +} + +/* +** The next function tells whether a key or value can be cleared from +** a weak table. Non-collectable objects are never removed from weak +** tables. Strings behave as `values', so are never removed too. for +** other objects: if really collected, cannot keep them. +*/ +static int isobjcleared(GCObject* o) +{ + if (o->gch.tt == LUA_TSTRING) + { + stringmark(&o->ts); // strings are `values', so are never weak + return 0; + } + + return iswhite(o); +} + +#define iscleared(o) (iscollectable(o) && isobjcleared(gcvalue(o))) + +static void tableresizeprotected(lua_State* L, LuaTable* t, int nhsize) +{ + struct CallContext + { + LuaTable* t; + int nhsize; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaH_resizehash(L, ctx->t, ctx->nhsize); + } + } ctx = {t, nhsize}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + +static void rebuildmetacache(lua_State* L, LuaTable* h) +{ + uint8_t tmcache = 0; + + for (int event = 0; event < TM_N; ++event) + { + const TValue* value = luaH_getstr(h, L->global->tmname[event]); + setobj(L, getmetacache(h, event), value); + + if (event <= TM_EQ && ttisnil(value)) + tmcache |= cast_byte(1u << event); + } + + h->tmcache = tmcache; +} + +/* +** clear collected entries from weaktables +*/ +static size_t cleartable(lua_State* L, GCObject* l) +{ + size_t work = 0; + while (l) + { + LuaTable* h = gco2h(l); + + work += sizeof(LuaTable) + sizeof(TValue) * h->sizearray + sizeof(LuaNode) * (h->node == &luaH_dummynode ? 0 : sizenode(h)); + + int i = h->sizearray; + while (i--) + { + TValue* o = &h->array[i]; + if (iscleared(o)) // value was collected? + setnilvalue(o); // remove value + } + i = sizenode(h); + int activevalues = 0; + while (i--) + { + LuaNode* n = gnode(h, i); + + // non-empty entry? + if (!ttisnil(gval(n))) + { + // can we clear key or value? + if (iscleared(gkey(n)) || iscleared(gval(n))) + { + setnilvalue(gval(n)); // remove value ... + removeentry(n); // remove entry from table + } + else + { + activevalues++; + } + } + } + + if (FFlag::LuauFrozenMetaButterfly && hasmetacache(h)) + rebuildmetacache(L, h); + + if (!FFlag::LuauFrozenMetaButterfly || !h->readonly) // frozen tables do not shrink + { + if (const char* modev = gettablemode(L->global, h)) + { + // are we allowed to shrink this weak table? + if (strchr(modev, 's')) + { + // shrink at 37.5% occupancy + if (activevalues < sizenode(h) * 3 / 8) + tableresizeprotected(L, h, activevalues); + } + } + } + + l = h->gclist; + } + return work; +} + +static void freeobj(lua_State* L, GCObject* o, lua_Page* page) +{ + switch (o->gch.tt) + { + case LUA_TPROTO: + luaF_freeproto(L, gco2p(o), page); + break; + case LUA_TFUNCTION: + luaF_freeclosure(L, gco2cl(o), page); + break; + case LUA_TUPVAL: + luaF_freeupval(L, gco2uv(o), page); + break; + case LUA_TTABLE: + luaH_free(L, gco2h(o), page); + break; + case LUA_TTHREAD: + LUAU_ASSERT(gco2th(o) != L && gco2th(o) != L->global->mainthread); + luaE_freethread(L, gco2th(o), page); + break; + case LUA_TSTRING: + luaS_free(L, gco2ts(o), page); + break; + case LUA_TUSERDATA: + luaU_freeudata(L, gco2u(o), page); + break; + case LUA_TVECTOR: + luaVec_freevector(L, gco2vec(o), page); + break; + case LUA_TBUFFER: + luaB_freebuffer(L, gco2buf(o), page); + break; + case LUA_TCLASS: + luaR_freeclass(L, gco2class(o), page); + break; + case LUA_TOBJECT: + luaR_freeobject(L, gco2object(o), page); + break; + default: + LUAU_ASSERT(0); + } +} + +static void stringresizeprotected(lua_State* L, int newsize) +{ + struct CallContext + { + int newsize; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaS_resize(L, ctx->newsize); + } + } ctx = {newsize}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + +static void shrinkbuffers(lua_State* L) +{ + global_State* g = L->global; + // check size of string hash + if (g->strt.nuse < cast_to(uint32_t, g->strt.size / 4) && g->strt.size > LUA_MINSTRTABSIZE * 2) + stringresizeprotected(L, g->strt.size / 2); // table is too big +} + +static void shrinkbuffersfull(lua_State* L) +{ + global_State* g = L->global; + // check size of string hash + int hashsize = g->strt.size; + while (g->strt.nuse < cast_to(uint32_t, hashsize / 4) && hashsize > LUA_MINSTRTABSIZE * 2) + hashsize /= 2; + if (hashsize != g->strt.size) + stringresizeprotected(L, hashsize); // table is too big +} + +static bool deletegco(void* context, lua_Page* page, GCObject* gco) +{ + lua_State* L = (lua_State*)context; + freeobj(L, gco, page); + return true; +} + +void luaC_freeall(lua_State* L) +{ + global_State* g = L->global; + + LUAU_ASSERT(L == g->mainthread); + + luaM_visitgco(L, L, deletegco); + + for (int i = 0; i < g->strt.size; i++) // free all string lists + LUAU_ASSERT(g->strt.hash[i] == NULL); + + LUAU_ASSERT(L->global->strt.nuse == 0); +} + +static void markudatadirectaccess(global_State* g) +{ + for (int i = 0; i < UTAG_INTERNAL_LIMIT; i++) + { + lua_UdataDirectAccessData& udatadirect = g->udatadirect[i]; + + markvalue(g, &udatadirect.indextm); + markvalue(g, &udatadirect.newindextm); + markvalue(g, &udatadirect.namecalltm); + } +} + +static void markudatadirectfields(global_State* g) +{ + for (int i = 0; i < UTAG_INTERNAL_LIMIT; i++) + if (g->udatadirectfields[i]) + markobject(g, g->udatadirectfields[i]); +} + +static void markmt(global_State* g) +{ + int i; + for (i = 0; i < LUA_T_COUNT; i++) + if (g->mt[i]) + markobject(g, g->mt[i]); +} + +static void marktaggetmt(global_State* g) +{ + for (int i = 0; i < LUA_UTAG_LIMIT; i++) + { + if (g->udatamt[i]) + markobject(g, g->udatamt[i]); + } +} + +static void markfastpcalls(global_State* g) +{ + if (g->builtinPcall) + markobject(g, g->builtinPcall); + + if (g->builtinXpcall) + markobject(g, g->builtinXpcall); +} + +// mark root set +static void markroot(lua_State* L) +{ + global_State* g = L->global; + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + markobject(g, g->mainthread); + // make global table be traversed before main stack + markobject(g, g->mainthread->gt); + markvalue(g, registry(L)); + if (FFlag::LuauGcTraceUdata) + { + markvalue(g, &g->weakregistry); + if (g->embeddergc) + g->embeddergc(g->mainthread, nullptr); + } + + markudatadirectaccess(g); + + markudatadirectfields(g); + + if (FFlag::LuauFastpcall) + markfastpcalls(g); + + markmt(g); + + marktaggetmt(g); + + g->gcstate = GCSpropagate; +} + +static size_t remarkupvals(global_State* g) +{ + size_t work = 0; + + for (UpVal* uv = g->uvhead.u.open.next; uv != &g->uvhead; uv = uv->u.open.next) + { + work += sizeof(UpVal); + + LUAU_ASSERT(upisopen(uv)); + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + LUAU_ASSERT(!isblack(obj2gco(uv))); // open upvalues are never black + + if (isgray(obj2gco(uv))) + markvalue(g, uv->v); + } + + return work; +} + +static size_t clearupvals(lua_State* L) +{ + global_State* g = L->global; + + size_t work = 0; + + for (UpVal* uv = g->uvhead.u.open.next; uv != &g->uvhead;) + { + work += sizeof(UpVal); + + LUAU_ASSERT(upisopen(uv)); + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + LUAU_ASSERT(!isblack(obj2gco(uv))); // open upvalues are never black + LUAU_ASSERT(iswhite(obj2gco(uv)) || !iscollectable(uv->v) || !iswhite(gcvalue(uv->v))); + + if (uv->markedopen) + { + // upvalue is still open (belongs to alive thread) + LUAU_ASSERT(isgray(obj2gco(uv))); + uv->markedopen = 0; // for next cycle + uv = uv->u.open.next; + } + else + { + // upvalue is either dead, or alive but the thread is dead; unlink and close + UpVal* next = uv->u.open.next; + luaF_closeupval(L, uv, /* dead= */ iswhite(obj2gco(uv))); + uv = next; + } + } + + return work; +} + +static size_t atomic(lua_State* L) +{ + global_State* g = L->global; + LUAU_ASSERT(g->gcstate == GCSatomic); + + size_t work = 0; + +#ifdef LUAI_GCMETRICS + double currts = lua_clock(); +#endif + + // remark occasional upvalues of (maybe) dead threads + work += remarkupvals(g); + // traverse objects caught by write barrier and by 'remarkupvals' + work += propagateall(g); + +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomictimeupval += recordGcDeltaTime(currts); +#endif + + // remark weak tables + g->gray = g->weak; + g->weak = NULL; + LUAU_ASSERT(!iswhite(obj2gco(g->mainthread))); + + markobject(g, L); // mark running thread + markmt(g); // mark basic metatables (again) + + marktaggetmt(g); // mark tagged userdata metatables (again) + + markudatadirectaccess(g); // mark tagged userdata direct access functions (again) + + markudatadirectfields(g); // mark direct field dispatch tables (again) + + if (FFlag::LuauFastpcall) + markfastpcalls(g); + + work += propagateall(g); + +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomictimeweak += recordGcDeltaTime(currts); +#endif + + // remark gray again + g->gray = g->grayagain; + g->grayagain = NULL; + work += propagateall(g); + +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomictimegray += recordGcDeltaTime(currts); +#endif + + // fixed-point algorithm for embedder references + if (FFlag::LuauGcTraceUdata && g->embeddergc) + { + g->embeddergc(g->mainthread, embeddermarkref); + while (g->gray) + { + work += propagateall(g); + g->embeddergc(g->mainthread, embeddermarkref); + } + } + +#ifdef LUAI_GCMETRICS + if (FFlag::LuauGcTraceUdata) + g->gcmetrics.currcycle.atomictimeembedder += recordGcDeltaTime(currts); +#endif + + // remove collected objects from weak tables + work += cleartable(L, g->weak); + g->weak = NULL; + +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomictimeclear += recordGcDeltaTime(currts); +#endif + + // close orphaned live upvalues of dead threads and clear dead upvalues + work += clearupvals(L); + +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomictimeupval += recordGcDeltaTime(currts); +#endif + + // flip current white + g->currentwhite = cast_byte(otherwhite(g)); + g->sweepgcopage = g->allgcopages; + g->sweepgcopage_cage = g->allgcopages_cage; + g->gcstate = GCSsweep; + + return work; +} + +// a version of generic luaM_visitpage specialized for the main sweep stage +static int sweepgcopage(lua_State* L, lua_Page* page) +{ + char* start; + char* end; + int busyBlocks; + int blockSize; + luaM_getpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + LUAU_ASSERT(busyBlocks > 0); + + global_State* g = L->global; + + int deadmask = otherwhite(g); + LUAU_ASSERT(testbit(deadmask, FIXEDBIT)); // make sure we never sweep fixed objects + + int newwhite = luaC_white(g); + + for (char* pos = start; pos != end; pos += blockSize) + { + GCObject* gco = (GCObject*)pos; + + // skip memory blocks that are already freed + if (gco->gch.tt == LUA_TNIL) + continue; + + // is the object alive? + if ((gco->gch.marked ^ WHITEBITS) & deadmask) + { + LUAU_ASSERT(!isdead(g, gco)); + // make it white (for next cycle) + gco->gch.marked = cast_byte((gco->gch.marked & maskmarks) | newwhite); + } + else + { + LUAU_ASSERT(isdead(g, gco)); + freeobj(L, gco, page); + + // if the last block was removed, page would be removed as well + if (--busyBlocks == 0) + return int(pos - start) / blockSize + 1; + } + } + + return int(end - start) / blockSize; +} + +static size_t gcstep(lua_State* L, size_t limit) +{ + size_t cost = 0; + global_State* g = L->global; + switch (g->gcstate) + { + case GCSpause: + { + markroot(L); // start a new collection + LUAU_ASSERT(g->gcstate == GCSpropagate); + break; + } + case GCSpropagate: + { + while (g->gray && cost < limit) + { + cost += propagatemark(g); + } + + if (!g->gray) + { +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.propagatework = g->gcmetrics.currcycle.explicitwork + g->gcmetrics.currcycle.assistwork; +#endif + + // perform one iteration over 'gray again' list + g->gray = g->grayagain; + g->grayagain = NULL; + + g->gcstate = GCSpropagateagain; + } + break; + } + case GCSpropagateagain: + { + while (g->gray && cost < limit) + { + cost += propagatemark(g); + } + + if (!g->gray) // no more `gray' objects + { +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.propagateagainwork = + g->gcmetrics.currcycle.explicitwork + g->gcmetrics.currcycle.assistwork - g->gcmetrics.currcycle.propagatework; +#endif + + g->gcstate = GCSatomic; + } + break; + } + case GCSatomic: + { +#ifdef LUAI_GCMETRICS + g->gcmetrics.currcycle.atomicstarttimestamp = lua_clock(); + g->gcmetrics.currcycle.atomicstarttotalsizebytes = g->totalbytes; +#endif + + g->gcstats.atomicstarttimestamp = lua_clock(); + g->gcstats.atomicstarttotalsizebytes = g->totalbytes; + + cost = atomic(L); // finish mark phase + + LUAU_ASSERT(g->gcstate == GCSsweep); + break; + } + case GCSsweep: + { + while (g->sweepgcopage && cost < limit) + { + lua_Page* next = luaM_getnextpage(g->sweepgcopage); // page sweep might destroy the page + + int steps = sweepgcopage(L, g->sweepgcopage); + + g->sweepgcopage = next; + cost += static_cast(steps * GC_SWEEPPAGESTEPCOST); + } + + while (g->sweepgcopage_cage && cost < limit) + { + lua_Page* next = luaM_getnextpage(g->sweepgcopage_cage); // page sweep might destroy the page + + int steps = sweepgcopage(L, g->sweepgcopage_cage); + + g->sweepgcopage_cage = next; + cost += static_cast(steps * GC_SWEEPPAGESTEPCOST); + } + + // nothing more to sweep? + if (g->sweepgcopage == NULL && g->sweepgcopage_cage == NULL) + { + // don't forget to visit main thread, it's the only object not allocated in GCO pages + LUAU_ASSERT(!isdead(g, obj2gco(g->mainthread))); + makewhite(g, obj2gco(g->mainthread)); // make it white (for next cycle) + + shrinkbuffers(L); + + g->gcstate = GCSpause; // end collection + } + break; + } + default: + LUAU_ASSERT(!"Unexpected GC state"); + } + return cost; +} + +static int64_t getheaptriggererroroffset(global_State* g) +{ + // adjust for error using Proportional-Integral controller + // https://en.wikipedia.org/wiki/PID_controller + int32_t errorKb = int32_t((g->gcstats.atomicstarttotalsizebytes - g->gcstats.heapgoalsizebytes) / 1024); + + // we use sliding window for the error integral to avoid error sum 'windup' when the desired target cannot be reached + const size_t triggertermcount = sizeof(g->gcstats.triggerterms) / sizeof(g->gcstats.triggerterms[0]); + + int32_t* slot = &g->gcstats.triggerterms[g->gcstats.triggertermpos % triggertermcount]; + int32_t prev = *slot; + *slot = errorKb; + g->gcstats.triggerintegral += errorKb - prev; + g->gcstats.triggertermpos++; + + // controller tuning + // https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method + const double Ku = 0.9; // ultimate gain (measured) + const double Tu = 2.5; // oscillation period (measured) + + const double Kp = 0.45 * Ku; // proportional gain + const double Ti = 0.8 * Tu; + const double Ki = 0.54 * Ku / Ti; // integral gain + + double proportionalTerm = Kp * errorKb; + double integralTerm = Ki * g->gcstats.triggerintegral; + + double totalTerm = proportionalTerm + integralTerm; + + return int64_t(totalTerm * 1024); +} + +// the heap can shrink between measurements (after a full collection, or when thread stacks are shrunk during marking), which counts as no growth +static size_t getheapgrowth(size_t current, size_t previous) +{ + return current > previous ? current - previous : 0; +} + +static size_t getheaptrigger(global_State* g, size_t heapgoal) +{ + // adjust threshold based on a guess of how many bytes will be allocated between the cycle start and sweep phase + // our goal is to begin the sweep when used memory has reached the heap goal + const double durationthreshold = 1e-3; + double allocationduration = g->gcstats.atomicstarttimestamp - g->gcstats.endtimestamp; + + // avoid measuring intervals smaller than 1ms + if (allocationduration < durationthreshold) + return heapgoal; + + double allocationrate; + if (DFFlag::LuauGcHeapShrinkFix) + allocationrate = getheapgrowth(g->gcstats.atomicstarttotalsizebytes, g->gcstats.endtotalsizebytes) / allocationduration; + else + allocationrate = (g->gcstats.atomicstarttotalsizebytes - g->gcstats.endtotalsizebytes) / allocationduration; + double markduration = g->gcstats.atomicstarttimestamp - g->gcstats.starttimestamp; + + int64_t expectedgrowth = int64_t(markduration * allocationrate); + int64_t offset = getheaptriggererroroffset(g); + int64_t heaptrigger = heapgoal - (expectedgrowth + offset); + + // clamp the trigger between memory use at the end of the cycle and the heap goal + return heaptrigger < int64_t(g->totalbytes) ? g->totalbytes : (heaptrigger > int64_t(heapgoal) ? heapgoal : size_t(heaptrigger)); +} + +size_t luaC_step(lua_State* L, bool assist) +{ + global_State* g = L->global; + + size_t lim = g->gcstepsize * g->gcstepmul / 100; // how much to work + LUAU_ASSERT(g->totalbytes >= g->GCthreshold); + size_t debt = g->totalbytes - g->GCthreshold; + + GC_INTERRUPT(0); + + // at the start of the new cycle + if (g->gcstate == GCSpause) + g->gcstats.starttimestamp = lua_clock(); + +#ifdef LUAI_GCMETRICS + if (g->gcstate == GCSpause) + startGcCycleMetrics(g); + + double lasttimestamp = lua_clock(); +#endif + + // if allocations outpace the GC to require an assist, adjust step size to cover the difference + if ((FFlag::LuauBackedgeHeapCheck || LUA_VECTOR_DOUBLE == 1) && assist) + { + size_t need = debt * g->gcstepmul / 100; + + if (need > lim) + lim = need; + } + + int lastgcstate = g->gcstate; + + size_t work = gcstep(L, lim); + +#ifdef LUAI_GCMETRICS + recordGcStateStep(g, lastgcstate, lua_clock() - lasttimestamp, assist, work); +#endif + + size_t actualstepsize = work * 100 / g->gcstepmul; + + // at the end of the last cycle + if (g->gcstate == GCSpause) + { + // at the end of a collection cycle, set goal based on gcgoal setting + size_t heapgoal = (g->totalbytes / 100) * g->gcgoal; + size_t heaptrigger = getheaptrigger(g, heapgoal); + + g->GCthreshold = heaptrigger; + + g->gcstats.heapgoalsizebytes = heapgoal; + g->gcstats.endtimestamp = lua_clock(); + g->gcstats.endtotalsizebytes = g->totalbytes; + +#ifdef LUAI_GCMETRICS + finishGcCycleMetrics(g); +#endif + } + else + { + g->GCthreshold = g->totalbytes + actualstepsize; + + // compensate if GC is "behind schedule" (has some debt to pay) + if (g->GCthreshold >= debt) + g->GCthreshold -= debt; + } + + GC_INTERRUPT(lastgcstate); + + return actualstepsize; +} + +void luaC_fullgc(lua_State* L) +{ + global_State* g = L->global; + +#ifdef LUAI_GCMETRICS + if (g->gcstate == GCSpause) + startGcCycleMetrics(g); +#endif + + if (keepinvariant(g)) + { + // reset sweep marks to sweep all elements (returning them to white) + g->sweepgcopage = g->allgcopages; + g->sweepgcopage_cage = g->allgcopages_cage; + // reset other collector lists + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + g->gcstate = GCSsweep; + } + LUAU_ASSERT(g->gcstate == GCSpause || g->gcstate == GCSsweep); + // finish any pending sweep phase + while (g->gcstate != GCSpause) + { + LUAU_ASSERT(g->gcstate == GCSsweep); + gcstep(L, SIZE_MAX); + } + + // clear markedopen bits for all open upvalues; these might be stuck from half-finished mark prior to full gc + for (UpVal* uv = g->uvhead.u.open.next; uv != &g->uvhead; uv = uv->u.open.next) + { + LUAU_ASSERT(upisopen(uv)); + uv->markedopen = 0; + } + +#ifdef LUAI_GCMETRICS + finishGcCycleMetrics(g); + startGcCycleMetrics(g); +#endif + + // run a full collection cycle + markroot(L); + while (g->gcstate != GCSpause) + { + gcstep(L, SIZE_MAX); + } + // reclaim as much buffer memory as possible (shrinkbuffers() called during sweep is incremental) + shrinkbuffersfull(L); + + size_t heapgoalsizebytes = (g->totalbytes / 100) * g->gcgoal; + + // trigger cannot be correctly adjusted after a forced full GC. + // we will try to place it so that we can reach the goal based on + // the rate at which we run the GC relative to allocation rate + // and on amount of bytes we need to traverse in propagation stage. + // goal and stepmul are defined in percents + g->GCthreshold = g->totalbytes * (g->gcgoal * g->gcstepmul / 100 - 100) / g->gcstepmul; + + // but it might be impossible to satisfy that directly + if (g->GCthreshold < g->totalbytes) + g->GCthreshold = g->totalbytes; + + g->gcstats.heapgoalsizebytes = heapgoalsizebytes; + + if (DFFlag::LuauGcHeapShrinkFix) + { + // full collection ends a cycle, so the heap growth is measured from this point + g->gcstats.endtimestamp = lua_clock(); + g->gcstats.endtotalsizebytes = g->totalbytes; + } + +#ifdef LUAI_GCMETRICS + finishGcCycleMetrics(g); +#endif +} + +void luaC_barrierf(lua_State* L, GCObject* o, GCObject* v) +{ + global_State* g = L->global; + LUAU_ASSERT(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); + LUAU_ASSERT(g->gcstate != GCSpause); + // must keep invariant? + if (keepinvariant(g)) + reallymarkobject(g, v); // restore invariant + else // don't mind + makewhite(g, o); // mark as white just to avoid other barriers +} + +void luaC_barriertable(lua_State* L, LuaTable* t, GCObject* v) +{ + global_State* g = L->global; + GCObject* o = obj2gco(t); + + // in the second propagation stage, table assignment barrier works as a forward barrier + if (g->gcstate == GCSpropagateagain) + { + LUAU_ASSERT(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); + reallymarkobject(g, v); + return; + } + + LUAU_ASSERT(isblack(o) && !isdead(g, o)); + LUAU_ASSERT(g->gcstate != GCSpause); + black2gray(o); // make table gray (again) + t->gclist = g->grayagain; + g->grayagain = o; +} + +void luaC_barrierback(lua_State* L, GCObject* o, GCObject** gclist) +{ + global_State* g = L->global; + LUAU_ASSERT(isblack(o) && !isdead(g, o)); + LUAU_ASSERT(g->gcstate != GCSpause); + + black2gray(o); // make object gray (again) + *gclist = g->grayagain; + g->grayagain = o; +} + +void luaC_upvalclosed(lua_State* L, UpVal* uv) +{ + global_State* g = L->global; + GCObject* o = obj2gco(uv); + + LUAU_ASSERT(!upisopen(uv)); // upvalue was closed but needs GC state fixup + + if (isgray(o)) + { + if (keepinvariant(g)) + { + gray2black(o); // closed upvalues need barrier + luaC_barrier(L, uv, uv->v); + } + else + { // sweep phase: sweep it (turning it into white) + makewhite(g, o); + LUAU_ASSERT(g->gcstate != GCSpause); + } + } +} + +// measure the allocation rate in bytes/sec +// returns -1 if allocation rate cannot be measured +int64_t luaC_allocationrate(lua_State* L) +{ + global_State* g = L->global; + const double durationthreshold = 1e-3; // avoid measuring intervals smaller than 1ms + + if (g->gcstate <= GCSatomic) + { + double duration = lua_clock() - g->gcstats.endtimestamp; + + if (duration < durationthreshold) + return -1; + + if (DFFlag::LuauGcHeapShrinkFix) + return int64_t(getheapgrowth(g->totalbytes, g->gcstats.endtotalsizebytes) / duration); + + return int64_t((g->totalbytes - g->gcstats.endtotalsizebytes) / duration); + } + + // totalbytes is unstable during the sweep, use the rate measured at the end of mark phase + double duration = g->gcstats.atomicstarttimestamp - g->gcstats.endtimestamp; + + if (duration < durationthreshold) + return -1; + + if (DFFlag::LuauGcHeapShrinkFix) + return int64_t(getheapgrowth(g->gcstats.atomicstarttotalsizebytes, g->gcstats.endtotalsizebytes) / duration); + + return int64_t((g->gcstats.atomicstarttotalsizebytes - g->gcstats.endtotalsizebytes) / duration); +} + +const char* luaC_statename(int state) +{ + switch (state) + { + case GCSpause: + return "pause"; + + case GCSpropagate: + return "mark"; + + case GCSpropagateagain: + return "remark"; + + case GCSatomic: + return "atomic"; + + case GCSsweep: + return "sweep"; + + default: + return NULL; + } +} diff --git a/External/luau/VM/src/lgc.h b/External/luau/VM/src/lgc.h new file mode 100644 index 00000000..f698435b --- /dev/null +++ b/External/luau/VM/src/lgc.h @@ -0,0 +1,150 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "ldo.h" +#include "lobject.h" +#include "lstate.h" + +/* +** Default settings for GC tunables (settable via lua_gc) +*/ +#define LUAI_GCGOAL 200 // 200% (allow heap to double compared to live heap size) +#define LUAI_GCSTEPMUL 200 // GC runs 'twice the speed' of memory allocation +#define LUAI_GCSTEPSIZE 1 // GC runs every KB of memory allocation + +/* +** Possible states of the Garbage Collector +*/ +#define GCSpause 0 +#define GCSpropagate 1 +#define GCSpropagateagain 2 +#define GCSatomic 3 +#define GCSsweep 4 + +/* +** The main invariant of the garbage collector, while marking objects, +** is that a black object can never point to a white one. This invariant +** is not being enforced during a sweep phase, and is restored when sweep +** ends. +*/ +#define keepinvariant(g) ((g)->gcstate == GCSpropagate || (g)->gcstate == GCSpropagateagain || (g)->gcstate == GCSatomic) + +/* +** some useful bit tricks +*/ +#define resetbits(x, m) ((x) &= cast_to(uint8_t, ~(m))) +#define setbits(x, m) ((x) |= (m)) +#define testbits(x, m) ((x) & (m)) +#define bitmask(b) (1 << (b)) +#define bit2mask(b1, b2) (bitmask(b1) | bitmask(b2)) +#define l_setbit(x, b) setbits(x, bitmask(b)) +#define resetbit(x, b) resetbits(x, bitmask(b)) +#define testbit(x, b) testbits(x, bitmask(b)) +#define set2bits(x, b1, b2) setbits(x, (bit2mask(b1, b2))) +#define reset2bits(x, b1, b2) resetbits(x, (bit2mask(b1, b2))) +#define test2bits(x, b1, b2) testbits(x, (bit2mask(b1, b2))) + +/* +** Layout for bit use in `marked' field: +** bit 0 - object is white (type 0) +** bit 1 - object is white (type 1) +** bit 2 - object is black +** bit 3 - object is fixed (should not be collected) +*/ + +#define WHITE0BIT 0 +#define WHITE1BIT 1 +#define BLACKBIT 2 +#define FIXEDBIT 3 +#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT) + +#define iswhite(x) test2bits((x)->gch.marked, WHITE0BIT, WHITE1BIT) +#define isblack(x) testbit((x)->gch.marked, BLACKBIT) +#define isgray(x) (!testbits((x)->gch.marked, WHITEBITS | bitmask(BLACKBIT))) +#define isfixed(x) testbit((x)->gch.marked, FIXEDBIT) + +#define otherwhite(g) (g->currentwhite ^ WHITEBITS) +#define isdead(g, v) (((v)->gch.marked & (WHITEBITS | bitmask(FIXEDBIT))) == (otherwhite(g) & WHITEBITS)) + +#define changewhite(x) ((x)->gch.marked ^= WHITEBITS) +#define gray2black(x) l_setbit((x)->gch.marked, BLACKBIT) + +#define luaC_white(g) cast_to(uint8_t, ((g)->currentwhite) & WHITEBITS) + +#define luaC_needsGC(L) (L->global->totalbytes >= L->global->GCthreshold) + +#define luaC_checkGC(L) \ + { \ + condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK, 0)); \ + if (luaC_needsGC(L)) \ + { \ + condhardmemtests(luaC_validate(L), 1); \ + luaC_step(L, true); \ + } \ + else \ + { \ + condhardmemtests(luaC_validate(L), 2); \ + } \ + } + +#define luaC_barrier(L, p, v) \ + { \ + if (iscollectable(v) && isblack(obj2gco(p)) && iswhite(gcvalue(v))) \ + luaC_barrierf(L, obj2gco(p), gcvalue(v)); \ + } + +#define luaC_barriert(L, t, v) \ + { \ + if (iscollectable(v) && isblack(obj2gco(t)) && iswhite(gcvalue(v))) \ + luaC_barriertable(L, t, gcvalue(v)); \ + } + +#define luaC_barrierfast(L, t) \ + { \ + if (isblack(obj2gco(t))) \ + luaC_barrierback(L, obj2gco(t), &t->gclist); \ + } + +#define luaC_objbarrier(L, p, o) \ + { \ + if (isblack(obj2gco(p)) && iswhite(obj2gco(o))) \ + luaC_barrierf(L, obj2gco(p), obj2gco(o)); \ + } + +#define luaC_threadbarrier(L) \ + { \ + if (isblack(obj2gco(L))) \ + luaC_barrierback(L, obj2gco(L), &L->gclist); \ + } + +#define luaC_objectbarrier(L) \ + { \ + if (isblack(obj2gco(L))) \ + luaC_barrierback(L, obj2gco(L), &L->gclist); \ + } + +#define luaC_init(L, o, tt_) \ + { \ + o->marked = luaC_white(L->global); \ + o->tt = tt_; \ + o->memcat = L->activememcat; \ + } + +LUAI_FUNC void luaC_freeall(lua_State* L); +LUAI_FUNC size_t luaC_step(lua_State* L, bool assist); +LUAI_FUNC void luaC_fullgc(lua_State* L); +LUAI_FUNC void luaC_upvalclosed(lua_State* L, UpVal* uv); +LUAI_FUNC void luaC_barrierf(lua_State* L, GCObject* o, GCObject* v); +LUAI_FUNC void luaC_barriertable(lua_State* L, LuaTable* t, GCObject* v); +LUAI_FUNC void luaC_barrierback(lua_State* L, GCObject* o, GCObject** gclist); +LUAI_FUNC void luaC_validate(lua_State* L); +LUAI_FUNC void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State* L, uint8_t memcat)); +LUAI_FUNC void luaC_enumheap( + lua_State* L, + void* context, + void (*node)(void* context, void* ptr, uint8_t tt, uint8_t memcat, size_t size, const char* name), + void (*edge)(void* context, void* from, void* to, const char* name) +); +LUAI_FUNC int64_t luaC_allocationrate(lua_State* L); +LUAI_FUNC const char* luaC_statename(int state); diff --git a/External/luau/VM/src/lgcdebug.cpp b/External/luau/VM/src/lgcdebug.cpp new file mode 100644 index 00000000..65e060b7 --- /dev/null +++ b/External/luau/VM/src/lgcdebug.cpp @@ -0,0 +1,1197 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lgc.h" + +#include "lfunc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ludata.h" +#include "lbuffer.h" + +#include +#include + +LUAU_FASTFLAGVARIABLE(LuauEnumMoreEdges) +LUAU_FASTFLAG(LuauFrozenMetaButterfly) + +static void validateobjref(global_State* g, GCObject* f, GCObject* t) +{ + LUAU_ASSERT(!isdead(g, t)); + + if (keepinvariant(g)) + { + // basic incremental invariant: black can't point to white + LUAU_ASSERT(!(isblack(f) && iswhite(t))); + } +} + +static void validateref(global_State* g, GCObject* f, TValue* v) +{ + if (iscollectable(v)) + { + LUAU_ASSERT(ttype(v) == gcvalue(v)->gch.tt); + validateobjref(g, f, gcvalue(v)); + } +} + +static void validatetable(global_State* g, LuaTable* h) +{ + int sizenode = 1 << h->lsizenode; + + LUAU_ASSERT(h->lastfree <= sizenode); + + if (h->metatable) + validateobjref(g, obj2gco(h), obj2gco(h->metatable)); + + for (int i = 0; i < h->sizearray; ++i) + validateref(g, obj2gco(h), &h->array[i]); + + for (int i = 0; i < sizenode; ++i) + { + LuaNode* n = &h->node[i]; + + LUAU_ASSERT(ttype(gkey(n)) != LUA_TDEADKEY || ttisnil(gval(n))); + LUAU_ASSERT(i + gnext(n) >= 0 && i + gnext(n) < sizenode); + + if (!ttisnil(gval(n))) + { + TValue k = {}; + k.tt = gkey(n)->tt; + k.value = gkey(n)->value; + + validateref(g, obj2gco(h), &k); + validateref(g, obj2gco(h), gval(n)); + } + } +} + +static void validateclosure(global_State* g, Closure* cl) +{ + validateobjref(g, obj2gco(cl), obj2gco(cl->env)); + + if (cl->isC) + { + for (int i = 0; i < cl->nupvalues; ++i) + validateref(g, obj2gco(cl), &cl->c.upvals[i]); + } + else + { + LUAU_ASSERT(cl->nupvalues == cl->l.p->nups); + + validateobjref(g, obj2gco(cl), obj2gco(cl->l.p)); + + for (int i = 0; i < cl->nupvalues; ++i) + validateref(g, obj2gco(cl), &cl->l.uprefs[i]); + } +} + +static void validatestack(global_State* g, lua_State* l) +{ + validateobjref(g, obj2gco(l), obj2gco(l->gt)); + + for (CallInfo* ci = l->base_ci; ci <= l->ci; ++ci) + { + LUAU_ASSERT(l->stack <= ci->base); + LUAU_ASSERT(ci->func <= ci->base && ci->base <= ci->top); + LUAU_ASSERT(ci->top <= l->stack_last); + } + + // note: stack refs can violate gc invariant so we only check for liveness + for (StkId o = l->stack; o < l->top; ++o) + checkliveness(g, o); + + if (l->namecall) + validateobjref(g, obj2gco(l), obj2gco(l->namecall)); + + if (l->finalizers) + validateobjref(g, obj2gco(l), obj2gco(l->finalizers)); + + for (UpVal* uv = l->openupval; uv; uv = uv->u.open.threadnext) + { + LUAU_ASSERT(uv->tt == LUA_TUPVAL); + LUAU_ASSERT(upisopen(uv)); + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + LUAU_ASSERT(!isblack(obj2gco(uv))); // open upvalues are never black + } +} + +static void validateproto(global_State* g, Proto* f) +{ + if (f->source) + validateobjref(g, obj2gco(f), obj2gco(f->source)); + + if (f->debugname) + validateobjref(g, obj2gco(f), obj2gco(f->debugname)); + + for (int i = 0; i < f->sizek; ++i) + validateref(g, obj2gco(f), &f->k[i]); + + for (int i = 0; i < f->sizeupvalues; ++i) + if (f->upvalues[i]) + validateobjref(g, obj2gco(f), obj2gco(f->upvalues[i])); + + for (int i = 0; i < f->sizep; ++i) + if (f->p[i]) + validateobjref(g, obj2gco(f), obj2gco(f->p[i])); + + for (int i = 0; i < f->sizelocvars; i++) + if (f->locvars[i].varname) + validateobjref(g, obj2gco(f), obj2gco(f->locvars[i].varname)); +} + +static void validateclass(global_State* g, LuauClass* lco) +{ + GCObject* obj = obj2gco(lco); + validateobjref(g, obj, obj2gco(lco->name)); + if (lco->super) + validateobjref(g, obj, obj2gco(lco->super)); + validateobjref(g, obj, obj2gco(lco->memberstooffset)); + for (uint32_t i = 0; i < lco->numberofallmembers; i++) + { + validateobjref(g, obj, obj2gco(lco->offsettomember[i])); + if (i >= lco->numberofinstancemembers) + validateref(g, obj, &lco->staticmembers[i - lco->numberofinstancemembers]); + } + if (lco->metatable) + validateobjref(g, obj, obj2gco(lco->metatable)); + if (lco->instancemetatable) + validateobjref(g, obj, obj2gco(lco->instancemetatable)); +} + +static void validateobject(global_State* g, LuauObject* inst) +{ + GCObject* obj = obj2gco(inst); + validateobjref(g, obj, obj2gco(inst->lclass)); + for (uint32_t i = 0; i < inst->numberofmembers; i++) + validateref(g, obj, &inst->members[i]); +} + +static void validateobj(global_State* g, GCObject* o) +{ + // dead objects can only occur during sweep + if (isdead(g, o)) + { + LUAU_ASSERT(g->gcstate == GCSsweep); + return; + } + + switch (o->gch.tt) + { + case LUA_TSTRING: + break; + + case LUA_TTABLE: + validatetable(g, gco2h(o)); + break; + + case LUA_TFUNCTION: + validateclosure(g, gco2cl(o)); + break; + + case LUA_TUSERDATA: + if (gco2u(o)->metatable) + validateobjref(g, o, obj2gco(gco2u(o)->metatable)); + break; + + case LUA_TTHREAD: + validatestack(g, gco2th(o)); + break; + + case LUA_TVECTOR: + break; + + case LUA_TBUFFER: + break; + + case LUA_TPROTO: + validateproto(g, gco2p(o)); + break; + + case LUA_TUPVAL: + validateref(g, o, gco2uv(o)->v); + break; + + case LUA_TCLASS: + validateclass(g, gco2class(o)); + break; + + case LUA_TOBJECT: + validateobject(g, gco2object(o)); + break; + + default: + LUAU_ASSERT(!"unexpected object type"); + } +} + +static void validategraylist(global_State* g, GCObject* o) +{ + if (!keepinvariant(g)) + return; + + while (o) + { + LUAU_ASSERT(isgray(o)); + + switch (o->gch.tt) + { + case LUA_TTABLE: + o = gco2h(o)->gclist; + break; + case LUA_TFUNCTION: + o = gco2cl(o)->gclist; + break; + case LUA_TTHREAD: + o = gco2th(o)->gclist; + break; + case LUA_TCLASS: + o = gco2class(o)->gclist; + break; + case LUA_TOBJECT: + o = gco2object(o)->gclist; + break; + case LUA_TPROTO: + o = gco2p(o)->gclist; + break; + default: + LUAU_ASSERT(!"unknown object in gray list"); + return; + } + } +} + +static bool validategco(void* context, lua_Page* page, GCObject* gco) +{ + lua_State* L = (lua_State*)context; + global_State* g = L->global; + + validateobj(g, gco); + return false; +} + +void luaC_validate(lua_State* L) +{ + global_State* g = L->global; + + LUAU_ASSERT(!isdead(g, obj2gco(g->mainthread))); + checkliveness(g, &g->registry); + + for (int i = 0; i < LUA_T_COUNT; ++i) + { + if (g->mt[i]) + LUAU_ASSERT(!isdead(g, obj2gco(g->mt[i]))); + } + + for (int i = 0; i < LUA_UTAG_LIMIT; i++) + { + if (g->udatamt[i]) + LUAU_ASSERT(!isdead(g, obj2gco(g->udatamt[i]))); + } + + for (int i = 0; i < UTAG_INTERNAL_LIMIT; i++) + { + checkliveness(g, &g->udatadirect[i].indextm); + checkliveness(g, &g->udatadirect[i].newindextm); + checkliveness(g, &g->udatadirect[i].namecalltm); + + if (g->udatadirectfields[i]) + LUAU_ASSERT(!isdead(g, obj2gco(g->udatadirectfields[i]))); + } + + validategraylist(g, g->weak); + validategraylist(g, g->gray); + validategraylist(g, g->grayagain); + + validategco(L, NULL, obj2gco(g->mainthread)); + + luaM_visitgco(L, L, validategco); + + for (UpVal* uv = g->uvhead.u.open.next; uv != &g->uvhead; uv = uv->u.open.next) + { + LUAU_ASSERT(uv->tt == LUA_TUPVAL); + LUAU_ASSERT(upisopen(uv)); + LUAU_ASSERT(uv->u.open.next->u.open.prev == uv && uv->u.open.prev->u.open.next == uv); + LUAU_ASSERT(!isblack(obj2gco(uv))); // open upvalues are never black + } +} + +inline bool safejson(char ch) +{ + return unsigned(ch) < 128 && ch >= 32 && ch != '\\' && ch != '\"'; +} + +static void dumpref(FILE* f, GCObject* o) +{ + fprintf(f, "\"%p\"", o); +} + +static void dumprefs(FILE* f, TValue* data, size_t size) +{ + bool first = true; + + for (size_t i = 0; i < size; ++i) + { + if (iscollectable(&data[i])) + { + if (!first) + fputc(',', f); + first = false; + + dumpref(f, gcvalue(&data[i])); + } + } +} + +static void dumpstringdata(FILE* f, const char* data, size_t len) +{ + for (size_t i = 0; i < len; ++i) + fputc(safejson(data[i]) ? data[i] : '?', f); +} + +static void dumpstring(FILE* f, TString* ts) +{ + fprintf(f, "{\"type\":\"string\",\"cat\":%d,\"size\":%d,\"data\":\"", ts->memcat, int(sizestring(ts->len))); + dumpstringdata(f, ts->data, ts->len); + fprintf(f, "\"}"); +} + +static void dumptable(FILE* f, LuaTable* h) +{ + size_t size = sizeof(LuaTable) + (h->node == &luaH_dummynode ? 0 : sizenode(h) * sizeof(LuaNode)) + h->sizearray * sizeof(TValue); + + if (FFlag::LuauFrozenMetaButterfly && hasmetacache(h)) + size += TM_N * sizeof(TValue); + + fprintf(f, "{\"type\":\"table\",\"cat\":%d,\"size\":%d", h->memcat, int(size)); + + if (h->node != &luaH_dummynode) + { + fprintf(f, ",\"pairs\":["); + + bool first = true; + + for (int i = 0; i < sizenode(h); ++i) + { + const LuaNode& n = h->node[i]; + + if (!ttisnil(&n.val) && (iscollectable(&n.key) || iscollectable(&n.val))) + { + if (!first) + fputc(',', f); + first = false; + + if (iscollectable(&n.key)) + dumpref(f, gcvalue(&n.key)); + else + fprintf(f, "null"); + + fputc(',', f); + + if (iscollectable(&n.val)) + dumpref(f, gcvalue(&n.val)); + else + fprintf(f, "null"); + } + } + + fprintf(f, "]"); + } + if (h->sizearray) + { + fprintf(f, ",\"array\":["); + dumprefs(f, h->array, h->sizearray); + fprintf(f, "]"); + } + if (h->metatable) + { + fprintf(f, ",\"metatable\":"); + dumpref(f, obj2gco(h->metatable)); + } + fprintf(f, "}"); +} + +static void dumpclosure(FILE* f, Closure* cl) +{ + fprintf( + f, "{\"type\":\"function\",\"cat\":%d,\"size\":%d", cl->memcat, cl->isC ? int(sizeCclosure(cl->nupvalues)) : int(sizeLclosure(cl->nupvalues)) + ); + + fprintf(f, ",\"env\":"); + dumpref(f, obj2gco(cl->env)); + + if (cl->isC) + { + if (TString* str = cl->c.debugname) + fprintf(f, ",\"name\":\"%s\"", getstr(str)); + + if (cl->nupvalues) + { + fprintf(f, ",\"upvalues\":["); + dumprefs(f, cl->c.upvals, cl->nupvalues); + fprintf(f, "]"); + } + } + else + { + if (cl->l.p->debugname) + fprintf(f, ",\"name\":\"%s\"", getstr(cl->l.p->debugname)); + + fprintf(f, ",\"proto\":"); + dumpref(f, obj2gco(cl->l.p)); + if (cl->nupvalues) + { + fprintf(f, ",\"upvalues\":["); + dumprefs(f, cl->l.uprefs, cl->nupvalues); + fprintf(f, "]"); + } + } + fprintf(f, "}"); +} + +static void dumpudata(FILE* f, Udata* u) +{ + fprintf(f, "{\"type\":\"userdata\",\"cat\":%d,\"size\":%d,\"tag\":%d", u->memcat, int(sizeudata(u->len)), u->tag); + + if (u->metatable) + { + fprintf(f, ",\"metatable\":"); + dumpref(f, obj2gco(u->metatable)); + } + fprintf(f, "}"); +} + +static void dumpthread(FILE* f, lua_State* th) +{ + size_t size = sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci; + + fprintf(f, "{\"type\":\"thread\",\"cat\":%d,\"size\":%d", th->memcat, int(size)); + + fprintf(f, ",\"env\":"); + dumpref(f, obj2gco(th->gt)); + + if (th->finalizers) + { + fprintf(f, ",\"finalizers\":"); + dumpref(f, obj2gco(th->finalizers)); + } + + Proto* cip = nullptr; + for (CallInfo* ci = th->base_ci; ci <= th->ci; ++ci) + { + if (ttisfunction(ci->func)) + { + cip = ci->p; + break; + } + } + + if (cip != nullptr && cip->source) + { + fprintf(f, ",\"source\":\""); + dumpstringdata(f, cip->source->data, cip->source->len); + fprintf(f, "\",\"line\":%d", cip->linedefined); + } + + if (th->top > th->stack) + { + fprintf(f, ",\"stack\":["); + dumprefs(f, th->stack, th->top - th->stack); + fprintf(f, "]"); + + CallInfo* ci = th->base_ci; + bool first = true; + + fprintf(f, ",\"stacknames\":["); + for (StkId v = th->stack; v < th->top; ++v) + { + if (!iscollectable(v)) + continue; + + while (ci < th->ci && v >= (ci + 1)->func) + ci++; + + if (!first) + fputc(',', f); + first = false; + + if (v == ci->func) + { + Closure* cl = ci_func(ci); + + if (cl->isC) + { + fprintf(f, "\"frame:%s\"", cl->c.debugname ? getstr(cl->c.debugname) : "[C]"); + } + else + { + Proto* p = ci->p; + fprintf(f, "\"frame:"); + if (p->source) + dumpstringdata(f, p->source->data, p->source->len); + fprintf(f, ":%d:%s\"", p->linedefined, p->debugname ? getstr(p->debugname) : ""); + } + } + else if (isLua(ci)) + { + Proto* p = ci->p; + int pc = pcRel(ci->savedpc, p); + const LocVar* var = luaF_findlocal(p, int(v - ci->base), pc); + + if (var && var->varname) + fprintf(f, "\"%s\"", getstr(var->varname)); + else + fprintf(f, "null"); + } + else + fprintf(f, "null"); + } + fprintf(f, "]"); + } + fprintf(f, "}"); +} + +static void dumpbuffer(FILE* f, Buffer* b) +{ + fprintf(f, "{\"type\":\"buffer\",\"cat\":%d,\"size\":%d}", b->memcat, int(sizebuffer(b->len))); +} + +static void dumpproto(FILE* f, Proto* p) +{ + size_t size = sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo + + sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues; + + fprintf(f, "{\"type\":\"proto\",\"cat\":%d,\"size\":%d", p->memcat, int(size)); + + if (p->source) + { + fprintf(f, ",\"source\":\""); + dumpstringdata(f, p->source->data, p->source->len); + fprintf(f, "\",\"line\":%d", p->abslineinfo ? p->abslineinfo[0] : 0); + } + + if (p->sizek) + { + fprintf(f, ",\"constants\":["); + dumprefs(f, p->k, p->sizek); + fprintf(f, "]"); + } + + if (p->sizep) + { + fprintf(f, ",\"protos\":["); + for (int i = 0; i < p->sizep; ++i) + { + if (i != 0) + fputc(',', f); + dumpref(f, obj2gco(p->p[i])); + } + fprintf(f, "]"); + } + + fprintf(f, "}"); +} + +static void dumpupval(FILE* f, UpVal* uv) +{ + fprintf(f, "{\"type\":\"upvalue\",\"cat\":%d,\"size\":%d,\"open\":%s", uv->memcat, int(sizeof(UpVal)), upisopen(uv) ? "true" : "false"); + + if (iscollectable(uv->v)) + { + fprintf(f, ",\"object\":"); + dumpref(f, gcvalue(uv->v)); + } + + fprintf(f, "}"); +} + +static void dumpclass(FILE* f, LuauClass* lco) +{ + fprintf(f, R"({"type":"class","cat":%d,"size":%d)", lco->memcat, int(sizeof(LuauClass))); + fprintf(f, R"(,"name":)"); + dumpstringdata(f, lco->name->data, lco->name->len); + fprintf(f, R"(,"super":)"); + if (lco->super) + dumpref(f, obj2gco(lco->super)); + else + fprintf(f, "null"); + fprintf(f, R"(,"membernames":[)"); + for (uint32_t i = 0; i < lco->numberofallmembers; i++) + { + if (i != 0) + fputc(',', f); + dumpref(f, (GCObject*)lco->offsettomember[i]); + } + fprintf(f, R"(],"staticmembers":[)"); + dumprefs(f, lco->staticmembers, lco->numberofallmembers - lco->numberofinstancemembers); + fprintf(f, R"(],"metatable":)"); + if (lco->metatable) + dumpref(f, obj2gco(lco->metatable)); + else + fprintf(f, "null"); + fprintf(f, R"(,"instancemetatable":)"); + if (lco->instancemetatable) + dumpref(f, obj2gco(lco->instancemetatable)); + else + fprintf(f, "null"); + fprintf(f, R"(,"memberstooffset":)"); + dumpref(f, obj2gco(lco->memberstooffset)); + fprintf(f, "}"); +} + +static void dumpobject(FILE* f, LuauObject* inst) +{ + fprintf(f, R"({"type":"object","cat":%d,"size":%d)", inst->memcat, int(sizeof(LuauObject))); + fprintf(f, R"(,"class":)"); + dumpref(f, obj2gco(inst->lclass)); + fprintf(f, R"(,"members":[)"); + dumprefs(f, inst->members, inst->numberofmembers); + fprintf(f, "]}"); +} + +static void dumpobj(FILE* f, GCObject* o) +{ + switch (o->gch.tt) + { + case LUA_TSTRING: + return dumpstring(f, gco2ts(o)); + + case LUA_TTABLE: + return dumptable(f, gco2h(o)); + + case LUA_TFUNCTION: + return dumpclosure(f, gco2cl(o)); + + case LUA_TUSERDATA: + return dumpudata(f, gco2u(o)); + + case LUA_TTHREAD: + return dumpthread(f, gco2th(o)); + + case LUA_TVECTOR: + return; // vector data is outlined, but is a constant cost of a vector + + case LUA_TBUFFER: + return dumpbuffer(f, gco2buf(o)); + + case LUA_TCLASS: + return dumpclass(f, gco2class(o)); + + case LUA_TOBJECT: + return dumpobject(f, gco2object(o)); + + case LUA_TPROTO: + return dumpproto(f, gco2p(o)); + + case LUA_TUPVAL: + return dumpupval(f, gco2uv(o)); + + default: + LUAU_ASSERT(0); + } +} + +static bool dumpgco(void* context, lua_Page* page, GCObject* gco) +{ + FILE* f = (FILE*)context; + + dumpref(f, gco); + fputc(':', f); + dumpobj(f, gco); + fputc(',', f); + fputc('\n', f); + + return false; +} + +void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State* L, uint8_t memcat)) +{ + global_State* g = L->global; + FILE* f = static_cast(file); + + fprintf(f, "{\"objects\":{\n"); + + dumpgco(f, NULL, obj2gco(g->mainthread)); + + luaM_visitgco(L, f, dumpgco); + + fprintf(f, "\"0\":{\"type\":\"userdata\",\"cat\":0,\"size\":0}\n"); // to avoid issues with trailing , + fprintf(f, "},\"roots\":{\n"); + fprintf(f, "\"mainthread\":"); + dumpref(f, obj2gco(g->mainthread)); + fprintf(f, ",\"registry\":"); + dumpref(f, gcvalue(&g->registry)); + + fprintf(f, "},\"stats\":{\n"); + + fprintf(f, "\"size\":%d,\n", int(g->totalbytes)); + + fprintf(f, "\"categories\":{\n"); + for (int i = 0; i < LUA_MEMORY_CATEGORIES; i++) + { + if (size_t bytes = g->memcatbytes[i]) + { + if (categoryName) + fprintf(f, "\"%d\":{\"name\":\"%s\", \"size\":%d},\n", i, categoryName(L, i), int(bytes)); + else + fprintf(f, "\"%d\":{\"size\":%d},\n", i, int(bytes)); + } + } + fprintf(f, "\"none\":{}\n"); // to avoid issues with trailing , + fprintf(f, "}\n"); + fprintf(f, "}}\n"); +} + +struct EnumContext +{ + lua_State* L; + void* context; + void (*node)(void* context, void* ptr, uint8_t tt, uint8_t memcat, size_t size, const char* name); + void (*edge)(void* context, void* from, void* to, const char* name); +}; + +static void* enumtopointer(GCObject* gco) +{ + // To match lua_topointer, userdata pointer is represented as a pointer to internal data + return gco->gch.tt == LUA_TUSERDATA ? (void*)gco2u(gco)->data : (void*)gco; +} + +static void enumnode(EnumContext* ctx, GCObject* gco, size_t size, const char* objname) +{ + ctx->node(ctx->context, enumtopointer(gco), gco->gch.tt, gco->gch.memcat, size, objname); +} + +static void enumedge(EnumContext* ctx, GCObject* from, GCObject* to, const char* edgename) +{ + ctx->edge(ctx->context, enumtopointer(from), enumtopointer(to), edgename); +} + +static void enumedges(EnumContext* ctx, GCObject* from, TValue* data, size_t size, const char* edgename) +{ + for (size_t i = 0; i < size; ++i) + { + if (iscollectable(&data[i])) + enumedge(ctx, from, gcvalue(&data[i]), edgename); + } +} + +static void enumstring(EnumContext* ctx, TString* ts) +{ + enumnode(ctx, obj2gco(ts), sizestring(ts->len), NULL); +} + +static void enumtable(EnumContext* ctx, LuaTable* h) +{ + size_t size = sizeof(LuaTable) + (h->node == &luaH_dummynode ? 0 : sizenode(h) * sizeof(LuaNode)) + h->sizearray * sizeof(TValue); + + if (FFlag::LuauFrozenMetaButterfly && hasmetacache(h)) + size += TM_N * sizeof(TValue); + + // Provide a name for a special registry table + enumnode(ctx, obj2gco(h), size, h == hvalue(registry(ctx->L)) ? "registry" : NULL); + + if (h->node != &luaH_dummynode) + { + bool weakkey = false; + bool weakvalue = false; + + if (const TValue* mode = gfasttm(ctx->L->global, h->metatable, TM_MODE)) + { + if (ttisstring(mode)) + { + weakkey = strchr(svalue(mode), 'k') != NULL; + weakvalue = strchr(svalue(mode), 'v') != NULL; + } + } + + for (int i = 0; i < sizenode(h); ++i) + { + const LuaNode& n = h->node[i]; + + if (!ttisnil(&n.val) && (iscollectable(&n.key) || iscollectable(&n.val))) + { + if (!weakkey && iscollectable(&n.key)) + enumedge(ctx, obj2gco(h), gcvalue(&n.key), "[key]"); + + if (!weakvalue && iscollectable(&n.val)) + { + if (ttisstring(&n.key)) + { + enumedge(ctx, obj2gco(h), gcvalue(&n.val), svalue(&n.key)); + } + else if (ttisnumber(&n.key)) + { + char buf[32]; + snprintf(buf, sizeof(buf), "%.14g", nvalue(&n.key)); + enumedge(ctx, obj2gco(h), gcvalue(&n.val), buf); + } + else + { + char buf[32]; + snprintf(buf, sizeof(buf), "[%s]", getstr(ctx->L->global->ttname[n.key.tt])); + enumedge(ctx, obj2gco(h), gcvalue(&n.val), buf); + } + } + } + } + } + + if (h->sizearray) + enumedges(ctx, obj2gco(h), h->array, h->sizearray, "array"); + + if (h->metatable) + enumedge(ctx, obj2gco(h), obj2gco(h->metatable), "metatable"); +} + +static void enumclosure(EnumContext* ctx, Closure* cl) +{ + if (cl->isC) + { + enumnode(ctx, obj2gco(cl), sizeCclosure(cl->nupvalues), cl->c.debugname ? getstr(cl->c.debugname) : nullptr); + + if (FFlag::LuauEnumMoreEdges && cl->c.debugname) + enumedge(ctx, obj2gco(cl), obj2gco(cl->c.debugname), "name"); + } + else + { + Proto* p = cl->l.p; + + char buf[LUA_IDSIZE]; + + if (p->source) + snprintf(buf, sizeof(buf), "%s:%d %s", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined, getstr(p->source)); + else + snprintf(buf, sizeof(buf), "%s:%d", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined); + + enumnode(ctx, obj2gco(cl), sizeLclosure(cl->nupvalues), buf); + } + + enumedge(ctx, obj2gco(cl), obj2gco(cl->env), "env"); + + if (cl->isC) + { + if (cl->nupvalues) + enumedges(ctx, obj2gco(cl), cl->c.upvals, cl->nupvalues, "upvalue"); + } + else + { + enumedge(ctx, obj2gco(cl), obj2gco(cl->l.p), "proto"); + + if (cl->nupvalues) + enumedges(ctx, obj2gco(cl), cl->l.uprefs, cl->nupvalues, "upvalue"); + } +} + +static void enumudata(EnumContext* ctx, Udata* u) +{ + const char* name = NULL; + + if (LuaTable* h = u->metatable) + { + if (h->node != &luaH_dummynode) + { + for (int i = 0; i < sizenode(h); ++i) + { + const LuaNode& n = h->node[i]; + + if (ttisstring(&n.key) && ttisstring(&n.val) && strcmp(svalue(&n.key), "__type") == 0) + { + name = svalue(&n.val); + break; + } + } + } + } + + enumnode(ctx, obj2gco(u), sizeudata(u->len), name); + + if (u->metatable) + enumedge(ctx, obj2gco(u), obj2gco(u->metatable), "metatable"); +} + +static void enumthread(EnumContext* ctx, lua_State* th) +{ + size_t size = sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci; + + Proto* cip = NULL; + for (CallInfo* ci = th->base_ci; ci <= th->ci; ++ci) + { + if (ttisfunction(ci->func)) + { + cip = ci->p; + break; + } + } + + if (cip && cip->source) + { + Proto* p = cip; + + char buf[LUA_IDSIZE]; + + if (p->source) + snprintf(buf, sizeof(buf), "thread at %s:%d %s", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined, getstr(p->source)); + else + snprintf(buf, sizeof(buf), "thread at %s:%d", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined); + + enumnode(ctx, obj2gco(th), size, buf); + } + else + { + enumnode(ctx, obj2gco(th), size, NULL); + } + + enumedge(ctx, obj2gco(th), obj2gco(th->gt), "globals"); + + if (th->finalizers) + enumedge(ctx, obj2gco(th), obj2gco(th->finalizers), "finalizers"); + + if (th->top > th->stack) + enumedges(ctx, obj2gco(th), th->stack, th->top - th->stack, "stack"); +} + +static void enumbuffer(EnumContext* ctx, Buffer* b) +{ + enumnode(ctx, obj2gco(b), sizebuffer(b->len), NULL); +} + +static void enumproto(EnumContext* ctx, Proto* p) +{ + size_t size = sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo + + sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues; + + if (p->execdata && ctx->L->global->ecb.getmemorysize) + { + size_t nativesize = ctx->L->global->ecb.getmemorysize(ctx->L, p); + + ctx->node(ctx->context, p->execdata, uint8_t(LUA_TNONE), p->memcat, nativesize, NULL); + ctx->edge(ctx->context, enumtopointer(obj2gco(p)), p->execdata, "[native]"); + } + + char buf[LUA_IDSIZE]; + + if (p->source) + snprintf(buf, sizeof(buf), "proto %s:%d %s", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined, getstr(p->source)); + else + snprintf(buf, sizeof(buf), "proto %s:%d", p->debugname ? getstr(p->debugname) : "unnamed", p->linedefined); + + enumnode(ctx, obj2gco(p), size, buf); + + if (p->sizek) + enumedges(ctx, obj2gco(p), p->k, p->sizek, "constants"); + + for (int i = 0; i < p->sizep; ++i) + enumedge(ctx, obj2gco(p), obj2gco(p->p[i]), "protos"); + + if (FFlag::LuauEnumMoreEdges) + { + if (p->debugname) + enumedge(ctx, obj2gco(p), obj2gco(p->debugname), "name"); + + if (p->source) + enumedge(ctx, obj2gco(p), obj2gco(p->source), "source"); + + for (int i = 0; i < p->sizelocvars; i++) + { + if (TString* str = p->locvars[i].varname) + enumedge(ctx, obj2gco(p), obj2gco(str), "[local name]"); + } + + for (int i = 0; i < p->sizeupvalues; ++i) + { + if (TString* str = p->upvalues[i]) + enumedge(ctx, obj2gco(p), obj2gco(str), "[upvalue name]"); + } + + if (p->optimized) + enumedge(ctx, obj2gco(p), obj2gco(p->optimized), "optimized"); + + if (p->deoptimized) + enumedge(ctx, obj2gco(p), obj2gco(p->deoptimized), "deoptimized"); + } +} + +static void enumupval(EnumContext* ctx, UpVal* uv) +{ + enumnode(ctx, obj2gco(uv), sizeof(UpVal), NULL); + + if (iscollectable(uv->v)) + enumedge(ctx, obj2gco(uv), gcvalue(uv->v), "value"); +} + +static void enumclass(EnumContext* ctx, LuauClass* lco) +{ + GCObject* obj = obj2gco(lco); + + char buf[LUA_IDSIZE]; + snprintf(buf, sizeof(buf), "class object %s", getstr(lco->name)); + enumnode(ctx, obj, sizeof(LuauClass), buf); + enumedge(ctx, obj, obj2gco(lco->name), "classname"); + + if (lco->super) + enumedge(ctx, obj, obj2gco(lco->super), "super"); + + enumedge(ctx, obj, obj2gco(lco->memberstooffset), "classoffsets"); + uint32_t numberofstaticmembers = lco->numberofallmembers - lco->numberofinstancemembers; + + for (uint32_t i = 0; i < numberofstaticmembers; i++) + { + if (!iscollectable(&lco->staticmembers[i])) + continue; + + char membername[32]; + snprintf(membername, sizeof(membername), "%s", getstr(lco->offsettomember[i + lco->numberofinstancemembers])); + enumedge(ctx, obj, gcvalue(&lco->staticmembers[i]), membername); + } + + for (uint32_t i = 0; i < lco->numberofallmembers; i++) + enumedge(ctx, obj, obj2gco(lco->offsettomember[i]), "membername"); + + if (lco->metatable) + enumedge(ctx, obj, obj2gco(lco->metatable), "metatable"); + + if (FFlag::LuauEnumMoreEdges && lco->instancemetatable) + enumedge(ctx, obj, obj2gco(lco->instancemetatable), "instancemetatable"); +} + +static void enumobject(EnumContext* ctx, LuauObject* inst) +{ + GCObject* obj = obj2gco(inst); + + char buf[LUA_IDSIZE]; + snprintf(buf, sizeof(buf), "object %s", getstr(inst->lclass->name)); + enumnode(ctx, obj, sizeof(LuauObject), buf); + + for (uint32_t i = 0; i < inst->lclass->numberofinstancemembers; i++) + { + if (!iscollectable(&inst->members[i])) + continue; + + char membername[32]; + snprintf(membername, sizeof(membername), "%s", getstr(inst->lclass->offsettomember[i])); + enumedge(ctx, obj, gcvalue(&inst->members[i]), membername); + } + + if (FFlag::LuauEnumMoreEdges && inst->lclass) + enumedge(ctx, obj, obj2gco(inst->lclass), "class"); +} + +static void enumobj(EnumContext* ctx, GCObject* o) +{ + switch (o->gch.tt) + { + case LUA_TSTRING: + return enumstring(ctx, gco2ts(o)); + + case LUA_TTABLE: + return enumtable(ctx, gco2h(o)); + + case LUA_TFUNCTION: + return enumclosure(ctx, gco2cl(o)); + + case LUA_TUSERDATA: + return enumudata(ctx, gco2u(o)); + + case LUA_TTHREAD: + return enumthread(ctx, gco2th(o)); + + case LUA_TVECTOR: + return enumnode(ctx, o, sizeof(LuauVector), NULL); + + case LUA_TBUFFER: + return enumbuffer(ctx, gco2buf(o)); + + case LUA_TCLASS: + return enumclass(ctx, gco2class(o)); + + case LUA_TOBJECT: + return enumobject(ctx, gco2object(o)); + + case LUA_TPROTO: + return enumproto(ctx, gco2p(o)); + + case LUA_TUPVAL: + return enumupval(ctx, gco2uv(o)); + + default: + LUAU_ASSERT(!"Unknown object tag"); + } +} + +static bool enumgco(void* context, lua_Page* page, GCObject* gco) +{ + enumobj((EnumContext*)context, gco); + return false; +} + +void luaC_enumheap( + lua_State* L, + void* context, + void (*node)(void* context, void* ptr, uint8_t tt, uint8_t memcat, size_t size, const char* name), + void (*edge)(void* context, void* from, void* to, const char* name) +) +{ + global_State* g = L->global; + + EnumContext ctx; + ctx.L = L; + ctx.context = context; + ctx.node = node; + ctx.edge = edge; + + enumgco(&ctx, NULL, obj2gco(g->mainthread)); + + // global state links from mainthread + if (FFlag::LuauEnumMoreEdges) + { + if (iscollectable(&g->weakregistry)) + enumedge(&ctx, obj2gco(g->mainthread), gcvalue(&g->weakregistry), "weakregistry"); + + for (int i = 0; i < LUA_T_COUNT; i++) + { + if (g->mt[i]) + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->mt[i]), "[type metatable]"); + + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->ttname[i]), "[type name]"); + } + + for (int i = 0; i < TM_N; i++) + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->tmname[i]), "[metamethod name]"); + + for (int i = 0; i < LUA_UTAG_LIMIT; i++) + { + if (g->udatamt[i]) + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->udatamt[i]), "[userdata metatable]"); + } + + for (int i = 0; i < LUA_LUTAG_LIMIT; i++) + { + if (g->lightuserdataname[i]) + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->lightuserdataname[i]), "[lightuserdata name]"); + } + + for (int i = 0; i < UTAG_INTERNAL_LIMIT; i++) + { + lua_UdataDirectAccessData& direct = g->udatadirect[i]; + + if (iscollectable(&direct.indextm)) + enumedge(&ctx, obj2gco(g->mainthread), gcvalue(&direct.indextm), "[direct userdata]"); + + if (iscollectable(&direct.newindextm)) + enumedge(&ctx, obj2gco(g->mainthread), gcvalue(&direct.newindextm), "[direct userdata]"); + + if (iscollectable(&direct.namecalltm)) + enumedge(&ctx, obj2gco(g->mainthread), gcvalue(&direct.namecalltm), "[direct userdata]"); + + if (g->udatadirectfields[i]) + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(g->udatadirectfields[i]), "[direct userdata]"); + } + + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(luaS_newliteral(L, LUA_MEMERRMSG)), "[fixed string]"); + enumedge(&ctx, obj2gco(g->mainthread), obj2gco(luaS_newliteral(L, LUA_ERRERRMSG)), "[fixed string]"); + } + + luaM_visitgco(L, &ctx, enumgco); +} diff --git a/External/luau/VM/src/linit.cpp b/External/luau/VM/src/linit.cpp new file mode 100644 index 00000000..7a80ab58 --- /dev/null +++ b/External/luau/VM/src/linit.cpp @@ -0,0 +1,146 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" +#include "lstate.h" + +#include + +LUAU_FASTFLAG(LuauIntegerLibrary) +LUAU_FASTFLAG(DebugLuauUserDefinedClassesRuntime) +LUAU_FASTFLAGVARIABLE(LuauSandboxFreezesVectorMetatable) + +static const luaL_Reg lualibs[] = { + {"", luaopen_base}, + {LUA_COLIBNAME, luaopen_coroutine}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_DBLIBNAME, luaopen_debug}, + {LUA_UTF8LIBNAME, luaopen_utf8}, + {LUA_BITLIBNAME, luaopen_bit32}, + {LUA_BUFFERLIBNAME, luaopen_buffer}, + {LUA_VECLIBNAME, luaopen_vector}, + {LUA_INTLIBNAME, luaopen_integer}, + {NULL, NULL}, +}; + +static const luaL_Reg lualibs_NOINTEGER[] = { + {"", luaopen_base}, + {LUA_COLIBNAME, luaopen_coroutine}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_DBLIBNAME, luaopen_debug}, + {LUA_UTF8LIBNAME, luaopen_utf8}, + {LUA_BITLIBNAME, luaopen_bit32}, + {LUA_BUFFERLIBNAME, luaopen_buffer}, + {LUA_VECLIBNAME, luaopen_vector}, + {NULL, NULL}, +}; + +void luaL_openlibs(lua_State* L) +{ + const luaL_Reg* lib; + if (FFlag::LuauIntegerLibrary) + lib = lualibs; + else + lib = lualibs_NOINTEGER; + + for (; lib->func; lib++) + { + lua_pushcfunction(L, lib->func, NULL); + lua_pushstring(L, lib->name); + lua_call(L, 1, 0); + } + + if (FFlag::DebugLuauUserDefinedClassesRuntime) + { + lua_pushcfunction(L, luaopen_class, NULL); + lua_pushstring(L, LUA_CLASSLIBNAME); + lua_call(L, 1, 0); + } +} + +void luaL_sandbox(lua_State* L) +{ + // set all libraries to read-only + lua_pushnil(L); + while (lua_next(L, LUA_GLOBALSINDEX) != 0) + { + if (lua_istable(L, -1)) + lua_setreadonly(L, -1, true); + + lua_pop(L, 1); + } + + // set all builtin metatables to read-only + lua_pushliteral(L, ""); + if (lua_getmetatable(L, -1)) + { + lua_setreadonly(L, -1, true); + lua_pop(L, 2); + } + else + { + lua_pop(L, 1); + } + + if (FFlag::LuauSandboxFreezesVectorMetatable) + { +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f); +#else + lua_pushvector(L, 0.0f, 0.0f, 0.0f); +#endif + if (lua_getmetatable(L, -1)) + { + lua_setreadonly(L, -1, true); + lua_pop(L, 2); + } + else + { + lua_pop(L, 1); + } + } + + // set globals to readonly and activate safeenv since the env is immutable + lua_setreadonly(L, LUA_GLOBALSINDEX, true); + lua_setsafeenv(L, LUA_GLOBALSINDEX, true); +} + +void luaL_sandboxthread(lua_State* L) +{ + // create new global table that proxies reads to original table + lua_newtable(L); + + lua_newtable(L); + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setfield(L, -2, "__index"); + lua_setreadonly(L, -1, true); + + lua_setmetatable(L, -2); + + // we can set safeenv now although it's important to set it to false if code is loaded twice into the thread + lua_replace(L, LUA_GLOBALSINDEX); + lua_setsafeenv(L, LUA_GLOBALSINDEX, true); +} + +static void* l_alloc(void* ud, void* ptr, size_t osize, size_t nsize) +{ + (void)ud; + (void)osize; + if (nsize == 0) + { + free(ptr); + return NULL; + } + else + return realloc(ptr, nsize); +} + +lua_State* luaL_newstate(void) +{ + return lua_newstate(l_alloc, NULL); +} diff --git a/External/luau/VM/src/lintlib.cpp b/External/luau/VM/src/lintlib.cpp new file mode 100644 index 00000000..2c57f89a --- /dev/null +++ b/External/luau/VM/src/lintlib.cpp @@ -0,0 +1,614 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" +#include "lnumutils.h" +#include "lobject.h" + +#include +#include + +#ifdef _MSC_VER +#include +#endif + +LUAU_FASTFLAGVARIABLE(LuauIntegerLibrary) + +#define mask64(w) (0xFFFFFFFFFFFFFFFFULL >> (64 - (w))) + +static int int64_create(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + if (x >= -9223372036854775808.0 && x < 9223372036854775808.0) + { + int64_t l = (int64_t)x; + if (((double)l) == x) + { + lua_pushinteger64(L, l); + return 1; + } + } + + lua_pushnil(L); + + return 1; +} + +static int int64_fromstring(lua_State* L) +{ + const char* s = luaL_checkstring(L, 1); + int base = luaL_optinteger(L, 2, 10); + luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range"); + + int64_t result; + if (luaO_str2l(s, &result, base)) + lua_pushinteger64(L, result); + else + lua_pushnil(L); + + return 1; +} + +static int int64_tonumber(lua_State* L) +{ + int64_t x = luaL_checkinteger64(L, 1); + + lua_pushnumber(L, (double)x); + + return 1; +} + +static int int64_neg(lua_State* L) +{ + int64_t x = luaL_checkinteger64(L, 1); + + lua_pushinteger64(L, (int64_t)(~(uint64_t)x + 1)); + + return 1; +} + +static int int64_add(lua_State* L) +{ + int64_t x = luaL_checkinteger64(L, 1); + int64_t y = luaL_checkinteger64(L, 2); + + lua_pushinteger64(L, (int64_t)((uint64_t)x + (uint64_t)y)); + + return 1; +} + +static int int64_sub(lua_State* L) +{ + int64_t x = luaL_checkinteger64(L, 1); + int64_t y = luaL_checkinteger64(L, 2); + + lua_pushinteger64(L, (int64_t)((uint64_t)x - (uint64_t)y)); + + return 1; +} + +static int int64_mul(lua_State* L) +{ + int64_t x = luaL_checkinteger64(L, 1); + int64_t y = luaL_checkinteger64(L, 2); + + lua_pushinteger64(L, (int64_t)((uint64_t)x * (uint64_t)y)); + + return 1; +} + +static int int64_div(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + if ((a == LLONG_MIN) && (b == -1)) + luaL_error(L, "integer overflow"); + + lua_pushinteger64(L, a / b); + + return 1; +} + +static int int64_idiv(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + if ((a == LLONG_MIN) && (b == -1)) + luaL_error(L, "integer overflow"); + + int64_t result = a / b; + // Floored division rounds toward -inf: adjust the truncated quotient down by 1 + // when the operands have opposite signs and the division is inexact. + if (((a ^ b) < 0) && (a % b)) + result -= 1; + lua_pushinteger64(L, result); + + return 1; +} + +static int int64_rem(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + + if ((a == LLONG_MIN) && (b == -1)) + { + lua_pushinteger64(L, 0); + return 1; + } + + lua_pushinteger64(L, a % b); + + return 1; +} + +static int int64_mod(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + + int64_t remainder = 0; + if ((a != LLONG_MIN) || (b != -1)) + { + remainder = a % b; + if (remainder && ((a < 0) != (b < 0))) + remainder += b; + } + + lua_pushinteger64(L, remainder); + + return 1; +} + +static int int64_udiv(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + + lua_pushinteger64(L, a / b); + + return 1; +} + +static int int64_urem(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + if (b == 0) + luaL_error(L, "division by zero"); + + lua_pushinteger64(L, a % b); + + return 1; +} + +static int int64_min(lua_State* L) +{ + int64_t tmin = luaL_checkinteger64(L, 1); + int n = lua_gettop(L); + for (int i = 2; i <= n; i++) + { + int64_t x = luaL_checkinteger64(L, i); + if (x < tmin) + tmin = x; + } + + lua_pushinteger64(L, tmin); + + return 1; +} + +static int int64_max(lua_State* L) +{ + int64_t tmax = luaL_checkinteger64(L, 1); + int n = lua_gettop(L); + for (int i = 2; i <= n; i++) + { + int64_t x = luaL_checkinteger64(L, i); + if (x > tmax) + tmax = x; + } + + lua_pushinteger64(L, tmax); + + return 1; +} + +static int int64_band(lua_State* L) +{ + uint64_t tres = ULLONG_MAX; + int n = lua_gettop(L); + + for (int i = 1; i <= n; i++) + { + uint64_t x = (uint64_t)luaL_checkinteger64(L, i); + tres &= x; + } + + lua_pushinteger64(L, tres); + + return 1; +} + +static int int64_bor(lua_State* L) +{ + uint64_t tres = 0; + int n = lua_gettop(L); + + for (int i = 1; i <= n; i++) + { + uint64_t x = (uint64_t)luaL_checkinteger64(L, i); + tres |= x; + } + + lua_pushinteger64(L, tres); + + return 1; +} + +static int int64_bnot(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + + lua_pushinteger64(L, ~a); + + return 1; +} + +static int int64_bxor(lua_State* L) +{ + uint64_t tres = 0; + int n = lua_gettop(L); + + for (int i = 1; i <= n; i++) + { + uint64_t x = (uint64_t)luaL_checkinteger64(L, i); + tres ^= x; + } + + lua_pushinteger64(L, tres); + + return 1; +} + +static int int64_lt(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a < b); + + return 1; +} + +static int int64_le(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a <= b); + + return 1; +} + +static int int64_ult(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a < b); + + return 1; +} + +static int int64_ule(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a <= b); + + return 1; +} + +static int int64_gt(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a > b); + + return 1; +} + +static int int64_ge(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a >= b); + + return 1; +} + +static int int64_ugt(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a > b); + + return 1; +} + +static int int64_uge(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + uint64_t b = luaL_checkinteger64(L, 2); + + lua_pushboolean(L, a >= b); + + return 1; +} + +static int int64_lshift(lua_State* L) +{ + uint64_t n = luaL_checkinteger64(L, 1); + int64_t i = luaL_checkinteger64(L, 2); + + if ((i >= -63) && (i <= 63)) + lua_pushinteger64(L, (i < 0) ? (n >> (-i)) : (n << i)); + else + lua_pushinteger64(L, 0); + + return 1; +} + +static int int64_rshift(lua_State* L) +{ + uint64_t n = luaL_checkinteger64(L, 1); + int64_t i = luaL_checkinteger64(L, 2); + + if ((i >= -63) && (i <= 63)) + lua_pushinteger64(L, (i < 0) ? (n << (-i)) : (n >> i)); + else + lua_pushinteger64(L, 0); + + return 1; +} + +static int int64_arshift(lua_State* L) +{ + int64_t n = luaL_checkinteger64(L, 1); + int64_t i = luaL_checkinteger64(L, 2); + + if ((i >= -63) && (i <= 63)) + lua_pushinteger64(L, (i < 0) ? (int64_t)((uint64_t)n << (-i)) : (n >> i)); + else if (i < -63) + lua_pushinteger64(L, 0); + else + lua_pushinteger64(L, (n < 0) ? -1 : 0); + + return 1; +} + +static int int64_lrotate(lua_State* L) +{ + uint64_t n = (uint64_t)luaL_checkinteger64(L, 1); + unsigned s = (unsigned)((uint64_t)luaL_checkinteger64(L, 2) % 64); + + lua_pushinteger64(L, (int64_t)(s != 0 ? (n << s) | (n >> (64 - s)) : n)); + + return 1; +} + +static int int64_rrotate(lua_State* L) +{ + uint64_t n = (uint64_t)luaL_checkinteger64(L, 1); + unsigned s = (unsigned)((uint64_t)luaL_checkinteger64(L, 2) % 64); + + lua_pushinteger64(L, (int64_t)(s != 0 ? (n >> s) | (n << (64 - s)) : n)); + + return 1; +} + +static int int64_extract(lua_State* L) +{ + int64_t n = luaL_checkinteger64(L, 1); + int64_t f = luaL_checkinteger64(L, 2); + int64_t w = luaL_optinteger64(L, 3, 1); + + luaL_argcheck(L, 0 <= f && f <= 63, 2, "field cannot be negative"); + luaL_argcheck(L, 0 < w, 3, "width must be positive"); + if (f + w > 64) + luaL_error(L, "trying to access non-existent bits"); + + lua_pushinteger64(L, ((uint64_t)n >> f) & mask64(w)); + + return 1; +} + +static int int64_replace(lua_State* L) +{ + int64_t n = luaL_checkinteger64(L, 1); + int64_t r = luaL_checkinteger64(L, 2); + int64_t f = luaL_checkinteger64(L, 3); + int64_t w = luaL_optinteger64(L, 4, 1); + + luaL_argcheck(L, 0 <= f && f <= 63, 3, "field cannot be negative"); + luaL_argcheck(L, 0 < w, 4, "width must be positive"); + if (f + w > 64) + luaL_error(L, "trying to access non-existent bits"); + + uint64_t baseMask = ((0xFFFFFFFFFFFFFFFFULL) >> (64 - w)); + uint64_t replacement = (((uint64_t)r) & baseMask) << f; + uint64_t mask = 0xFFFFFFFFFFFFFFFFULL ^ (baseMask << f); + lua_pushinteger64(L, (((uint64_t)n) & mask) | replacement); + + return 1; +} + +static int int64_clamp(lua_State* L) +{ + int64_t a = luaL_checkinteger64(L, 1); + int64_t mi = luaL_checkinteger64(L, 2); + int64_t mx = luaL_checkinteger64(L, 3); + + luaL_argcheck(L, mi <= mx, 3, "max must be greater than or equal to min"); + + if (a < mi) + lua_pushinteger64(L, mi); + else if (a > mx) + lua_pushinteger64(L, mx); + else + lua_pushinteger64(L, a); + + return 1; +} + +static int int64_btest(lua_State* L) +{ + uint64_t tres = ULLONG_MAX; + int n = lua_gettop(L); + + for (int i = 1; i <= n; i++) + { + uint64_t x = (uint64_t)luaL_checkinteger64(L, i); + tres &= x; + } + + lua_pushboolean(L, (tres != 0)); + + return 1; +} + +static int int64_countrz(lua_State* L) +{ + uint64_t n = luaL_checkinteger64(L, 1); + int result; +#ifdef _MSC_VER +#ifdef _WIN64 + unsigned long rl; + result = _BitScanForward64(&rl, n) ? int(rl) : 64; +#else + unsigned long rl; + if (_BitScanForward(&rl, uint32_t(n))) + result = int(rl); + else + result = _BitScanForward(&rl, uint32_t(n >> 32)) ? int(rl) + 32 : 64; +#endif +#else + result = (n == 0) ? 64 : __builtin_ctzll(n); +#endif + + lua_pushinteger64(L, result); + + return 1; +} + +static int int64_countlz(lua_State* L) +{ + uint64_t n = luaL_checkinteger64(L, 1); + int result; +#ifdef _MSC_VER +#ifdef _WIN64 + unsigned long rl; + result = _BitScanReverse64(&rl, n) ? 63 - int(rl) : 64; +#else + unsigned long rl; + if (_BitScanReverse(&rl, uint32_t(n >> 32))) + result = 31 - int(rl); + else + result = _BitScanReverse(&rl, uint32_t(n)) ? 63 - int(rl) : 64; +#endif +#else + result = (n == 0) ? 64 : __builtin_clzll(n); +#endif + lua_pushinteger64(L, result); + + return 1; +} + +static int int64_bswap(lua_State* L) +{ + uint64_t a = luaL_checkinteger64(L, 1); + + lua_pushinteger64( + L, + (a >> 56) | ((a & 0xFF000000000000) >> 40) | ((a & 0xFF0000000000) >> 24) | ((a & 0xFF00000000) >> 8) | ((a & 0xFF000000) << 8) | + ((a & 0xFF0000) << 24) | ((a & 0xFF00) << 40) | ((a & 0xFF) << 56) + ); + + return 1; +} + +static const luaL_Reg int64lib[] = { + {"create", int64_create}, + {"tonumber", int64_tonumber}, + {"neg", int64_neg}, + {"add", int64_add}, + {"sub", int64_sub}, + {"mul", int64_mul}, + {"div", int64_div}, + {"min", int64_min}, + {"max", int64_max}, + {"rem", int64_rem}, + {"idiv", int64_idiv}, + {"udiv", int64_udiv}, + {"urem", int64_urem}, + {"mod", int64_mod}, + {"clamp", int64_clamp}, + {"band", int64_band}, + {"bor", int64_bor}, + {"bnot", int64_bnot}, + {"bxor", int64_bxor}, + {"lt", int64_lt}, + {"le", int64_le}, + {"ult", int64_ult}, + {"ule", int64_ule}, + {"gt", int64_gt}, + {"ge", int64_ge}, + {"ugt", int64_ugt}, + {"uge", int64_uge}, + {"lshift", int64_lshift}, + {"rshift", int64_rshift}, + {"arshift", int64_arshift}, + {"lrotate", int64_lrotate}, + {"rrotate", int64_rrotate}, + {"extract", int64_extract}, + {"replace", int64_replace}, + {"btest", int64_btest}, + {"countrz", int64_countrz}, + {"countlz", int64_countlz}, + {"bswap", int64_bswap}, + {"fromstring", int64_fromstring}, + {NULL, NULL}, +}; + +int luaopen_integer(lua_State* L) +{ + luaL_register(L, LUA_INTLIBNAME, int64lib); + + lua_pushinteger64(L, LLONG_MAX); + lua_setfield(L, -2, "maxsigned"); + lua_pushinteger64(L, LLONG_MIN); + lua_setfield(L, -2, "minsigned"); + + return 1; +} diff --git a/External/luau/VM/src/lmathlib.cpp b/External/luau/VM/src/lmathlib.cpp new file mode 100644 index 00000000..e65c2b06 --- /dev/null +++ b/External/luau/VM/src/lmathlib.cpp @@ -0,0 +1,543 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lstate.h" + +#include +#include +#include + +#define LUAU_PI (3.14159265358979323846) +#define RADIANS_PER_DEGREE (LUAU_PI / 180.0) + +#define LUAU_NAN (std::numeric_limits::quiet_NaN()) +#define LUAU_E (2.71828182845904523536) +#define LUAU_PHI (1.61803398874989484820) +#define LUAU_SQRT2 (1.41421356237309504880) +#define LUAU_TAU (6.28318530717958647692) + +#define PCG32_INC 105 + +LUAU_FASTFLAGVARIABLE(FixMathNoisePrecision) + +static uint32_t pcg32_random(uint64_t* state) +{ + uint64_t oldstate = *state; + *state = oldstate * 6364136223846793005ULL + (PCG32_INC | 1); + uint32_t xorshifted = uint32_t(((oldstate >> 18u) ^ oldstate) >> 27u); + uint32_t rot = uint32_t(oldstate >> 59u); + return (xorshifted >> rot) | (xorshifted << ((-int32_t(rot)) & 31)); +} + +static void pcg32_seed(uint64_t* state, uint64_t seed) +{ + *state = 0; + pcg32_random(state); + *state += seed; + pcg32_random(state); +} + +static int math_abs(lua_State* L) +{ + lua_pushnumber(L, fabs(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sin(lua_State* L) +{ + lua_pushnumber(L, sin(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sinh(lua_State* L) +{ + lua_pushnumber(L, sinh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_cos(lua_State* L) +{ + lua_pushnumber(L, cos(luaL_checknumber(L, 1))); + return 1; +} + +static int math_cosh(lua_State* L) +{ + lua_pushnumber(L, cosh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tan(lua_State* L) +{ + lua_pushnumber(L, tan(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tanh(lua_State* L) +{ + lua_pushnumber(L, tanh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_asin(lua_State* L) +{ + lua_pushnumber(L, asin(luaL_checknumber(L, 1))); + return 1; +} + +static int math_acos(lua_State* L) +{ + lua_pushnumber(L, acos(luaL_checknumber(L, 1))); + return 1; +} + +static int math_atan(lua_State* L) +{ + lua_pushnumber(L, atan(luaL_checknumber(L, 1))); + return 1; +} + +static int math_atan2(lua_State* L) +{ + lua_pushnumber(L, atan2(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_ceil(lua_State* L) +{ + lua_pushnumber(L, ceil(luaL_checknumber(L, 1))); + return 1; +} + +static int math_floor(lua_State* L) +{ + lua_pushnumber(L, floor(luaL_checknumber(L, 1))); + return 1; +} + +static int math_fmod(lua_State* L) +{ + lua_pushnumber(L, fmod(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_modf(lua_State* L) +{ + double ip; + double fp = modf(luaL_checknumber(L, 1), &ip); + lua_pushnumber(L, ip); + lua_pushnumber(L, fp); + return 2; +} + +static int math_sqrt(lua_State* L) +{ + lua_pushnumber(L, sqrt(luaL_checknumber(L, 1))); + return 1; +} + +static int math_pow(lua_State* L) +{ + lua_pushnumber(L, pow(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_log(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + double res; + if (lua_isnoneornil(L, 2)) + res = log(x); + else + { + double base = luaL_checknumber(L, 2); + if (base == 2.0) + res = log2(x); + else if (base == 10.0) + res = log10(x); + else + res = log(x) / log(base); + } + lua_pushnumber(L, res); + return 1; +} + +static int math_log10(lua_State* L) +{ + lua_pushnumber(L, log10(luaL_checknumber(L, 1))); + return 1; +} + +static int math_exp(lua_State* L) +{ + lua_pushnumber(L, exp(luaL_checknumber(L, 1))); + return 1; +} + +static int math_deg(lua_State* L) +{ + lua_pushnumber(L, luaL_checknumber(L, 1) / RADIANS_PER_DEGREE); + return 1; +} + +static int math_rad(lua_State* L) +{ + lua_pushnumber(L, luaL_checknumber(L, 1) * RADIANS_PER_DEGREE); + return 1; +} + +static int math_frexp(lua_State* L) +{ + int e; + lua_pushnumber(L, frexp(luaL_checknumber(L, 1), &e)); + lua_pushinteger(L, e); + return 2; +} + +static int math_ldexp(lua_State* L) +{ + lua_pushnumber(L, ldexp(luaL_checknumber(L, 1), luaL_checkinteger(L, 2))); + return 1; +} + +static int math_min(lua_State* L) +{ + int n = lua_gettop(L); // number of arguments + double dmin = luaL_checknumber(L, 1); + int i; + for (i = 2; i <= n; i++) + { + double d = luaL_checknumber(L, i); + if (d < dmin) + dmin = d; + } + lua_pushnumber(L, dmin); + return 1; +} + +static int math_max(lua_State* L) +{ + int n = lua_gettop(L); // number of arguments + double dmax = luaL_checknumber(L, 1); + int i; + for (i = 2; i <= n; i++) + { + double d = luaL_checknumber(L, i); + if (d > dmax) + dmax = d; + } + lua_pushnumber(L, dmax); + return 1; +} + +static int math_random(lua_State* L) +{ + global_State* g = L->global; + switch (lua_gettop(L)) + { // check number of arguments + case 0: + { // no arguments + // Using ldexp instead of division for speed & clarity. + // See http://mumble.net/~campbell/tmp/random_real.c for details on generating doubles from integer ranges. + uint32_t rl = pcg32_random(&g->rngstate); + uint32_t rh = pcg32_random(&g->rngstate); + double rd = ldexp(double(rl | (uint64_t(rh) << 32)), -64); + lua_pushnumber(L, rd); // number between 0 and 1 + break; + } + case 1: + { // only upper limit + int u = luaL_checkinteger(L, 1); + luaL_argcheck(L, 1 <= u, 1, "interval is empty"); + + uint64_t x = uint64_t(u) * pcg32_random(&g->rngstate); + int r = int(1 + (x >> 32)); + lua_pushinteger(L, r); // int between 1 and `u' + break; + } + case 2: + { // lower and upper limits + int l = luaL_checkinteger(L, 1); + int u = luaL_checkinteger(L, 2); + luaL_argcheck(L, l <= u, 2, "interval is empty"); + + uint32_t ul = uint32_t(u) - uint32_t(l); + luaL_argcheck(L, ul < UINT_MAX, 2, "interval is too large"); // -INT_MIN..INT_MAX interval can result in integer overflow + uint64_t x = uint64_t(ul + 1) * pcg32_random(&g->rngstate); + int r = int(l + (x >> 32)); + lua_pushinteger(L, r); // int between `l' and `u' + break; + } + default: + luaL_error(L, "wrong number of arguments"); + } + return 1; +} + +static int math_randomseed(lua_State* L) +{ + int seed = luaL_checkinteger(L, 1); + + pcg32_seed(&L->global->rngstate, seed); + return 0; +} + +static const unsigned char kPerlinHash[257] = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, + 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, + 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, + 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, + 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, + 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, + 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, + 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, + 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, 151 +}; + +const float kPerlinGrad[16][3] = { + {1, 1, 0}, + {-1, 1, 0}, + {1, -1, 0}, + {-1, -1, 0}, + {1, 0, 1}, + {-1, 0, 1}, + {1, 0, -1}, + {-1, 0, -1}, + {0, 1, 1}, + {0, -1, 1}, + {0, 1, -1}, + {0, -1, -1}, + {1, 1, 0}, + {0, -1, 1}, + {-1, 1, 0}, + {0, -1, -1} +}; + +inline float perlin_fade(float t) +{ + return t * t * t * (t * (t * 6 - 15) + 10); +} + +inline float perlin_lerp(float t, float a, float b) +{ + return a + t * (b - a); +} + +inline float perlin_grad(int hash, float x, float y, float z) +{ + const float* g = kPerlinGrad[hash & 15]; + return g[0] * x + g[1] * y + g[2] * z; +} + +static float perlin(float x, float y, float z) +{ + float xflr = floorf(x); + float yflr = floorf(y); + float zflr = floorf(z); + + int xi = int(xflr) & 255; + int yi = int(yflr) & 255; + int zi = int(zflr) & 255; + + float xf = x - xflr; + float yf = y - yflr; + float zf = z - zflr; + + float u = perlin_fade(xf); + float v = perlin_fade(yf); + float w = perlin_fade(zf); + + const unsigned char* p = kPerlinHash; + + int a = (p[xi] + yi) & 255; + int aa = (p[a] + zi) & 255; + int ab = (p[a + 1] + zi) & 255; + + int b = (p[xi + 1] + yi) & 255; + int ba = (p[b] + zi) & 255; + int bb = (p[b + 1] + zi) & 255; + + float la = perlin_lerp(u, perlin_grad(p[aa], xf, yf, zf), perlin_grad(p[ba], xf - 1, yf, zf)); + float lb = perlin_lerp(u, perlin_grad(p[ab], xf, yf - 1, zf), perlin_grad(p[bb], xf - 1, yf - 1, zf)); + float la1 = perlin_lerp(u, perlin_grad(p[aa + 1], xf, yf, zf - 1), perlin_grad(p[ba + 1], xf - 1, yf, zf - 1)); + float lb1 = perlin_lerp(u, perlin_grad(p[ab + 1], xf, yf - 1, zf - 1), perlin_grad(p[bb + 1], xf - 1, yf - 1, zf - 1)); + + return perlin_lerp(w, perlin_lerp(v, la, lb), perlin_lerp(v, la1, lb1)); +} + +static int math_noise(lua_State* L) +{ + int nx, ny, nz; + double x = lua_tonumberx(L, 1, &nx); + double y = lua_tonumberx(L, 2, &ny); + double z = lua_tonumberx(L, 3, &nz); + + luaL_argexpected(L, nx, 1, "number"); + luaL_argexpected(L, ny || lua_isnoneornil(L, 2), 2, "number"); + luaL_argexpected(L, nz || lua_isnoneornil(L, 3), 3, "number"); + + if (FFlag::FixMathNoisePrecision) + { + // NOTES: input numbers from Luau are double with higher precision and range than float used by perlin(). + // If we don't do this, for large numbers, perlin() will return almost always 0, since with larger inputs, + // most of the mantissa is used to store the integer part and perlin() is always 0 at integer cell values. + // Noise repeat exactly every 256 units in all dimensions, so we can wrap to prevent loss of precision for large numbers. + x = fmod(x, 256.0); + y = fmod(y, 256.0); + z = fmod(z, 256.0); + } + + double r = perlin((float)x, (float)y, (float)z); + + lua_pushnumber(L, r); + return 1; +} + +static int math_clamp(lua_State* L) +{ + double v = luaL_checknumber(L, 1); + double min = luaL_checknumber(L, 2); + double max = luaL_checknumber(L, 3); + + luaL_argcheck(L, min <= max, 3, "max must be greater than or equal to min"); + + double r = v < min ? min : v; + r = r > max ? max : r; + + lua_pushnumber(L, r); + return 1; +} + +static int math_sign(lua_State* L) +{ + double v = luaL_checknumber(L, 1); + lua_pushnumber(L, v > 0.0 ? 1.0 : v < 0.0 ? -1.0 : 0.0); + return 1; +} + +static int math_round(lua_State* L) +{ + lua_pushnumber(L, round(luaL_checknumber(L, 1))); + return 1; +} + +static int math_map(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + double inmin = luaL_checknumber(L, 2); + double inmax = luaL_checknumber(L, 3); + double outmin = luaL_checknumber(L, 4); + double outmax = luaL_checknumber(L, 5); + + double result = outmin + (x - inmin) * (outmax - outmin) / (inmax - inmin); + lua_pushnumber(L, result); + return 1; +} + +static int math_lerp(lua_State* L) +{ + double a = luaL_checknumber(L, 1); + double b = luaL_checknumber(L, 2); + double t = luaL_checknumber(L, 3); + + double r = (t == 1.0) ? b : a + (b - a) * t; + lua_pushnumber(L, r); + return 1; +} + +static int math_isnan(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + + lua_pushboolean(L, isnan(x)); + return 1; +} + +static int math_isinf(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + + lua_pushboolean(L, isinf(x)); + return 1; +} + +static int math_isfinite(lua_State* L) +{ + double x = luaL_checknumber(L, 1); + + lua_pushboolean(L, isfinite(x)); + return 1; +} + +static const luaL_Reg mathlib[] = { + {"abs", math_abs}, + {"acos", math_acos}, + {"asin", math_asin}, + {"atan2", math_atan2}, + {"atan", math_atan}, + {"ceil", math_ceil}, + {"cosh", math_cosh}, + {"cos", math_cos}, + {"deg", math_deg}, + {"exp", math_exp}, + {"floor", math_floor}, + {"fmod", math_fmod}, + {"frexp", math_frexp}, + {"ldexp", math_ldexp}, + {"log10", math_log10}, + {"log", math_log}, + {"max", math_max}, + {"min", math_min}, + {"modf", math_modf}, + {"pow", math_pow}, + {"rad", math_rad}, + {"random", math_random}, + {"randomseed", math_randomseed}, + {"sinh", math_sinh}, + {"sin", math_sin}, + {"sqrt", math_sqrt}, + {"tanh", math_tanh}, + {"tan", math_tan}, + {"noise", math_noise}, + {"clamp", math_clamp}, + {"sign", math_sign}, + {"round", math_round}, + {"map", math_map}, + {"lerp", math_lerp}, + {"isnan", math_isnan}, + {"isinf", math_isinf}, + {"isfinite", math_isfinite}, + {NULL, NULL}, +}; + +/* +** Open math library +*/ +int luaopen_math(lua_State* L) +{ + uint64_t seed = lua_encodepointer(L, uintptr_t(L)); + seed ^= time(NULL); + seed ^= clock(); + + pcg32_seed(&L->global->rngstate, seed); + + luaL_register(L, LUA_MATHLIBNAME, mathlib); + + lua_pushnumber(L, LUAU_PI); + lua_setfield(L, -2, "pi"); + lua_pushnumber(L, HUGE_VAL); + lua_setfield(L, -2, "huge"); + lua_pushnumber(L, LUAU_NAN); + lua_setfield(L, -2, "nan"); + lua_pushnumber(L, LUAU_E); + lua_setfield(L, -2, "e"); + lua_pushnumber(L, LUAU_PHI); + lua_setfield(L, -2, "phi"); + lua_pushnumber(L, LUAU_SQRT2); + lua_setfield(L, -2, "sqrt2"); + lua_pushnumber(L, LUAU_TAU); + lua_setfield(L, -2, "tau"); + + return 1; +} diff --git a/External/luau/VM/src/lmem.cpp b/External/luau/VM/src/lmem.cpp new file mode 100644 index 00000000..746654af --- /dev/null +++ b/External/luau/VM/src/lmem.cpp @@ -0,0 +1,936 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lmem.h" + +#include "lstate.h" +#include "ldo.h" +#include "ldebug.h" + +#include + +LUAU_FASTFLAG(LuauBufferCage) + +/* + * Luau heap uses a size-segregated page structure, with individual pages and large allocations + * allocated using system heap (via frealloc callback). + * + * frealloc callback serves as a general, if slow, allocation callback that can allocate, free or + * resize allocations: + * + * void* frealloc(void* ud, void* ptr, size_t oldsize, size_t newsize); + * + * frealloc(ud, NULL, 0, x) creates a new block of size x + * frealloc(ud, p, x, 0) frees the block p (must return NULL) + * frealloc(ud, NULL, 0, 0) does nothing, equivalent to free(NULL) + * + * frealloc returns NULL if it cannot create or reallocate the area + * (any reallocation to an equal or smaller size cannot fail!) + * + * On top of this, Luau implements heap storage which is split into two types of allocations: + * + * - GCO, short for "garbage collected objects" + * - other objects (for example, arrays stored inside table objects) + * + * The heap layout for these two allocation types is a bit different. + * + * All GCO are allocated in pages, which is a block of memory of ~16K in size that has a page header + * (lua_Page). Each page contains 1..N blocks of the same size, where N is selected to fill the page + * completely. This amortizes the allocation cost and increases locality. Each GCO block starts with + * the GC header (GCheader) which contains the object type, mark bits and other GC metadata. If the + * GCO block is free (not used), then it must have the type set to TNIL; in this case the block can + * be part of the per-page free list, the link for that list is stored after the header (freegcolink). + * + * Importantly, the GCO block doesn't have any back references to the page it's allocated in, so it's + * impossible to free it in isolation - GCO blocks are freed by sweeping the pages they belong to, + * using luaM_freegco which must specify the page; this is called by page sweeper that traverses the + * entire page's worth of objects. For this reason it's also important that freed GCO blocks keep the + * GC header intact and accessible (with type = NIL) so that the sweeper can access it. + * + * Some GCOs are too large to fit in a 16K page without excessive fragmentation (the size threshold is + * currently 512 bytes); in this case, we allocate a dedicated small page with just a single block's worth + * storage space, but that requires allocating an extra page header. In effect large GCOs are a little bit + * less memory efficient, but this allows us to uniformly sweep small and large GCOs using page lists. + * + * All GCO pages are linked in a large intrusive linked list (global_State::allgcopages). Additionally, + * for each block size there's a page free list that contains pages that have at least one free block + * (global_State::freegcopages). This free list is used to make sure object allocation is O(1). + * + * When LUAU_ASSERTENABLED is enabled, all non-GCO pages are also linked in a list (global_State::allpages). + * Because this list is not strictly required for runtime operations, it is only tracked for the purposes of + * debugging. While overhead of linking those pages together is very small, unnecessary operations are avoided. + * + * Compared to GCOs, regular allocations have two important differences: they can be freed in isolation, + * and they don't start with a GC header. Because of this, each allocation is prefixed with block metadata, + * which contains the pointer to the page for allocated blocks, and the pointer to the next free block + * inside the page for freed blocks. + * For regular allocations that are too large to fit in a page (using the same threshold of 512 bytes), + * we don't allocate a separate page, instead simply using frealloc to allocate a vanilla block of memory. + * + * Just like GCO pages, we store a page free list (global_State::freepages) that allows O(1) allocation; + * there is no global list for non-GCO pages since we never need to traverse them directly. + * + * In both cases, we pick the page by computing the size class from the block size which rounds the block + * size up to reduce the chance that we'll allocate pages that have very few allocated blocks. The size + * class strategy is determined by SizeClassConfig constructor. + * + * Note that when the last block in a page is freed, we immediately free the page with frealloc - the + * memory manager doesn't currently attempt to keep unused memory around. This can result in excessive + * allocation traffic and can be mitigated by adding a page cache in the future. + * + * For both GCO and non-GCO pages, the per-page block allocation combines bump pointer style allocation + * (lua_Page::freeNext) and per-page free list (lua_Page::freeList). We use the bump allocator to allocate + * the contents of the page, and the free list for further reuse; this allows shorter page setup times + * which results in less variance between allocation cost, as well as tighter sweep bounds for newly + * allocated pages. + */ + +#ifndef __has_feature +#define __has_feature(x) 0 +#endif + +#if __has_feature(address_sanitizer) || defined(LUAU_ENABLE_ASAN) +#include +#define ASAN_POISON_MEMORY_REGION(addr, size) __asan_poison_memory_region((addr), (size)) +#define ASAN_UNPOISON_MEMORY_REGION(addr, size) __asan_unpoison_memory_region((addr), (size)) +#else +#define ASAN_POISON_MEMORY_REGION(addr, size) (void)0 +#define ASAN_UNPOISON_MEMORY_REGION(addr, size) (void)0 +#endif + +/* + * The sizes of most Luau objects aren't crucial for code correctness, but they are crucial for memory efficiency + * To prevent some of them accidentally growing and us losing memory without realizing it, we're going to lock + * the sizes of all critical structures down. + */ +#if defined(__APPLE__) +#define ABISWITCH(x64, ms32, gcc32) (sizeof(void*) == 8 ? x64 : gcc32) +#elif defined(__i386__) && defined(__MINGW32__) && !defined(__MINGW64__) +#define ABISWITCH(x64, ms32, gcc32) (ms32) +#elif defined(__i386__) && !defined(_MSC_VER) +#define ABISWITCH(x64, ms32, gcc32) (gcc32) +#else +// Android somehow uses a similar ABI to MSVC, *not* to iOS... +#define ABISWITCH(x64, ms32, gcc32) (sizeof(void*) == 8 ? x64 : ms32) +#endif + +#if LUA_VECTOR_SIZE == 4 +static_assert(sizeof(TValue) == ABISWITCH(24, 24, 24), "size mismatch for value"); +static_assert(sizeof(LuaNode) == ABISWITCH(48, 48, 48), "size mismatch for table entry"); +#else +static_assert(sizeof(TValue) == ABISWITCH(16, 16, 16), "size mismatch for value"); +static_assert(sizeof(LuaNode) == ABISWITCH(32, 32, 32), "size mismatch for table entry"); +#endif + +static_assert(offsetof(TString, data) == ABISWITCH(24, 20, 20), "size mismatch for string header"); +static_assert(sizeof(LuaTable) == ABISWITCH(48, 32, 32), "size mismatch for table header"); +static_assert(offsetof(Buffer, data) == ABISWITCH(8, 8, 8), "size mismatch for buffer header"); + +// The userdata is designed to provide 16 byte alignment for 16 byte and larger userdata sizes +static_assert(offsetof(Udata, data) == 16, "data must be at precise offset provide proper alignment"); + +const size_t kSizeClasses = LUA_SIZECLASSES; + +// Controls the number of entries in SizeClassConfig and define the maximum possible paged allocation size +// Modifications require updates the SizeClassConfig initialization +const size_t kMaxSmallSize = 1024; + +// Effective limit on object size to use paged allocation +// Can be modified without additional changes to code, provided it is smaller or equal to kMaxSmallSize +const size_t kMaxSmallSizeUsed = 1024; + +const size_t kLargePageThreshold = 512; // larger pages are used for objects larger than this size to fit more of them into a page + +// constant factor to reduce our page sizes by, to increase the chances that pages we allocate will +// allow external allocators to allocate them without wasting space due to rounding introduced by their heap meta data +const size_t kExternalAllocatorMetaDataReduction = 24; + +const size_t kSmallPageSize = 16 * 1024 - kExternalAllocatorMetaDataReduction; +const size_t kLargePageSize = 32 * 1024 - kExternalAllocatorMetaDataReduction; + +const size_t kBlockHeader = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*); // suitable for aligning double & void* on all platforms +const size_t kGCOLinkOffset = (sizeof(GCheader) + sizeof(void*) - 1) & ~(sizeof(void*) - 1); // GCO pages contain freelist links after the GC header + +struct SizeClassConfig +{ + int sizeOfClass[kSizeClasses]; + int8_t classForSize[kMaxSmallSize + 1]; + int classCount = 0; + + SizeClassConfig() + { + memset(sizeOfClass, 0, sizeof(sizeOfClass)); + memset(classForSize, -1, sizeof(classForSize)); + + // we use a progressive size class scheme: + // - all size classes are aligned by 8b to satisfy pointer alignment requirements + // - we first allocate sizes classes in multiples of 8 + // - after the first cutoff we allocate size classes in multiples of 16 + // - after the second cutoff we allocate size classes in multiples of 32 + // - after the third cutoff we allocate size classes in multiples of 64 + // this balances internal fragmentation vs external fragmentation + for (int size = 8; size < 64; size += 8) + sizeOfClass[classCount++] = size; + + for (int size = 64; size < 256; size += 16) + sizeOfClass[classCount++] = size; + + for (int size = 256; size < 512; size += 32) + sizeOfClass[classCount++] = size; + + for (int size = 512; size <= 1024; size += 64) + sizeOfClass[classCount++] = size; + + LUAU_ASSERT(size_t(classCount) <= kSizeClasses); + + // fill the lookup table for all classes + for (int klass = 0; klass < classCount; ++klass) + classForSize[sizeOfClass[klass]] = int8_t(klass); + + // fill the gaps in lookup table + for (int size = kMaxSmallSize - 1; size >= 0; --size) + if (classForSize[size] < 0) + classForSize[size] = classForSize[size + 1]; + } +}; + +const SizeClassConfig kSizeClassConfig; + +// size class for a block of size sz; returns -1 for size=0 because empty allocations take no space +#define sizeclass(sz) (size_t((sz) - 1) < kMaxSmallSizeUsed ? kSizeClassConfig.classForSize[sz] : -1) + +// metadata for a block is stored in the first pointer of the block +#define metadata(block) (*(void**)(block)) +#define freegcolink(block) (*(void**)((char*)block + kGCOLinkOffset)) + +#if defined(LUAU_ASSERTENABLED) +#define debugpageset(x) (x) +#else +#define debugpageset(x) NULL +#endif + +struct lua_Page +{ + // list of pages with free blocks + lua_Page* prev; + lua_Page* next; + + // list of all pages + lua_Page* listprev; + lua_Page* listnext; + + int pageSize; // page size in bytes, including page header + int blockSize; // block size in bytes, including block header (for non-GCO) + + void* freeList; // next free block in this page; linked with metadata()/freegcolink() + int freeNext; // next free block offset in this page, in bytes; when negative, freeList is used instead + int busyBlocks; // number of blocks allocated out of this page + + // provide additional padding based on current object size to provide 16 byte alignment of data + // later static_assert checks that this requirement is held + char padding[sizeof(void*) == 8 ? 8 : 12]; + + char data[1]; +}; + +static_assert(offsetof(lua_Page, data) % 16 == 0, "data must be 16 byte aligned to provide properly aligned allocation of userdata objects"); + +l_noret luaM_toobig(lua_State* L) +{ + luaG_runerror(L, "memory allocation error: block too big"); +} + +enum class AllocationPath +{ + Frealloc, + BufferFrealloc +}; + +template +static LUAU_FORCEINLINE void* pagealloc(global_State* g, void* block, size_t oldsize, size_t newsize) +{ + if (path == AllocationPath::BufferFrealloc) + return (*g->cagealloc)(g->cageud, block, oldsize, newsize, type); + else + return (*g->frealloc)(g->ud, block, oldsize, newsize); +} + +template +static LUAU_FORCEINLINE lua_Page** freegcopages(global_State* g) +{ + if (path == AllocationPath::BufferFrealloc) + return g->freegcopages_cage; + else + return g->freegcopages; +} + +template +static LUAU_FORCEINLINE lua_Page** allgcopages(global_State* g) +{ + if (path == AllocationPath::BufferFrealloc) + return &g->allgcopages_cage; + else + return &g->allgcopages; +} + +template +static lua_Page* newpage(lua_State* L, lua_Page** pageset, int pageSize, int blockSize, int blockCount) +{ + global_State* g = L->global; + + LUAU_ASSERT(pageSize - int(offsetof(lua_Page, data)) >= blockSize * blockCount); + + lua_Page* page = (lua_Page*)pagealloc(g, NULL, 0, pageSize); + if (!page) + { + if (nothrow) + return nullptr; + else + luaD_throw(L, LUA_ERRMEM); + } + + ASAN_POISON_MEMORY_REGION(page->data, blockSize * blockCount); + + // setup page header + page->prev = NULL; + page->next = NULL; + + page->listprev = NULL; + page->listnext = NULL; + + page->pageSize = pageSize; + page->blockSize = blockSize; + + // note: we start with the last block in the page and move downward + // either order would work, but that way we don't need to store the block count in the page + // additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order + page->freeList = NULL; + page->freeNext = (blockCount - 1) * blockSize; + page->busyBlocks = 0; + + if (pageset) + { + page->listnext = *pageset; + if (page->listnext) + page->listnext->listprev = page; + *pageset = page; + } + + return page; +} + +// this is part of a cold path in newblock and newgcoblock +// it is marked as noinline to prevent it from being inlined into those functions +// if it is inlined, then the compiler may determine those functions are "too big" to be profitably inlined, which results in reduced performance +template +LUAU_NOINLINE static lua_Page* newclasspage(lua_State* L, lua_Page** freepageset, lua_Page** pageset, uint8_t sizeClass, bool storeMetadata) +{ + int sizeOfClass = kSizeClassConfig.sizeOfClass[sizeClass]; + int pageSize = sizeOfClass > int(kLargePageThreshold) ? kLargePageSize : kSmallPageSize; + int blockSize = sizeOfClass + (storeMetadata ? kBlockHeader : 0); + int blockCount = (pageSize - offsetof(lua_Page, data)) / blockSize; + + lua_Page* page = newpage(L, pageset, pageSize, blockSize, blockCount); + + // prepend a page to page freelist (which is empty because we only ever allocate a new page when it is!) + LUAU_ASSERT(!freepageset[sizeClass]); + freepageset[sizeClass] = page; + + return page; +} + +template +static void freepage(lua_State* L, lua_Page** pageset, lua_Page* page) +{ + global_State* g = L->global; + + if (pageset) + { + // remove page from alllist + if (page->listnext) + page->listnext->listprev = page->listprev; + + if (page->listprev) + page->listprev->listnext = page->listnext; + else if (*pageset == page) + *pageset = page->listnext; + } + + // so long + pagealloc(g, page, page->pageSize, 0); +} + +template +static void freeclasspage(lua_State* L, lua_Page** freepageset, lua_Page** pageset, lua_Page* page, uint8_t sizeClass) +{ + // remove page from freelist + if (page->next) + page->next->prev = page->prev; + + if (page->prev) + page->prev->next = page->next; + else if (freepageset[sizeClass] == page) + freepageset[sizeClass] = page->next; + + freepage(L, pageset, page); +} + +template +static void* newblock(lua_State* L, int sizeClass) +{ + global_State* g = L->global; + lua_Page* page = g->freepages[sizeClass]; + + // slow path: no page in the freelist, allocate a new one + if (!page) + { + page = newclasspage(L, g->freepages, debugpageset(&g->allpages), sizeClass, true); + + if (nothrow && !page) + return nullptr; + } + + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(page->freeList || page->freeNext >= 0); + LUAU_ASSERT(size_t(page->blockSize) == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader); + + void* block; + + if (page->freeNext >= 0) + { + block = &page->data + page->freeNext; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + else + { + block = page->freeList; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeList = metadata(block); + page->busyBlocks++; + } + + // the first word in a block point back to the page + metadata(block) = page; + + // if we allocate the last block out of a page, we need to remove it from free list + if (!page->freeList && page->freeNext < 0) + { + g->freepages[sizeClass] = page->next; + if (page->next) + page->next->prev = NULL; + page->next = NULL; + } + + // the user data is right after the metadata + return (char*)block + kBlockHeader; +} + +template +static LUAU_FORCEINLINE void* newgcoblock(lua_State* L, int sizeClass) +{ + global_State* g = L->global; + lua_Page** freepages = freegcopages(g); + lua_Page** allpages = allgcopages(g); + + lua_Page* page = freepages[sizeClass]; + + // slow path: no page in the freelist, allocate a new one + if (!page) + page = newclasspage(L, freepages, allpages, sizeClass, false); + + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(page->freeList || page->freeNext >= 0); + LUAU_ASSERT(page->blockSize == kSizeClassConfig.sizeOfClass[sizeClass]); + + void* block; + + if (page->freeNext >= 0) + { + block = &page->data + page->freeNext; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + else + { + block = page->freeList; + ASAN_UNPOISON_MEMORY_REGION((char*)block + sizeof(GCheader), page->blockSize - sizeof(GCheader)); + + // when separate block metadata is not used, free list link is stored inside the block data itself + page->freeList = freegcolink(block); + page->busyBlocks++; + } + + // if we allocate the last block out of a page, we need to remove it from free list + if (!page->freeList && page->freeNext < 0) + { + freepages[sizeClass] = page->next; + if (page->next) + page->next->prev = NULL; + page->next = NULL; + } + + return block; +} + +static void freeblock(lua_State* L, int sizeClass, void* block) +{ + global_State* g = L->global; + + // the user data is right after the metadata + LUAU_ASSERT(block); + block = (char*)block - kBlockHeader; + + lua_Page* page = (lua_Page*)metadata(block); + LUAU_ASSERT(page && page->busyBlocks > 0); + LUAU_ASSERT(size_t(page->blockSize) == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader); + LUAU_ASSERT(block >= page->data && block < (char*)page + page->pageSize); + + // if the page wasn't in the page free list, it should be now since it got a block! + if (!page->freeList && page->freeNext < 0) + { + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(!page->next); + + page->next = g->freepages[sizeClass]; + if (page->next) + page->next->prev = page; + g->freepages[sizeClass] = page; + } + + // add the block to the free list inside the page + metadata(block) = page->freeList; + page->freeList = block; + + ASAN_POISON_MEMORY_REGION(block, page->blockSize); + + page->busyBlocks--; + + // if it's the last block in the page, we don't need the page + if (page->busyBlocks == 0) + freeclasspage(L, g->freepages, debugpageset(&g->allpages), page, sizeClass); +} + +template +static LUAU_FORCEINLINE void freegcoblock(lua_State* L, int sizeClass, void* block, lua_Page* page) +{ + LUAU_ASSERT(page && page->busyBlocks > 0); + LUAU_ASSERT(page->blockSize == kSizeClassConfig.sizeOfClass[sizeClass]); + LUAU_ASSERT(block >= page->data && block < (char*)page + page->pageSize); + + global_State* g = L->global; + lua_Page** freepages = freegcopages(g); + lua_Page** allpages = allgcopages(g); + + // if the page wasn't in the page free list, it should be now since it got a block! + if (!page->freeList && page->freeNext < 0) + { + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(!page->next); + + page->next = freepages[sizeClass]; + if (page->next) + page->next->prev = page; + freepages[sizeClass] = page; + } + + // when separate block metadata is not used, free list link is stored inside the block data itself + freegcolink(block) = page->freeList; + page->freeList = block; + + ASAN_POISON_MEMORY_REGION((char*)block + sizeof(GCheader), page->blockSize - sizeof(GCheader)); + + page->busyBlocks--; + + // if it's the last block in the page, we don't need the page + if (page->busyBlocks == 0) + freeclasspage(L, freepages, allpages, page, sizeClass); +} + +void* luaM_new_(lua_State* L, size_t nsize, uint8_t memcat) +{ + global_State* g = L->global; + + int nclass = sizeclass(nsize); + + void* block = nclass >= 0 ? newblock(L, nclass) : (*g->frealloc)(g->ud, NULL, 0, nsize); + if (block == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, block, 0, nsize, memcat, LUA_T_ALL, 0); + } + + return block; +} + +void* luaM_trynew_(lua_State* L, size_t nsize, uint8_t memcat) +{ + LUAU_ASSERT(nsize != 0); + global_State* g = L->global; + + int nclass = sizeclass(nsize); + + void* block = nclass >= 0 ? newblock(L, nclass) : (*g->frealloc)(g->ud, NULL, 0, nsize); + if (block == NULL) + return nullptr; + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, block, 0, nsize, memcat, LUA_T_ALL, 0); + } + + return block; +} + +GCObject* luaM_newgco_(lua_State* L, size_t nsize, uint8_t memcat, int tt, int tag) +{ + // we need to accommodate space for link for free blocks (freegcolink) + LUAU_ASSERT(nsize >= kGCOLinkOffset + sizeof(void*)); + + global_State* g = L->global; + + int nclass = sizeclass(nsize); + + void* block = NULL; + + if (nclass >= 0) + { + block = newgcoblock(L, nclass); + } + else + { + lua_Page* page = newpage(L, &g->allgcopages, offsetof(lua_Page, data) + int(nsize), int(nsize), 1); + + block = &page->data; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + + if (block == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, block, 0, nsize, memcat, tt, tag); + } + + return (GCObject*)block; +} + +GCObject* luaM_newgcocaged_(lua_State* L, size_t nsize, uint8_t memcat, int tt, int tag) +{ + LUAU_ASSERT(FFlag::LuauBufferCage); + // we need to accommodate space for link for free blocks (freegcolink) + LUAU_ASSERT(nsize >= kGCOLinkOffset + sizeof(void*)); + + global_State* g = L->global; + + int nclass = sizeclass(nsize); + + void* block = NULL; + + LUAU_ASSERT(g->cagealloc); + + if (nclass >= 0) + { + block = newgcoblock(L, nclass); + } + else + { + lua_Page* page = + newpage(L, &g->allgcopages_cage, offsetof(lua_Page, data) + int(nsize), int(nsize), 1); + + block = &page->data; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + + if (block == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, block, 0, nsize, memcat, tt, tag); + } + + return (GCObject*)block; +} + +GCObject* luaM_newgcofixed_(lua_State* L, size_t nsize, uint8_t memcat, int tt) +{ + // we need to accommodate space for link for free blocks (freegcolink) + LUAU_ASSERT(nsize >= kGCOLinkOffset + sizeof(void*)); + + global_State* g = L->global; + + int nclass = sizeclass(nsize); + LUAU_ASSERT(nclass >= 0); + + void* block = newgcoblock(L, nclass); + + if (block == nullptr) + luaD_throw(L, LUA_ERRMEM); + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, block, 0, nsize, memcat, tt, 0); + } + + return (GCObject*)block; +} + +void luaM_free_(lua_State* L, void* block, size_t osize, uint8_t memcat) +{ + global_State* g = L->global; + LUAU_ASSERT((osize == 0) == (block == NULL)); + + if (LUAU_UNLIKELY(!!g->cb.onfree)) + { + g->cb.onfree(L, block); + } + + int oclass = sizeclass(osize); + + if (oclass >= 0) + freeblock(L, oclass, block); + else + (*g->frealloc)(g->ud, block, osize, 0); + + g->totalbytes -= osize; + g->memcatbytes[memcat] -= osize; +} + +void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page) +{ + global_State* g = L->global; + LUAU_ASSERT((osize == 0) == (block == NULL)); + + if (LUAU_UNLIKELY(!!g->cb.onfree)) + { + g->cb.onfree(L, block); + } + + int oclass = sizeclass(osize); + + if (oclass >= 0) + { + block->gch.tt = LUA_TNIL; + freegcoblock(L, oclass, block, page); + } + else + { + LUAU_ASSERT(page->busyBlocks == 1); + LUAU_ASSERT(size_t(page->blockSize) == osize); + LUAU_ASSERT((void*)block == page->data); + + freepage(L, &g->allgcopages, page); + } + + g->totalbytes -= osize; + g->memcatbytes[memcat] -= osize; +} + +void luaM_freegcofixed_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page) +{ + global_State* g = L->global; + + if (LUAU_UNLIKELY(!!g->cb.onfree)) + { + g->cb.onfree(L, block); + } + + int oclass = sizeclass(osize); + LUAU_ASSERT(oclass >= 0); + + + block->gch.tt = LUA_TNIL; + + freegcoblock(L, oclass, block, page); + + g->totalbytes -= osize; + g->memcatbytes[memcat] -= osize; +} + +void luaM_freegcocaged_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page) +{ + LUAU_ASSERT(FFlag::LuauBufferCage); + global_State* g = L->global; + LUAU_ASSERT((osize == 0) == (block == NULL)); + LUAU_ASSERT(g->cagealloc); + + if (LUAU_UNLIKELY(!!g->cb.onfree)) + { + g->cb.onfree(L, block); + } + + int oclass = sizeclass(osize); + + if (oclass >= 0) + { + block->gch.tt = LUA_TNIL; + + freegcoblock(L, oclass, block, page); + } + else + { + LUAU_ASSERT(page->busyBlocks == 1); + LUAU_ASSERT(size_t(page->blockSize) == osize); + LUAU_ASSERT((void*)block == page->data); + + freepage(L, &g->allgcopages_cage, page); + } + + g->totalbytes -= osize; + g->memcatbytes[memcat] -= osize; +} + +void* luaM_realloc_(lua_State* L, void* block, size_t osize, size_t nsize, uint8_t memcat) +{ + global_State* g = L->global; + LUAU_ASSERT((osize == 0) == (block == NULL)); + + int nclass = sizeclass(nsize); + int oclass = sizeclass(osize); + void* result; + + if (LUAU_UNLIKELY(!!g->cb.onfree) && block != NULL) + { + g->cb.onfree(L, block); + } + + // if either block needs to be allocated using a block allocator, we can't use realloc directly + if (nclass >= 0 || oclass >= 0) + { + result = nclass >= 0 ? newblock(L, nclass) : (*g->frealloc)(g->ud, NULL, 0, nsize); + if (result == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + + if (osize > 0 && nsize > 0) + memcpy(result, block, osize < nsize ? osize : nsize); + + if (oclass >= 0) + freeblock(L, oclass, block); + else + (*g->frealloc)(g->ud, block, osize, 0); + } + else + { + result = (*g->frealloc)(g->ud, block, osize, nsize); + if (result == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + } + + LUAU_ASSERT((nsize == 0) == (result == NULL)); + g->totalbytes = (g->totalbytes - osize) + nsize; + g->memcatbytes[memcat] += nsize - osize; + + if (LUAU_UNLIKELY(!!g->cb.onallocate)) + { + g->cb.onallocate(L, result, osize, nsize, memcat, LUA_T_ALL, 0); + } + + return result; +} + +void luaM_getpagewalkinfo(lua_Page* page, char** start, char** end, int* busyBlocks, int* blockSize) +{ + int blockCount = (page->pageSize - offsetof(lua_Page, data)) / page->blockSize; + + LUAU_ASSERT(page->freeNext >= -page->blockSize && page->freeNext <= (blockCount - 1) * page->blockSize); + + char* data = page->data; // silences ubsan when indexing page->data + + *start = data + page->freeNext + page->blockSize; + *end = data + blockCount * page->blockSize; + *busyBlocks = page->busyBlocks; + *blockSize = page->blockSize; +} + +void luaM_getpageinfo(lua_Page* page, int* pageBlocks, int* busyBlocks, int* blockSize, int* pageSize) +{ + *pageBlocks = (page->pageSize - offsetof(lua_Page, data)) / page->blockSize; + *busyBlocks = page->busyBlocks; + *blockSize = page->blockSize; + *pageSize = page->pageSize; +} + +lua_Page* luaM_getnextpage(lua_Page* page) +{ + return page->listnext; +} + +void luaM_visitpage(lua_Page* page, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)) +{ + char* start; + char* end; + int busyBlocks; + int blockSize; + luaM_getpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + for (char* pos = start; pos != end; pos += blockSize) + { + GCObject* gco = (GCObject*)pos; + + // skip memory blocks that are already freed + if (gco->gch.tt == LUA_TNIL) + continue; + + // when true is returned it means that the element was deleted + if (visitor(context, page, gco)) + { + LUAU_ASSERT(busyBlocks > 0); + + // if the last block was removed, page would be removed as well + if (--busyBlocks == 0) + break; + } + } +} + +void luaM_visitgco(lua_State* L, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)) +{ + global_State* g = L->global; + + for (lua_Page* curr = g->allgcopages; curr;) + { + lua_Page* next = curr->listnext; // block visit might destroy the page + + luaM_visitpage(curr, context, visitor); + + curr = next; + } + + for (lua_Page* curr = g->allgcopages_cage; curr;) + { + lua_Page* next = curr->listnext; // block visit might destroy the page + + luaM_visitpage(curr, context, visitor); + + curr = next; + } +} diff --git a/External/luau/VM/src/lmem.h b/External/luau/VM/src/lmem.h new file mode 100644 index 00000000..beb8875a --- /dev/null +++ b/External/luau/VM/src/lmem.h @@ -0,0 +1,44 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lua.h" + +struct lua_Page; +union GCObject; + +#define luaM_newgco(L, t, size, memcat, tt) cast_to(t*, luaM_newgco_(L, size, memcat, tt, 0)) +#define luaM_newgcoudata(L, t, size, memcat, tt, tag) cast_to(t*, luaM_newgco_(L, size, memcat, tt, tag)) +#define luaM_newgcofixed(L, t, size, memcat, tt) cast_to(t*, luaM_newgcofixed_(L, size, memcat, tt)) +#define luaM_newgcocaged(L, t, size, memcat, tt) cast_to(t*, luaM_newgcocaged_(L, size, memcat, tt, 0)) +#define luaM_freegco(L, p, size, memcat, page) luaM_freegco_(L, obj2gco(p), size, memcat, page) +#define luaM_freegcofixed(L, p, size, memcat, page) luaM_freegcofixed_(L, obj2gco(p), size, memcat, page) +#define luaM_freegcocaged(L, p, size, memcat, page) luaM_freegcocaged_(L, obj2gco(p), size, memcat, page) + +#define luaM_arraysize_(L, n, e) ((cast_to(size_t, (n)) <= SIZE_MAX / (e)) ? (n) * (e) : (luaM_toobig(L), SIZE_MAX)) + +#define luaM_newarray(L, n, t, memcat) cast_to(t*, luaM_new_(L, luaM_arraysize_(L, n, sizeof(t)), memcat)) +#define luaM_trynewarray(L, n, t, memcat) cast_to(t*, luaM_trynew_(L, luaM_arraysize_(L, n, sizeof(t)), memcat)) +#define luaM_freearray(L, b, n, t, memcat) luaM_free_(L, (b), (n) * sizeof(t), memcat) +#define luaM_reallocarray(L, v, oldn, n, t, memcat) \ + ((v) = cast_to(t*, luaM_realloc_(L, v, (oldn) * sizeof(t), luaM_arraysize_(L, n, sizeof(t)), memcat))) + +LUAI_FUNC void* luaM_new_(lua_State* L, size_t nsize, uint8_t memcat); +LUAI_FUNC void* luaM_trynew_(lua_State* L, size_t nsize, uint8_t memcat); +LUAI_FUNC GCObject* luaM_newgco_(lua_State* L, size_t nsize, uint8_t memcat, int tt, int tag); +LUAI_FUNC GCObject* luaM_newgcofixed_(lua_State* L, size_t nsize, uint8_t memcat, int tt); +LUAI_FUNC GCObject* luaM_newgcocaged_(lua_State* L, size_t nsize, uint8_t memcat, int tt, int tag); +LUAI_FUNC void luaM_free_(lua_State* L, void* block, size_t osize, uint8_t memcat); +LUAI_FUNC void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page); +LUAI_FUNC void luaM_freegcofixed_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page); +LUAI_FUNC void luaM_freegcocaged_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page); +LUAI_FUNC void* luaM_realloc_(lua_State* L, void* block, size_t osize, size_t nsize, uint8_t memcat); + +LUAI_FUNC l_noret luaM_toobig(lua_State* L); + +LUAI_FUNC void luaM_getpagewalkinfo(lua_Page* page, char** start, char** end, int* busyBlocks, int* blockSize); +LUAI_FUNC void luaM_getpageinfo(lua_Page* page, int* pageBlocks, int* busyBlocks, int* blockSize, int* pageSize); +LUAI_FUNC lua_Page* luaM_getnextpage(lua_Page* page); + +LUAI_FUNC void luaM_visitpage(lua_Page* page, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)); +LUAI_FUNC void luaM_visitgco(lua_State* L, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)); diff --git a/External/luau/VM/src/lnumprint.cpp b/External/luau/VM/src/lnumprint.cpp new file mode 100644 index 00000000..aea83d2a --- /dev/null +++ b/External/luau/VM/src/lnumprint.cpp @@ -0,0 +1,393 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "luaconf.h" +#include "lnumutils.h" + +#include "lcommon.h" + +#include + +#ifdef _MSC_VER +#include +#endif + +// This work is based on: +// Raffaello Giulietti. The Schubfach way to render doubles. 2021 +// https://drive.google.com/file/d/1IEeATSVnEE6TkrHlCYNY2GjaraBjOT4f/edit + +// The code uses the notation from the paper for local variables where appropriate, and refers to paper sections/figures/results. + +// 9.8.2. Precomputed table for 128-bit overestimates of powers of 10 (see figure 3 for table bounds) +// To avoid storing 616 128-bit numbers directly we use a technique inspired by Dragonbox implementation and store 16 consecutive +// powers using a 128-bit baseline and a bitvector with 1-bit scale and 3-bit offset for the delta between each entry and base*5^k +static const int kPow10TableMin = -292; +static const int kPow10TableMax = 324; + +// clang-format off +static const uint64_t kPow5Table[16] = { + 0x8000000000000000, 0xa000000000000000, 0xc800000000000000, 0xfa00000000000000, 0x9c40000000000000, 0xc350000000000000, + 0xf424000000000000, 0x9896800000000000, 0xbebc200000000000, 0xee6b280000000000, 0x9502f90000000000, 0xba43b74000000000, + 0xe8d4a51000000000, 0x9184e72a00000000, 0xb5e620f480000000, 0xe35fa931a0000000, +}; +static const uint64_t kPow10Table[(kPow10TableMax - kPow10TableMin + 1 + 15) / 16][3] = { + {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b, 0x333443443333443b}, {0x8dd01fad907ffc3b, 0xae3da7d97f6792e4, 0xbbb3ab3cb3ba3cbc}, + {0x9d71ac8fada6c9b5, 0x6f773fc3603db4aa, 0x4ba4bc4bb4bb4bcc}, {0xaecc49914078536d, 0x58fae9f773886e19, 0x3ba3bc33b43b43bb}, + {0xc21094364dfb5636, 0x985915fc12f542e5, 0x33b43b43a33b33cb}, {0xd77485cb25823ac7, 0x7d633293366b828c, 0x34b44c444343443c}, + {0xef340a98172aace4, 0x86fb897116c87c35, 0x333343333343334b}, {0x84c8d4dfd2c63f3b, 0x29ecd9f40041e074, 0xccaccbbcbcbb4bbc}, + {0x936b9fcebb25c995, 0xcab10dd900beec35, 0x3ab3ab3ab3bb3bbb}, {0xa3ab66580d5fdaf5, 0xc13e60d0d2e0ebbb, 0x4cc3dc4db4db4dbb}, + {0xb5b5ada8aaff80b8, 0x0d819992132456bb, 0x33b33a34c33b34ab}, {0xc9bcff6034c13052, 0xfc89b393dd02f0b6, 0x33c33b44b43c34bc}, + {0xdff9772470297ebd, 0x59787e2b93bc56f8, 0x43b444444443434c}, {0xf8a95fcf88747d94, 0x75a44c6397ce912b, 0x443334343443343b}, + {0x8a08f0f8bf0f156b, 0x1b8e9ecb641b5900, 0xbbabab3aa3ab4ccc}, {0x993fe2c6d07b7fab, 0xe546a8038efe402a, 0x4cb4bc4db4db4bcc}, + {0xaa242499697392d2, 0xdde50bd1d5d0b9ea, 0x3ba3ba3bb33b33bc}, {0xbce5086492111aea, 0x88f4bb1ca6bcf585, 0x44b44c44c44c43cb}, + {0xd1b71758e219652b, 0xd3c36113404ea4a9, 0x44c44c44c444443b}, {0xe8d4a51000000000, 0x0000000000000000, 0x444444444444444c}, + {0x813f3978f8940984, 0x4000000000000000, 0xcccccccccccccccc}, {0x8f7e32ce7bea5c6f, 0xe4820023a2000000, 0xbba3bc4cc4cc4ccc}, + {0x9f4f2726179a2245, 0x01d762422c946591, 0x4aa3bb3aa3ba3bab}, {0xb0de65388cc8ada8, 0x3b25a55f43294bcc, 0x3ca33b33b44b43bc}, + {0xc45d1df942711d9a, 0x3ba5d0bd324f8395, 0x44c44c34c44b44cb}, {0xda01ee641a708de9, 0xe80e6f4820cc9496, 0x33b33b343333333c}, + {0xf209787bb47d6b84, 0xc0678c5dbd23a49b, 0x443444444443443b}, {0x865b86925b9bc5c2, 0x0b8a2392ba45a9b3, 0xdbccbcccb4cb3bbb}, + {0x952ab45cfa97a0b2, 0xdd945a747bf26184, 0x3bc4bb4ab3ca3cbc}, {0xa59bc234db398c25, 0x43fab9837e699096, 0x3bb3ac3ab3bb33ac}, + {0xb7dcbf5354e9bece, 0x0c11ed6d538aeb30, 0x33b43b43b34c34dc}, {0xcc20ce9bd35c78a5, 0x31ec038df7b441f5, 0x34c44c43c44b44cb}, + {0xe2a0b5dc971f303a, 0x2e44ae64840fd61e, 0x333333333333333c}, {0xfb9b7cd9a4a7443c, 0x169840ef017da3b2, 0x433344443333344c}, + {0x8bab8eefb6409c1a, 0x1ad089b6c2f7548f, 0xdcbdcc3cc4cc4bcb}, {0x9b10a4e5e9913128, 0xca7cf2b4191c8327, 0x3ab3cb3bc3bb4bbb}, + {0xac2820d9623bf429, 0x546345fa9fbdcd45, 0x3bb3cc43c43c43cb}, {0xbf21e44003acdd2c, 0xe0470a63e6bd56c4, 0x44b34a43b44c44bc}, + {0xd433179d9c8cb841, 0x5fa60692a46151ec, 0x43a33a33a333333c}, +}; +// clang-format on + +static const char kDigitTable[] = "0001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849" + "5051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899"; + +// x*y => 128-bit product (lo+hi) +inline uint64_t mul128(uint64_t x, uint64_t y, uint64_t* hi) +{ +#if defined(_MSC_VER) && defined(_M_X64) + return _umul128(x, y, hi); +#elif defined(__SIZEOF_INT128__) + unsigned __int128 r = x; + r *= y; + *hi = uint64_t(r >> 64); + return uint64_t(r); +#else + uint32_t x0 = uint32_t(x), x1 = uint32_t(x >> 32); + uint32_t y0 = uint32_t(y), y1 = uint32_t(y >> 32); + uint64_t p11 = uint64_t(x1) * y1, p01 = uint64_t(x0) * y1; + uint64_t p10 = uint64_t(x1) * y0, p00 = uint64_t(x0) * y0; + uint64_t mid = p10 + (p00 >> 32) + uint32_t(p01); + uint64_t r0 = (mid << 32) | uint32_t(p00); + uint64_t r1 = p11 + (mid >> 32) + (p01 >> 32); + *hi = r1; + return r0; +#endif +} + +// (x*y)>>64 => 128-bit product (lo+hi) +inline uint64_t mul192hi(uint64_t xhi, uint64_t xlo, uint64_t y, uint64_t* hi) +{ + uint64_t z2; + uint64_t z1 = mul128(xhi, y, &z2); + + uint64_t z1c; + uint64_t z0 = mul128(xlo, y, &z1c); + (void)z0; + + z1 += z1c; + z2 += (z1 < z1c); + + *hi = z2; + return z1; +} + +// 9.3. Rounding to odd (+ figure 8 + result 23) +inline uint64_t roundodd(uint64_t ghi, uint64_t glo, uint64_t cp) +{ + uint64_t xhi; + uint64_t xlo = mul128(glo, cp, &xhi); + (void)xlo; + + uint64_t yhi; + uint64_t ylo = mul128(ghi, cp, &yhi); + + uint64_t z = ylo + xhi; + return (yhi + (z < xhi)) | (z > 1); +} + +struct Decimal +{ + uint64_t s; + int k; +}; + +static Decimal schubfach(int exponent, uint64_t fraction) +{ + // Extract c & q such that c*2^q == |v| + uint64_t c = fraction; + int q = exponent - 1023 - 51; + + if (exponent != 0) // normal numbers have implicit leading 1 + { + c |= (1ull << 52); + q--; + } + + // 8.3. Fast path for integers + if (unsigned(-q) < 53 && (c & ((1ull << (-q)) - 1)) == 0) + return {c >> (-q), 0}; + + // 5. Rounding interval + int irr = (c == (1ull << 52) && q != -1074); // Qmin + int out = int(c & 1); + + // 9.8.1. Boundaries for c + uint64_t cbl = 4 * c - 2 + irr; + uint64_t cb = 4 * c; + uint64_t cbr = 4 * c + 2; + + // 9.1. Computing k and h + const int Q = 20; + const int C = 315652; // floor(2^Q * log10(2)) + const int A = -131008; // floor(2^Q * log10(3/4)) + const int C2 = 3483294; // floor(2^Q * log2(10)) + int k = (q * C + (irr ? A : 0)) >> Q; + int h = q + ((-k * C2) >> Q) + 1; // see (9) in 9.9 + + // 9.8.2. Overestimates of powers of 10 + // Recover 10^-k fraction using compact tables generated by tools/numutils.py + // The 128-bit fraction is encoded as 128-bit baseline * power-of-5 * scale + offset + LUAU_ASSERT(-k >= kPow10TableMin && -k <= kPow10TableMax); + int gtoff = -k - kPow10TableMin; + const uint64_t* gt = kPow10Table[gtoff >> 4]; + + uint64_t ghi; + uint64_t glo = mul192hi(gt[0], gt[1], kPow5Table[gtoff & 15], &ghi); + + // Apply 1-bit scale + 3-bit offset; note, offset is intentionally applied without carry, numutils.py validates that this is sufficient + int gterr = (gt[2] >> ((gtoff & 15) * 4)) & 15; + int gtscale = gterr >> 3; + + ghi <<= gtscale; + ghi += (glo >> 63) & gtscale; + glo <<= gtscale; + glo -= (gterr & 7) - 4; + + // 9.9. Boundaries for v + uint64_t vbl = roundodd(ghi, glo, cbl << h); + uint64_t vb = roundodd(ghi, glo, cb << h); + uint64_t vbr = roundodd(ghi, glo, cbr << h); + + // Main algorithm; see figure 7 + figure 9 + uint64_t s = vb / 4; + + if (s >= 10) + { + uint64_t sp = s / 10; + + bool upin = vbl + out <= 40 * sp; + bool wpin = vbr >= 40 * sp + 40 + out; + + if (upin != wpin) + return {sp + wpin, k + 1}; + } + + // Figure 7 contains the algorithm to select between u (s) and w (s+1) + // rup computes the last 4 conditions in that algorithm + // rup is only used when uin == win, but since these branches predict poorly we use branchless selects + bool uin = vbl + out <= 4 * s; + bool win = 4 * s + 4 + out <= vbr; + bool rup = vb >= 4 * s + 2 + 1 - (s & 1); + + return {s + (uin != win ? win : rup), k}; +} + +static char* printspecial(char* buf, int sign, uint64_t fraction) +{ + if (fraction == 0) + { + memcpy(buf, ("-inf") + (1 - sign), 4); + return buf + 3 + sign; + } + else + { + memcpy(buf, "nan", 4); + return buf + 3; + } +} + +static char* printunsignedrev(char* end, uint64_t num) +{ + while (num >= 10000) + { + unsigned int tail = unsigned(num % 10000); + + memcpy(end - 4, &kDigitTable[int(tail / 100) * 2], 2); + memcpy(end - 2, &kDigitTable[int(tail % 100) * 2], 2); + num /= 10000; + end -= 4; + } + + unsigned int rest = unsigned(num); + + while (rest >= 10) + { + memcpy(end - 2, &kDigitTable[int(rest % 100) * 2], 2); + rest /= 100; + end -= 2; + } + + if (rest) + { + end[-1] = '0' + int(rest); + end -= 1; + } + + return end; +} + +static char* printexp(char* buf, int num) +{ + *buf++ = 'e'; + *buf++ = num < 0 ? '-' : '+'; + + int v = num < 0 ? -num : num; + + if (v >= 100) + { + *buf++ = '0' + (v / 100); + v %= 100; + } + + memcpy(buf, &kDigitTable[v * 2], 2); + return buf + 2; +} + +inline char* trimzero(char* end) +{ + while (end[-1] == '0') + end--; + + return end; +} + +// We use fixed-length memcpy/memset since they lower to fast SIMD+scalar writes; the target buffers should have padding space +#define fastmemcpy(dst, src, size, sizefast) check_exp((size) <= sizefast, memcpy(dst, src, sizefast)) +#define fastmemset(dst, val, size, sizefast) check_exp((size) <= sizefast, memset(dst, val, sizefast)) + +char* luai_num2str(char* buf, double n) +{ + // IEEE-754 + union + { + double v; + uint64_t bits; + } v = {n}; + int sign = int(v.bits >> 63); + int exponent = int(v.bits >> 52) & 2047; + uint64_t fraction = v.bits & ((1ull << 52) - 1); + + // specials + if (LUAU_UNLIKELY(exponent == 0x7ff)) + return printspecial(buf, sign, fraction); + + // sign bit + *buf = '-'; + buf += sign; + + // zero + if (exponent == 0 && fraction == 0) + { + buf[0] = '0'; + return buf + 1; + } + + // convert binary to decimal using Schubfach + Decimal d = schubfach(exponent, fraction); + LUAU_ASSERT(d.s < uint64_t(1e17)); + + // print the decimal to a temporary buffer; we'll need to insert the decimal point and figure out the format + char decbuf[40]; + char* decend = decbuf + 20; // significand needs at most 17 digits; the rest of the buffer may be copied using fixed length memcpy + char* dec = printunsignedrev(decend, d.s); + + int declen = int(decend - dec); + LUAU_ASSERT(declen <= 17); + + int dot = declen + d.k; + + // the limits are somewhat arbitrary but changing them may require changing fastmemset/fastmemcpy sizes below + if (dot >= -5 && dot <= 21) + { + // fixed point format + if (dot <= 0) + { + buf[0] = '0'; + buf[1] = '.'; + + fastmemset(buf + 2, '0', -dot, 5); + fastmemcpy(buf + 2 + (-dot), dec, declen, 17); + + return trimzero(buf + 2 + (-dot) + declen); + } + else if (dot == declen) + { + // no dot + fastmemcpy(buf, dec, dot, 17); + + return buf + dot; + } + else if (dot < declen) + { + // dot in the middle + fastmemcpy(buf, dec, dot, 16); + + buf[dot] = '.'; + + fastmemcpy(buf + dot + 1, dec + dot, declen - dot, 16); + + return trimzero(buf + declen + 1); + } + else + { + // no dot, zero padding + fastmemcpy(buf, dec, declen, 17); + fastmemset(buf + declen, '0', dot - declen, 8); + + return buf + dot; + } + } + else + { + // scientific format + buf[0] = dec[0]; + buf[1] = '.'; + fastmemcpy(buf + 2, dec + 1, declen - 1, 16); + + char* exp = trimzero(buf + declen + 1); + + if (exp[-1] == '.') + exp--; + + return printexp(exp, dot - 1); + } +} + +char* luai_int2str(char* buf, int64_t l) +{ + uint64_t val = (l < 0) ? ~(uint64_t)l + 1 : (uint64_t)l; + + int numDigits = 1; + for (uint64_t cap = 10; (numDigits < 19) && (cap <= val); cap *= 10) + numDigits++; + + int pos = (l < 0) ? numDigits : (numDigits - 1); + buf[pos + 1] = 0; + do + { + buf[pos--] = '0' + (val % 10); + val /= 10; + } while (val != 0); + + if (l < 0) + buf[pos--] = '-'; + + LUAU_ASSERT(pos == -1); + + return &buf[(l < 0) ? (numDigits + 1) : numDigits]; +} diff --git a/External/luau/VM/src/lnumutils.h b/External/luau/VM/src/lnumutils.h new file mode 100644 index 00000000..5efa9a2b --- /dev/null +++ b/External/luau/VM/src/lnumutils.h @@ -0,0 +1,134 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include +#include + +#define luai_numadd(a, b) ((a) + (b)) +#define luai_numsub(a, b) ((a) - (b)) +#define luai_nummul(a, b) ((a) * (b)) +#define luai_numdiv(a, b) ((a) / (b)) +#define luai_numpow(a, b) (pow(a, b)) +#define luai_numunm(a) (-(a)) +#define luai_numisnan(a) ((a) != (a)) +#define luai_numeq(a, b) ((a) == (b)) +#define luai_numlt(a, b) ((a) < (b)) +#define luai_numle(a, b) ((a) <= (b)) +#define luai_inteq(a, b) ((a) == (b)) + +inline bool luai_veceq(const float* a, const float* b) +{ +#if LUA_VECTOR_SIZE == 4 + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]; +#else + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2]; +#endif +} + +inline bool luai_veceq(const double* a, const double* b) +{ +#if LUA_VECTOR_SIZE == 4 + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]; +#else + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2]; +#endif +} + +inline bool luai_vecisnan(const float* a) +{ +#if LUA_VECTOR_SIZE == 4 + return a[0] != a[0] || a[1] != a[1] || a[2] != a[2] || a[3] != a[3]; +#else + return a[0] != a[0] || a[1] != a[1] || a[2] != a[2]; +#endif +} + +inline bool luai_vecisnan(const double* a) +{ +#if LUA_VECTOR_SIZE == 4 + return a[0] != a[0] || a[1] != a[1] || a[2] != a[2] || a[3] != a[3]; +#else + return a[0] != a[0] || a[1] != a[1] || a[2] != a[2]; +#endif +} + +inline float luai_signf(float v) +{ + return v > 0.0f ? 1.0f : v < 0.0f ? -1.0f : 0.0f; +} + +inline double luai_signd(double v) +{ + return v > 0.0 ? 1.0 : v < 0.0 ? -1.0 : 0.0; +} + +inline float luai_clampf(float v, float min, float max) +{ + float r = v < min ? min : v; + return r > max ? max : r; +} + +inline double luai_clampd(double v, double min, double max) +{ + double r = v < min ? min : v; + return r > max ? max : r; +} + +LUAU_FASTMATH_BEGIN +inline double luai_nummod(double a, double b) +{ + return a - floor(a / b) * b; +} +LUAU_FASTMATH_END + +LUAU_FASTMATH_BEGIN +inline double luai_numidiv(double a, double b) +{ + return floor(a / b); +} +LUAU_FASTMATH_END + +inline float luai_lerpf(float a, float b, float t) +{ + return (t == 1.0f) ? b : a + (b - a) * t; +} + +inline double luai_lerpd(double a, double b, double t) +{ + return (t == 1.0) ? b : a + (b - a) * t; +} + +#define luai_num2int(i, d) ((i) = (int)(d)) + +#define luai_num2long(i, d) ((i) = (int64_t)(d)) + +#define luai_fabs condvectordouble(fabs, fabsf) +#define luai_sqrt condvectordouble(sqrt, sqrtf) +#define luai_floor condvectordouble(floor, floorf) +#define luai_ceil condvectordouble(ceil, ceilf) +#define luai_sign condvectordouble(luai_signd, luai_signf) +#define luai_clamp condvectordouble(luai_clampd, luai_clampf) +#define luai_lerp condvectordouble(luai_lerpd, luai_lerpf) + +// On MSVC in 32-bit, double to unsigned cast compiles into a call to __dtoui3, so we invoke x87->int64 conversion path manually +#if defined(_MSC_VER) && defined(_M_IX86) +#define luai_num2unsigned(i, n) \ + { \ + __int64 l; \ + __asm { __asm fld n __asm fistp l} \ + ; \ + i = (unsigned int)l; \ + } +#else +#define luai_num2unsigned(i, n) ((i) = (unsigned)(long long)(n)) +#endif + +#define LUAI_MAXNUM2STR 48 +#define LUAI_MAXINT2STR 30 + +LUAI_FUNC char* luai_num2str(char* buf, double n); +LUAI_FUNC char* luai_int2str(char* buf, int64_t n); + +#define luai_str2num(s, p) strtod((s), (p)) +#define luai_str2long(s, p, base) strtoll((s), (p), base) diff --git a/External/luau/VM/src/lobject.cpp b/External/luau/VM/src/lobject.cpp new file mode 100644 index 00000000..d3beb3c1 --- /dev/null +++ b/External/luau/VM/src/lobject.cpp @@ -0,0 +1,193 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lobject.h" + +#include "lstate.h" +#include "lstring.h" +#include "lgc.h" +#include "ldo.h" +#include "lnumutils.h" + +#include +#include +#include +#include + +const TValue luaO_nilobject_ = {{NULL}, {0}, LUA_TNIL}; + +int luaO_log2(unsigned int x) +{ + static const uint8_t log_2[256] = {0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}; + int l = -1; + while (x >= 256) + { + l += 8; + x >>= 8; + } + return l + log_2[x]; +} + +int luaO_rawequalObj(const TValue* t1, const TValue* t2) +{ + if (ttype(t1) != ttype(t2)) + return 0; + else + switch (ttype(t1)) + { + case LUA_TNIL: + return 1; + case LUA_TNUMBER: + return luai_numeq(nvalue(t1), nvalue(t2)); + case LUA_TINTEGER: + return luai_inteq(lvalue(t1), lvalue(t2)); + case LUA_TVECTOR: + return luai_veceq(vvalue(t1), vvalue(t2)); + case LUA_TBOOLEAN: + return bvalue(t1) == bvalue(t2); // boolean true must be 1 !! + case LUA_TLIGHTUSERDATA: + return pvalue(t1) == pvalue(t2) && lightuserdatatag(t1) == lightuserdatatag(t2); + default: + LUAU_ASSERT(iscollectable(t1)); + return gcvalue(t1) == gcvalue(t2); + } +} + +int luaO_rawequalKey(const TKey* t1, const TValue* t2) +{ + if (ttype(t1) != ttype(t2)) + return 0; + else + switch (ttype(t1)) + { + case LUA_TNIL: + return 1; + case LUA_TNUMBER: + return luai_numeq(nvalue(t1), nvalue(t2)); + case LUA_TINTEGER: + return luai_inteq(lvalue(t1), lvalue(t2)); + case LUA_TVECTOR: + return luai_veceq(vvalue(t1), vvalue(t2)); + case LUA_TBOOLEAN: + return bvalue(t1) == bvalue(t2); // boolean true must be 1 !! + case LUA_TLIGHTUSERDATA: + return pvalue(t1) == pvalue(t2) && lightuserdatatag(t1) == lightuserdatatag(t2); + default: + LUAU_ASSERT(iscollectable(t1)); + return gcvalue(t1) == gcvalue(t2); + } +} + +int luaO_str2d(const char* s, double* result) +{ + char* endptr; + *result = luai_str2num(s, &endptr); + if (endptr == s) + return 0; // conversion failed + if (*endptr == 'x' || *endptr == 'X') // maybe an hexadecimal constant? + *result = cast_num(strtoul(s, &endptr, 16)); + if (*endptr == '\0') + return 1; // most common case + while (isspace(cast_to(unsigned char, *endptr))) + endptr++; + if (*endptr != '\0') + return 0; // invalid trailing characters? + return 1; +} + +int luaO_str2l(const char* s, int64_t* result, int base) +{ + char* endptr = nullptr; + if (base == 10) + { + *result = luai_str2long(s, &endptr, base); + if (endptr == s) + return 0; // conversion failed + if (*endptr == 'x' || *endptr == 'X') // maybe an hexadecimal constant? + *result = (int64_t)strtoull(s, &endptr, 16); + } + else + { + // unsigned parse in other bases + *result = (int64_t)strtoull(s, &endptr, base); + if (endptr == s) + return 0; + } + if (*endptr == '\0') + return 1; // most common case + while (isspace(cast_to(unsigned char, *endptr))) + endptr++; + if (*endptr != '\0') + return 0; // invalid trailing characters? + return 1; +} + +const char* luaO_pushvfstring(lua_State* L, const char* fmt, va_list argp) +{ + char result[LUA_BUFFERSIZE]; + vsnprintf(result, sizeof(result), fmt, argp); + + setsvalue(L, L->top, luaS_new(L, result)); + incr_top(L); + return svalue(L->top - 1); +} + +const char* luaO_pushfstring(lua_State* L, const char* fmt, ...) +{ + const char* msg; + va_list argp; + va_start(argp, fmt); + msg = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + return msg; +} + +// Possible chunkname prefixes: +// +// '=' prefix: meant to represent custom chunknames. When truncation is needed, +// the beginning of the chunkname is kept. +// +// '@' prefix: meant to represent filepaths. When truncation is needed, the end +// of the filepath is kept, as this is more useful for identifying the file. +const char* luaO_chunkid(char* buf, size_t buflen, const char* source, size_t srclen) +{ + if (*source == '=') + { + if (srclen <= buflen) + return source + 1; + // truncate the part after = + memcpy(buf, source + 1, buflen - 1); + buf[buflen - 1] = '\0'; + } + else if (*source == '@') + { + if (srclen <= buflen) + return source + 1; + // truncate the part after @ + memcpy(buf, "...", 3); + memcpy(buf + 3, source + srclen - (buflen - 4), buflen - 4); + buf[buflen - 1] = '\0'; + } + else + { // buf = [string "string"] + size_t len = strcspn(source, "\n\r"); // stop at first newline + buflen -= sizeof("[string \"...\"]"); + if (len > buflen) + len = buflen; + strcpy(buf, "[string \""); + if (source[len] != '\0') + { // must truncate? + strncat(buf, source, len); + strcat(buf, "..."); + } + else + strcat(buf, source); + strcat(buf, "\"]"); + } + return buf; +} diff --git a/External/luau/VM/src/lobject.h b/External/luau/VM/src/lobject.h new file mode 100644 index 00000000..ab2a41d0 --- /dev/null +++ b/External/luau/VM/src/lobject.h @@ -0,0 +1,651 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lua.h" +#include "lcommon.h" + +/* +** Union of all collectible objects +*/ +typedef union GCObject GCObject; + +/* +** Common Header for all collectible objects (in macro form, to be included in other objects) +*/ +// clang-format off +#define CommonHeader \ + uint8_t tt; uint8_t marked; uint8_t memcat +// clang-format on + +/* +** Common header in struct form +*/ +typedef struct GCheader +{ + CommonHeader; +} GCheader; + +/* +** Union of all Lua values +*/ +typedef union +{ + GCObject* gc; + void* p; + double n; + int b; + int64_t l; + float v[2]; // v[0], v[1] live here; v[2] lives in TValue::extra +} Value; + +/* +** Tagged Values +*/ + +typedef struct lua_TValue +{ + Value value; + int extra[LUA_EXTRA_SIZE]; + int tt; +} TValue; + +#if LUA_VECTOR_SIZE == 4 +#define condvector4(vec4expr, vec3expr) vec4expr +#else +#define condvector4(vec4expr, vec3expr) vec3expr +#endif + +#if LUA_VECTOR_DOUBLE == 1 +#define condvectordouble(vecdoubleexpr, vecfloatexpr) vecdoubleexpr +#else +#define condvectordouble(vecdoubleexpr, vecfloatexpr) vecfloatexpr +#endif + +// Macros to test type +#define ttisnil(o) (ttype(o) == LUA_TNIL) +#define ttisnumber(o) (ttype(o) == LUA_TNUMBER) +#define ttisinteger(o) (ttype(o) == LUA_TINTEGER) +#define ttisstring(o) (ttype(o) == LUA_TSTRING) +#define ttistable(o) (ttype(o) == LUA_TTABLE) +#define ttisfunction(o) (ttype(o) == LUA_TFUNCTION) +#define ttisboolean(o) (ttype(o) == LUA_TBOOLEAN) +#define ttisuserdata(o) (ttype(o) == LUA_TUSERDATA) +#define ttisthread(o) (ttype(o) == LUA_TTHREAD) +#define ttisbuffer(o) (ttype(o) == LUA_TBUFFER) +#define ttislightuserdata(o) (ttype(o) == LUA_TLIGHTUSERDATA) +#define ttisvector(o) (ttype(o) == LUA_TVECTOR) +#define ttisupval(o) (ttype(o) == LUA_TUPVAL) +#define ttisclass(o) (ttype(o) == LUA_TCLASS) +#define ttisobject(o) (ttype(o) == LUA_TOBJECT) + +// Macros to access values +#define ttype(o) ((o)->tt) +#define gcvalue(o) check_exp(iscollectable(o), (o)->value.gc) +#define pvalue(o) check_exp(ttislightuserdata(o), (o)->value.p) +#define nvalue(o) check_exp(ttisnumber(o), (o)->value.n) +#define lvalue(o) check_exp(ttisinteger(o), (o)->value.l) +#define vvalue(o) check_exp(ttisvector(o), condvectordouble((o)->value.gc->vec.v, (o)->value.v)) +#define tsvalue(o) check_exp(ttisstring(o), &(o)->value.gc->ts) +#define uvalue(o) check_exp(ttisuserdata(o), &(o)->value.gc->u) +#define clvalue(o) check_exp(ttisfunction(o), &(o)->value.gc->cl) +#define hvalue(o) check_exp(ttistable(o), &(o)->value.gc->h) +#define bvalue(o) check_exp(ttisboolean(o), (o)->value.b) +#define thvalue(o) check_exp(ttisthread(o), &(o)->value.gc->th) +#define bufvalue(o) check_exp(ttisbuffer(o), &(o)->value.gc->buf) +#define upvalue(o) check_exp(ttisupval(o), &(o)->value.gc->uv) +#define classvalue(o) check_exp(ttisclass(o), &(o)->value.gc->lclass) +#define objectvalue(o) check_exp(ttisobject(o), &(o)->value.gc->lobject) + +#define l_isfalse(o) (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0)) + +#define lightuserdatatag(o) check_exp(ttislightuserdata(o), (o)->extra[0]) + +// Internal tags used by the VM +#define LU_TAG_ITERATOR LUA_UTAG_LIMIT + +/* +** for internal debug only +*/ +#define checkconsistency(obj) LUAU_ASSERT(!iscollectable(obj) || (ttype(obj) == (obj)->value.gc->gch.tt)) + +#define checkliveness(g, obj) LUAU_ASSERT(!iscollectable(obj) || ((ttype(obj) == (obj)->value.gc->gch.tt) && !isdead(g, (obj)->value.gc))) + +// Macros to set values +#define setnilvalue(obj) ((obj)->tt = LUA_TNIL) + +#define setnvalue(obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.n = (x); \ + i_o->tt = LUA_TNUMBER; \ + } + +#define setlvalue(obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.l = (x); \ + i_o->tt = LUA_TINTEGER; \ + } + +#if LUA_VECTOR_DOUBLE == 1 +#define setvvalue(L, obj, x, y, z, w) \ + { \ + TValue* i_o = (obj); \ + LuauVector* i_vec = luaVec_newvector((L), double(x), double(y), double(z), condvector4(double(w), 0)); \ + i_o->value.gc = cast_to(GCObject*, i_vec); \ + i_o->tt = LUA_TVECTOR; \ + checkliveness((L)->global, i_o); \ + } +#else +#define setvvalue(L, obj, x, y, z, w) \ + { \ + TValue* i_o = (obj); \ + float* i_v = i_o->value.v; \ + i_v[0] = float(x); \ + i_v[1] = float(y); \ + i_v[2] = float(z); \ + condvector4(i_v[3] = float(w), (void)(w)); \ + i_o->tt = LUA_TVECTOR; \ + } +#endif + +#define setpvalue(obj, x, tag) \ + { \ + TValue* i_o = (obj); \ + i_o->value.p = (x); \ + i_o->extra[0] = (tag); \ + i_o->tt = LUA_TLIGHTUSERDATA; \ + } + +#define setbvalue(obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.b = (x); \ + i_o->tt = LUA_TBOOLEAN; \ + } + +#define setsvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TSTRING; \ + checkliveness(L->global, i_o); \ + } + +#define setuvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TUSERDATA; \ + checkliveness(L->global, i_o); \ + } + +#define setthvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TTHREAD; \ + checkliveness(L->global, i_o); \ + } + +#define setbufvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TBUFFER; \ + checkliveness(L->global, i_o); \ + } + +#define setclvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TFUNCTION; \ + checkliveness(L->global, i_o); \ + } + +#define sethvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TTABLE; \ + checkliveness(L->global, i_o); \ + } + +#define setptvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TPROTO; \ + checkliveness(L->global, i_o); \ + } + +#define setupvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TUPVAL; \ + checkliveness(L->global, i_o); \ + } + +#define setobj(L, obj1, obj2) \ + { \ + const TValue* o2 = (obj2); \ + TValue* o1 = (obj1); \ + *o1 = *o2; \ + checkliveness(L->global, o1); \ + } + +#define setclassvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TCLASS; \ + checkliveness(L->global, i_o); \ + } + + +#define setobjectvalue(L, obj, x) \ + { \ + TValue* i_o = (obj); \ + i_o->value.gc = cast_to(GCObject*, (x)); \ + i_o->tt = LUA_TOBJECT; \ + checkliveness(L->global, i_o); \ + } + +/* +** different types of sets, according to destination +*/ + +// to stack +#define setobj2s setobj +// from table to same table (no barrier) +#define setobjt2t setobj +// to table (needs barrier) +#define setobj2t setobj +// to new object (no barrier) +#define setobj2n setobj +// to class instance or static member (needs barrier) +#define setobj2class setobj + +#define setttype(obj, tt) (ttype(obj) = (tt)) + +#define iscollectable(o) (ttype(o) >= LUA_TSTRING) + +// wrapper for returning userdata properties directly +struct DirectFieldResult +{ + lua_State* L; + TValue* slot; +}; + +typedef TValue* StkId; // index to stack elements + +/* +** String headers for string table +*/ +typedef struct TString +{ + CommonHeader; + // 1 byte padding + + int16_t atom; + + // 2 byte padding + + TString* next; // next string in the hash table bucket + + unsigned int hash; + unsigned int len; + + char data[1]; // string data is allocated right after the header +} TString; + + +#define getstr(ts) (ts)->data +#define svalue(o) getstr(tsvalue(o)) + +typedef struct Udata +{ + CommonHeader; + + uint8_t tag; + + int len; + + struct LuaTable* metatable; + + // userdata is allocated right after the header + // while the alignment is only 8 here, for sizes starting at 16 bytes, 16 byte alignment is provided + alignas(8) char data[1]; +} Udata; + +typedef struct LuauBuffer +{ + CommonHeader; + + unsigned int len; + + alignas(8) char data[1]; +} Buffer; + +typedef struct LuauVector +{ + CommonHeader; + // 1 byte padding + + LUA_VECTOR_TYPE v[LUA_VECTOR_SIZE]; +} LuauVector; + +enum FeedbackVectorSlotKind +{ + CALL_TARGET +}; + +struct FeedbackVectorSlot +{ + FeedbackVectorSlotKind kind; + + union + { + struct + { + uint32_t pc; + uint32_t proto; + uint32_t hits; + } call_target; + }; +}; + +/* +** Function Prototypes +*/ +// clang-format off +typedef struct Proto +{ + CommonHeader; + + uint8_t nups; // number of upvalues + uint8_t numparams; + uint8_t is_vararg; + uint8_t maxstacksize; + uint8_t flags; + + TValue* k; // constants used by the function + Instruction* code; // function bytecode + struct Proto** p; // functions defined inside the function + const Instruction* codeentry; + + void* execdata; + uintptr_t exectarget; + + uint8_t* lineinfo; // for each instruction, line number as a delta from baseline + int* abslineinfo; // baseline line info, one entry for each 1<v != &(up)->u.value) + +/* +** Closures +*/ + +typedef struct Closure +{ + CommonHeader; + + uint8_t isC; + uint8_t nupvalues; + uint8_t stacksize; + uint8_t preload; + + GCObject* gclist; + struct LuaTable* env; + + union + { + struct + { + lua_CFunction f; + lua_Continuation cont; + TString* debugname; + TValue upvals[1]; + } c; + + struct + { + struct Proto* p; + TValue uprefs[1]; + } l; + }; +} Closure; + +#define iscfunction(o) (ttype(o) == LUA_TFUNCTION && clvalue(o)->isC) +#define isLfunction(o) (ttype(o) == LUA_TFUNCTION && !clvalue(o)->isC) + +/* +** Tables +*/ + +typedef struct TKey +{ + ::Value value; + int extra[LUA_EXTRA_SIZE]; + unsigned tt : 4; + int next : 28; // for chaining +} TKey; + +typedef struct LuaNode +{ + TValue val; + TKey key; +} LuaNode; + +// copy a value into a key +#define setnodekey(L, node, obj) \ + { \ + LuaNode* n_ = (node); \ + const TValue* i_o = (obj); \ + n_->key.value = i_o->value; \ + memcpy(n_->key.extra, i_o->extra, sizeof(n_->key.extra)); \ + n_->key.tt = i_o->tt; \ + checkliveness(L->global, i_o); \ + } + +// copy a value from a key +#define getnodekey(L, obj, node) \ + { \ + TValue* i_o = (obj); \ + const LuaNode* n_ = (node); \ + i_o->value = n_->key.value; \ + memcpy(i_o->extra, n_->key.extra, sizeof(i_o->extra)); \ + i_o->tt = n_->key.tt; \ + checkliveness(L->global, i_o); \ + } + +// clang-format off +typedef struct LuaTable +{ + CommonHeader; + + uint8_t tmcache; // 1<

lsizenode)) +#define hasmetacache(t) (((t)->readonly & 2) != 0) +#define getmetacache(t, event) ((t)->array - (1 + (event))) + +#define luaO_nilobject (&luaO_nilobject_) + +LUAI_DATA const TValue luaO_nilobject_; + +#define ceillog2(x) (luaO_log2((x) - 1) + 1) + +LUAI_FUNC int luaO_log2(unsigned int x); +LUAI_FUNC int luaO_rawequalObj(const TValue* t1, const TValue* t2); +LUAI_FUNC int luaO_rawequalKey(const TKey* t1, const TValue* t2); +LUAI_FUNC int luaO_str2d(const char* s, double* result); +LUAI_FUNC int luaO_str2l(const char* s, int64_t* result, int base = 10); +LUAI_FUNC const char* luaO_pushvfstring(lua_State* L, const char* fmt, va_list argp); +LUAI_FUNC const char* luaO_pushfstring(lua_State* L, const char* fmt, ...); +LUAI_FUNC const char* luaO_chunkid(char* buf, size_t buflen, const char* source, size_t srclen); diff --git a/External/luau/VM/src/loslib.cpp b/External/luau/VM/src/loslib.cpp new file mode 100644 index 00000000..e3500e6c --- /dev/null +++ b/External/luau/VM/src/loslib.cpp @@ -0,0 +1,225 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" + +#include +#include + +#define LUA_STRFTIMEOPTIONS "aAbBcdHIjmMpSUwWxXyYzZ%" + +#if defined(_WIN32) +static tm* gmtime_r(const time_t* timep, tm* result) +{ + return gmtime_s(result, timep) == 0 ? result : NULL; +} + +static tm* localtime_r(const time_t* timep, tm* result) +{ + return localtime_s(result, timep) == 0 ? result : NULL; +} +#endif + +static time_t os_timegm(struct tm* timep) +{ + // Julian day number calculation + int day = timep->tm_mday; + int month = timep->tm_mon + 1; + int year = timep->tm_year + 1900; + + // year adjustment, pretend that it starts in March + int a = timep->tm_mon % 12 < 2 ? 1 : 0; + + // also adjust for out-of-range month numbers in input + a -= timep->tm_mon / 12; + + int y = year + 4800 - a; + int m = month + (12 * a) - 3; + + int julianday = day + ((153 * m + 2) / 5) + (365 * y) + (y / 4) - (y / 100) + (y / 400) - 32045; + + const int utcstartasjulianday = 2440588; // Jan 1st 1970 offset in Julian calendar + const int64_t utcstartasjuliansecond = utcstartasjulianday * 86400ll; // same in seconds + + // fail the dates before UTC start + if (julianday < utcstartasjulianday) + return time_t(-1); + + int64_t daysecond = timep->tm_hour * 3600ll + timep->tm_min * 60ll + timep->tm_sec; + int64_t julianseconds = int64_t(julianday) * 86400ull + daysecond; + + if (julianseconds < utcstartasjuliansecond) + return time_t(-1); + + int64_t utc = julianseconds - utcstartasjuliansecond; + return time_t(utc); +} + +static int os_clock(lua_State* L) +{ + lua_pushnumber(L, lua_clock()); + return 1; +} + +/* +** {====================================================== +** Time/Date operations +** { year=%Y, month=%m, day=%d, hour=%H, min=%M, sec=%S, +** wday=%w+1, yday=%j, isdst=? } +** ======================================================= +*/ + +static void setfield(lua_State* L, const char* key, int value) +{ + lua_pushinteger(L, value); + lua_setfield(L, -2, key); +} + +static void setboolfield(lua_State* L, const char* key, int value) +{ + if (value < 0) // undefined? + return; // does not set field + lua_pushboolean(L, value); + lua_setfield(L, -2, key); +} + +static int getboolfield(lua_State* L, const char* key) +{ + int res; + lua_rawgetfield(L, -1, key); + res = lua_isnil(L, -1) ? -1 : lua_toboolean(L, -1); + lua_pop(L, 1); + return res; +} + +static int getfield(lua_State* L, const char* key, int d) +{ + int res; + lua_rawgetfield(L, -1, key); + if (lua_isnumber(L, -1)) + res = (int)lua_tointeger(L, -1); + else + { + if (d < 0) + luaL_error(L, "field '%s' missing in date table", key); + res = d; + } + lua_pop(L, 1); + return res; +} + +static int os_date(lua_State* L) +{ + const char* s = luaL_optstring(L, 1, "%c"); + time_t t = luaL_opt(L, (time_t)luaL_checknumber, 2, time(NULL)); + + struct tm tm; + struct tm* stm; + if (*s == '!') + { // UTC? + stm = gmtime_r(&t, &tm); + s++; // skip `!' + } + else + { + // on Windows, localtime() fails with dates before epoch start so we disallow that + stm = t < 0 ? NULL : localtime_r(&t, &tm); + } + + if (stm == NULL) // invalid date? + { + lua_pushnil(L); + } + else if (strcmp(s, "*t") == 0) + { + lua_createtable(L, 0, 9); // 9 = number of fields + setfield(L, "sec", stm->tm_sec); + setfield(L, "min", stm->tm_min); + setfield(L, "hour", stm->tm_hour); + setfield(L, "day", stm->tm_mday); + setfield(L, "month", stm->tm_mon + 1); + setfield(L, "year", stm->tm_year + 1900); + setfield(L, "wday", stm->tm_wday + 1); + setfield(L, "yday", stm->tm_yday + 1); + setboolfield(L, "isdst", stm->tm_isdst); + } + else + { + char cc[3]; + cc[0] = '%'; + cc[2] = '\0'; + + luaL_Strbuf b; + luaL_buffinit(L, &b); + for (; *s; s++) + { + if (*s != '%' || *(s + 1) == '\0') // no conversion specifier? + { + luaL_addchar(&b, *s); + } + else if (strchr(LUA_STRFTIMEOPTIONS, *(s + 1)) == 0) + { + luaL_argerror(L, 1, "invalid conversion specifier"); + } + else + { + size_t reslen; + char buff[200]; // should be big enough for any conversion result + cc[1] = *(++s); + reslen = strftime(buff, sizeof(buff), cc, stm); + luaL_addlstring(&b, buff, reslen); + } + } + luaL_pushresult(&b); + } + return 1; +} + +static int os_time(lua_State* L) +{ + time_t t; + if (lua_isnoneornil(L, 1)) // called without args? + t = time(NULL); // get current time + else + { + struct tm ts; + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 1); // make sure table is at the top + ts.tm_sec = getfield(L, "sec", 0); + ts.tm_min = getfield(L, "min", 0); + ts.tm_hour = getfield(L, "hour", 12); + ts.tm_mday = getfield(L, "day", -1); + ts.tm_mon = getfield(L, "month", -1) - 1; + ts.tm_year = getfield(L, "year", -1) - 1900; + ts.tm_isdst = getboolfield(L, "isdst"); + + // Note: upstream Lua uses mktime() here which assumes input is local time, but we prefer UTC for consistency + t = os_timegm(&ts); + } + if (t == (time_t)(-1)) + lua_pushnil(L); + else + lua_pushnumber(L, (double)t); + return 1; +} + +static int os_difftime(lua_State* L) +{ + lua_pushnumber(L, difftime((time_t)(luaL_checknumber(L, 1)), (time_t)(luaL_optnumber(L, 2, 0)))); + return 1; +} + +static const luaL_Reg syslib[] = { + {"clock", os_clock}, + {"date", os_date}, + {"difftime", os_difftime}, + {"time", os_time}, + {NULL, NULL}, +}; + +int luaopen_os(lua_State* L) +{ + luaL_register(L, LUA_OSLIBNAME, syslib); + return 1; +} diff --git a/External/luau/VM/src/lperf.cpp b/External/luau/VM/src/lperf.cpp new file mode 100644 index 00000000..7e19d0c4 --- /dev/null +++ b/External/luau/VM/src/lperf.cpp @@ -0,0 +1,70 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lua.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +#ifdef __APPLE__ +#include +#include +#endif + + +#ifdef __EMSCRIPTEN__ +#include +#endif + +#include + +static double clock_period() +{ +#if defined(_WIN32) + LARGE_INTEGER result = {}; + QueryPerformanceFrequency(&result); + return 1.0 / double(result.QuadPart); +#elif defined(__APPLE__) + mach_timebase_info_data_t result = {}; + mach_timebase_info(&result); + return double(result.numer) / double(result.denom) * 1e-9; +#elif defined(__linux__) || defined(__FreeBSD__) + return 1e-9; +#elif defined(__EMSCRIPTEN__) + return 1e-3; +#else + return 1.0 / double(CLOCKS_PER_SEC); +#endif +} + +static double clock_timestamp() +{ +#if defined(_WIN32) + LARGE_INTEGER result = {}; + QueryPerformanceCounter(&result); + return double(result.QuadPart); +#elif defined(__APPLE__) + return double(mach_absolute_time()); +#elif defined(__linux__) || defined(__FreeBSD__) + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1e9 + now.tv_nsec; +#elif defined(__EMSCRIPTEN__) + return emscripten_get_now(); +#else + return double(clock()); +#endif +} + +double lua_clock() +{ + static double period = clock_period(); + + return clock_timestamp() * period; +} diff --git a/External/luau/VM/src/lstate.cpp b/External/luau/VM/src/lstate.cpp new file mode 100644 index 00000000..822c3651 --- /dev/null +++ b/External/luau/VM/src/lstate.cpp @@ -0,0 +1,394 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lstate.h" + +#include "ltable.h" +#include "lstring.h" +#include "lfunc.h" +#include "lmem.h" +#include "lgc.h" +#include "ldo.h" +#include "ldebug.h" +#include "ludata.h" + +#include + +LUAU_FASTFLAG(LuauGcTraceUdata) +LUAU_FASTFLAGVARIABLE(LuauEasyStateInit) +LUAU_FASTFLAGVARIABLE(LuauBufferCage) + +/* +** Main thread combines a thread state and the global state +*/ +typedef struct LG +{ + lua_State l; + global_State g; +} LG; + +static void stack_init(lua_State* L1, lua_State* L) +{ + // initialize CallInfo array + L1->base_ci = luaM_newarray(L, BASIC_CI_SIZE, CallInfo, L1->memcat); + L1->ci = L1->base_ci; + L1->size_ci = BASIC_CI_SIZE; + L1->end_ci = L1->base_ci + L1->size_ci - 1; + // initialize stack array + L1->stack = luaM_newarray(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue, L1->memcat); + L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK; + TValue* stack = L1->stack; + for (int i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++) + setnilvalue(stack + i); // erase new stack + L1->top = stack; + L1->stack_last = stack + (L1->stacksize - EXTRA_STACK); + // initialize first ci + L1->ci->func = L1->top; + L1->ci->p = nullptr; + setnilvalue(L1->top++); // `function' entry for this `ci' + L1->base = L1->ci->base = L1->top; + L1->ci->top = L1->top + LUA_MINSTACK; +} + +static void freestack(lua_State* L, lua_State* L1) +{ + luaM_freearray(L, L1->base_ci, L1->size_ci, CallInfo, L1->memcat); + luaM_freearray(L, L1->stack, L1->stacksize, TValue, L1->memcat); +} + +static LuaTable* weakenvalues(lua_State* L, LuaTable* wt) +{ + LUAU_ASSERT(FFlag::LuauGcTraceUdata); + LuaTable* mt = luaH_new(L, 0, 1); + TValue* slot = luaH_setstr(L, mt, L->global->tmname[TM_MODE]); + setsvalue(L, slot, luaS_newliteral(L, "v")); + wt->metatable = mt; + return wt; +} + +/* +** open parts that may cause memory-allocation errors +*/ +static void f_luaopen(lua_State* L, void* ud) +{ + global_State* g = L->global; + stack_init(L, L); // init stack + L->gt = luaH_new(L, 0, 2); // table of globals + sethvalue(L, registry(L), luaH_new(L, 0, 2)); // registry + luaS_resize(L, LUA_MINSTRTABSIZE); // initial size of string table + luaT_init(L); + if (FFlag::LuauGcTraceUdata) + { + LuaTable* wt = weakenvalues(L, luaH_new(L, 0, 0)); // weakregistry + sethvalue(L, &L->global->weakregistry, wt); + } + luaS_fix(luaS_newliteral(L, LUA_MEMERRMSG)); // pin to make sure we can always throw this error + luaS_fix(luaS_newliteral(L, LUA_ERRERRMSG)); // pin to make sure we can always throw this error + g->GCthreshold = 4 * g->totalbytes; +} + +static void preinit_state(lua_State* L, global_State* g) +{ + LUAU_ASSERT(!FFlag::LuauEasyStateInit); + + L->global = g; + L->stack = NULL; + L->stacksize = 0; + L->gt = NULL; + L->openupval = NULL; + L->size_ci = 0; + L->nCcalls = L->baseCcalls = 0; + L->status = 0; + L->base_ci = L->ci = NULL; + L->namecall = NULL; + L->cachedslot = 0; + L->singlestep = false; + L->isactive = false; + L->activememcat = 0; + L->finalizers = NULL; + L->userdata = NULL; +} + +static void close_state(lua_State* L) +{ + global_State* g = L->global; + luaF_close(L, L->stack); // close all upvalues for this thread + luaC_freeall(L); // collect all objects + LUAU_ASSERT(g->strt.nuse == 0); + luaM_freearray(L, L->global->strt.hash, L->global->strt.size, TString*, 0); + freestack(L, L); + for (int i = 0; i < LUA_SIZECLASSES; i++) + { + LUAU_ASSERT(g->freepages[i] == NULL); + LUAU_ASSERT(g->freegcopages[i] == NULL); + LUAU_ASSERT(g->freegcopages_cage[i] == NULL); + } + LUAU_ASSERT(g->allgcopages == NULL); + LUAU_ASSERT(g->allgcopages_cage == NULL); + LUAU_ASSERT(g->totalbytes == sizeof(LG)); + LUAU_ASSERT(g->memcatbytes[0] == sizeof(LG)); + for (int i = 1; i < LUA_MEMORY_CATEGORIES; i++) + LUAU_ASSERT(g->memcatbytes[i] == 0); + + if (L->global->ecb.close) + L->global->ecb.close(L); + + (*g->frealloc)(g->ud, L, sizeof(LG), 0); +} + +lua_State* luaE_newthread(lua_State* L) +{ + lua_State* L1 = luaM_newgco(L, lua_State, sizeof(lua_State), L->activememcat, LUA_TTHREAD); + + if (FFlag::LuauEasyStateInit) + memset(L1, 0, sizeof(lua_State)); + + luaC_init(L, L1, LUA_TTHREAD); + + if (FFlag::LuauEasyStateInit) + L1->global = L->global; + else + preinit_state(L1, L->global); + + L1->activememcat = L->activememcat; // inherit the active memory category + stack_init(L1, L); // init stack + L1->gt = L->gt; // share table of globals + L1->singlestep = L->singlestep; + LUAU_ASSERT(iswhite(obj2gco(L1))); + return L1; +} + +void luaE_freethread(lua_State* L, lua_State* L1, lua_Page* page) +{ + global_State* g = L->global; + if (g->cb.userthread) + g->cb.userthread(NULL, L1); + + freestack(L, L1); + luaM_freegco(L, L1, sizeof(lua_State), L1->memcat, page); +} + +void lua_resetthread(lua_State* L) +{ + api_check(L, !L->isactive); + api_check(L, L->status != LUA_OK || L->ci == L->base_ci); + + // close upvalues before clearing anything + luaF_close(L, L->stack); + + // clear call frames + CallInfo* ci = L->base_ci; + ci->p = nullptr; + ci->func = L->stack; + ci->base = ci->func + 1; + ci->top = ci->base + LUA_MINSTACK; + setnilvalue(ci->func); + L->ci = ci; + if (L->size_ci != BASIC_CI_SIZE) + luaD_reallocCI(L, BASIC_CI_SIZE); + // clear thread state + L->status = LUA_OK; + L->base = L->ci->base; + L->top = L->ci->base; + L->nCcalls = L->baseCcalls = 0; + // clear thread stack + if (L->stacksize != BASIC_STACK_SIZE + EXTRA_STACK) + luaD_reallocstack(L, BASIC_STACK_SIZE, 0); + for (int i = 0; i < L->stacksize; i++) + setnilvalue(L->stack + i); + L->finalizers = nullptr; +} + +int lua_isthreadreset(lua_State* L) +{ + return L->ci == L->base_ci && L->base == L->top && L->status == LUA_OK; +} + +void lua_setmemorycage(lua_State* L, lua_CageAlloc alloc, void* ud) +{ + global_State* g = L->global; + g->cagealloc = alloc; + g->cageud = ud; +} + +lua_State* lua_newstate(lua_Alloc allocator, void* ud) +{ + if (FFlag::LuauEasyStateInit) + { + void* l = allocator(ud, nullptr, 0, sizeof(LG)); + if (l == nullptr) + return nullptr; + + memset(l, 0, sizeof(LG)); + + lua_State* L = &((LG*)l)->l; + global_State* g = &((LG*)l)->g; + + L->tt = LUA_TTHREAD; + L->marked = g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT); + + L->global = g; + g->mainthread = L; + + g->frealloc = allocator; + g->ud = ud; + + g->uvhead.u.open.prev = &g->uvhead; + g->uvhead.u.open.next = &g->uvhead; + + + g->ptrenckey[0] = 1; + + g->totalbytes = sizeof(LG); + g->memcatbytes[0] = sizeof(LG); + + g->gcstate = GCSpause; + g->gcgoal = LUAI_GCGOAL; + g->gcstepmul = LUAI_GCSTEPMUL; + g->gcstepsize = LUAI_GCSTEPSIZE << 10; + + g->cb = lua_Callbacks(); + g->ecb = lua_ExecutionCallbacks(); + g->gcstats = GCStats(); + + g->lastprotoid = 1; + +#ifdef LUAI_GCMETRICS + g->gcmetrics = GCMetrics(); +#endif + + if (luaD_rawrunprotected(L, f_luaopen, nullptr) != 0) + { + // memory allocation error: free partial state + close_state(L); + L = nullptr; + } + return L; + } + else + { + int i; + lua_State* L; + global_State* g; + void* l = (*allocator)(ud, NULL, 0, sizeof(LG)); + if (l == NULL) + return NULL; + L = (lua_State*)l; + g = &((LG*)L)->g; + L->tt = LUA_TTHREAD; + L->marked = g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT); + L->memcat = 0; + preinit_state(L, g); + g->frealloc = allocator; + g->ud = ud; + g->mainthread = L; + g->uvhead.u.open.prev = &g->uvhead; + g->uvhead.u.open.next = &g->uvhead; + g->GCthreshold = 0; // mark it as unfinished state + g->registryfree = 0; + g->errorjmp = NULL; + g->rngstate = 0; + g->ptrenckey[0] = 1; + g->ptrenckey[1] = 0; + g->ptrenckey[2] = 0; + g->ptrenckey[3] = 0; + for (int i = 0; i < 8; i++) + g->ptrenckeynew[i] = 0; + g->ptrencactive = 0; + g->strt.size = 0; + g->strt.nuse = 0; + g->strt.hash = NULL; + setnilvalue(&g->pseudotemp); + setnilvalue(registry(L)); + setnilvalue(&g->weakregistry); + g->weakregistryfree = 0; + g->weakregistrytop = 0; + g->embeddergc = NULL; + g->gcstate = GCSpause; + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + g->totalbytes = sizeof(LG); + g->gcgoal = LUAI_GCGOAL; + g->gcstepmul = LUAI_GCSTEPMUL; + g->gcstepsize = LUAI_GCSTEPSIZE << 10; + + for (i = 0; i < LUA_SIZECLASSES; i++) + { + g->freepages[i] = NULL; + g->freegcopages[i] = NULL; + g->freegcopages_cage[i] = NULL; + } + + g->allpages = NULL; + g->allgcopages = NULL; + g->sweepgcopage = NULL; + g->cagealloc = NULL; + g->cageud = NULL; + g->allgcopages_cage = NULL; + g->sweepgcopage_cage = NULL; + + for (i = 0; i < LUA_T_COUNT; i++) + g->mt[i] = NULL; + + for (i = 0; i < LUA_UTAG_LIMIT; i++) + { + g->udatagc[i] = NULL; + g->udatamark[i] = NULL; + g->udatamt[i] = NULL; + } + + for (i = 0; i < UTAG_INTERNAL_LIMIT; i++) + { + lua_UdataDirectAccessData& udatadirect = L->global->udatadirect[i]; + + setnilvalue(&udatadirect.indextm); + setnilvalue(&udatadirect.newindextm); + setnilvalue(&udatadirect.namecalltm); + udatadirect.index = NULL; + udatadirect.newindex = NULL; + udatadirect.namecall = NULL; + } + + for (i = 0; i < LUA_LUTAG_LIMIT; i++) + g->lightuserdataname[i] = NULL; + + for (i = 0; i < UTAG_INTERNAL_LIMIT; i++) + g->udatadirectfields[i] = NULL; + + for (i = 0; i < LUA_MEMORY_CATEGORIES; i++) + g->memcatbytes[i] = 0; + + g->memcatbytes[0] = sizeof(LG); + + g->cb = lua_Callbacks(); + + g->ecb = lua_ExecutionCallbacks(); + + memset(g->ecbdata, 0, LUA_EXECUTION_CALLBACK_STORAGE * sizeof(g->ecbdata[0])); + + g->gcstats = GCStats(); + g->lastprotoid = 1; + + g->builtinPcall = NULL; + g->builtinXpcall = NULL; + +#ifdef LUAI_GCMETRICS + g->gcmetrics = GCMetrics(); +#endif + + if (luaD_rawrunprotected(L, f_luaopen, NULL) != 0) + { + // memory allocation error: free partial state + close_state(L); + L = NULL; + } + return L; + } +} + +void lua_close(lua_State* L) +{ + L = L->global->mainthread; // only the main thread can be closed + luaF_close(L, L->stack); // close all upvalues for this thread + close_state(L); +} diff --git a/External/luau/VM/src/lstate.h b/External/luau/VM/src/lstate.h new file mode 100644 index 00000000..fd88fb48 --- /dev/null +++ b/External/luau/VM/src/lstate.h @@ -0,0 +1,360 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" +#include "ltm.h" +#include "ludata.h" + +// registry +#define registry(L) (&L->global->registry) + +// extra stack space to handle TM calls and some other extras +#define EXTRA_STACK 5 + +#define BASIC_CI_SIZE 8 + +#define BASIC_STACK_SIZE (2 * LUA_MINSTACK) + +// clang-format off +typedef struct stringtable +{ + TString** hash; + uint32_t nuse; // number of elements + int size; +} stringtable; +// clang-format on + +/* +** information about a call +** +** the general Lua stack frame structure is as follows: +** - each function gets a stack frame, with function "registers" being stack slots on the frame +** - function arguments are associated with registers 0+ +** - function locals and temporaries follow after; usually locals are a consecutive block per scope, and temporaries are allocated after this, but +*this is up to the compiler +** +** when function doesn't have varargs, the stack layout is as follows: +** ^ (func) ^^ [fixed args] [locals + temporaries] +** where ^ is the 'func' pointer in CallInfo struct, and ^^ is the 'base' pointer (which is what registers are relative to) +** +** when function *does* have varargs, the stack layout is more complex - the runtime has to copy the fixed arguments so that the 0+ addressing still +*works as follows: +** ^ (func) [fixed args] [varargs] ^^ [fixed args] [locals + temporaries] +** +** computing the sizes of these individual blocks works as follows: +** - the number of fixed args is always matching the `numparams` in a function's Proto object; runtime adds `nil` during the call execution as +*necessary +** - the number of variadic args can be computed by evaluating (ci->base - ci->func - 1 - numparams) +** +** the CallInfo structures are allocated as an array, with each subsequent call being *appended* to this array (so if f calls g, CallInfo for g +*immediately follows CallInfo for f) +** the `nresults` field in CallInfo is set by the caller to tell the function how many arguments the caller is expecting on the stack after the +*function returns +** the `flags` field in CallInfo contains internal execution flags that are important for pcall/etc, see LUA_CALLINFO_* +*/ +// clang-format off +typedef struct CallInfo +{ + StkId base; // base for this function + StkId func; // function index in the stack + StkId top; // top for this function + Proto* p; + + union + { + const Instruction* savedpc; + int errfunc; // For C functions, the error function index in the stack + }; + + int nresults; // expected number of results from this function + unsigned int flags; // call frame flags, see LUA_CALLINFO_* +} CallInfo; +// clang-format on + +#define LUA_CALLINFO_RETURN (1 << 0) // should the interpreter return after returning from this callinfo? first frame must have this set +#define LUA_CALLINFO_HANDLE (1 << 1) // should the error thrown during execution get handled by continuation from this callinfo? func must be C +#define LUA_CALLINFO_NATIVE (1 << 2) // should this function be executed using execution callback for native code +#define LUA_CALLINFO_OPYIELD (1 << 3) // call frame has yielded on a non-call opcode and requires luau_finishop +#define LUA_CALLINFO_PCALL (1 << 4) // call frame was setup by a synthetic protected call and requires luau_pospcallsuccess + +#define curr_func(L) (clvalue(L->ci->func)) +#define ci_func(ci) (clvalue((ci)->func)) +#define f_isLua(ci) (!ci_func(ci)->isC) +#define isLua(ci) (ttisfunction((ci)->func) && f_isLua(ci)) + +struct GCStats +{ + // data for proportional-integral controller of heap trigger value + int32_t triggerterms[32] = {0}; + uint32_t triggertermpos = 0; + int32_t triggerintegral = 0; + + size_t atomicstarttotalsizebytes = 0; + size_t endtotalsizebytes = 0; + size_t heapgoalsizebytes = 0; + + double starttimestamp = 0; + double atomicstarttimestamp = 0; + double endtimestamp = 0; +}; + +#ifdef LUAI_GCMETRICS +struct GCCycleMetrics +{ + size_t starttotalsizebytes = 0; + size_t heaptriggersizebytes = 0; + + double pausetime = 0.0; // time from end of the last cycle to the start of a new one + + double starttimestamp = 0.0; + double endtimestamp = 0.0; + + double marktime = 0.0; + double markassisttime = 0.0; + double markmaxexplicittime = 0.0; + size_t markexplicitsteps = 0; + size_t markwork = 0; + + double atomicstarttimestamp = 0.0; + size_t atomicstarttotalsizebytes = 0; + double atomictime = 0.0; + + // specific atomic stage parts + double atomictimeupval = 0.0; + double atomictimeweak = 0.0; + double atomictimegray = 0.0; + double atomictimeembedder = 0.0; + double atomictimeclear = 0.0; + + double sweeptime = 0.0; + double sweepassisttime = 0.0; + double sweepmaxexplicittime = 0.0; + size_t sweepexplicitsteps = 0; + size_t sweepwork = 0; + + size_t assistwork = 0; + size_t explicitwork = 0; + + size_t propagatework = 0; + size_t propagateagainwork = 0; + + size_t endtotalsizebytes = 0; +}; + +struct GCMetrics +{ + double stepexplicittimeacc = 0.0; + double stepassisttimeacc = 0.0; + + // when cycle is completed, last cycle values are updated + uint64_t completedcycles = 0; + + GCCycleMetrics lastcycle; + GCCycleMetrics currcycle; +}; +#endif + +// Callbacks that can be used to to redirect code execution from Luau bytecode VM to a custom implementation (AoT/JiT/sandboxing/...) +struct lua_ExecutionCallbacks +{ + void* context; + void (*close)(lua_State* L); // called when global VM state is closed + void (*destroy)(lua_State* L, Proto* proto); // called when function is destroyed + int (*enter)(lua_State* L, Proto* proto); // called when function is about to start/resume (when execdata is present), return 0 to exit VM + void (*disable)(lua_State* L, Proto* proto); // called when function has to be switched from native to bytecode in the debugger + size_t (*getmemorysize)(lua_State* L, Proto* proto); // called to request the size of memory associated with native part of the Proto + uint8_t (*gettypemapping)(lua_State* L, const char* str, size_t len); // called to get the userdata type index + char* (*getcounterdata)( + lua_State* L, + Proto* proto, + size_t* count + ); // called to get the execution counter data and count {uint32_t, uint32_t, uint64_t} + Proto* (*inlinefunction)(lua_State* L, Closure* caller, Closure* target, uint32_t pc); // called when inlining threshold is reached +}; + +struct lua_UdataDirectAccessData +{ + TValue indextm; + TValue newindextm; + TValue namecalltm; + lua_UserdataDirectAccess index; + lua_UserdataDirectAccess newindex; + lua_UserdataDirectNamecall namecall; +}; + +/* +** `global state', shared by all threads of this state +*/ +// clang-format off +typedef struct global_State +{ + stringtable strt; // hash table for strings + + lua_Alloc frealloc; // function to reallocate memory + void* ud; // auxiliary data to `frealloc' + + uint8_t currentwhite; + uint8_t gcstate; // state of garbage collector + + GCObject* gray; // list of gray objects + GCObject* grayagain; // list of objects to be traversed atomically + GCObject* weak; // list of weak tables (to be cleared) + + size_t GCthreshold; // when totalbytes >= GCthreshold, run GC step + size_t totalbytes; // number of bytes currently allocated + + int gcgoal; // see LUAI_GCGOAL + int gcstepmul; // see LUAI_GCSTEPMUL + int gcstepsize; // see LUAI_GCSTEPSIZE + + struct lua_Page* freepages[LUA_SIZECLASSES]; // free page linked list for each size class for non-collectable objects + struct lua_Page* freegcopages[LUA_SIZECLASSES]; // free page linked list for each size class for collectable objects + struct lua_Page* allpages; // page linked list with all pages for all non-collectable object classes (available with LUAU_ASSERTENABLED) + struct lua_Page* allgcopages; // page linked list with all pages for all collectable object classes + struct lua_Page* sweepgcopage; // position of the sweep in `allgcopages' + + struct lua_State* mainthread; + UpVal uvhead; // head of double-linked list of all open upvalues + struct LuaTable* mt[LUA_T_COUNT]; // metatables for basic types + TString* ttname[LUA_T_COUNT]; // names for basic types + TString* tmname[TM_N]; // array with tag-method names + + lua_CageAlloc cagealloc; + void* cageud; + struct lua_Page* freegcopages_cage[LUA_SIZECLASSES]; // cage GCO free pages, per size class + struct lua_Page* allgcopages_cage; // all cage GCO pages (sweep target) + struct lua_Page* sweepgcopage_cage; // sweep cursor + + TValue pseudotemp; // storage for temporary values used in pseudo2addr + + TValue registry; // registry table, used by lua_ref and LUA_REGISTRYINDEX + int registryfree; // next free slot in registry + + struct lua_jmpbuf* errorjmp; // jump buffer data for longjmp-style error handling + + uint64_t rngstate; // PCG random number generator state + uint64_t ptrenckey[4]; // pointer encoding key for display (remove when unreferenced) + + lua_Callbacks cb; + + lua_ExecutionCallbacks ecb; + + alignas(16) uint8_t ecbdata[LUA_EXECUTION_CALLBACK_STORAGE]; + + // Set of userdata __index/__newindex/__namecall metamethods for a direct access + lua_UdataDirectAccessData udatadirect[UTAG_INTERNAL_LIMIT]; + + size_t memcatbytes[LUA_MEMORY_CATEGORIES]; // total amount of memory used by each memory category + + void (*udatagc[LUA_UTAG_LIMIT])(lua_State*, void*); // for each userdata tag, a gc callback to be called immediately before freeing memory + lua_UserdataMark udatamark[LUA_UTAG_LIMIT]; // gc callbacks allowing the embedder to mark underlying native objects for the given userdata + LuaTable* udatamt[LUA_UTAG_LIMIT]; // metatables for tagged userdata + + TValue weakregistry; // backing table for lua_weakref/lua_weakunref/lua_getweakref + int weakregistryfree; // next free slot in weakregistry + int weakregistrytop; // top of the weakregistry (highest used index) + lua_EmbedderGc embeddergc; // embedder GC callback for keeping weak references alive + + TString* lightuserdataname[LUA_LUTAG_LIMIT]; // names for tagged lightuserdata + + // per-tag direct field dispatch tables; NULL until first field is registered for that tag + struct LuaTable* udatadirectfields[UTAG_INTERNAL_LIMIT]; + + // cached closures for fast protected calls + struct Closure* builtinPcall; + struct Closure* builtinXpcall; + + uint64_t ptrenckeynew[8]; // pointer encoding key for display + bool ptrencactive; + + GCStats gcstats; + uint32_t lastprotoid; + +#ifdef LUAI_GCMETRICS + GCMetrics gcmetrics; +#endif +} global_State; +// clang-format on + +/* +** `per thread' state +*/ +// clang-format off +struct lua_State +{ + CommonHeader; + uint8_t status; + + uint8_t activememcat; // memory category that is used for new GC object allocations + + bool isactive; // thread is currently executing, stack may be mutated without barriers + bool singlestep; // call debugstep hook after each instruction + + StkId top; // first free slot in the stack + StkId base; // base of current function + global_State* global; + CallInfo* ci; // call info for current function + StkId stack_last; // last free slot in the stack + StkId stack; // stack base + + CallInfo* end_ci; // points after end of ci array + CallInfo* base_ci; // array of CallInfo's + + int stacksize; + int size_ci; // size of array `base_ci' + + unsigned short nCcalls; // number of nested C calls + unsigned short baseCcalls; // nested C calls when resuming coroutine + + int cachedslot; // when table operations or INDEX/NEWINDEX is invoked from Luau, what is the expected slot for lookup? + + LuaTable* gt; // table of globals + UpVal* openupval; // list of open upvalues in this stack + GCObject* gclist; + + TString* namecall; // when invoked from Luau using NAMECALL, what method do we need to invoke? + + LuaTable* finalizers; // optional table of all the callbacks to run when this thread completes (see coroutine.finally) + + void* userdata; +}; +// clang-format on + +/* +** Union of all collectible objects +*/ +union GCObject +{ + GCheader gch; + struct TString ts; + struct Udata u; + struct Closure cl; + struct LuaTable h; + struct Proto p; + struct UpVal uv; + struct lua_State th; // thread + struct LuauBuffer buf; + struct LuauClass lclass; + struct LuauObject lobject; + struct LuauVector vec; +}; + +// macros to convert a GCObject into a specific value +#define gco2ts(o) check_exp((o)->gch.tt == LUA_TSTRING, &((o)->ts)) +#define gco2u(o) check_exp((o)->gch.tt == LUA_TUSERDATA, &((o)->u)) +#define gco2cl(o) check_exp((o)->gch.tt == LUA_TFUNCTION, &((o)->cl)) +#define gco2h(o) check_exp((o)->gch.tt == LUA_TTABLE, &((o)->h)) +#define gco2p(o) check_exp((o)->gch.tt == LUA_TPROTO, &((o)->p)) +#define gco2uv(o) check_exp((o)->gch.tt == LUA_TUPVAL, &((o)->uv)) +#define gco2th(o) check_exp((o)->gch.tt == LUA_TTHREAD, &((o)->th)) +#define gco2buf(o) check_exp((o)->gch.tt == LUA_TBUFFER, &((o)->buf)) +#define gco2class(o) check_exp((o)->gch.tt == LUA_TCLASS, &((o)->lclass)) +#define gco2object(o) check_exp((o)->gch.tt == LUA_TOBJECT, &((o)->lobject)) +#define gco2vec(o) check_exp((o)->gch.tt == LUA_TVECTOR, &((o)->vec)) + +// macro to convert any Lua object into a GCObject +#define obj2gco(v) check_exp(iscollectable(v), cast_to(GCObject*, (v) + 0)) + +LUAI_FUNC lua_State* luaE_newthread(lua_State* L); +LUAI_FUNC void luaE_freethread(lua_State* L, lua_State* L1, struct lua_Page* page); diff --git a/External/luau/VM/src/lstring.cpp b/External/luau/VM/src/lstring.cpp new file mode 100644 index 00000000..cf95d5b4 --- /dev/null +++ b/External/luau/VM/src/lstring.cpp @@ -0,0 +1,193 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lstring.h" + +#include "lgc.h" +#include "lmem.h" + +#include + +unsigned int luaS_hash(const char* str, size_t len) +{ + // Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash + unsigned int a = 0, b = 0; + unsigned int h = unsigned(len); + + // hash prefix in 12b chunks (using aligned reads) with ARX based hash (LuaJIT v2.1, lookup3) + // note that we stop at length<32 to maintain compatibility with Lua 5.1 + while (len >= 32) + { +#define rol(x, s) ((x >> s) | (x << (32 - s))) +#define mix(u, v, w) a ^= h, a -= rol(h, u), b ^= a, b -= rol(a, v), h ^= b, h -= rol(b, w) + + // should compile into fast unaligned reads + uint32_t block[3]; + memcpy(block, str, 12); + + a += block[0]; + b += block[1]; + h += block[2]; + mix(14, 11, 25); + str += 12; + len -= 12; + +#undef mix +#undef rol + } + + // original Lua 5.1 hash for compatibility (exact match when len<32) + for (size_t i = len; i > 0; --i) + h ^= (h << 5) + (h >> 2) + (uint8_t)str[i - 1]; + + return h; +} + +void luaS_resize(lua_State* L, int newsize) +{ + TString** newhash = luaM_newarray(L, newsize, TString*, 0); + stringtable* tb = &L->global->strt; + for (int i = 0; i < newsize; i++) + newhash[i] = NULL; + // rehash + for (int i = 0; i < tb->size; i++) + { + TString* p = tb->hash[i]; + while (p) + { // for each node in the list + TString* next = p->next; // save next + unsigned int h = p->hash; + int h1 = lmod(h, newsize); // new position + LUAU_ASSERT(cast_int(h % newsize) == lmod(h, newsize)); + p->next = newhash[h1]; // chain it + newhash[h1] = p; + p = next; + } + } + luaM_freearray(L, tb->hash, tb->size, TString*, 0); + tb->size = newsize; + tb->hash = newhash; +} + +static TString* newlstr(lua_State* L, const char* str, size_t l, unsigned int h) +{ + if (l > MAXSSIZE) + luaM_toobig(L); + + TString* ts = luaM_newgco(L, TString, sizestring(l), L->activememcat, LUA_TSTRING); + luaC_init(L, ts, LUA_TSTRING); + ts->atom = ATOM_UNDEF; + ts->hash = h; + ts->len = unsigned(l); + + memcpy(ts->data, str, l); + ts->data[l] = '\0'; // ending 0 + + stringtable* tb = &L->global->strt; + h = lmod(h, tb->size); + ts->next = tb->hash[h]; // chain new entry + tb->hash[h] = ts; + + tb->nuse++; + if (tb->nuse > cast_to(uint32_t, tb->size) && tb->size <= INT_MAX / 2) + luaS_resize(L, tb->size * 2); // too crowded + + return ts; +} + +TString* luaS_bufstart(lua_State* L, size_t size) +{ + if (size > MAXSSIZE) + luaM_toobig(L); + + TString* ts = luaM_newgco(L, TString, sizestring(size), L->activememcat, LUA_TSTRING); + luaC_init(L, ts, LUA_TSTRING); + ts->atom = ATOM_UNDEF; + ts->hash = 0; // computed in luaS_buffinish + ts->len = unsigned(size); + + ts->next = NULL; + + return ts; +} + +TString* luaS_buffinish(lua_State* L, TString* ts) +{ + unsigned int h = luaS_hash(ts->data, ts->len); + stringtable* tb = &L->global->strt; + int bucket = lmod(h, tb->size); + + // search if we already have this string in the hash table + for (TString* el = tb->hash[bucket]; el != NULL; el = el->next) + { + if (el->len == ts->len && memcmp(el->data, ts->data, ts->len) == 0) + { + // string may be dead + if (isdead(L->global, obj2gco(el))) + changewhite(obj2gco(el)); + + return el; + } + } + + LUAU_ASSERT(ts->next == NULL); + + ts->hash = h; + ts->data[ts->len] = '\0'; // ending 0 + ts->atom = ATOM_UNDEF; + ts->next = tb->hash[bucket]; // chain new entry + tb->hash[bucket] = ts; + + tb->nuse++; + if (tb->nuse > cast_to(uint32_t, tb->size) && tb->size <= INT_MAX / 2) + luaS_resize(L, tb->size * 2); // too crowded + + return ts; +} + +TString* luaS_newlstr(lua_State* L, const char* str, size_t l) +{ + unsigned int h = luaS_hash(str, l); + for (TString* el = L->global->strt.hash[lmod(h, L->global->strt.size)]; el != NULL; el = el->next) + { + if (el->len == l && (memcmp(str, getstr(el), l) == 0)) + { + // string may be dead + if (isdead(L->global, obj2gco(el))) + changewhite(obj2gco(el)); + return el; + } + } + return newlstr(L, str, l, h); // not found +} + +static bool unlinkstr(lua_State* L, TString* ts) +{ + global_State* g = L->global; + + TString** p = &g->strt.hash[lmod(ts->hash, g->strt.size)]; + + while (TString* curr = *p) + { + if (curr == ts) + { + *p = curr->next; + return true; + } + else + { + p = &curr->next; + } + } + + return false; +} + +void luaS_free(lua_State* L, TString* ts, lua_Page* page) +{ + if (unlinkstr(L, ts)) + L->global->strt.nuse--; + else + LUAU_ASSERT(ts->next == NULL); // orphaned string buffer + + luaM_freegco(L, ts, sizestring(ts->len), ts->memcat, page); +} diff --git a/External/luau/VM/src/lstring.h b/External/luau/VM/src/lstring.h new file mode 100644 index 00000000..f4151e21 --- /dev/null +++ b/External/luau/VM/src/lstring.h @@ -0,0 +1,35 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" +#include "lstate.h" + +// string size limit +#define MAXSSIZE (1 << 30) + +// string atoms are not defined by default; the storage is 16-bit integer +#define ATOM_UNDEF -32768 + +#define sizestring(len) (offsetof(TString, data) + len + 1) + +#define luaS_new(L, s) (luaS_newlstr(L, s, strlen(s))) +#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, (sizeof(s) / sizeof(char)) - 1)) + +#define luaS_fix(s) l_setbit((s)->marked, FIXEDBIT) + +#define luaS_updateatom(L, ts) \ + { \ + if (ts->atom == ATOM_UNDEF) \ + ts->atom = L->global->cb.useratom ? L->global->cb.useratom(L, ts->data, ts->len) : -1; \ + } + +LUAI_FUNC unsigned int luaS_hash(const char* str, size_t len); + +LUAI_FUNC void luaS_resize(lua_State* L, int newsize); + +LUAI_FUNC TString* luaS_newlstr(lua_State* L, const char* str, size_t l); +LUAI_FUNC void luaS_free(lua_State* L, TString* ts, struct lua_Page* page); + +LUAI_FUNC TString* luaS_bufstart(lua_State* L, size_t size); +LUAI_FUNC TString* luaS_buffinish(lua_State* L, TString* ts); diff --git a/External/luau/VM/src/lstrlib.cpp b/External/luau/VM/src/lstrlib.cpp new file mode 100644 index 00000000..2c85efed --- /dev/null +++ b/External/luau/VM/src/lstrlib.cpp @@ -0,0 +1,1767 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lstring.h" + +#include +#include +#include + +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauOptimizeStringSplit, false) + +// macro to `unsign' a character +#define uchar(c) ((unsigned char)(c)) + +static int str_len(lua_State* L) +{ + size_t l; + luaL_checklstring(L, 1, &l); + lua_pushinteger(L, (int)l); + return 1; +} + +static int posrelat(int pos, size_t len) +{ + // relative string position: negative means back from end + if (pos < 0) + pos += (int)len + 1; + return (pos >= 0) ? pos : 0; +} + +static int str_sub(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + int start = posrelat(luaL_checkinteger(L, 2), l); + int end = posrelat(luaL_optinteger(L, 3, -1), l); + if (start < 1) + start = 1; + if (end > (int)l) + end = (int)l; + if (start <= end) + lua_pushlstring(L, s + start - 1, end - start + 1); + else + lua_pushliteral(L, ""); + return 1; +} + +static int str_reverse(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + luaL_Strbuf b; + char* ptr = luaL_buffinitsize(L, &b, l); + while (l--) + *ptr++ = s[l]; + luaL_pushresultsize(&b, ptr - b.p); + return 1; +} + +static int str_lower(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + luaL_Strbuf b; + char* ptr = luaL_buffinitsize(L, &b, l); + for (size_t i = 0; i < l; i++) + *ptr++ = tolower(uchar(s[i])); + luaL_pushresultsize(&b, l); + return 1; +} + +static int str_upper(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + luaL_Strbuf b; + char* ptr = luaL_buffinitsize(L, &b, l); + for (size_t i = 0; i < l; i++) + *ptr++ = toupper(uchar(s[i])); + luaL_pushresultsize(&b, l); + return 1; +} + +static int str_rep(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + int n = luaL_checkinteger(L, 2); + + if (n <= 0) + { + lua_pushliteral(L, ""); + return 1; + } + + if (l > MAXSSIZE / (size_t)n) // may overflow? + luaL_error(L, "resulting string too large"); + + luaL_Strbuf b; + char* ptr = luaL_buffinitsize(L, &b, l * n); + + const char* start = ptr; + + size_t left = l * n; + size_t step = l; + + memcpy(ptr, s, l); + ptr += l; + left -= l; + + // use the increasing 'pattern' inside our target buffer to fill the next part + while (step < left) + { + memcpy(ptr, start, step); + ptr += step; + left -= step; + step <<= 1; + } + + // fill tail + memcpy(ptr, start, left); + ptr += left; + + luaL_pushresultsize(&b, l * n); + + return 1; +} + +static int str_byte(lua_State* L) +{ + size_t l; + const char* s = luaL_checklstring(L, 1, &l); + int posi = posrelat(luaL_optinteger(L, 2, 1), l); + int pose = posrelat(luaL_optinteger(L, 3, posi), l); + int n, i; + if (posi <= 0) + posi = 1; + if ((size_t)pose > l) + pose = (int)l; + if (posi > pose) + return 0; // empty interval; return no values + n = (int)(pose - posi + 1); + if (posi + n <= pose) // overflow? + luaL_error(L, "string slice too long"); + luaL_checkstack(L, n, "string slice too long"); + for (i = 0; i < n; i++) + lua_pushinteger(L, uchar(s[posi + i - 1])); + return n; +} + +static int str_char(lua_State* L) +{ + int n = lua_gettop(L); // number of arguments + + luaL_Strbuf b; + char* ptr = luaL_buffinitsize(L, &b, n); + + for (int i = 1; i <= n; i++) + { + int c = luaL_checkinteger(L, i); + luaL_argcheck(L, uchar(c) == c, i, "invalid value"); + + *ptr++ = uchar(c); + } + luaL_pushresultsize(&b, n); + return 1; +} + +/* +** {====================================================== +** PATTERN MATCHING +** ======================================================= +*/ + +#define CAP_UNFINISHED (-1) +#define CAP_POSITION (-2) + +typedef struct MatchState +{ + int matchdepth; // control for recursive depth (to avoid C stack overflow) + const char* src_init; // init of source string + const char* src_end; // end ('\0') of source string + const char* p_end; // end ('\0') of pattern + lua_State* L; + int level; // total number of captures (finished or unfinished) + struct + { + const char* init; + ptrdiff_t len; + } capture[LUA_MAXCAPTURES]; +} MatchState; + +// recursive function +static const char* match(MatchState* ms, const char* s, const char* p); + +#define L_ESC '%' +#define SPECIALS "^$*+?.([%-" + +static int check_capture(MatchState* ms, int l) +{ + l -= '1'; + if (l < 0 || l >= ms->level || ms->capture[l].len == CAP_UNFINISHED) + luaL_error(ms->L, "invalid capture index %%%d", l + 1); + return l; +} + +static int capture_to_close(MatchState* ms) +{ + int level = ms->level; + for (level--; level >= 0; level--) + if (ms->capture[level].len == CAP_UNFINISHED) + return level; + luaL_error(ms->L, "invalid pattern capture"); +} + +static const char* classend(MatchState* ms, const char* p) +{ + switch (*p++) + { + case L_ESC: + { + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (ends with '%%')"); + return p + 1; + } + case '[': + { + if (*p == '^') + p++; + do + { // look for a `]' + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (missing ']')"); + if (*(p++) == L_ESC && p < ms->p_end) + p++; // skip escapes (e.g. `%]') + } while (*p != ']'); + return p + 1; + } + default: + { + return p; + } + } +} + +static int match_class(int c, int cl) +{ + int res; + switch (tolower(cl)) + { + case 'a': + res = isalpha(c); + break; + case 'c': + res = iscntrl(c); + break; + case 'd': + res = isdigit(c); + break; + case 'g': + res = isgraph(c); + break; + case 'l': + res = islower(c); + break; + case 'p': + res = ispunct(c); + break; + case 's': + res = isspace(c); + break; + case 'u': + res = isupper(c); + break; + case 'w': + res = isalnum(c); + break; + case 'x': + res = isxdigit(c); + break; + case 'z': + res = (c == 0); + break; // deprecated option + default: + return (cl == c); + } + return (islower(cl) ? res : !res); +} + +static int matchbracketclass(int c, const char* p, const char* ec) +{ + int sig = 1; + if (*(p + 1) == '^') + { + sig = 0; + p++; // skip the `^' + } + while (++p < ec) + { + if (*p == L_ESC) + { + p++; + if (match_class(c, uchar(*p))) + return sig; + } + else if ((*(p + 1) == '-') && (p + 2 < ec)) + { + p += 2; + if (uchar(*(p - 2)) <= c && c <= uchar(*p)) + return sig; + } + else if (uchar(*p) == c) + return sig; + } + return !sig; +} + +static int singlematch(MatchState* ms, const char* s, const char* p, const char* ep) +{ + if (s >= ms->src_end) + return 0; + else + { + int c = uchar(*s); + switch (*p) + { + case '.': + return 1; // matches any char + case L_ESC: + return match_class(c, uchar(*(p + 1))); + case '[': + return matchbracketclass(c, p, ep - 1); + default: + return (uchar(*p) == c); + } + } +} + +static const char* matchbalance(MatchState* ms, const char* s, const char* p) +{ + if (p >= ms->p_end - 1) + luaL_error(ms->L, "malformed pattern (missing arguments to '%%b')"); + if (*s != *p) + return NULL; + else + { + int b = *p; + int e = *(p + 1); + int cont = 1; + while (++s < ms->src_end) + { + if (*s == e) + { + if (--cont == 0) + return s + 1; + } + else if (*s == b) + cont++; + } + } + return NULL; // string ends out of balance +} + +static const char* max_expand(MatchState* ms, const char* s, const char* p, const char* ep) +{ + ptrdiff_t i = 0; // counts maximum expand for item + while (singlematch(ms, s + i, p, ep)) + i++; + // keeps trying to match with the maximum repetitions + while (i >= 0) + { + const char* res = match(ms, (s + i), ep + 1); + if (res) + return res; + i--; // else didn't match; reduce 1 repetition to try again + } + return NULL; +} + +static const char* min_expand(MatchState* ms, const char* s, const char* p, const char* ep) +{ + for (;;) + { + const char* res = match(ms, s, ep + 1); + if (res != NULL) + return res; + else if (singlematch(ms, s, p, ep)) + s++; // try with one more repetition + else + return NULL; + } +} + +static const char* start_capture(MatchState* ms, const char* s, const char* p, int what) +{ + const char* res; + int level = ms->level; + if (level >= LUA_MAXCAPTURES) + luaL_error(ms->L, "too many captures"); + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level + 1; + if ((res = match(ms, s, p)) == NULL) // match failed? + ms->level--; // undo capture + return res; +} + +static const char* end_capture(MatchState* ms, const char* s, const char* p) +{ + int l = capture_to_close(ms); + const char* res; + ms->capture[l].len = s - ms->capture[l].init; // close capture + if ((res = match(ms, s, p)) == NULL) // match failed? + ms->capture[l].len = CAP_UNFINISHED; // undo capture + return res; +} + +static const char* match_capture(MatchState* ms, const char* s, int l) +{ + size_t len; + l = check_capture(ms, l); + len = ms->capture[l].len; + if ((size_t)(ms->src_end - s) >= len && memcmp(ms->capture[l].init, s, len) == 0) + return s + len; + else + return NULL; +} + +static const char* match(MatchState* ms, const char* s, const char* p) +{ + if (ms->matchdepth-- == 0) + luaL_error(ms->L, "pattern too complex"); + + lua_State* L = ms->L; + void (*interrupt)(lua_State*, int) = L->global->cb.interrupt; + + if (LUAU_UNLIKELY(!!interrupt)) + { + // this interrupt is not yieldable + L->nCcalls++; + interrupt(L, -1); + L->nCcalls--; + } + +init: // using goto's to optimize tail recursion + if (p != ms->p_end) + { // end of pattern? + switch (*p) + { + case '(': + { // start capture + if (*(p + 1) == ')') // position capture? + s = start_capture(ms, s, p + 2, CAP_POSITION); + else + s = start_capture(ms, s, p + 1, CAP_UNFINISHED); + break; + } + case ')': + { // end capture + s = end_capture(ms, s, p + 1); + break; + } + case '$': + { + if ((p + 1) != ms->p_end) // is the `$' the last char in pattern? + goto dflt; // no; go to default + s = (s == ms->src_end) ? s : NULL; // check end of string + break; + } + case L_ESC: + { // escaped sequences not in the format class[*+?-]? + switch (*(p + 1)) + { + case 'b': + { // balanced string? + s = matchbalance(ms, s, p + 2); + if (s != NULL) + { + p += 4; + goto init; // return match(ms, s, p + 4); + } // else fail (s == NULL) + break; + } + case 'f': + { // frontier? + const char* ep; + char previous; + p += 2; + if (*p != '[') + luaL_error(ms->L, "missing '[' after '%%f' in pattern"); + ep = classend(ms, p); // points to what is next + previous = (s == ms->src_init) ? '\0' : *(s - 1); + if (!matchbracketclass(uchar(previous), p, ep - 1) && matchbracketclass(uchar(*s), p, ep - 1)) + { + p = ep; + goto init; // return match(ms, s, ep); + } + s = NULL; // match failed + break; + } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { // capture results (%0-%9)? + s = match_capture(ms, s, uchar(*(p + 1))); + if (s != NULL) + { + p += 2; + goto init; // return match(ms, s, p + 2) + } + break; + } + default: + goto dflt; + } + break; + } + default: + dflt: + { // pattern class plus optional suffix + const char* ep = classend(ms, p); // points to optional suffix + // does not match at least once? + if (!singlematch(ms, s, p, ep)) + { + if (*ep == '*' || *ep == '?' || *ep == '-') + { // accept empty? + p = ep + 1; + goto init; // return match(ms, s, ep + 1); + } + else // '+' or no suffix + s = NULL; // fail + } + else + { // matched once + switch (*ep) + { // handle optional suffix + case '?': + { // optional + const char* res; + if ((res = match(ms, s + 1, ep + 1)) != NULL) + s = res; + else + { + p = ep + 1; + goto init; // else return match(ms, s, ep + 1); + } + break; + } + case '+': // 1 or more repetitions + s++; // 1 match already done + LUAU_FALLTHROUGH; // go through + case '*': // 0 or more repetitions + s = max_expand(ms, s, p, ep); + break; + case '-': // 0 or more repetitions (minimum) + s = min_expand(ms, s, p, ep); + break; + default: // no suffix + s++; + p = ep; + goto init; // return match(ms, s + 1, ep); + } + } + break; + } + } + } + ms->matchdepth++; + return s; +} + +static const char* lmemfind(const char* s1, size_t l1, const char* s2, size_t l2) +{ + if (l2 == 0) + return s1; // empty strings are everywhere + else if (l2 > l1) + return NULL; // avoids a negative `l1' + else + { + const char* init; // to search for a `*s2' inside `s1' + l2--; // 1st char will be checked by `memchr' + l1 = l1 - l2; // `s2' cannot be found after that + while (l1 > 0 && (init = (const char*)memchr(s1, *s2, l1)) != NULL) + { + init++; // 1st char is already checked + if (memcmp(init, s2 + 1, l2) == 0) + return init - 1; + else + { // correct `l1' and `s1' to try again + l1 -= init - s1; + s1 = init; + } + } + return NULL; // not found + } +} + +static void push_onecapture(MatchState* ms, int i, const char* s, const char* e) +{ + if (i >= ms->level) + { + if (i == 0) // ms->level == 0, too + lua_pushlstring(ms->L, s, e - s); // add whole match + else + luaL_error(ms->L, "invalid capture index"); + } + else + { + ptrdiff_t l = ms->capture[i].len; + if (l == CAP_UNFINISHED) + luaL_error(ms->L, "unfinished capture"); + if (l == CAP_POSITION) + lua_pushinteger(ms->L, (int)(ms->capture[i].init - ms->src_init) + 1); + else + lua_pushlstring(ms->L, ms->capture[i].init, l); + } +} + +static int push_captures(MatchState* ms, const char* s, const char* e) +{ + int i; + int nlevels = (ms->level == 0 && s) ? 1 : ms->level; + luaL_checkstack(ms->L, nlevels, "too many captures"); + for (i = 0; i < nlevels; i++) + push_onecapture(ms, i, s, e); + return nlevels; // number of strings pushed +} + +// check whether pattern has no special characters +static int nospecials(const char* p, size_t l) +{ + size_t upto = 0; + do + { + if (strpbrk(p + upto, SPECIALS)) + return 0; // pattern has a special character + upto += strlen(p + upto) + 1; // may have more after \0 + } while (upto <= l); + return 1; // no special chars found +} + +static void prepstate(MatchState* ms, lua_State* L, const char* s, size_t ls, const char* p, size_t lp) +{ + ms->L = L; + ms->matchdepth = LUAI_MAXCCALLS; + ms->src_init = s; + ms->src_end = s + ls; + ms->p_end = p + lp; +} + +static void reprepstate(MatchState* ms) +{ + ms->level = 0; + LUAU_ASSERT(ms->matchdepth == LUAI_MAXCCALLS); +} + +static int str_find_aux(lua_State* L, int find) +{ + size_t ls, lp; + const char* s = luaL_checklstring(L, 1, &ls); + const char* p = luaL_checklstring(L, 2, &lp); + int init = posrelat(luaL_optinteger(L, 3, 1), ls); + if (init < 1) + init = 1; + else if (init > (int)ls + 1) + { // start after string's end? + lua_pushnil(L); // cannot find anything + return 1; + } + // explicit request or no special characters? + if (find && (lua_toboolean(L, 4) || nospecials(p, lp))) + { + // do a plain search + const char* s2 = lmemfind(s + init - 1, ls - init + 1, p, lp); + if (s2) + { + lua_pushinteger(L, (int)(s2 - s + 1)); + lua_pushinteger(L, (int)(s2 - s + lp)); + return 2; + } + } + else + { + MatchState ms; + const char* s1 = s + init - 1; + int anchor = (*p == '^'); + if (anchor) + { + p++; + lp--; // skip anchor character + } + prepstate(&ms, L, s, ls, p, lp); + do + { + const char* res; + reprepstate(&ms); + if ((res = match(&ms, s1, p)) != NULL) + { + if (find) + { + lua_pushinteger(L, (int)(s1 - s + 1)); // start + lua_pushinteger(L, (int)(res - s)); // end + return push_captures(&ms, NULL, 0) + 2; + } + else + return push_captures(&ms, s1, res); + } + } while (s1++ < ms.src_end && !anchor); + } + lua_pushnil(L); // not found + return 1; +} + +static int str_find(lua_State* L) +{ + return str_find_aux(L, 1); +} + +static int str_match(lua_State* L) +{ + return str_find_aux(L, 0); +} + +static int gmatch_aux(lua_State* L) +{ + MatchState ms; + size_t ls, lp; + const char* s = lua_tolstring(L, lua_upvalueindex(1), &ls); + const char* p = lua_tolstring(L, lua_upvalueindex(2), &lp); + const char* src; + prepstate(&ms, L, s, ls, p, lp); + for (src = s + (size_t)lua_tointeger(L, lua_upvalueindex(3)); src <= ms.src_end; src++) + { + const char* e; + reprepstate(&ms); + if ((e = match(&ms, src, p)) != NULL) + { + int newstart = (int)(e - s); + if (e == src) + newstart++; // empty match? go at least one position + lua_pushinteger(L, newstart); + lua_replace(L, lua_upvalueindex(3)); + return push_captures(&ms, src, e); + } + } + return 0; // not found +} + +static int gmatch(lua_State* L) +{ + luaL_checkstring(L, 1); + luaL_checkstring(L, 2); + lua_settop(L, 2); + lua_pushinteger(L, 0); + lua_pushcclosure(L, gmatch_aux, NULL, 3); + return 1; +} + +static void add_s(MatchState* ms, luaL_Strbuf* b, const char* s, const char* e) +{ + size_t l, i; + const char* news = lua_tolstring(ms->L, 3, &l); + + luaL_prepbuffsize(b, l); + + for (i = 0; i < l; i++) + { + if (news[i] != L_ESC) + luaL_addchar(b, news[i]); + else + { + i++; // skip ESC + if (!isdigit(uchar(news[i]))) + { + if (news[i] != L_ESC) + luaL_error(ms->L, "invalid use of '%c' in replacement string", L_ESC); + luaL_addchar(b, news[i]); + } + else if (news[i] == '0') + luaL_addlstring(b, s, e - s); + else + { + push_onecapture(ms, news[i] - '1', s, e); + luaL_addvalue(b); // add capture to accumulated result + } + } + } +} + +static void add_value(MatchState* ms, luaL_Strbuf* b, const char* s, const char* e, int tr) +{ + lua_State* L = ms->L; + switch (tr) + { + case LUA_TFUNCTION: + { + int n; + lua_pushvalue(L, 3); + n = push_captures(ms, s, e); + lua_call(L, n, 1); + break; + } + case LUA_TTABLE: + { + push_onecapture(ms, 0, s, e); + lua_gettable(L, 3); + break; + } + default: + { // LUA_TNUMBER or LUA_TSTRING + add_s(ms, b, s, e); + return; + } + } + if (!lua_toboolean(L, -1)) + { // nil or false? + lua_pop(L, 1); + lua_pushlstring(L, s, e - s); // keep original text + } + else if (!lua_isstring(L, -1)) + luaL_error(L, "invalid replacement value (a %s)", luaL_typename(L, -1)); + luaL_addvalue(b); // add result to accumulator +} + +static int str_gsub(lua_State* L) +{ + size_t srcl, lp; + const char* src = luaL_checklstring(L, 1, &srcl); + const char* p = luaL_checklstring(L, 2, &lp); + int tr = lua_type(L, 3); + int max_s = luaL_optinteger(L, 4, (int)srcl + 1); + int anchor = (*p == '^'); + int n = 0; + MatchState ms; + luaL_Strbuf b; + luaL_argexpected(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, "string/function/table"); + luaL_buffinit(L, &b); + if (anchor) + { + p++; + lp--; // skip anchor character + } + prepstate(&ms, L, src, srcl, p, lp); + while (n < max_s) + { + const char* e; + reprepstate(&ms); + e = match(&ms, src, p); + if (e) + { + n++; + add_value(&ms, &b, src, e, tr); + } + if (e && e > src) // non empty match? + src = e; // skip it + else if (src < ms.src_end) + luaL_addchar(&b, *src++); + else + break; + if (anchor) + break; + } + luaL_addlstring(&b, src, ms.src_end - src); + luaL_pushresult(&b); + lua_pushinteger(L, n); // number of substitutions + return 2; +} + +// }====================================================== + +// valid flags in a format specification +#define FLAGS "-+ #0" +// maximum size of each formatted item (> len(format('%99.99f', -1e308))) +#define MAX_ITEM 512 +// maximum size of each format specification (such as '%-099.99d') +#define MAX_FORMAT 32 + +static void addquoted(lua_State* L, luaL_Strbuf* b, int arg) +{ + size_t l; + const char* s = luaL_checklstring(L, arg, &l); + + luaL_prepbuffsize(b, l + 2); + + luaL_addchar(b, '"'); + while (l--) + { + switch (*s) + { + case '"': + case '\\': + case '\n': + { + luaL_addchar(b, '\\'); + luaL_addchar(b, *s); + break; + } + case '\r': + { + luaL_addlstring(b, "\\r", 2); + break; + } + case '\0': + { + luaL_addlstring(b, "\\000", 4); + break; + } + default: + { + luaL_addchar(b, *s); + break; + } + } + s++; + } + luaL_addchar(b, '"'); +} + +static const char* scanformat(lua_State* L, const char* strfrmt, char* form, size_t* size) +{ + const char* p = strfrmt; + while (*p != '\0' && strchr(FLAGS, *p) != NULL) + p++; // skip flags + if ((size_t)(p - strfrmt) >= sizeof(FLAGS)) + luaL_error(L, "invalid format (repeated flags)"); + if (isdigit(uchar(*p))) + p++; // skip width + if (isdigit(uchar(*p))) + p++; // (2 digits at most) + if (*p == '.') + { + p++; + if (isdigit(uchar(*p))) + p++; // skip precision + if (isdigit(uchar(*p))) + p++; // (2 digits at most) + } + if (isdigit(uchar(*p))) + luaL_error(L, "invalid format (width or precision too long)"); + *(form++) = '%'; + *size = p - strfrmt + 1; + strncpy(form, strfrmt, *size); + form += *size; + *form = '\0'; + return p; +} + +static void addInt64Format(char form[MAX_FORMAT], char formatIndicator, size_t formatItemSize) +{ + LUAU_ASSERT((formatItemSize + 3) <= MAX_FORMAT); + LUAU_ASSERT(form[0] == '%'); + LUAU_ASSERT(form[formatItemSize] != 0); + LUAU_ASSERT(form[formatItemSize + 1] == 0); + form[formatItemSize + 0] = 'l'; + form[formatItemSize + 1] = 'l'; + form[formatItemSize + 2] = formatIndicator; + form[formatItemSize + 3] = 0; +} + +static int str_format(lua_State* L) +{ + int top = lua_gettop(L); + int arg = 1; + size_t sfl; + const char* strfrmt = luaL_checklstring(L, arg, &sfl); + const char* strfrmt_end = strfrmt + sfl; + luaL_Strbuf b; + luaL_buffinit(L, &b); + while (strfrmt < strfrmt_end) + { + if (*strfrmt != L_ESC) + luaL_addchar(&b, *strfrmt++); + else if (*++strfrmt == L_ESC) + luaL_addchar(&b, *strfrmt++); // %% + else if (*strfrmt == '*') + { + strfrmt++; + if (++arg > top) + luaL_error(L, "missing argument #%d", arg); + + luaL_addvalueany(&b, arg); + } + else + { // format item + char form[MAX_FORMAT]; // to store the format (`%...') + char buff[MAX_ITEM]; // to store the formatted item + if (++arg > top) + luaL_error(L, "missing argument #%d", arg); + size_t formatItemSize = 0; + strfrmt = scanformat(L, strfrmt, form, &formatItemSize); + char formatIndicator = *strfrmt++; + switch (formatIndicator) + { + case 'c': + { + int count = snprintf(buff, sizeof(buff), form, (int)luaL_checknumber(L, arg)); + luaL_addlstring(&b, buff, count); + continue; // skip the 'luaL_addlstring' at the end + } + case 'd': + case 'i': + { + long long value = lua_isinteger64(L, arg) ? luaL_checkinteger64(L, arg) : (int64_t)luaL_checknumber(L, arg); + addInt64Format(form, formatIndicator, formatItemSize); + snprintf(buff, sizeof(buff), form, value); + break; + } + case 'o': + case 'u': + case 'x': + case 'X': + { + uint64_t v; + if (lua_isinteger64(L, arg)) + { + v = luaL_checkinteger64(L, arg); + } + else + { + double argValue = luaL_checknumber(L, arg); + v = (argValue < 0) ? (unsigned long long)(long long)argValue : (unsigned long long)argValue; + } + addInt64Format(form, formatIndicator, formatItemSize); + snprintf(buff, sizeof(buff), form, v); + break; + } + case 'e': + case 'E': + case 'f': + case 'g': + case 'G': + { + snprintf(buff, sizeof(buff), form, (double)luaL_checknumber(L, arg)); + break; + } + case 'q': + { + addquoted(L, &b, arg); + continue; // skip the 'luaL_addlstring' at the end + } + case 's': + { + size_t l; + const char* s = luaL_checklstring(L, arg, &l); + // no precision and string is too long to be formatted, or no format necessary to begin with + if (form[2] == '\0' || (!strchr(form, '.') && l >= 100)) + { + luaL_addlstring(&b, s, l); + continue; // skip the `luaL_addlstring' at the end + } + else + { + snprintf(buff, sizeof(buff), form, s); + break; + } + } + case '*': + { + // %* is parsed above, so if we got here we must have %...* + luaL_error(L, "'%%*' does not take a form"); + } + default: + { // also treat cases `pnLlh' + luaL_error(L, "invalid option '%%%c' to 'format'", *(strfrmt - 1)); + } + } + luaL_addlstring(&b, buff, strlen(buff)); + } + } + luaL_pushresult(&b); + return 1; +} + +static int str_split(lua_State* L) +{ + size_t haystackLen; + const char* haystack = luaL_checklstring(L, 1, &haystackLen); + size_t needleLen; + const char* needle = luaL_optlstring(L, 2, ",", &needleLen); + + if (DFFlag::LuauOptimizeStringSplit) + { + const char* end = haystack + haystackLen; + const char* spanStart = haystack; + int numMatches = 0; + + // Use of memchr/memcmp here instead of strchr/strncmp is so that we allow + // embedded nulls to be used in either of the haystack or the needle + // strings. Most Lua string APIs allow embedded nulls, and this should be + // no exception. + if (needleLen == 0) + { + // empty separator splits the string into individual characters, so the result size is known up front + lua_createtable(L, int(haystackLen), 0); + + for (const char* iter = haystack; iter < end; iter++) + { + lua_pushlstring(L, iter, 1); + lua_rawseti(L, -2, ++numMatches); + } + + return 1; + } + else if (needleLen == 1) + { + // every occurrence of a single character separator is a split, so we can cheaply count them up front + // and allocate the result table at its final size + char sep = needle[0]; + + int count = 1; + for (const char* iter = haystack; (iter = (const char*)memchr(iter, sep, end - iter)) != NULL; iter++) + count++; + + lua_createtable(L, count, 0); + + for (const char* found; (found = (const char*)memchr(spanStart, sep, end - spanStart)) != NULL; spanStart = found + 1) + { + lua_pushlstring(L, spanStart, found - spanStart); + lua_rawseti(L, -2, ++numMatches); + } + } + else + { + lua_createtable(L, 0, 0); + + if (needleLen <= haystackLen) + { + // Don't iterate the last needleLen - 1 bytes of the string - they are + // impossible to be splits and would let us memcmp past the end of the + // buffer. + const char* last = end - needleLen; + + for (const char* iter = haystack; iter <= last;) + { + // the first and the last characters are checked inline to avoid a memcmp call at most positions + if (iter[0] == needle[0] && iter[needleLen - 1] == needle[needleLen - 1] && memcmp(iter, needle, needleLen) == 0) + { + lua_pushlstring(L, spanStart, iter - spanStart); + lua_rawseti(L, -2, ++numMatches); + + spanStart = iter + needleLen; + iter = spanStart; + } + else + { + iter++; + } + } + } + } + + lua_pushlstring(L, spanStart, end - spanStart); + lua_rawseti(L, -2, ++numMatches); + } + else + { + const char* begin = haystack; + const char* end = haystack + haystackLen; + const char* spanStart = begin; + int numMatches = 0; + + lua_createtable(L, 0, 0); + + if (needleLen == 0) + begin++; + + // Don't iterate the last needleLen - 1 bytes of the string - they are + // impossible to be splits and would let us memcmp past the end of the + // buffer. + for (const char* iter = begin; iter <= end - needleLen; iter++) + { + // Use of memcmp here instead of strncmp is so that we allow embedded + // nulls to be used in either of the haystack or the needle strings. + // Most Lua string APIs allow embedded nulls, and this should be no + // exception. + if (memcmp(iter, needle, needleLen) == 0) + { + lua_pushinteger(L, ++numMatches); + lua_pushlstring(L, spanStart, iter - spanStart); + lua_settable(L, -3); + + spanStart = iter + needleLen; + if (needleLen > 0) + iter += needleLen - 1; + } + } + + if (needleLen > 0) + { + lua_pushinteger(L, ++numMatches); + lua_pushlstring(L, spanStart, end - spanStart); + lua_settable(L, -3); + } + } + + return 1; +} + +/* +** {====================================================== +** PACK/UNPACK +** ======================================================= +*/ + +// value used for padding +#if !defined(LUAL_PACKPADBYTE) +#define LUAL_PACKPADBYTE 0x00 +#endif + +// maximum size for the binary representation of an integer +#define MAXINTSIZE 16 + +// number of bits in a character +#define NB CHAR_BIT + +// mask for one character (NB 1's) +#define MC ((1 << NB) - 1) + +// internal size of integers used for pack/unpack +#define SZINT (int)sizeof(long long) + +// dummy union to get native endianness +static const union +{ + int dummy; + char little; // true iff machine is little endian +} nativeendian = {1}; + +// assume we need to align for double & pointers +#define MAXALIGN 8 + +/* +** Union for serializing floats +*/ +typedef union Ftypes +{ + float f; + double d; + double n; + char buff[5 * sizeof(double)]; // enough for any float type +} Ftypes; + +/* +** information to pack/unpack stuff +*/ +typedef struct Header +{ + lua_State* L; + int islittle; + int maxalign; +} Header; + +/* +** options for pack/unpack +*/ +typedef enum KOption +{ + Kint, // signed integers + Kuint, // unsigned integers + Kfloat, // floating-point numbers + Kchar, // fixed-length strings + Kstring, // strings with prefixed length + Kzstr, // zero-terminated strings + Kpadding, // padding + Kpaddalign, // padding for alignment + Knop // no-op (configuration or spaces) +} KOption; + +/* +** Read an integer numeral from string 'fmt' or return 'df' if +** there is no numeral +*/ +static int digit(int c) +{ + return '0' <= c && c <= '9'; +} + +static int getnum(Header* h, const char** fmt, int df) +{ + if (!digit(**fmt)) // no number? + return df; // return default value + else + { + int a = 0; + do + { + a = a * 10 + (*((*fmt)++) - '0'); + } while (digit(**fmt) && a <= (INT_MAX - 9) / 10); + if (a > MAXSSIZE || digit(**fmt)) + luaL_error(h->L, "size specifier is too large"); + return a; + } +} + +/* +** Read an integer numeral and raises an error if it is larger +** than the maximum size for integers. +*/ +static int getnumlimit(Header* h, const char** fmt, int df) +{ + int sz = getnum(h, fmt, df); + if (sz > MAXINTSIZE || sz <= 0) + luaL_error(h->L, "integral size (%d) out of limits [1,%d]", sz, MAXINTSIZE); + return sz; +} + +/* +** Initialize Header +*/ +static void initheader(lua_State* L, Header* h) +{ + h->L = L; + h->islittle = nativeendian.little; + h->maxalign = 1; +} + +/* +** Read and classify next option. 'size' is filled with option's size. +*/ +static KOption getoption(Header* h, const char** fmt, int* size) +{ + int opt = *((*fmt)++); + *size = 0; // default + switch (opt) + { + case 'b': + *size = 1; + return Kint; + case 'B': + *size = 1; + return Kuint; + case 'h': + *size = 2; + return Kint; + case 'H': + *size = 2; + return Kuint; + case 'l': + *size = 8; + return Kint; + case 'L': + *size = 8; + return Kuint; + case 'j': + *size = 4; + return Kint; + case 'J': + *size = 4; + return Kuint; + case 'T': + *size = 4; + return Kuint; + case 'f': + *size = 4; + return Kfloat; + case 'd': + *size = 8; + return Kfloat; + case 'n': + *size = 8; + return Kfloat; + case 'i': + *size = getnumlimit(h, fmt, 4); + return Kint; + case 'I': + *size = getnumlimit(h, fmt, 4); + return Kuint; + case 's': + *size = getnumlimit(h, fmt, 4); + return Kstring; + case 'c': + *size = getnum(h, fmt, -1); + if (*size == -1) + luaL_error(h->L, "missing size for format option 'c'"); + return Kchar; + case 'z': + return Kzstr; + case 'x': + *size = 1; + return Kpadding; + case 'X': + return Kpaddalign; + case ' ': + break; + case '<': + h->islittle = 1; + break; + case '>': + h->islittle = 0; + break; + case '=': + h->islittle = nativeendian.little; + break; + case '!': + h->maxalign = getnumlimit(h, fmt, MAXALIGN); + break; + default: + luaL_error(h->L, "invalid format option '%c'", opt); + } + return Knop; +} + +/* +** Read, classify, and fill other details about the next option. +** 'psize' is filled with option's size, 'notoalign' with its +** alignment requirements. +** Local variable 'size' gets the size to be aligned. (Kpaddalign option +** always gets its full alignment, other options are limited by +** the maximum alignment ('maxalign'). Kchar option needs no alignment +** despite its size. +*/ +static KOption getdetails(Header* h, size_t totalsize, const char** fmt, int* psize, int* ntoalign) +{ + KOption opt = getoption(h, fmt, psize); + int align = *psize; // usually, alignment follows size + if (opt == Kpaddalign) + { // 'X' gets alignment from following option + if (**fmt == '\0' || getoption(h, fmt, &align) == Kchar || align == 0) + luaL_argerror(h->L, 1, "invalid next option for option 'X'"); + } + if (align <= 1 || opt == Kchar) // need no alignment? + *ntoalign = 0; + else + { + if (align > h->maxalign) // enforce maximum alignment + align = h->maxalign; + if ((align & (align - 1)) != 0) // is 'align' not a power of 2? + luaL_argerror(h->L, 1, "format asks for alignment not power of 2"); + *ntoalign = (align - (int)(totalsize & (align - 1))) & (align - 1); + } + return opt; +} + +/* +** Pack integer 'n' with 'size' bytes and 'islittle' endianness. +** The final 'if' handles the case when 'size' is larger than +** the size of a Lua integer, correcting the extra sign-extension +** bytes if necessary (by default they would be zeros). +*/ +static void packint(luaL_Strbuf* b, unsigned long long n, int islittle, int size, int neg) +{ + LUAU_ASSERT(size <= MAXINTSIZE); + char buff[MAXINTSIZE]; + int i; + buff[islittle ? 0 : size - 1] = (char)(n & MC); // first byte + for (i = 1; i < size; i++) + { + n >>= NB; + buff[islittle ? i : size - 1 - i] = (char)(n & MC); + } + if (neg && size > SZINT) + { // negative number need sign extension? + for (i = SZINT; i < size; i++) // correct extra bytes + buff[islittle ? i : size - 1 - i] = (char)MC; + } + luaL_addlstring(b, buff, size); // add result to buffer +} + +/* +** Copy 'size' bytes from 'src' to 'dest', correcting endianness if +** given 'islittle' is different from native endianness. +*/ +static void copywithendian(volatile char* dest, volatile const char* src, int size, int islittle) +{ + if (islittle == nativeendian.little) + { + while (size-- != 0) + *(dest++) = *(src++); + } + else + { + dest += size - 1; + while (size-- != 0) + *(dest--) = *(src++); + } +} + +static int str_pack(lua_State* L) +{ + luaL_Strbuf b; + Header h; + const char* fmt = luaL_checkstring(L, 1); // format string + int arg = 1; // current argument to pack + size_t totalsize = 0; // accumulate total size of result + initheader(L, &h); + lua_pushnil(L); // mark to separate arguments from string buffer + luaL_buffinit(L, &b); + while (*fmt != '\0') + { + int size, ntoalign; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + totalsize += ntoalign + size; + while (ntoalign-- > 0) + luaL_addchar(&b, LUAL_PACKPADBYTE); // fill alignment + arg++; + switch (opt) + { + case Kint: + { // signed integers + long long n = (long long)luaL_checknumber(L, arg); + if (size < SZINT) + { // need overflow check? + long long lim = (long long)1 << ((size * NB) - 1); + luaL_argcheck(L, -lim <= n && n < lim, arg, "integer overflow"); + } + packint(&b, n, h.islittle, size, (n < 0)); + break; + } + case Kuint: + { // unsigned integers + long long n = (long long)luaL_checknumber(L, arg); + if (size < SZINT) // need overflow check? + luaL_argcheck(L, (unsigned long long)n < ((unsigned long long)1 << (size * NB)), arg, "unsigned overflow"); + packint(&b, (unsigned long long)n, h.islittle, size, 0); + break; + } + case Kfloat: + { // floating-point options + volatile Ftypes u; + char buff[MAXINTSIZE]; + double n = luaL_checknumber(L, arg); // get argument + if (size == sizeof(u.f)) + u.f = (float)n; // copy it into 'u' + else if (size == sizeof(u.d)) + u.d = (double)n; + else + u.n = n; + // move 'u' to final result, correcting endianness if needed + copywithendian(buff, u.buff, size, h.islittle); + luaL_addlstring(&b, buff, size); + break; + } + case Kchar: + { // fixed-size string + size_t len; + const char* s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, len <= (size_t)size, arg, "string longer than given size"); + luaL_addlstring(&b, s, len); // add string + while (len++ < (size_t)size) // pad extra space + luaL_addchar(&b, LUAL_PACKPADBYTE); + break; + } + case Kstring: + { // strings with length count + size_t len; + const char* s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, size >= (int)sizeof(size_t) || len < ((size_t)1 << (size * NB)), arg, "string length does not fit in given size"); + packint(&b, len, h.islittle, size, 0); // pack length + luaL_addlstring(&b, s, len); + totalsize += len; + break; + } + case Kzstr: + { // zero-terminated string + size_t len; + const char* s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, strlen(s) == len, arg, "string contains zeros"); + luaL_addlstring(&b, s, len); + luaL_addchar(&b, '\0'); // add zero at the end + totalsize += len + 1; + break; + } + case Kpadding: + luaL_addchar(&b, LUAL_PACKPADBYTE); + LUAU_FALLTHROUGH; + case Kpaddalign: + case Knop: + arg--; // undo increment + break; + } + } + luaL_pushresult(&b); + return 1; +} + +static int str_packsize(lua_State* L) +{ + Header h; + const char* fmt = luaL_checkstring(L, 1); // format string + int totalsize = 0; // accumulate total size of result + initheader(L, &h); + while (*fmt != '\0') + { + int size, ntoalign; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + luaL_argcheck(L, opt != Kstring && opt != Kzstr, 1, "variable-length format"); + size += ntoalign; // total space used by option + luaL_argcheck(L, totalsize <= MAXSSIZE - size, 1, "format result too large"); + totalsize += size; + } + lua_pushinteger(L, totalsize); + return 1; +} + +/* +** Unpack an integer with 'size' bytes and 'islittle' endianness. +** If size is smaller than the size of a Lua integer and integer +** is signed, must do sign extension (propagating the sign to the +** higher bits); if size is larger than the size of a Lua integer, +** it must check the unread bytes to see whether they do not cause an +** overflow. +*/ +static long long unpackint(lua_State* L, const char* str, int islittle, int size, int issigned) +{ + unsigned long long res = 0; + int i; + int limit = (size <= SZINT) ? size : SZINT; + for (i = limit - 1; i >= 0; i--) + { + res <<= NB; + res |= (unsigned char)str[islittle ? i : size - 1 - i]; + } + if (size < SZINT) + { // real size smaller than int? + if (issigned) + { // needs sign extension? + unsigned long long mask = (unsigned long long)1 << (size * NB - 1); + res = ((res ^ mask) - mask); // do sign extension + } + } + else if (size > SZINT) + { // must check unread bytes + int mask = (!issigned || (long long)res >= 0) ? 0 : MC; + for (i = limit; i < size; i++) + { + if ((unsigned char)str[islittle ? i : size - 1 - i] != mask) + luaL_error(L, "%d-byte integer does not fit into Lua Integer", size); + } + } + return (long long)res; +} + +static int str_unpack(lua_State* L) +{ + Header h; + const char* fmt = luaL_checkstring(L, 1); + size_t ld; + const char* data = luaL_checklstring(L, 2, &ld); + int pos = posrelat(luaL_optinteger(L, 3, 1), ld) - 1; + if (pos < 0) + pos = 0; + int n = 0; // number of results + luaL_argcheck(L, size_t(pos) <= ld, 3, "initial position out of string"); + initheader(L, &h); + while (*fmt != '\0') + { + int size, ntoalign; + KOption opt = getdetails(&h, pos, &fmt, &size, &ntoalign); + luaL_argcheck(L, (size_t)ntoalign + size <= ld - pos, 2, "data string too short"); + pos += ntoalign; // skip alignment + // stack space for item + next position + luaL_checkstack(L, 2, "too many results"); + n++; + switch (opt) + { + case Kint: + { + long long res = unpackint(L, data + pos, h.islittle, size, true); + lua_pushnumber(L, (double)res); + break; + } + case Kuint: + { + unsigned long long res = unpackint(L, data + pos, h.islittle, size, false); + lua_pushnumber(L, (double)res); + break; + } + case Kfloat: + { + volatile Ftypes u; + double num; + copywithendian(u.buff, data + pos, size, h.islittle); + if (size == sizeof(u.f)) + num = (double)u.f; + else if (size == sizeof(u.d)) + num = (double)u.d; + else + num = u.n; + lua_pushnumber(L, num); + break; + } + case Kchar: + { + lua_pushlstring(L, data + pos, size); + break; + } + case Kstring: + { + size_t len = (size_t)unpackint(L, data + pos, h.islittle, size, 0); + luaL_argcheck(L, len <= ld - pos - size, 2, "data string too short"); + lua_pushlstring(L, data + pos + size, len); + pos += (int)len; // skip string + break; + } + case Kzstr: + { + size_t len = strlen(data + pos); + luaL_argcheck(L, pos + len < ld, 2, "unfinished string for format 'z'"); + lua_pushlstring(L, data + pos, len); + pos += (int)len + 1; // skip string plus final '\0' + break; + } + case Kpaddalign: + case Kpadding: + case Knop: + n--; // undo increment + break; + } + pos += size; + } + lua_pushinteger(L, pos + 1); // next position + return n + 1; +} + +// }====================================================== + +static const luaL_Reg strlib[] = { + {"byte", str_byte}, + {"char", str_char}, + {"find", str_find}, + {"format", str_format}, + {"gmatch", gmatch}, + {"gsub", str_gsub}, + {"len", str_len}, + {"lower", str_lower}, + {"match", str_match}, + {"rep", str_rep}, + {"reverse", str_reverse}, + {"sub", str_sub}, + {"upper", str_upper}, + {"split", str_split}, + {"pack", str_pack}, + {"packsize", str_packsize}, + {"unpack", str_unpack}, + {NULL, NULL}, +}; + +static void createmetatable(lua_State* L) +{ + lua_createtable(L, 0, 1); // create metatable for strings + lua_pushliteral(L, ""); // dummy string + lua_pushvalue(L, -2); + lua_setmetatable(L, -2); // set string metatable + lua_pop(L, 1); // pop dummy string + lua_pushvalue(L, -2); // string library... + lua_setfield(L, -2, "__index"); // ...is the __index metamethod + lua_pop(L, 1); // pop metatable +} + +/* +** Open string library +*/ +int luaopen_string(lua_State* L) +{ + luaL_register(L, LUA_STRLIBNAME, strlib); + createmetatable(L); + + return 1; +} diff --git a/External/luau/VM/src/ltable.cpp b/External/luau/VM/src/ltable.cpp new file mode 100644 index 00000000..292891c1 --- /dev/null +++ b/External/luau/VM/src/ltable.cpp @@ -0,0 +1,1664 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details + +/* + * Implementation of tables (aka arrays, objects, or hash tables). + * + * Tables keep the elements in two parts: an array part and a hash part. + * Integer keys >=1 are all candidates to be kept in the array part. The actual size of the array is the + * largest n such that at least half the slots between 0 and n are in use. + * Hash uses a mix of chained scatter table with Brent's variation. + * + * A main invariant of these tables is that, if an element is not in its main position (i.e. the original + * position that its hash gives to it), then the colliding element is in its own main position. + * Hence even when the load factor reaches 100%, performance remains good. + * + * Table keys can be arbitrary values unless they contain NaN. Keys are hashed and compared using raw equality, + * so even if the key is a userdata with an overridden __eq, it's not used during hash lookups. + * + * Each table has a "boundary", defined as the index k where t[k] ~= nil and t[k+1] == nil. The boundary can be + * computed using a binary search and can be adjusted when the table is modified; crucially, Luau enforces an + * invariant where the boundary must be in the array part - this enforces a consistent iteration order through the + * prefix of the table when using pairs(), and allows to implement algorithms that access elements in 1..#t range + * more efficiently. + */ + +#include "ltable.h" + +#include "lobject.h" +#include "ldebug.h" +#include "lgc.h" +#include "lmem.h" +#include "lnumutils.h" +#include "ltm.h" + +#include + +LUAU_FASTFLAG(LuauFrozenMetaButterfly) +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauSplitTableLookups, false) +LUAU_FASTFLAGVARIABLE(LuauTableArrayAdjustCheck) +LUAU_FASTFLAGVARIABLE(LuauTableArrayShrinkOrder) +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauTableRobustOom, false) + +// Set this to 1 to change the hash function to something different (and possibly trivial). Useful +// for checking if a Lua program's behavior depends on the hash function. +#define ALT_HASH_FUNCTION (0) + +// max size of both array and hash part is 2^MAXBITS +#define MAXBITS 26 +#define MAXSIZE (1 << MAXBITS) + +static_assert(offsetof(LuaNode, val) == 0, "Unexpected Node memory layout, pointer cast in gval2slot is incorrect"); + +// TKey is bitpacked for memory efficiency so we need to validate bit counts for worst case +static_assert(TKey{{NULL}, {0}, LUA_TDEADKEY, 0}.tt == LUA_TDEADKEY, "not enough bits for tt"); +static_assert(TKey{{NULL}, {0}, LUA_TNIL, MAXSIZE - 1}.next == MAXSIZE - 1, "not enough bits for next"); +static_assert(TKey{{NULL}, {0}, LUA_TNIL, -(MAXSIZE - 1)}.next == -(MAXSIZE - 1), "not enough bits for next"); + +// empty hash data points to dummynode so that we can always dereference it +const LuaNode luaH_dummynode = { + {{NULL}, {0}, LUA_TNIL}, // value + {{NULL}, {0}, LUA_TNIL, 0} // key +}; + +#define dummynode (&luaH_dummynode) + +// hash is always reduced mod 2^k +#define hashpow2(t, n) (gnode(t, lmod((n), sizenode(t)))) + +#define hashstr(t, str) hashpow2(t, (ALT_HASH_FUNCTION ? 0x87654321 : 0) ^ (str)->hash) +#define hashboolean(t, p) hashpow2(t, (ALT_HASH_FUNCTION ? 0x87654321 : 0) ^ p) + +static LuaNode* hashpointer(const LuaTable* t, const void* p) +{ + // we discard the high 32-bit portion of the pointer on 64-bit platforms as it doesn't carry much entropy anyway + unsigned int h = unsigned(uintptr_t(p)); + + // MurmurHash3 32-bit finalizer + if (!ALT_HASH_FUNCTION) + { + h ^= h >> 16; + h *= 0x85ebca6bu; + h ^= h >> 13; + h *= 0xc2b2ae35u; + h ^= h >> 16; + } + + return hashpow2(t, h); +} + +static LuaNode* hashnum(const LuaTable* t, double n) +{ + static_assert(sizeof(double) == sizeof(unsigned int) * 2, "expected a 8-byte double"); + unsigned int i[2]; + memcpy(i, &n, sizeof(i)); + + // mask out sign bit to make sure -0 and 0 hash to the same value + uint32_t h1 = i[0]; + uint32_t h2 = i[1] & 0x7fffffff; + + // finalizer from MurmurHash64B + const uint32_t m = 0x5bd1e995; + + if (!ALT_HASH_FUNCTION) + { + h1 ^= h2 >> 18; + h1 *= m; + h2 ^= h1 >> 22; + h2 *= m; + h1 ^= h2 >> 17; + h1 *= m; + h2 ^= h1 >> 19; + h2 *= m; + } + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return hashpow2(t, h2); +} + +static LuaNode* hashint(const LuaTable* t, int64_t n) +{ + static_assert(sizeof(n) == sizeof(unsigned int) * 2, "expected a 8-byte integer"); + unsigned int i[2]; + memcpy(i, &n, sizeof(i)); + + uint32_t h1 = i[0]; + uint32_t h2 = i[1]; + + // finalizer from MurmurHash64B + const uint32_t m = 0x5bd1e995; + + if (!ALT_HASH_FUNCTION) + { + h1 ^= h2 >> 18; + h1 *= m; + h2 ^= h1 >> 22; + h2 *= m; + h1 ^= h2 >> 17; + h1 *= m; + h2 ^= h1 >> 19; + h2 *= m; + } + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return hashpow2(t, h2); +} + +LUAU_MAYBE_UNUSED static LuaNode* hashvec(const LuaTable* t, const float* v) +{ + unsigned int i[LUA_VECTOR_SIZE]; + memcpy(i, v, sizeof(i)); + + // convert -0 to 0 to make sure they hash to the same value + i[0] = (i[0] == 0x80000000) ? 0 : i[0]; + i[1] = (i[1] == 0x80000000) ? 0 : i[1]; + i[2] = (i[2] == 0x80000000) ? 0 : i[2]; + + // scramble bits to make sure that integer coordinates have entropy in lower bits + if (!ALT_HASH_FUNCTION) + { + i[0] ^= i[0] >> 17; + i[1] ^= i[1] >> 17; + i[2] ^= i[2] >> 17; + } + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + unsigned int h = (i[0] * 73856093) ^ (i[1] * 19349663) ^ (i[2] * 83492791); + +#if LUA_VECTOR_SIZE == 4 + i[3] = (i[3] == 0x80000000) ? 0 : i[3]; + i[3] ^= i[3] >> 17; + h ^= i[3] * 39916801; +#endif + + return hashpow2(t, h); +} + +static LUAU_MAYBE_UNUSED LuaNode* hashvec(const LuaTable* t, const double* v) +{ + uint64_t i[LUA_VECTOR_SIZE]; + memcpy(i, v, sizeof(i)); + + // convert -0 to 0 to make sure they hash to the same value + i[0] = (i[0] == 0x8000000000000000ull) ? 0 : i[0]; + i[1] = (i[1] == 0x8000000000000000ull) ? 0 : i[1]; + i[2] = (i[2] == 0x8000000000000000ull) ? 0 : i[2]; + + // scramble bits to make sure that integer coordinates have entropy in lower bits + if (!ALT_HASH_FUNCTION) + { + i[0] ^= i[0] >> 32; + i[1] ^= i[1] >> 32; + i[2] ^= i[2] >> 32; + } + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + unsigned int h = uint32_t(i[0] * 73856093) ^ uint32_t(i[1] * 19349663) ^ uint32_t(i[2] * 83492791); + +#if LUA_VECTOR_SIZE == 4 + i[3] = (i[3] == 0x8000000000000000ull) ? 0 : i[3]; + i[3] ^= i[3] >> 32; + h ^= uint32_t(i[3] * 39916801); +#endif + + return hashpow2(t, h); +} + +/* +** returns the `main' position of an element in a table (that is, the index +** of its hash value) +*/ +static LuaNode* mainposition(const LuaTable* t, const TValue* key) +{ + switch (ttype(key)) + { + case LUA_TNUMBER: + return hashnum(t, nvalue(key)); + case LUA_TINTEGER: + return hashint(t, lvalue(key)); + case LUA_TVECTOR: + return hashvec(t, vvalue(key)); + case LUA_TSTRING: + return hashstr(t, tsvalue(key)); + case LUA_TBOOLEAN: + return hashboolean(t, bvalue(key)); + case LUA_TLIGHTUSERDATA: + return hashpointer(t, pvalue(key)); + default: + return hashpointer(t, gcvalue(key)); + } +} + +static const int LUA_TGCO = -1; // Marker for template dispatch into a generic GCObject* version of a function (not for LUA_TSTRING!) + +// For static mainposition dispatch on a known tag (or GCO) +template +static LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, T key) = delete; + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, int key) +{ + return hashboolean(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, void* key) +{ + return hashpointer(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, double key) +{ + return hashnum(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, int64_t key) +{ + return hashint(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, const LUA_VECTOR_TYPE* key) +{ + return hashvec(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, TString* key) +{ + return hashstr(t, key); +} + +template<> +LUAU_FORCEINLINE LuaNode* mainpositiontagged(const LuaTable* t, GCObject* key) +{ + return hashpointer(t, key); +} + +// For static luaO_rawequalKey dispatch on a known tag (or GCO). Only used specializations are implemented +template +static LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, T key) = delete; + +template<> +LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, int key) +{ + return bvalue(t1) == key; +} + +template<> +LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, double key) +{ + return luai_numeq(nvalue(t1), key); +} + +template<> +LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, int64_t key) +{ + return luai_inteq(lvalue(t1), key); +} + +template<> +LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, const LUA_VECTOR_TYPE* key) +{ + return luai_veceq(vvalue(t1), key); +} + +template<> +LUAU_FORCEINLINE int rawequalkeyvalue(const TKey* t1, GCObject* key) +{ + return gcvalue(t1) == key; +} + +// Lookup a node in the main position of the key or look through the node chain +template +static LUAU_FORCEINLINE const TValue* chainsearchtagged(const LuaTable* t, T key, int keytag) +{ + LuaNode* n = mainpositiontagged(t, key); + + // check whether `key' is somewhere in the chain + for (;;) + { + if (ttype(gkey(n)) == keytag && rawequalkeyvalue(gkey(n), key)) + return gval(n); + + if (gnext(n) == 0) + break; + n += gnext(n); + } + + return luaO_nilobject; +} + +/* +** returns the index for `key' if `key' is an appropriate key to live in +** the array part of the table, -1 otherwise. +*/ +static int arrayindex(double key) +{ + int i; + luai_num2int(i, key); + + return luai_numeq(cast_num(i), key) ? i : -1; +} + +/* +** returns the index of a `key' for table traversals. First goes all +** elements in the array part, then elements in the hash part. The +** beginning of a traversal is signalled by -1. +*/ +static int findindex(lua_State* L, LuaTable* t, StkId key) +{ + int i; + if (ttisnil(key)) + return -1; // first iteration + i = ttisnumber(key) ? arrayindex(nvalue(key)) : -1; + if (0 < i && i <= t->sizearray) // is `key' inside array part? + return i - 1; // yes; that's the index (corrected to C) + else + { + LuaNode* n = mainposition(t, key); + for (;;) + { // check whether `key' is somewhere in the chain + // key may be dead already, but it is ok to use it in `next' + if (luaO_rawequalKey(gkey(n), key) || (ttype(gkey(n)) == LUA_TDEADKEY && iscollectable(key) && gcvalue(gkey(n)) == gcvalue(key))) + { + i = cast_int(n - gnode(t, 0)); // key index in hash table + // hash elements are numbered after array ones + return i + t->sizearray; + } + if (gnext(n) == 0) + break; + n += gnext(n); + } + luaG_runerror(L, "invalid key to 'next'"); // key not found + } +} + +int luaH_next(lua_State* L, LuaTable* t, StkId key) +{ + int i = findindex(L, t, key); // find original element + for (i++; i < t->sizearray; i++) + { // try first array part + if (!ttisnil(&t->array[i])) + { // a non-nil value? + setnvalue(key, cast_num(i + 1)); + setobj2s(L, key + 1, &t->array[i]); + return 1; + } + } + for (i -= t->sizearray; i < sizenode(t); i++) + { // then hash part + if (!ttisnil(gval(gnode(t, i)))) + { // a non-nil value? + getnodekey(L, key, gnode(t, i)); + setobj2s(L, key + 1, gval(gnode(t, i))); + return 1; + } + } + return 0; // no more elements +} + +/* +** {============================================================= +** Rehash +** ============================================================== +*/ + +#define maybesetaboundary(t, boundary) \ + { \ + if (t->aboundary <= 0) \ + t->aboundary = -int(boundary); \ + } + +#define getaboundary(t) (t->aboundary < 0 ? -t->aboundary : t->sizearray) + +static int computesizes(int nums[], int* narray) +{ + int i; + int twotoi; // 2^i + int a = 0; // number of elements smaller than 2^i + int na = 0; // number of elements to go to array part + int n = 0; // optimal size for array part + for (i = 0, twotoi = 1; twotoi / 2 < *narray; i++, twotoi *= 2) + { + if (nums[i] > 0) + { + a += nums[i]; + if (a > twotoi / 2) + { // more than half elements present? + n = twotoi; // optimal size (till now) + na = a; // all elements smaller than n will go to array part + } + } + if (a == *narray) + break; // all elements already counted + } + *narray = n; + LUAU_ASSERT(*narray / 2 <= na && na <= *narray); + return na; +} + +static int countint(double key, int* nums) +{ + int k = arrayindex(key); + if (0 < k && k <= MAXSIZE) + { // is `key' an appropriate array index? + nums[ceillog2(k)]++; // count as such + return 1; + } + else + return 0; +} + +static int numusearray(const LuaTable* t, int* nums) +{ + int lg; + int ttlg; // 2^lg + int ause = 0; // summation of `nums' + int i = 1; // count to traverse all array keys + for (lg = 0, ttlg = 1; lg <= MAXBITS; lg++, ttlg *= 2) + { // for each slice + int lc = 0; // counter + int lim = ttlg; + if (lim > t->sizearray) + { + lim = t->sizearray; // adjust upper limit + if (i > lim) + break; // no more elements to count + } + // count elements in range (2^(lg-1), 2^lg] + for (; i <= lim; i++) + { + if (!ttisnil(&t->array[i - 1])) + lc++; + } + nums[lg] += lc; + ause += lc; + } + return ause; +} + +static int numusehash(const LuaTable* t, int* nums, int* pnasize) +{ + int totaluse = 0; // total number of elements + int ause = 0; // summation of `nums' + int i = sizenode(t); + while (i--) + { + LuaNode* n = &t->node[i]; + if (!ttisnil(gval(n))) + { + if (ttisnumber(gkey(n))) + ause += countint(nvalue(gkey(n)), nums); + totaluse++; + } + } + *pnasize += ause; + return totaluse; +} + +static int hashslotsused(const LuaTable* t) +{ + int totaluse = 0; + int i = sizenode(t); + + while (i--) + totaluse += !ttisnil(gval(&t->node[i])); + + return totaluse; +} + +static void setarrayvector(lua_State* L, LuaTable* t, int size) +{ + if (size > MAXSIZE) + luaG_runerror(L, "table overflow"); + luaM_reallocarray(L, t->array, t->sizearray, size, TValue, t->memcat); + TValue* array = t->array; + for (int i = t->sizearray; i < size; i++) + setnilvalue(&array[i]); + t->sizearray = size; +} + +static void setnodevector(lua_State* L, LuaTable* t, int size) +{ + int lsize; + if (size == 0) + { // no elements to hash part? + t->node = cast_to(LuaNode*, dummynode); // use common `dummynode' + lsize = 0; + } + else + { + int i; + lsize = ceillog2(size); + if (lsize > MAXBITS) + luaG_runerror(L, "table overflow"); + size = twoto(lsize); + t->node = luaM_newarray(L, size, LuaNode, t->memcat); + for (i = 0; i < size; i++) + { + LuaNode* n = gnode(t, i); + gnext(n) = 0; + setnilvalue(gkey(n)); + setnilvalue(gval(n)); + } + } + t->lsizenode = cast_byte(lsize); + t->nodemask8 = cast_byte((1 << lsize) - 1); + t->lastfree = size; // all positions are free +} + +// TODO: forward declaration not needed with DFFlagLuauSplitTableLookups removal +static TValue* newkey_DEPRECATED(lua_State* L, LuaTable* t, const TValue* key); + +static TValue* reinsertkey(lua_State* L, LuaTable* t, const TValue* key, bool checkarraypart); + +// TODO: remove with DFFlagLuauSplitTableLookups +static TValue* arrayornewkey_DEPRECATED(lua_State* L, LuaTable* t, const TValue* key) +{ + if (ttisnumber(key)) + { + int k; + double n = nvalue(key); + luai_num2int(k, n); + if (luai_numeq(cast_num(k), n) && unsigned(k) - 1 < unsigned(t->sizearray)) + return &t->array[k - 1]; + } + + return newkey_DEPRECATED(L, t, key); +} + +static void resize(lua_State* L, LuaTable* t, int nasize, int nhsize) +{ + LUAU_ASSERT(!hasmetacache(t)); + + if (nasize > MAXSIZE || nhsize > MAXSIZE) + luaG_runerror(L, "table overflow"); + + if (DFFlag::LuauTableRobustOom) + { + // check for node size bit mask overflow and round up requested hash size + int lsizenode = 0; + + LUAU_ASSERT(nhsize >= 0); + if (nhsize != 0) + { + lsizenode = ceillog2(nhsize); + if (lsizenode > MAXBITS) + luaG_runerror(L, "table overflow"); + + nhsize = twoto(lsizenode); + } + + int oldsizearray = t->sizearray; + int oldsizenode = twoto(t->lsizenode); + TValue* oldarray = t->array; + LuaNode* oldnode = t->node; + + TValue* newarray = t->array; + + if (nasize != oldsizearray) + { + newarray = luaM_newarray(L, nasize, TValue, t->memcat); + + if (nasize < oldsizearray) + { + // copy elements from remaining slice + if (nasize != 0) + memcpy(newarray, oldarray, nasize * sizeof(TValue)); + } + else + { + // copy old elements + if (oldsizearray != 0) + memcpy(newarray, oldarray, oldsizearray * sizeof(TValue)); + + // clear new elements + for (int i = oldsizearray; i < nasize; i++) + setnilvalue(&newarray[i]); + } + } + + LuaNode* newnode = cast_to(LuaNode*, dummynode); + + if (nhsize != 0) + { + // if we allocated a new array part, we have to be careful to with hash part allocation failure + if (oldarray != newarray) + { + newnode = luaM_trynewarray(L, nhsize, LuaNode, t->memcat); + + if (newnode == nullptr) + { + luaM_freearray(L, newarray, nasize, TValue, t->memcat); + luaD_throw(L, LUA_ERRMEM); + } + } + else + { + newnode = luaM_newarray(L, nhsize, LuaNode, t->memcat); + } + + for (int i = 0; i < nhsize; i++) + { + LuaNode* n = &newnode[i]; + gnext(n) = 0; + setnilvalue(gkey(n)); + setnilvalue(gval(n)); + } + } + + // update table structure now that both allocations succeeded + t->array = newarray; + t->sizearray = nasize; + + t->node = newnode; + t->lsizenode = cast_byte(lsizenode); + t->nodemask8 = cast_byte((1 << lsizenode) - 1); + t->lastfree = nhsize; // all positions are free + + // re-insert elements from vanishing array slice + for (int i = nasize; i < oldsizearray; i++) + { + if (!ttisnil(&oldarray[i])) + { + TValue ok; + setnvalue(&ok, cast_num(i + 1)); + if (DFFlag::LuauSplitTableLookups) + { + // integer number keys from a shrinking slice cannot get into the new array part + setobjt2t(L, reinsertkey(L, t, &ok, false), &oldarray[i]); + } + else + { + setobjt2t(L, newkey_DEPRECATED(L, t, &ok), &oldarray[i]); + } + } + } + + // re-insert elements from hash part + for (int i = oldsizenode - 1; i >= 0; i--) + { + LuaNode* old = oldnode + i; + if (!ttisnil(gval(old))) + { + TValue ok; + getnodekey(L, &ok, old); + + if (DFFlag::LuauSplitTableLookups) + { + setobjt2t(L, reinsertkey(L, t, &ok, true), gval(old)); + } + else + { + setobjt2t(L, arrayornewkey_DEPRECATED(L, t, &ok), gval(old)); + } + } + } + + // make sure we haven't recursively rehashed during element migration + LUAU_ASSERT(newnode == t->node); + LUAU_ASSERT(newarray == t->array); + + // free old array + if (nasize != oldsizearray) + luaM_freearray(L, oldarray, oldsizearray, TValue, t->memcat); + + // free old node + if (oldnode != dummynode) + luaM_freearray(L, oldnode, oldsizenode, LuaNode, t->memcat); + + return; + } + + int oldasize = t->sizearray; + int oldhsize = t->lsizenode; + LuaNode* nold = t->node; // save old hash ... + if (nasize > oldasize) // array part must grow? + setarrayvector(L, t, nasize); + // create new hash part with appropriate size + setnodevector(L, t, nhsize); + // used for the migration check at the end + LuaNode* nnew = t->node; + if (nasize < oldasize) + { // array part must shrink? + if (FFlag::LuauTableArrayShrinkOrder) + { + TValue* oldarray = t->array; + + // shrink array + t->array = luaM_newarray(L, nasize, TValue, t->memcat); + t->sizearray = nasize; + + // copy elements from remaining slice + if (nasize != 0) + memcpy(t->array, oldarray, nasize * sizeof(TValue)); + + // re-insert elements from vanishing slice (landing into the hash part) + for (int i = nasize; i < oldasize; i++) + { + if (!ttisnil(&oldarray[i])) + { + TValue ok; + setnvalue(&ok, cast_num(i + 1)); + if (DFFlag::LuauSplitTableLookups) + { + // integer number keys from a shrinking slice cannot get into the new array part + setobjt2t(L, reinsertkey(L, t, &ok, false), &oldarray[i]); + } + else + { + setobjt2t(L, newkey_DEPRECATED(L, t, &ok), &oldarray[i]); + } + } + } + + luaM_freearray(L, oldarray, oldasize, TValue, t->memcat); + } + else + { + t->sizearray = nasize; + // re-insert elements from vanishing slice + for (int i = nasize; i < oldasize; i++) + { + if (!ttisnil(&t->array[i])) + { + TValue ok; + setnvalue(&ok, cast_num(i + 1)); + + if (DFFlag::LuauSplitTableLookups) + { + // integer number keys from a shrinking slice cannot get into the new array part + setobjt2t(L, reinsertkey(L, t, &ok, false), &t->array[i]); + } + else + { + setobjt2t(L, newkey_DEPRECATED(L, t, &ok), &t->array[i]); + } + } + } + // shrink array + luaM_reallocarray(L, t->array, oldasize, nasize, TValue, t->memcat); + } + } + // used for the migration check at the end + TValue* anew = t->array; + // re-insert elements from hash part + for (int i = twoto(oldhsize) - 1; i >= 0; i--) + { + LuaNode* old = nold + i; + if (!ttisnil(gval(old))) + { + TValue ok; + getnodekey(L, &ok, old); + + if (DFFlag::LuauSplitTableLookups) + { + setobjt2t(L, reinsertkey(L, t, &ok, true), gval(old)); + } + else + { + setobjt2t(L, arrayornewkey_DEPRECATED(L, t, &ok), gval(old)); + } + } + } + + // make sure we haven't recursively rehashed during element migration + LUAU_ASSERT(nnew == t->node); + LUAU_ASSERT(anew == t->array); + + if (nold != dummynode) + luaM_freearray(L, nold, twoto(oldhsize), LuaNode, t->memcat); // free old array +} + +static int adjustasize_DEPRECATED(LuaTable* t, int size, const TValue* ek) +{ + bool tbound = t->node != dummynode || size < t->sizearray; + int ekindex = ek && ttisnumber(ek) ? arrayindex(nvalue(ek)) : -1; + // move the array size up until the boundary is guaranteed to be inside the array part + while (size + 1 == ekindex || (tbound && !ttisnil(luaH_getnum(t, size + 1)))) + size++; + return size; +} + +// When we already know that new key is an integer number +static int adjustasize(LuaTable* t, int size, int extraintkey) +{ + bool tbound = t->node != dummynode || size < t->sizearray; + // move the array size up until the boundary is guaranteed to be inside the array part + while (size + 1 == extraintkey || (tbound && !ttisnil(luaH_getnum(t, size + 1)))) + size++; + return size; +} + +void luaH_resizearray(lua_State* L, LuaTable* t, int nasize) +{ + if (FFlag::LuauTableArrayAdjustCheck && nasize > MAXSIZE) + luaG_runerror(L, "table overflow"); + + int nsize = (t->node == dummynode) ? 0 : sizenode(t); + int asize = DFFlag::LuauSplitTableLookups ? adjustasize(t, nasize, -1) : adjustasize_DEPRECATED(t, nasize, NULL); + resize(L, t, asize, nsize); +} + +void luaH_resizehash(lua_State* L, LuaTable* t, int nhsize) +{ + resize(L, t, t->sizearray, nhsize); +} + +static void rehash_DEPRECATED(lua_State* L, LuaTable* t, const TValue* ek) +{ + int nums[MAXBITS + 1]; // nums[i] = number of keys between 2^(i-1) and 2^i + for (int i = 0; i <= MAXBITS; i++) + nums[i] = 0; // reset counts + int nasize = numusearray(t, nums); // count keys in array part + int totaluse = nasize; // all those keys are integer keys + totaluse += numusehash(t, nums, &nasize); // count keys in hash part + + // count extra key + if (ttisnumber(ek)) + nasize += countint(nvalue(ek), nums); + totaluse++; + + // compute new size for array part + int na = computesizes(nums, &nasize); + int nh = totaluse - na; + + // enforce the boundary invariant; for performance, only do hash lookups if we must + int nadjusted = adjustasize_DEPRECATED(t, nasize, ek); + + // count how many extra elements belong to array part instead of hash part + int aextra = nadjusted - nasize; + + if (aextra != 0) + { + // we no longer need to store those extra array elements in hash part + nh -= aextra; + + // because hash nodes are twice as large as array nodes, the memory we saved for hash parts can be used by array part + // this follows the general sparse array part optimization where array is allocated when 50% occupation is reached + nasize = nadjusted + aextra; + + // since the size was changed, it's again important to enforce the boundary invariant at the new size + nasize = adjustasize_DEPRECATED(t, nasize, ek); + } + + // resize the table to new computed sizes + resize(L, t, nasize, nh); +} + +template +static void rehash(lua_State* L, LuaTable* t, int newintkey) +{ + // insertions of keys that are not integer numbers do not have to rehash the array part + // this can mean that such key insert will not cause the sparse array/node rebalance, but this is actually less surprising + if (!IsIntKey) + { + int nh = hashslotsused(t) + 1; + resize(L, t, t->sizearray, nh); + return; + } + + // nums[i] = number of keys between 2^(i-1) and 2^i + int nums[MAXBITS + 1]; + for (int i = 0; i <= MAXBITS; i++) + nums[i] = 0; + + int nasize = 0; + int totaluse = 0; + + if (t->sizearray != 0) + { + nasize = numusearray(t, nums); // count keys in array part + totaluse = nasize; // all those keys are integer keys + } + + if (t->node != dummynode) + totaluse += numusehash(t, nums, &nasize); // count keys in hash part + + // count extra key + if (IsIntKey && 0 < newintkey && newintkey <= MAXSIZE) + { + nums[ceillog2(newintkey)]++; + nasize++; + } + + totaluse++; + + // compute new size for array part + int na = computesizes(nums, &nasize); + int nh = totaluse - na; + + // enforce the boundary invariant; for performance, only do hash lookups if we must + int nadjusted = adjustasize(t, nasize, newintkey); + + // count how many extra elements belong to array part instead of hash part + int aextra = nadjusted - nasize; + + if (aextra != 0) + { + // we no longer need to store those extra array elements in hash part + nh -= aextra; + + // because hash nodes are twice as large as array nodes, the memory we saved for hash parts can be used by array part + // this follows the general sparse array part optimization where array is allocated when 50% occupation is reached + nasize = nadjusted + aextra; + + // since the size was changed, it's again important to enforce the boundary invariant at the new size + nasize = adjustasize(t, nasize, newintkey); + } + + // resize the table to new computed sizes + resize(L, t, nasize, nh); +} + +/* +** }============================================================= +*/ + +LuaTable* luaH_new(lua_State* L, int narray, int nhash) +{ + LuaTable* t = luaM_newgco(L, LuaTable, sizeof(LuaTable), L->activememcat, LUA_TTABLE); + luaC_init(L, t, LUA_TTABLE); + t->metatable = NULL; + t->tmcache = cast_byte(~0); + t->array = NULL; + t->sizearray = 0; + t->lastfree = 0; + t->lsizenode = 0; + t->readonly = 0; + t->safeenv = 0; + t->nodemask8 = 0; + t->node = cast_to(LuaNode*, dummynode); + if (narray > 0) + setarrayvector(L, t, narray); + if (nhash > 0) + setnodevector(L, t, nhash); + return t; +} + +void luaH_free(lua_State* L, LuaTable* t, lua_Page* page) +{ + if (t->node != dummynode) + luaM_freearray(L, t->node, sizenode(t), LuaNode, t->memcat); + + if (FFlag::LuauFrozenMetaButterfly) + { + if (t->array) + { + if (hasmetacache(t)) + luaM_freearray(L, t->array - TM_N, t->sizearray + TM_N, TValue, t->memcat); + else + luaM_freearray(L, t->array, t->sizearray, TValue, t->memcat); + } + } + else + { + if (t->array) + luaM_freearray(L, t->array, t->sizearray, TValue, t->memcat); + } + + luaM_freegco(L, t, sizeof(LuaTable), t->memcat, page); +} + +static LuaNode* getfreepos(LuaTable* t) +{ + while (t->lastfree > 0) + { + t->lastfree--; + + LuaNode* n = gnode(t, t->lastfree); + if (ttisnil(gkey(n))) + return n; + } + return NULL; // could not find a free place +} + +static LuaNode* newcollidingkey(lua_State* L, LuaTable* t, LuaNode* mp, LuaNode* n) +{ + LUAU_ASSERT(n != dummynode); + + TValue mk; + getnodekey(L, &mk, mp); + LuaNode* othern = mainposition(t, &mk); + + // is colliding node out of its main position? + if (othern != mp) + { + // yes, move colliding node into free position + while (othern + gnext(othern) != mp) + othern += gnext(othern); // find previous + + gnext(othern) = cast_int(n - othern); // redo the chain with `n' in place of `mp' + *n = *mp; // copy colliding node into free pos. (mp->next also goes) + + if (gnext(mp) != 0) + { + gnext(n) += cast_int(mp - n); // correct 'next' + gnext(mp) = 0; // now 'mp' is free + } + + setnilvalue(gval(mp)); + return mp; + } + else + { + // colliding node is in its own main position + // new node will go into free position + if (gnext(mp) != 0) + gnext(n) = cast_int((mp + gnext(mp)) - n); // chain new position + else + LUAU_ASSERT(gnext(n) == 0); + gnext(mp) = cast_int(n - mp); + return n; + } +} + +static TValue* newkey_DEPRECATED(lua_State* L, LuaTable* t, const TValue* key) +{ + // enforce boundary invariant + if (ttisnumber(key) && nvalue(key) == t->sizearray + 1) + { + rehash_DEPRECATED(L, t, key); // grow table + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return arrayornewkey_DEPRECATED(L, t, key); + } + + LuaNode* mp = mainposition(t, key); + if (!ttisnil(gval(mp)) || mp == dummynode) + { + LuaNode* n = getfreepos(t); // get a free place + if (n == NULL) + { // cannot find a free place? + rehash_DEPRECATED(L, t, key); // grow table + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return arrayornewkey_DEPRECATED(L, t, key); + } + LUAU_ASSERT(n != dummynode); + TValue mk; + getnodekey(L, &mk, mp); + LuaNode* othern = mainposition(t, &mk); + if (othern != mp) + { // is colliding node out of its main position? + // yes; move colliding node into free position + while (othern + gnext(othern) != mp) + othern += gnext(othern); // find previous + gnext(othern) = cast_int(n - othern); // redo the chain with `n' in place of `mp' + *n = *mp; // copy colliding node into free pos. (mp->next also goes) + if (gnext(mp) != 0) + { + gnext(n) += cast_int(mp - n); // correct 'next' + gnext(mp) = 0; // now 'mp' is free + } + setnilvalue(gval(mp)); + } + else + { // colliding node is in its own main position + // new node will go into free position + if (gnext(mp) != 0) + gnext(n) = cast_int((mp + gnext(mp)) - n); // chain new position + else + LUAU_ASSERT(gnext(n) == 0); + gnext(mp) = cast_int(n - mp); + mp = n; + } + } + + setnodekey(L, mp, key); + luaC_barriert(L, t, key); + LUAU_ASSERT(ttisnil(gval(mp))); + return gval(mp); +} + +/* +** inserts a new key into a hash table; first, check whether key's main +** position is free. If not, check whether colliding node is in its main +** position or not: if it is not, move colliding node to an empty place and +** put new key in its main position; otherwise (colliding node is in its main +** position), new key goes to an empty position. +*/ +template +static LUAU_NOINLINE TValue* newkeytagged(lua_State* L, LuaTable* t, const TValue* key, Key hashkey, int newintkey) +{ + // enforce boundary invariant + if (IsIntKey && newintkey == t->sizearray + 1) + { + rehash(L, t, newintkey); // grow table + + if (unsigned(newintkey) - 1 < unsigned(t->sizearray)) + return &t->array[newintkey - 1]; + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return newkeytagged(L, t, key, hashkey, newintkey); + } + + LuaNode* mp = mainpositiontagged(t, hashkey); + + if (!ttisnil(gval(mp)) || mp == dummynode) + { + LuaNode* n = getfreepos(t); // get a free place + + // cannot find a free place? + if (n == nullptr) + { + rehash(L, t, newintkey); // grow table + + if (IsIntKey && unsigned(newintkey) - 1 < unsigned(t->sizearray)) + return &t->array[newintkey - 1]; + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return newkeytagged(L, t, key, hashkey, newintkey); + } + + mp = newcollidingkey(L, t, mp, n); + } + + setnodekey(L, mp, key); + + if (Tag >= LUA_TSTRING || Tag == LUA_TGCO) + luaC_barriert(L, t, key); + + LUAU_ASSERT(ttisnil(gval(mp))); + return gval(mp); +} + +// for the table rehash path, we cannot trigger nested reshashing +static TValue* reinsertkey(lua_State* L, LuaTable* t, const TValue* key, bool checkarraypart) +{ + if (checkarraypart && ttisnumber(key)) + { + int k; + double n = nvalue(key); + luai_num2int(k, n); + if (luai_numeq(cast_num(k), n) && unsigned(k) - 1 < unsigned(t->sizearray)) + return &t->array[k - 1]; + } + + LuaNode* mp = mainposition(t, key); + + if (!ttisnil(gval(mp)) || mp == dummynode) + mp = newcollidingkey(L, t, mp, getfreepos(t)); + + setnodekey(L, mp, key); + luaC_barriert(L, t, key); + LUAU_ASSERT(ttisnil(gval(mp))); + return gval(mp); +} + +/* +** search function for integers +*/ +const TValue* luaH_getnum(LuaTable* t, int key) +{ + // (1 <= key && key <= t->sizearray) + if (unsigned(key) - 1 < unsigned(t->sizearray)) + return &t->array[key - 1]; + else if (t->node != dummynode) + { + double nk = cast_num(key); + LuaNode* n = hashnum(t, nk); + for (;;) + { // check whether `key' is somewhere in the chain + if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk)) + return gval(n); // that's it + if (gnext(n) == 0) + break; + n += gnext(n); + } + return luaO_nilobject; + } + else + return luaO_nilobject; +} + +/* +** search function for strings +*/ +const TValue* luaH_getstr(LuaTable* t, TString* key) +{ + LuaNode* n = hashstr(t, key); + for (;;) + { // check whether `key' is somewhere in the chain + if (ttisstring(gkey(n)) && tsvalue(gkey(n)) == key) + return gval(n); // that's it + if (gnext(n) == 0) + break; + n += gnext(n); + } + return luaO_nilobject; +} + +/* +** search function for lightuserdata +*/ +const TValue* luaH_getp(LuaTable* t, void* key, int tag) +{ + LuaNode* n = hashpointer(t, key); + for (;;) + { // check whether `key' is somewhere in the chain + const TKey* nk = gkey(n); + if (ttislightuserdata(nk) && pvalue(nk) == key && lightuserdatatag(nk) == tag) + return gval(n); // that's it + if (gnext(n) == 0) + break; + n += gnext(n); + } + return luaO_nilobject; +} + +/* +** main search function +*/ +const TValue* luaH_get(LuaTable* t, const TValue* key) +{ + if (DFFlag::LuauSplitTableLookups) + { + switch (ttype(key)) + { + case LUA_TNIL: + return luaO_nilobject; + case LUA_TBOOLEAN: + return chainsearchtagged(t, bvalue(key), LUA_TBOOLEAN); + case LUA_TLIGHTUSERDATA: + return luaH_getp(t, pvalue(key), lightuserdatatag(key)); // special search function with tag comparison + case LUA_TNUMBER: + { + int k; + double n = nvalue(key); + luai_num2int(k, n); + if (luai_numeq(cast_num(k), nvalue(key))) // index is int? + return luaH_getnum(t, k); // use specialized version + + return chainsearchtagged(t, nvalue(key), LUA_TNUMBER); + } + case LUA_TINTEGER: + return chainsearchtagged(t, lvalue(key), LUA_TINTEGER); + case LUA_TVECTOR: + return chainsearchtagged(t, (const LUA_VECTOR_TYPE*)vvalue(key), LUA_TVECTOR); + case LUA_TSTRING: + return luaH_getstr(t, tsvalue(key)); + default: + return chainsearchtagged(t, gcvalue(key), ttype(key)); + } + } + else + { + switch (ttype(key)) + { + case LUA_TNIL: + return luaO_nilobject; + case LUA_TSTRING: + return luaH_getstr(t, tsvalue(key)); + case LUA_TNUMBER: + { + int k; + double n = nvalue(key); + luai_num2int(k, n); + if (luai_numeq(cast_num(k), nvalue(key))) // index is int? + return luaH_getnum(t, k); // use specialized version + LUAU_FALLTHROUGH; // else go through + } + default: + { + LuaNode* n = mainposition(t, key); + for (;;) + { // check whether `key' is somewhere in the chain + if (luaO_rawequalKey(gkey(n), key)) + return gval(n); // that's it + if (gnext(n) == 0) + break; + n += gnext(n); + } + return luaO_nilobject; + } + } + } +} + +TValue* luaH_set(lua_State* L, LuaTable* t, const TValue* key) +{ + const TValue* p = luaH_get(t, key); + invalidateTMcache(t); + if (p != luaO_nilobject) + return cast_to(TValue*, p); + else + return luaH_newkey(L, t, key); +} + +TValue* luaH_newkey(lua_State* L, LuaTable* t, const TValue* key) +{ + if (DFFlag::LuauSplitTableLookups) + { + switch (ttype(key)) + { + case LUA_TNIL: + luaG_runerror(L, "table index is nil"); + break; + case LUA_TBOOLEAN: + return newkeytagged(L, t, key, bvalue(key), -1); + case LUA_TLIGHTUSERDATA: + return newkeytagged(L, t, key, pvalue(key), -1); + case LUA_TNUMBER: + { + double n = nvalue(key); + + if (luai_numisnan(n)) + luaG_runerror(L, "table index is NaN"); + + int k; + luai_num2int(k, n); + if (luai_numeq(cast_num(k), n)) + return newkeytagged(L, t, key, n, k); + + return newkeytagged(L, t, key, n, -1); + } + case LUA_TINTEGER: + return newkeytagged(L, t, key, lvalue(key), -1); + case LUA_TVECTOR: + if (luai_vecisnan(vvalue(key))) + luaG_runerror(L, "table index contains NaN"); + + return newkeytagged(L, t, key, (const LUA_VECTOR_TYPE*)vvalue(key), -1); + case LUA_TSTRING: + return newkeytagged(L, t, key, tsvalue(key), -1); + default: + return newkeytagged(L, t, key, gcvalue(key), -1); + } + } + else + { + if (ttisnil(key)) + luaG_runerror(L, "table index is nil"); + else if (ttisnumber(key) && luai_numisnan(nvalue(key))) + luaG_runerror(L, "table index is NaN"); + else if (ttisvector(key) && luai_vecisnan(vvalue(key))) + luaG_runerror(L, "table index contains NaN"); + return newkey_DEPRECATED(L, t, key); + } +} + +TValue* luaH_setnum(lua_State* L, LuaTable* t, int key) +{ + // (1 <= key && key <= t->sizearray) + if (unsigned(key) - 1 < unsigned(t->sizearray)) + return &t->array[key - 1]; + // hash fallback + const TValue* p = luaH_getnum(t, key); + if (p != luaO_nilobject) + return cast_to(TValue*, p); + else + { + if (DFFlag::LuauSplitTableLookups) + { + double n = cast_num(key); + + TValue k; + setnvalue(&k, n); + return newkeytagged(L, t, &k, n, key); + } + else + { + TValue k; + setnvalue(&k, cast_num(key)); + return newkey_DEPRECATED(L, t, &k); + } + } +} + +TValue* luaH_setstr(lua_State* L, LuaTable* t, TString* key) +{ + const TValue* p = luaH_getstr(t, key); + invalidateTMcache(t); + if (p != luaO_nilobject) + return cast_to(TValue*, p); + else + { + TValue k; + setsvalue(L, &k, key); + + if (DFFlag::LuauSplitTableLookups) + return newkeytagged(L, t, &k, key, -1); + else + return newkey_DEPRECATED(L, t, &k); + } +} + +TValue* luaH_setp(lua_State* L, LuaTable* t, void* key, int tag) +{ + const TValue* p = luaH_getp(t, key, tag); + if (p != luaO_nilobject) + return cast_to(TValue*, p); + else + { + TValue k; + setpvalue(&k, key, tag); + + if (DFFlag::LuauSplitTableLookups) + return newkeytagged(L, t, &k, key, -1); + else + return newkey_DEPRECATED(L, t, &k); + } +} + +static int updateaboundary(LuaTable* t, int boundary) +{ + if (boundary < t->sizearray && ttisnil(&t->array[boundary - 1])) + { + if (boundary >= 2 && !ttisnil(&t->array[boundary - 2])) + { + maybesetaboundary(t, boundary - 1); + return boundary - 1; + } + } + else if (boundary + 1 < t->sizearray && !ttisnil(&t->array[boundary]) && ttisnil(&t->array[boundary + 1])) + { + maybesetaboundary(t, boundary + 1); + return boundary + 1; + } + + return 0; +} + +/* +** Try to find a boundary in table `t'. A `boundary' is an integer index +** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil). +*/ +int luaH_getn(LuaTable* t) +{ + int boundary = getaboundary(t); + + if (boundary > 0) + { + if (!ttisnil(&t->array[t->sizearray - 1]) && t->node == dummynode) + return t->sizearray; // fast-path: the end of the array in `t' already refers to a boundary + if (boundary < t->sizearray && !ttisnil(&t->array[boundary - 1]) && ttisnil(&t->array[boundary])) + return boundary; // fast-path: boundary already refers to a boundary in `t' + + int foundboundary = updateaboundary(t, boundary); + if (foundboundary > 0) + return foundboundary; + } + + int j = t->sizearray; + + if (j > 0 && ttisnil(&t->array[j - 1])) + { + // "branchless" binary search from Array Layouts for Comparison-Based Searching, Paul Khuong, Pat Morin, 2017. + // note that clang is cmov-shy on cmovs around memory operands, so it will compile this to a branchy loop. + TValue* base = t->array; + int rest = j; + while (int half = rest >> 1) + { + base = ttisnil(&base[half]) ? base : base + half; + rest -= half; + } + int boundary = !ttisnil(base) + int(base - t->array); + maybesetaboundary(t, boundary); + return boundary; + } + else + { + // validate boundary invariant + LUAU_ASSERT(t->node == dummynode || ttisnil(luaH_getnum(t, j + 1))); + return j; + } +} + +LuaTable* luaH_clone(lua_State* L, LuaTable* tt) +{ + LuaTable* t = luaM_newgco(L, LuaTable, sizeof(LuaTable), L->activememcat, LUA_TTABLE); + luaC_init(L, t, LUA_TTABLE); + t->metatable = tt->metatable; + t->tmcache = tt->tmcache; + t->array = NULL; + t->sizearray = 0; + t->lsizenode = 0; + t->nodemask8 = 0; + t->readonly = 0; + t->safeenv = 0; + t->node = cast_to(LuaNode*, dummynode); + t->lastfree = 0; + + if (tt->sizearray) + { + t->array = luaM_newarray(L, tt->sizearray, TValue, t->memcat); + maybesetaboundary(t, getaboundary(tt)); + t->sizearray = tt->sizearray; + + memcpy(t->array, tt->array, t->sizearray * sizeof(TValue)); + } + + if (tt->node != dummynode) + { + int size = 1 << tt->lsizenode; + t->node = luaM_newarray(L, size, LuaNode, t->memcat); + t->lsizenode = tt->lsizenode; + t->nodemask8 = tt->nodemask8; + memcpy(t->node, tt->node, size * sizeof(LuaNode)); + t->lastfree = tt->lastfree; + } + + return t; +} + +void luaH_clear(LuaTable* tt) +{ + // clear array part + for (int i = 0; i < tt->sizearray; ++i) + { + setnilvalue(&tt->array[i]); + } + + maybesetaboundary(tt, 0); + + // clear hash part + if (tt->node != dummynode) + { + int size = sizenode(tt); + tt->lastfree = size; + for (int i = 0; i < size; ++i) + { + LuaNode* n = gnode(tt, i); + setnilvalue(gkey(n)); + setnilvalue(gval(n)); + gnext(n) = 0; + } + } + + // back to empty -> no tag methods present + tt->tmcache = cast_byte(~0); +} + +void luaH_setreadonly(lua_State* L, LuaTable* t, bool readonly) +{ + if (luaH_getreadonly(t) == (readonly ? 1 : 0)) + return; + + if (readonly) + { + t->readonly = 1; + + // check if this table is likely to be used as a metatable + if (t->node == dummynode) + return; + + // collect all metamethods + TValue metamethods[TM_N]; + bool found = false; + uint8_t tmcache = 0; + + for (int i = 0; i < TM_N; i++) + { + const TValue* val = luaH_getstr(t, L->global->tmname[i]); + setobj(L, &metamethods[TM_N - (i + 1)], val); // metamethods will be stored in reverse + bool hasValue = !ttisnil(val); + found |= hasValue; + + if (i <= TM_EQ && !hasValue) + tmcache |= cast_byte(1u << i); + } + + // update the metamethod presence cache + t->tmcache = tmcache; + + // if there are metamethods, place the metamethod cache before the array elements + if (found) + { + if (DFFlag::LuauTableRobustOom) + { + // if allocation fails, the optimization is skipped instead of erroring as an error might be unexpected from a table freeze + if (TValue* newarray = luaM_trynewarray(L, t->sizearray + TM_N, TValue, t->memcat)) + { + TValue* oldarray = t->array; + + if (t->sizearray != 0) + memcpy(newarray + TM_N, oldarray, t->sizearray * sizeof(TValue)); + + memcpy(newarray, metamethods, sizeof(metamethods)); + t->array = newarray + TM_N; + t->readonly |= 2; + + luaM_freearray(L, oldarray, t->sizearray, TValue, t->memcat); + } + } + else + { + TValue* array = t->array; + luaM_reallocarray(L, array, t->sizearray, t->sizearray + TM_N, TValue, t->memcat); + memmove(array + TM_N, array, t->sizearray * sizeof(TValue)); + memcpy(array, metamethods, sizeof(metamethods)); + t->array = array + TM_N; + t->readonly |= 2; + } + } + } + else + { + // if we have a metamethod cache, transform the array part back to the original layout (or delete when sizearray is 0) + if (hasmetacache(t)) + { + TValue* oldarray = t->array; + TValue* newarray = nullptr; + + if (t->sizearray != 0) + { + newarray = luaM_newarray(L, t->sizearray, TValue, t->memcat); + memcpy(newarray, oldarray, t->sizearray * sizeof(TValue)); + } + + t->array = newarray; + luaM_freearray(L, oldarray - TM_N, t->sizearray + TM_N, TValue, t->memcat); + } + + t->readonly = 0; + } +} + +int luaH_getreadonly(LuaTable* t) +{ + return t->readonly != 0 ? 1 : 0; +} diff --git a/External/luau/VM/src/ltable.h b/External/luau/VM/src/ltable.h new file mode 100644 index 00000000..06a2c898 --- /dev/null +++ b/External/luau/VM/src/ltable.h @@ -0,0 +1,39 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +#define gnode(t, i) (&(t)->node[i]) +#define gkey(n) (&(n)->key) +#define gval(n) (&(n)->val) +#define gnext(n) ((n)->key.next) + +#define gval2slot(t, v) int(cast_to(LuaNode*, static_cast(v)) - t->node) + +// reset cache of absent metamethods, cache is updated in luaT_gettm +#define invalidateTMcache(t) t->tmcache = 0 + +LUAI_FUNC const TValue* luaH_getnum(LuaTable* t, int key); +LUAI_FUNC TValue* luaH_setnum(lua_State* L, LuaTable* t, int key); +LUAI_FUNC const TValue* luaH_getstr(LuaTable* t, TString* key); +LUAI_FUNC TValue* luaH_setstr(lua_State* L, LuaTable* t, TString* key); +LUAI_FUNC const TValue* luaH_getp(LuaTable* t, void* key, int tag); +LUAI_FUNC TValue* luaH_setp(lua_State* L, LuaTable* t, void* key, int tag); +LUAI_FUNC const TValue* luaH_get(LuaTable* t, const TValue* key); +LUAI_FUNC TValue* luaH_set(lua_State* L, LuaTable* t, const TValue* key); +LUAI_FUNC TValue* luaH_newkey(lua_State* L, LuaTable* t, const TValue* key); +LUAI_FUNC LuaTable* luaH_new(lua_State* L, int narray, int lnhash); +LUAI_FUNC void luaH_resizearray(lua_State* L, LuaTable* t, int nasize); +LUAI_FUNC void luaH_resizehash(lua_State* L, LuaTable* t, int nhsize); +LUAI_FUNC void luaH_free(lua_State* L, LuaTable* t, struct lua_Page* page); +LUAI_FUNC int luaH_next(lua_State* L, LuaTable* t, StkId key); +LUAI_FUNC int luaH_getn(LuaTable* t); +LUAI_FUNC LuaTable* luaH_clone(lua_State* L, LuaTable* tt); +LUAI_FUNC void luaH_clear(LuaTable* tt); +LUAI_FUNC void luaH_setreadonly(lua_State* L, LuaTable* t, bool readonly); +LUAI_FUNC int luaH_getreadonly(LuaTable* t); + +#define luaH_setslot(L, t, slot, key) (invalidateTMcache(t), (slot == luaO_nilobject ? luaH_newkey(L, t, key) : cast_to(TValue*, slot))) + +extern const LuaNode luaH_dummynode; diff --git a/External/luau/VM/src/ltablib.cpp b/External/luau/VM/src/ltablib.cpp new file mode 100644 index 00000000..25f29791 --- /dev/null +++ b/External/luau/VM/src/ltablib.cpp @@ -0,0 +1,703 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lapi.h" +#include "lnumutils.h" +#include "lstate.h" +#include "ltable.h" +#include "lstring.h" +#include "lgc.h" +#include "ldebug.h" +#include "lvm.h" + +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauTableMoveTimeoutFix, false) + +static int foreachi(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, 2, LUA_TFUNCTION); + int i; + int n = lua_objlen(L, 1); + for (i = 1; i <= n; i++) + { + lua_pushvalue(L, 2); // function + lua_pushinteger(L, i); // 1st argument + lua_rawgeti(L, 1, i); // 2nd argument + lua_call(L, 2, 1); + if (!lua_isnil(L, -1)) + return 1; + lua_pop(L, 1); // remove nil result + } + return 0; +} + +static int foreach (lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_pushnil(L); // first key + while (lua_next(L, 1)) + { + lua_pushvalue(L, 2); // function + lua_pushvalue(L, -3); // key + lua_pushvalue(L, -3); // value + lua_call(L, 2, 1); + if (!lua_isnil(L, -1)) + return 1; + lua_pop(L, 2); // remove value and result + } + return 0; +} + +static int maxn(lua_State* L) +{ + double max = 0; + luaL_checktype(L, 1, LUA_TTABLE); + + LuaTable* t = hvalue(L->base); + + for (int i = 0; i < t->sizearray; i++) + { + if (!ttisnil(&t->array[i])) + max = i + 1; + } + + for (int i = 0; i < sizenode(t); i++) + { + LuaNode* n = gnode(t, i); + + if (!ttisnil(gval(n)) && ttisnumber(gkey(n))) + { + double v = nvalue(gkey(n)); + + if (v > max) + max = v; + } + } + + lua_pushnumber(L, max); + return 1; +} + +static int getn(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushinteger(L, lua_objlen(L, 1)); + return 1; +} + +static bool shouldsparsemove(LuaTable* src, LuaTable* dst, int n) +{ + int srcelems = src->sizearray + sizenode(src); + int dstelems = dst->sizearray + sizenode(dst); + int maxelems = srcelems > dstelems ? srcelems : dstelems; + const int minsparsemoveelems = 32; + + return n > minsparsemoveelems && n / 2 > maxelems; +} + +static bool tovalidintkey(lua_State* L, int idx, int f, int e, int* result) +{ + if (lua_type(L, idx) == LUA_TNUMBER) + { + double nkey = lua_tonumber(L, idx); + + if (nkey >= f && nkey <= e) + { + luai_num2int(*result, nkey); + return luai_numeq(cast_num(*result), nkey); + } + } + + return false; +} + +static void moveelements(lua_State* L, int srct, int dstt, int f, int e, int t, bool sparsemove) +{ + LuaTable* src = hvalue(L->base + (srct - 1)); + LuaTable* dst = hvalue(L->base + (dstt - 1)); + + if (dst->readonly) + luaG_readonlyerror(L); + + int n = e - f + 1; // number of elements to move + + if (unsigned(f) - 1 < unsigned(src->sizearray) && unsigned(t) - 1 < unsigned(dst->sizearray) && + unsigned(f) - 1 + unsigned(n) <= unsigned(src->sizearray) && unsigned(t) - 1 + unsigned(n) <= unsigned(dst->sizearray)) + { + TValue* srcarray = src->array; + TValue* dstarray = dst->array; + + if (t > e || t <= f || (dstt != srct && dst != src)) + { + for (int i = 0; i < n; ++i) + { + TValue* s = &srcarray[f + i - 1]; + TValue* d = &dstarray[t + i - 1]; + setobj2t(L, d, s); + } + } + else + { + for (int i = n - 1; i >= 0; i--) + { + TValue* s = &srcarray[(f + i) - 1]; + TValue* d = &dstarray[(t + i) - 1]; + setobj2t(L, d, s); + } + } + + luaC_barrierfast(L, dst); + } + else if (DFFlag::LuauTableMoveTimeoutFix && sparsemove) + { + int srcta = lua_absindex(L, srct); + int dstta = lua_absindex(L, dstt); + + int te = t + (n - 1); + + // temporary table to hold elements from source that are being moved + lua_newtable(L); + + // collect integer key elements that are being moved + for (int iter = 0; (iter = lua_rawiter(L, srcta, iter)) != -1;) + { + int ikey = 0; + if (tovalidintkey(L, -2, f, e, &ikey)) + lua_rawseti(L, -3, ikey); // pops value + else + lua_pop(L, 1); // pop value + + lua_pop(L, 1); // pop key + } + + // clear the destination range + for (int iter = 0; (iter = lua_rawiter(L, dstta, iter)) != -1;) + { + int ikey = 0; + if (tovalidintkey(L, -2, t, te, &ikey)) + { + lua_pushnil(L); + lua_rawseti(L, dstta, ikey); + } + + lua_pop(L, 2); + } + + // copy the elements from the temporary table to the destination table + for (int iter = 0; (iter = lua_rawiter(L, -1, iter)) != -1;) + { + int ikey = lua_tointeger(L, -2); + lua_rawseti(L, dstta, ikey - f + t); + lua_pop(L, 1); + } + + lua_pop(L, 1); + } + else + { + if (t > e || t <= f || dst != src) + { + for (int i = 0; i < n; ++i) + { + lua_rawgeti(L, srct, f + i); + lua_rawseti(L, dstt, t + i); + } + } + else + { + for (int i = n - 1; i >= 0; i--) + { + lua_rawgeti(L, srct, f + i); + lua_rawseti(L, dstt, t + i); + } + } + } +} + +static int tinsert(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + int n = lua_objlen(L, 1); + int pos; // where to insert new element + switch (lua_gettop(L)) + { + case 2: + { // called with only 2 arguments + pos = n + 1; // insert new element at the end + break; + } + case 3: + { + pos = luaL_checkinteger(L, 2); // 2nd argument is the position + + // move up elements if necessary + if (1 <= pos && pos <= n) + moveelements(L, 1, 1, pos, n, pos + 1, /* sparsemove */ false); + break; + } + default: + { + luaL_error(L, "wrong number of arguments to 'insert'"); + } + } + lua_rawseti(L, 1, pos); // t[pos] = v + return 0; +} + +static int tremove(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + int n = lua_objlen(L, 1); + int pos = luaL_optinteger(L, 2, n); + + if (!(1 <= pos && pos <= n)) // position is outside bounds? + return 0; // nothing to remove + lua_rawgeti(L, 1, pos); // result = t[pos] + + moveelements(L, 1, 1, pos + 1, n, pos, /* sparsemove */ false); + + lua_pushnil(L); + lua_rawseti(L, 1, n); // t[n] = nil + return 1; +} + +/* +** Copy elements (1[f], ..., 1[e]) into (tt[t], tt[t+1], ...). Whenever +** possible, copy in increasing order, which is better for rehashing. +** "possible" means destination after original range, or smaller +** than origin, or copying to another table. +*/ +static int tmove(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + int f = luaL_checkinteger(L, 2); + int e = luaL_checkinteger(L, 3); + int t = luaL_checkinteger(L, 4); + int tt = !lua_isnoneornil(L, 5) ? 5 : 1; // destination table + luaL_checktype(L, tt, LUA_TTABLE); + + if (e >= f) + { // otherwise, nothing to move + luaL_argcheck(L, f > 0 || e < INT_MAX + f, 3, "too many elements to move"); + int n = e - f + 1; // number of elements to move + luaL_argcheck(L, t <= INT_MAX - n + 1, 4, "destination wrap around"); + + LuaTable* dst = hvalue(L->base + (tt - 1)); + + if (dst->readonly) // also checked in moveelements, but this blocks resizes of r/o tables + luaG_readonlyerror(L); + + bool sparsemove = DFFlag::LuauTableMoveTimeoutFix && shouldsparsemove(hvalue(L->base), dst, n); + + if (t > 0 && (t - 1) <= dst->sizearray && (t - 1 + n) > dst->sizearray) + { // grow the destination table array + luaH_resizearray(L, dst, t - 1 + n); + } + + moveelements(L, 1, tt, f, e, t, sparsemove); + } + lua_pushvalue(L, tt); // return destination table + return 1; +} + +static void addfield(lua_State* L, luaL_Strbuf* b, int i, LuaTable* t) +{ + if (t && unsigned(i - 1) < unsigned(t->sizearray) && ttisstring(&t->array[i - 1])) + { + TString* ts = tsvalue(&t->array[i - 1]); + luaL_addlstring(b, getstr(ts), ts->len); + } + else + { + int tt = lua_rawgeti(L, 1, i); + if (tt != LUA_TSTRING && tt != LUA_TNUMBER) + luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", luaL_typename(L, -1), i); + luaL_addvalue(b); + } +} + +static int tconcat(lua_State* L) +{ + size_t lsep; + const char* sep = luaL_optlstring(L, 2, "", &lsep); + luaL_checktype(L, 1, LUA_TTABLE); + int i = luaL_optinteger(L, 3, 1); + int last = luaL_opt(L, luaL_checkinteger, 4, lua_objlen(L, 1)); + + LuaTable* t = hvalue(L->base); + + luaL_Strbuf b; + luaL_buffinit(L, &b); + for (; i < last; i++) + { + addfield(L, &b, i, t); + if (lsep != 0) + luaL_addlstring(&b, sep, lsep); + } + if (i == last) // add last value (if interval was not empty) + addfield(L, &b, i, t); + luaL_pushresult(&b); + return 1; +} + +static int tpack(lua_State* L) +{ + int n = lua_gettop(L); // number of elements to pack + lua_createtable(L, n, 1); // create result table + + LuaTable* t = hvalue(L->top - 1); + + for (int i = 0; i < n; ++i) + { + TValue* e = &t->array[i]; + setobj2t(L, e, L->base + i); + } + + // t.n = number of elements + TValue* nv = luaH_setstr(L, t, luaS_newliteral(L, "n")); + setnvalue(nv, n); + + return 1; // return table +} + +static int tunpack(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + LuaTable* t = hvalue(L->base); + + int i = luaL_optinteger(L, 2, 1); + int e = luaL_opt(L, luaL_checkinteger, 3, lua_objlen(L, 1)); + if (i > e) + return 0; // empty range + unsigned n = (unsigned)e - i; // number of elements minus 1 (avoid overflows) + if (n >= (unsigned int)INT_MAX || !lua_checkstack(L, (int)(++n))) + luaL_error(L, "too many results to unpack"); + + // fast-path: direct array-to-stack copy + if (i == 1 && int(n) <= t->sizearray) + { + for (i = 0; i < int(n); i++) + setobj2s(L, L->top + i, &t->array[i]); + L->top += n; + } + else + { + // push arg[i..e - 1] (to avoid overflows) + for (; i < e; i++) + lua_rawgeti(L, 1, i); + lua_rawgeti(L, 1, e); // push last element + } + return (int)n; +} + +typedef int (*SortPredicate)(lua_State* L, const TValue* l, const TValue* r); + +static int sort_func(lua_State* L, const TValue* l, const TValue* r) +{ + LUAU_ASSERT(L->top == L->base + 2); // table, function + + setobj2s(L, L->top, &L->base[1]); + setobj2s(L, L->top + 1, l); + setobj2s(L, L->top + 2, r); + L->top += 3; // safe because of LUA_MINSTACK guarantee + luaD_call(L, L->top - 3, 1); + L->top -= 1; // maintain stack depth + + return !l_isfalse(L->top); +} + +inline void sort_swap(lua_State* L, LuaTable* t, int i, int j) +{ + TValue* arr = t->array; + int n = t->sizearray; + LUAU_ASSERT(unsigned(i) < unsigned(n) && unsigned(j) < unsigned(n)); // contract maintained in sort_less after predicate call + + // no barrier required because both elements are in the array before and after the swap + TValue temp; + setobj2s(L, &temp, &arr[i]); + setobj2t(L, &arr[i], &arr[j]); + setobj2t(L, &arr[j], &temp); +} + +inline int sort_less(lua_State* L, LuaTable* t, int i, int j, SortPredicate pred) +{ + TValue* arr = t->array; + int n = t->sizearray; + LUAU_ASSERT(unsigned(i) < unsigned(n) && unsigned(j) < unsigned(n)); // contract maintained in sort_less after predicate call + + int res = pred(L, &arr[i], &arr[j]); + + // predicate call may resize the table, which is invalid + if (t->sizearray != n) + luaL_error(L, "table modified during sorting"); + + return res; +} + +static void sort_siftheap(lua_State* L, LuaTable* t, int l, int u, SortPredicate pred, int root) +{ + LUAU_ASSERT(l <= u); + int count = u - l + 1; + + // process all elements with two children + while (root * 2 + 2 < count) + { + int left = root * 2 + 1, right = root * 2 + 2; + int next = root; + next = sort_less(L, t, l + next, l + left, pred) ? left : next; + next = sort_less(L, t, l + next, l + right, pred) ? right : next; + + if (next == root) + break; + + sort_swap(L, t, l + root, l + next); + root = next; + } + + // process last element if it has just one child + int lastleft = root * 2 + 1; + if (lastleft == count - 1 && sort_less(L, t, l + root, l + lastleft, pred)) + sort_swap(L, t, l + root, l + lastleft); +} + +static void sort_heap(lua_State* L, LuaTable* t, int l, int u, SortPredicate pred) +{ + LUAU_ASSERT(l <= u); + int count = u - l + 1; + + for (int i = count / 2 - 1; i >= 0; --i) + sort_siftheap(L, t, l, u, pred, i); + + for (int i = count - 1; i > 0; --i) + { + sort_swap(L, t, l, l + i); + sort_siftheap(L, t, l, l + i - 1, pred, 0); + } +} + +static void sort_rec(lua_State* L, LuaTable* t, int l, int u, int limit, SortPredicate pred) +{ + // sort range [l..u] (inclusive, 0-based) + while (l < u) + { + // if the limit has been reached, quick sort is going over the permitted nlogn complexity, so we fall back to heap sort + if (limit == 0) + return sort_heap(L, t, l, u, pred); + + // sort elements a[l], a[(l+u)/2] and a[u] + // note: this simultaneously acts as a small sort and a median selector + if (sort_less(L, t, u, l, pred)) // a[u] < a[l]? + sort_swap(L, t, u, l); // swap a[l] - a[u] + if (u - l == 1) + break; // only 2 elements + int m = l + ((u - l) >> 1); // midpoint + if (sort_less(L, t, m, l, pred)) // a[m]= P + while (sort_less(L, t, ++i, p, pred)) + { + if (i >= u) + luaL_error(L, "invalid order function for sorting"); + } + // repeat --j until a[j] <= P + while (sort_less(L, t, p, --j, pred)) + { + if (j <= l) + luaL_error(L, "invalid order function for sorting"); + } + if (j < i) + break; + sort_swap(L, t, i, j); + } + + // swap pivot a[p] with a[i], which is the new midpoint + sort_swap(L, t, p, i); + + // adjust limit to allow 1.5 log2N recursive steps + limit = (limit >> 1) + (limit >> 2); + + // a[l..i-1] <= a[i] == P <= a[i+1..u] + // sort smaller half recursively; the larger half is sorted in the next loop iteration + if (i - l < u - i) + { + sort_rec(L, t, l, i - 1, limit, pred); + l = i + 1; + } + else + { + sort_rec(L, t, i + 1, u, limit, pred); + u = i - 1; + } + } +} + +static int tsort(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + LuaTable* t = hvalue(L->base); + int n = luaH_getn(t); + if (t->readonly) + luaG_readonlyerror(L); + + SortPredicate pred = luaV_lessthan; + if (!lua_isnoneornil(L, 2)) // is there a 2nd argument? + { + luaL_checktype(L, 2, LUA_TFUNCTION); + pred = sort_func; + } + lua_settop(L, 2); // make sure there are two arguments + + if (n > 0) + sort_rec(L, t, 0, n - 1, n, pred); + return 0; +} + +static int tcreate(lua_State* L) +{ + int size = luaL_checkinteger(L, 1); + if (size < 0) + luaL_argerror(L, 1, "size out of range"); + + if (!lua_isnoneornil(L, 2)) + { + lua_createtable(L, size, 0); + LuaTable* t = hvalue(L->top - 1); + + StkId v = L->base + 1; + + for (int i = 0; i < size; ++i) + { + TValue* e = &t->array[i]; + setobj2t(L, e, v); + } + } + else + { + lua_createtable(L, size, 0); + } + + return 1; +} + +static int tfind(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + int init = luaL_optinteger(L, 3, 1); + if (init < 1) + luaL_argerror(L, 3, "index out of range"); + + LuaTable* t = hvalue(L->base); + + for (int i = init;; ++i) + { + const TValue* e = luaH_getnum(t, i); + if (ttisnil(e)) + break; + + StkId v = L->base + 1; + + if (equalobj(L, v, e)) + { + lua_pushinteger(L, i); + return 1; + } + } + + lua_pushnil(L); + return 1; +} + +static int tclear(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + + LuaTable* tt = hvalue(L->base); + if (tt->readonly) + luaG_readonlyerror(L); + + luaH_clear(tt); + return 0; +} + +static int tfreeze(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argcheck(L, !lua_getreadonly(L, 1), 1, "table is already frozen"); + luaL_argcheck(L, !luaL_getmetafield(L, 1, "__metatable"), 1, "table has a protected metatable"); + + lua_setreadonly(L, 1, true); + + lua_pushvalue(L, 1); + return 1; +} + +static int tisfrozen(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + + lua_pushboolean(L, lua_getreadonly(L, 1)); + return 1; +} + +static int tclone(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argcheck(L, !luaL_getmetafield(L, 1, "__metatable"), 1, "table has a protected metatable"); + + LuaTable* tt = luaH_clone(L, hvalue(L->base)); + + TValue v; + sethvalue(L, &v, tt); + luaA_pushvalue(L, &v); + + return 1; +} + +static const luaL_Reg tab_funcs[] = { + {"concat", tconcat}, + {"foreach", foreach}, + {"foreachi", foreachi}, + {"getn", getn}, + {"maxn", maxn}, + {"insert", tinsert}, + {"remove", tremove}, + {"sort", tsort}, + {"pack", tpack}, + {"unpack", tunpack}, + {"move", tmove}, + {"create", tcreate}, + {"find", tfind}, + {"clear", tclear}, + {"freeze", tfreeze}, + {"isfrozen", tisfrozen}, + {"clone", tclone}, + {NULL, NULL}, +}; + +int luaopen_table(lua_State* L) +{ + luaL_register(L, LUA_TABLIBNAME, tab_funcs); + + // Lua 5.1 compat + lua_pushcfunction(L, tunpack, "unpack"); + lua_setglobal(L, "unpack"); + + return 1; +} diff --git a/External/luau/VM/src/ltm.cpp b/External/luau/VM/src/ltm.cpp new file mode 100644 index 00000000..e75b50bf --- /dev/null +++ b/External/luau/VM/src/ltm.cpp @@ -0,0 +1,188 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "ltm.h" + +#include "lfunc.h" +#include "lstate.h" +#include "lstring.h" +#include "lua.h" +#include "ludata.h" +#include "ltable.h" +#include "lgc.h" +#include "lclass.h" + +#include + +LUAU_FASTFLAG(LuauFrozenMetaButterfly) + +// clang-format off +const char* const luaT_typenames[] = { + // ORDER TYPE + "nil", + "boolean", + + "userdata", + "number", + "integer", + +#if LUA_VECTOR_DOUBLE == 0 + "vector", +#endif + + "string", + + "table", + "function", + "userdata", + "thread", + "buffer", + "class", + "object", + +#if LUA_VECTOR_DOUBLE == 1 + "vector", +#endif +}; + +const char* const luaT_eventname[] = { + // ORDER TM + "__index", + "__newindex", + "__mode", + "__namecall", + "__call", + "__iter", + "__len", + + "__eq", + + "__add", + "__sub", + "__mul", + "__div", + "__idiv", + "__mod", + "__pow", + "__unm", + + "__lt", + "__le", + "__concat", + "__type", + "__metatable", +}; +// clang-format on + +static_assert(sizeof(luaT_typenames) / sizeof(luaT_typenames[0]) == LUA_T_COUNT, "luaT_typenames size mismatch"); +static_assert(sizeof(luaT_eventname) / sizeof(luaT_eventname[0]) == TM_N, "luaT_eventname size mismatch"); +static_assert(TM_EQ < 8, "fasttm optimization stores a bitfield with metamethods in a byte"); + +void luaT_init(lua_State* L) +{ + int i; + for (i = 0; i < LUA_T_COUNT; i++) + { + L->global->ttname[i] = luaS_new(L, luaT_typenames[i]); + luaS_fix(L->global->ttname[i]); // never collect these names + } + for (i = 0; i < TM_N; i++) + { + L->global->tmname[i] = luaS_new(L, luaT_eventname[i]); + luaS_fix(L->global->tmname[i]); // never collect these names + } +} + +/* +** function to be used with macro "fasttm": optimized for absence of +** tag methods. +*/ +const TValue* luaT_gettm(LuaTable* events, TMS event, TString* ename) +{ + const TValue* tm = luaH_getstr(events, ename); + LUAU_ASSERT(event <= TM_EQ); + if (ttisnil(tm)) + { // no tag method? + events->tmcache |= cast_byte(1u << event); // cache this fact + return NULL; + } + else + return tm; +} + +const TValue* luaT_gettmbyobj(lua_State* L, const TValue* o, TMS event) +{ + /* + NB: Tag-methods were replaced by meta-methods in Lua 5.0, but the + old names are still around (this function, for example). + */ + LuaTable* mt; + switch (ttype(o)) + { + case LUA_TTABLE: + mt = hvalue(o)->metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(o)->metatable; + break; + case LUA_TCLASS: + { + // We store a metatable for class objects on the + // class object itself, use that. + mt = classvalue(o)->metatable; + break; + } + case LUA_TOBJECT: + mt = objectvalue(o)->lclass->instancemetatable; + break; + default: + mt = L->global->mt[ttype(o)]; + } + + if (FFlag::LuauFrozenMetaButterfly && mt && hasmetacache(mt)) + return getmetacache(mt, event); + + return (mt ? luaH_getstr(mt, L->global->tmname[event]) : luaO_nilobject); +} + +const TString* luaT_objtypenamestr(lua_State* L, const TValue* o) +{ + // Userdata created by the environment can have a custom type name set in the individual metatable + // If there is no custom name, 'userdata' is returned + if (ttisuserdata(o) && uvalue(o)->tag != UTAG_PROXY && uvalue(o)->metatable) + { + const TValue* type = luaH_getstr(uvalue(o)->metatable, L->global->tmname[TM_TYPE]); + + if (ttisstring(type)) + return tsvalue(type); + + return L->global->ttname[ttype(o)]; + } + + // Tagged lightuserdata can be named using lua_setlightuserdataname + if (ttislightuserdata(o)) + { + int tag = lightuserdatatag(o); + + if (unsigned(tag) < LUA_LUTAG_LIMIT) + { + if (const TString* name = L->global->lightuserdataname[tag]) + return name; + } + } + + // For all types except userdata and table, a global metatable can be set with a global name override + if (LuaTable* mt = L->global->mt[ttype(o)]) + { + const TValue* type = luaH_getstr(mt, L->global->tmname[TM_TYPE]); + + if (ttisstring(type)) + return tsvalue(type); + } + + return L->global->ttname[ttype(o)]; +} + +const char* luaT_objtypename(lua_State* L, const TValue* o) +{ + return getstr(luaT_objtypenamestr(L, o)); +} diff --git a/External/luau/VM/src/ltm.h b/External/luau/VM/src/ltm.h new file mode 100644 index 00000000..21ca5801 --- /dev/null +++ b/External/luau/VM/src/ltm.h @@ -0,0 +1,63 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +LUAU_FASTFLAG(LuauFrozenMetaButterfly) + +/* + * WARNING: if you change the order of this enumeration, + * grep "ORDER TM" + */ +// clang-format off +typedef enum +{ + TM_INDEX, + TM_NEWINDEX, + TM_MODE, + TM_NAMECALL, + TM_CALL, + TM_ITER, + TM_LEN, + + TM_EQ, // last tag method with `fast' access + + TM_ADD, + TM_SUB, + TM_MUL, + TM_DIV, + TM_IDIV, + TM_MOD, + TM_POW, + TM_UNM, + + TM_LT, + TM_LE, + TM_CONCAT, + TM_TYPE, + TM_METATABLE, + + TM_N // number of elements in the enum +} TMS; +// clang-format on + +#define gfasttm(g, et, e) \ + ((et) == NULL ? NULL \ + : ((et)->tmcache & (1u << (e))) \ + ? NULL \ + : (FFlag::LuauFrozenMetaButterfly && hasmetacache(et) ? getmetacache(et, e) : luaT_gettm(et, e, (g)->tmname[e]))) + +#define fasttm(l, et, e) gfasttm(l->global, et, e) +#define fastnotm(et, e) ((et) == NULL || ((et)->tmcache & (1u << (e)))) + +LUAI_DATA const char* const luaT_typenames[]; +LUAI_DATA const char* const luaT_eventname[]; + +LUAI_FUNC const TValue* luaT_gettm(LuaTable* events, TMS event, TString* ename); +LUAI_FUNC const TValue* luaT_gettmbyobj(lua_State* L, const TValue* o, TMS event); + +LUAI_FUNC const TString* luaT_objtypenamestr(lua_State* L, const TValue* o); +LUAI_FUNC const char* luaT_objtypename(lua_State* L, const TValue* o); + +LUAI_FUNC void luaT_init(lua_State* L); diff --git a/External/luau/VM/src/ludata.cpp b/External/luau/VM/src/ludata.cpp new file mode 100644 index 00000000..74783032 --- /dev/null +++ b/External/luau/VM/src/ludata.cpp @@ -0,0 +1,38 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "ludata.h" + +#include "lgc.h" +#include "lmem.h" + +#include + +Udata* luaU_newudata(lua_State* L, size_t s, int tag) +{ + if (s > INT_MAX - sizeof(Udata)) + luaM_toobig(L); + Udata* u = luaM_newgcoudata(L, Udata, sizeudata(s), L->activememcat, LUA_TUSERDATA, tag); + luaC_init(L, u, LUA_TUSERDATA); + u->len = int(s); + u->metatable = NULL; + LUAU_ASSERT(tag >= 0 && tag <= 255); + u->tag = uint8_t(tag); + return u; +} + +void luaU_freeudata(lua_State* L, Udata* u, lua_Page* page) +{ + lua_Destructor dtor = nullptr; + + if (u->tag < LUA_UTAG_LIMIT) + dtor = L->global->udatagc[u->tag]; + else if (u->tag == UTAG_IDTOR) + memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor)); + + // TODO: access to L here is highly unsafe since this is called during internal GC traversal + // certain operations such as lua_getthreaddata are okay, but by and large this risks crashes on improper use + if (dtor) + dtor(L, u->data); + + luaM_freegco(L, u, sizeudata(u->len), u->memcat, page); +} diff --git a/External/luau/VM/src/ludata.h b/External/luau/VM/src/ludata.h new file mode 100644 index 00000000..ed85b20b --- /dev/null +++ b/External/luau/VM/src/ludata.h @@ -0,0 +1,20 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" + +// special tag value is used for user data with inline dtors +#define UTAG_IDTOR LUA_UTAG_LIMIT + +// special tag value is used for newproxy-created user data (all other user data objects are host-exposed) +#define UTAG_PROXY (LUA_UTAG_LIMIT + 1) + +// must be updated if more internal tags are added +#define UTAG_INTERNAL_LIMIT (UTAG_PROXY + 1) + +// userdata larger than 16 bytes will be extended to guarantee 16 byte alignment of subsequent blocks +#define sizeudata(len) (offsetof(Udata, data) + (len > 16 ? ((len + 15) & ~15) : len)) + +LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag); +LUAI_FUNC void luaU_freeudata(lua_State* L, Udata* u, struct lua_Page* page); diff --git a/External/luau/VM/src/lutf8lib.cpp b/External/luau/VM/src/lutf8lib.cpp new file mode 100644 index 00000000..7af5b4e6 --- /dev/null +++ b/External/luau/VM/src/lutf8lib.cpp @@ -0,0 +1,296 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" + +#define MAXUNICODE 0x10FFFF + +#define iscont(p) ((*(p) & 0xC0) == 0x80) + +// from strlib +// translate a relative string position: negative means back from end +static int u_posrelat(int pos, size_t len) +{ + if (pos >= 0) + return pos; + else if (0u - (size_t)pos > len) + return 0; + else + return (int)len + pos + 1; +} + +/* +** Decode one UTF-8 sequence, returning NULL if byte sequence is invalid. +*/ +static const char* utf8_decode(const char* o, int* val) +{ + static const unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF}; + const unsigned char* s = (const unsigned char*)o; + unsigned int c = s[0]; + unsigned int res = 0; // final result + if (c < 0x80) // ascii? + res = c; + else + { + int count = 0; // to count number of continuation bytes + while (c & 0x40) + { // still have continuation bytes? + int cc = s[++count]; // read next byte + if ((cc & 0xC0) != 0x80) // not a continuation byte? + return NULL; // invalid byte sequence + res = (res << 6) | (cc & 0x3F); // add lower 6 bits from cont. byte + c <<= 1; // to test next bit + } + res |= ((c & 0x7F) << (count * 5)); // add first byte + if (count > 3 || res > MAXUNICODE || res <= limits[count]) + return NULL; // invalid byte sequence + if (unsigned(res - 0xD800) < 0x800) + return NULL; // surrogate + s += count; // skip continuation bytes read + } + if (val) + *val = res; + return (const char*)s + 1; // +1 to include first byte +} + +/* +** utf8len(s [, i [, j]]) --> number of characters that start in the +** range [i,j], or nil + current position if 's' is not well formed in +** that interval +*/ +static int utflen(lua_State* L) +{ + int n = 0; + size_t len; + const char* s = luaL_checklstring(L, 1, &len); + int posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + int posj = u_posrelat(luaL_optinteger(L, 3, -1), len); + luaL_argcheck(L, 1 <= posi && --posi <= (int)len, 2, "initial position out of string"); + luaL_argcheck(L, --posj < (int)len, 3, "final position out of string"); + while (posi <= posj) + { + const char* s1 = utf8_decode(s + posi, NULL); + if (s1 == NULL) + { // conversion error? + lua_pushnil(L); // return nil ... + lua_pushinteger(L, posi + 1); // ... and current position + return 2; + } + posi = (int)(s1 - s); + n++; + } + lua_pushinteger(L, n); + return 1; +} + +/* +** codepoint(s, [i, [j]]) -> returns codepoints for all characters +** that start in the range [i,j] +*/ +static int codepoint(lua_State* L) +{ + size_t len; + const char* s = luaL_checklstring(L, 1, &len); + int posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + int pose = u_posrelat(luaL_optinteger(L, 3, posi), len); + int n; + const char* se; + luaL_argcheck(L, posi >= 1, 2, "out of range"); + luaL_argcheck(L, pose <= (int)len, 3, "out of range"); + if (posi > pose) + return 0; // empty interval; return no values + if (pose - posi >= INT_MAX) // (int -> int) overflow? + luaL_error(L, "string slice too long"); + n = (int)(pose - posi) + 1; + luaL_checkstack(L, n, "string slice too long"); + n = 0; + se = s + pose; + for (s += posi - 1; s < se;) + { + int code; + s = utf8_decode(s, &code); + if (s == NULL) + luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, code); + n++; + } + return n; +} + +// from Lua 5.3 lobject.h +#define UTF8BUFFSZ 8 + +// from Lua 5.3 lobject.c, copied verbatim + static +static int luaO_utf8esc(char* buff, unsigned long x) +{ + int n = 1; // number of bytes put in buffer (backwards) + LUAU_ASSERT(x <= 0x10FFFF); + if (x < 0x80) // ascii? + buff[UTF8BUFFSZ - 1] = cast_to(char, x); + else + { // need continuation bytes + unsigned int mfb = 0x3f; // maximum that fits in first byte + do + { // add continuation bytes + buff[UTF8BUFFSZ - (n++)] = cast_to(char, 0x80 | (x & 0x3f)); + x >>= 6; // remove added bits + mfb >>= 1; // now there is one less bit available in first byte + } while (x > mfb); // still needs continuation byte? + buff[UTF8BUFFSZ - n] = cast_to(char, (~mfb << 1) | x); // add first byte + } + return n; +} + +// lighter replacement for pushutfchar; doesn't push any string onto the stack +static int buffutfchar(lua_State* L, int arg, char* buff, const char** charstr) +{ + int code = luaL_checkinteger(L, arg); + luaL_argcheck(L, 0 <= code && code <= MAXUNICODE, arg, "value out of range"); + int l = luaO_utf8esc(buff, cast_to(long, code)); + *charstr = buff + UTF8BUFFSZ - l; + return l; +} + +/* +** utfchar(n1, n2, ...) -> char(n1)..char(n2)... +** +** This version avoids the need to make more invasive upgrades elsewhere (like +** implementing the %U escape in lua_pushfstring) and avoids pushing string +** objects for each codepoint in the multi-argument case. -Jovanni +*/ +static int utfchar(lua_State* L) +{ + char buff[UTF8BUFFSZ]; + const char* charstr; + + int n = lua_gettop(L); // number of arguments + if (n == 1) + { // optimize common case of single char + int l = buffutfchar(L, 1, buff, &charstr); + lua_pushlstring(L, charstr, l); + } + else + { + luaL_Strbuf b; + luaL_buffinit(L, &b); + for (int i = 1; i <= n; i++) + { + int l = buffutfchar(L, i, buff, &charstr); + luaL_addlstring(&b, charstr, l); + } + luaL_pushresult(&b); + } + return 1; +} + +/* +** offset(s, n, [i]) -> index where n-th character counting from +** position 'i' starts; 0 means character at 'i'. +*/ +static int byteoffset(lua_State* L) +{ + size_t len; + const char* s = luaL_checklstring(L, 1, &len); + int n = luaL_checkinteger(L, 2); + int posi = (n >= 0) ? 1 : (int)len + 1; + posi = u_posrelat(luaL_optinteger(L, 3, posi), len); + luaL_argcheck(L, 1 <= posi && --posi <= (int)len, 3, "position out of range"); + if (n == 0) + { + // find beginning of current byte sequence + while (posi > 0 && iscont(s + posi)) + posi--; + } + else + { + if (iscont(s + posi)) + luaL_error(L, "initial position is a continuation byte"); + if (n < 0) + { + while (n < 0 && posi > 0) + { // move back + do + { // find beginning of previous character + posi--; + } while (posi > 0 && iscont(s + posi)); + n++; + } + } + else + { + n--; // do not move for 1st character + while (n > 0 && posi < (int)len) + { + do + { // find beginning of next character + posi++; + } while (iscont(s + posi)); // (cannot pass final '\0') + n--; + } + } + } + if (n == 0) // did it find given character? + lua_pushinteger(L, posi + 1); + else // no such character + lua_pushnil(L); + return 1; +} + +static int iter_aux(lua_State* L) +{ + size_t len; + const char* s = luaL_checklstring(L, 1, &len); + int n = lua_tointeger(L, 2) - 1; + if (n < 0) // first iteration? + n = 0; // start from here + else if (n < (int)len) + { + n++; // skip current byte + while (iscont(s + n)) + n++; // and its continuations + } + if (n >= (int)len) + return 0; // no more codepoints + else + { + int code; + const char* next = utf8_decode(s + n, &code); + if (next == NULL || iscont(next)) + luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, n + 1); + lua_pushinteger(L, code); + return 2; + } +} + +static int iter_codes(lua_State* L) +{ + luaL_checkstring(L, 1); + lua_pushcfunction(L, iter_aux, NULL); + lua_pushvalue(L, 1); + lua_pushinteger(L, 0); + return 3; +} + +// pattern to match a single UTF-8 character +#define UTF8PATT "[\0-\x7F\xC2-\xF4][\x80-\xBF]*" + +static const luaL_Reg funcs[] = { + {"offset", byteoffset}, + {"codepoint", codepoint}, + {"char", utfchar}, + {"len", utflen}, + {"codes", iter_codes}, + {NULL, NULL}, +}; + +int luaopen_utf8(lua_State* L) +{ + luaL_register(L, LUA_UTF8LIBNAME, funcs); + + lua_pushlstring(L, UTF8PATT, sizeof(UTF8PATT) / sizeof(char) - 1); + lua_setfield(L, -2, "charpattern"); + + return 1; +} diff --git a/External/luau/VM/src/lveclib.cpp b/External/luau/VM/src/lveclib.cpp new file mode 100644 index 00000000..39e2e0cb --- /dev/null +++ b/External/luau/VM/src/lveclib.cpp @@ -0,0 +1,357 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lualib.h" + +#include "lcommon.h" +#include "lnumutils.h" +#include "lobject.h" + +#include + +static int vector_create(lua_State* L) +{ + // checking argument count to avoid accepting 'nil' as a valid value + int count = lua_gettop(L); + + double x = luaL_checknumber(L, 1); + double y = luaL_checknumber(L, 2); + double z = count >= 3 ? luaL_checknumber(L, 3) : 0.0; + +#if LUA_VECTOR_SIZE == 4 + double w = count >= 4 ? luaL_checknumber(L, 4) : 0.0; + + lua_pushvector(L, LUA_VECTOR_TYPE(x), LUA_VECTOR_TYPE(y), LUA_VECTOR_TYPE(z), LUA_VECTOR_TYPE(w)); +#else + lua_pushvector(L, LUA_VECTOR_TYPE(x), LUA_VECTOR_TYPE(y), LUA_VECTOR_TYPE(z)); +#endif + + return 1; +} + +static int vector_magnitude(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + lua_pushnumber(L, luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3])); +#else + lua_pushnumber(L, luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])); +#endif + + return 1; +} + +static int vector_normalize(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + LUA_VECTOR_TYPE invSqrt = LUA_VECTOR_TYPE(1.0) / luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]); + + lua_pushvector(L, v[0] * invSqrt, v[1] * invSqrt, v[2] * invSqrt, v[3] * invSqrt); +#else + LUA_VECTOR_TYPE invSqrt = LUA_VECTOR_TYPE(1.0) / luai_sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + + lua_pushvector(L, v[0] * invSqrt, v[1] * invSqrt, v[2] * invSqrt); +#endif + + return 1; +} + +static int vector_cross(lua_State* L) +{ + const LUA_VECTOR_TYPE* a = luaL_checkvector(L, 1); + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, 2); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], 0.0); +#else + lua_pushvector(L, a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]); +#endif + + return 1; +} + +static int vector_dot(lua_State* L) +{ + const LUA_VECTOR_TYPE* a = luaL_checkvector(L, 1); + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, 2); + +#if LUA_VECTOR_SIZE == 4 + lua_pushnumber(L, a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); +#else + lua_pushnumber(L, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]); +#endif + + return 1; +} + +static int vector_angle(lua_State* L) +{ + const LUA_VECTOR_TYPE* a = luaL_checkvector(L, 1); + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, 2); + const LUA_VECTOR_TYPE* axis = luaL_optvector(L, 3, nullptr); + + // cross(a, b) + LUA_VECTOR_TYPE cross[] = {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; + + double sinA = sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]); + double cosA = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + double angle = atan2(sinA, cosA); + + if (axis) + { + if (cross[0] * axis[0] + cross[1] * axis[1] + cross[2] * axis[2] < 0.0) + angle = -angle; + } + + lua_pushnumber(L, angle); + return 1; +} + +static int vector_floor(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, luai_floor(v[0]), luai_floor(v[1]), luai_floor(v[2]), luai_floor(v[3])); +#else + lua_pushvector(L, luai_floor(v[0]), luai_floor(v[1]), luai_floor(v[2])); +#endif + + return 1; +} + +static int vector_ceil(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, luai_ceil(v[0]), luai_ceil(v[1]), luai_ceil(v[2]), luai_ceil(v[3])); +#else + lua_pushvector(L, luai_ceil(v[0]), luai_ceil(v[1]), luai_ceil(v[2])); +#endif + + return 1; +} + +static int vector_abs(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, luai_fabs(v[0]), luai_fabs(v[1]), luai_fabs(v[2]), luai_fabs(v[3])); +#else + lua_pushvector(L, luai_fabs(v[0]), luai_fabs(v[1]), luai_fabs(v[2])); +#endif + + return 1; +} + +static int vector_sign(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, luai_sign(v[0]), luai_sign(v[1]), luai_sign(v[2]), luai_sign(v[3])); +#else + lua_pushvector(L, luai_sign(v[0]), luai_sign(v[1]), luai_sign(v[2])); +#endif + + return 1; +} + +static int vector_clamp(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + const LUA_VECTOR_TYPE* min = luaL_checkvector(L, 2); + const LUA_VECTOR_TYPE* max = luaL_checkvector(L, 3); + + luaL_argcheck(L, min[0] <= max[0], 3, "max.x must be greater than or equal to min.x"); + luaL_argcheck(L, min[1] <= max[1], 3, "max.y must be greater than or equal to min.y"); + luaL_argcheck(L, min[2] <= max[2], 3, "max.z must be greater than or equal to min.z"); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector( + L, luai_clamp(v[0], min[0], max[0]), luai_clamp(v[1], min[1], max[1]), luai_clamp(v[2], min[2], max[2]), luai_clamp(v[3], min[3], max[3]) + ); +#else + lua_pushvector(L, luai_clamp(v[0], min[0], max[0]), luai_clamp(v[1], min[1], max[1]), luai_clamp(v[2], min[2], max[2])); +#endif + + return 1; +} + +static int vector_min(lua_State* L) +{ + int n = lua_gettop(L); + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + LUA_VECTOR_TYPE result[] = {v[0], v[1], v[2], v[3]}; +#else + LUA_VECTOR_TYPE result[] = {v[0], v[1], v[2]}; +#endif + + for (int i = 2; i <= n; i++) + { + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, i); + + if (b[0] < result[0]) + result[0] = b[0]; + if (b[1] < result[1]) + result[1] = b[1]; + if (b[2] < result[2]) + result[2] = b[2]; +#if LUA_VECTOR_SIZE == 4 + if (b[3] < result[3]) + result[3] = b[3]; +#endif + } + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, result[0], result[1], result[2], result[3]); +#else + lua_pushvector(L, result[0], result[1], result[2]); +#endif + + return 1; +} + +static int vector_max(lua_State* L) +{ + int n = lua_gettop(L); + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + +#if LUA_VECTOR_SIZE == 4 + LUA_VECTOR_TYPE result[] = {v[0], v[1], v[2], v[3]}; +#else + LUA_VECTOR_TYPE result[] = {v[0], v[1], v[2]}; +#endif + + for (int i = 2; i <= n; i++) + { + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, i); + + if (b[0] > result[0]) + result[0] = b[0]; + if (b[1] > result[1]) + result[1] = b[1]; + if (b[2] > result[2]) + result[2] = b[2]; +#if LUA_VECTOR_SIZE == 4 + if (b[3] > result[3]) + result[3] = b[3]; +#endif + } + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, result[0], result[1], result[2], result[3]); +#else + lua_pushvector(L, result[0], result[1], result[2]); +#endif + + return 1; +} + +static int vector_index(lua_State* L) +{ + const LUA_VECTOR_TYPE* v = luaL_checkvector(L, 1); + size_t namelen = 0; + const char* name = luaL_checklstring(L, 2, &namelen); + + // field access implementation mirrors the fast-path we have in the VM + if (namelen == 1) + { + int ic = (name[0] | ' ') - 'x'; + +#if LUA_VECTOR_SIZE == 4 + // 'w' is before 'x' in ascii, so ic is -1 when indexing with 'w' + if (ic == -1) + ic = 3; +#endif + + if (unsigned(ic) < LUA_VECTOR_SIZE) + { + lua_pushnumber(L, v[ic]); + return 1; + } + } + + luaL_error(L, "attempt to index vector with '%s'", name); +} + +static int vector_lerp(lua_State* L) +{ + const LUA_VECTOR_TYPE* a = luaL_checkvector(L, 1); + const LUA_VECTOR_TYPE* b = luaL_checkvector(L, 2); + const LUA_VECTOR_TYPE t = LUA_VECTOR_TYPE(luaL_checknumber(L, 3)); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, luai_lerp(a[0], b[0], t), luai_lerp(a[1], b[1], t), luai_lerp(a[2], b[2], t), luai_lerp(a[3], b[3], t)); +#else + lua_pushvector(L, luai_lerp(a[0], b[0], t), luai_lerp(a[1], b[1], t), luai_lerp(a[2], b[2], t)); +#endif + + return 1; +} + +static const luaL_Reg vectorlib[] = { + {"create", vector_create}, + {"magnitude", vector_magnitude}, + {"normalize", vector_normalize}, + {"cross", vector_cross}, + {"dot", vector_dot}, + {"angle", vector_angle}, + {"floor", vector_floor}, + {"ceil", vector_ceil}, + {"abs", vector_abs}, + {"sign", vector_sign}, + {"clamp", vector_clamp}, + {"max", vector_max}, + {"min", vector_min}, + {"lerp", vector_lerp}, + {NULL, NULL}, +}; + +static void createmetatable(lua_State* L) +{ + lua_createtable(L, 0, 1); // create metatable for vectors + + // push dummy vector +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, 0.0, 0.0, 0.0, 0.0); +#else + lua_pushvector(L, 0.0, 0.0, 0.0); +#endif + + lua_pushvalue(L, -2); + lua_setmetatable(L, -2); // set vector metatable + lua_pop(L, 1); // pop dummy vector + + lua_pushcfunction(L, vector_index, nullptr); + lua_setfield(L, -2, "__index"); + + lua_setreadonly(L, -1, true); + lua_pop(L, 1); // pop the metatable +} + +int luaopen_vector(lua_State* L) +{ + luaL_register(L, LUA_VECLIBNAME, vectorlib); + +#if LUA_VECTOR_SIZE == 4 + lua_pushvector(L, 0.0, 0.0, 0.0, 0.0); + lua_setfield(L, -2, "zero"); + lua_pushvector(L, 1.0, 1.0, 1.0, 1.0); + lua_setfield(L, -2, "one"); +#else + lua_pushvector(L, 0.0, 0.0, 0.0); + lua_setfield(L, -2, "zero"); + lua_pushvector(L, 1.0, 1.0, 1.0); + lua_setfield(L, -2, "one"); +#endif + + createmetatable(L); + + return 1; +} diff --git a/External/luau/VM/src/lvector.cpp b/External/luau/VM/src/lvector.cpp new file mode 100644 index 00000000..e82ee367 --- /dev/null +++ b/External/luau/VM/src/lvector.cpp @@ -0,0 +1,21 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lvector.h" + +#include "lgc.h" +#include "lmem.h" + +LuauVector* luaVec_newvector(lua_State* L, double x, double y, double z, double w) +{ + LuauVector* v = luaM_newgcofixed(L, LuauVector, sizevector(), L->activememcat, LUA_TVECTOR); + luaC_init(L, v, LUA_TVECTOR); + v->v[0] = (LUA_VECTOR_TYPE)x; + v->v[1] = (LUA_VECTOR_TYPE)y; + v->v[2] = (LUA_VECTOR_TYPE)z; + condvector4(v->v[3] = (LUA_VECTOR_TYPE)w, (void)w); + return v; +} + +void luaVec_freevector(lua_State* L, LuauVector* v, lua_Page* page) +{ + luaM_freegcofixed(L, v, sizevector(), v->memcat, page); +} diff --git a/External/luau/VM/src/lvector.h b/External/luau/VM/src/lvector.h new file mode 100644 index 00000000..a3f9d25a --- /dev/null +++ b/External/luau/VM/src/lvector.h @@ -0,0 +1,9 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "lobject.h" + +#define sizevector() (sizeof(LuauVector)) + +LUAI_FUNC LuauVector* luaVec_newvector(lua_State* L, double x, double y, double z, double w); +LUAI_FUNC void luaVec_freevector(lua_State* L, LuauVector* v, struct lua_Page* page); diff --git a/External/luau/VM/src/lvm.h b/External/luau/VM/src/lvm.h new file mode 100644 index 00000000..b5c436e0 --- /dev/null +++ b/External/luau/VM/src/lvm.h @@ -0,0 +1,40 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#pragma once + +#include "lobject.h" +#include "ltm.h" + +#define tostring(L, o) ((ttype(o) == LUA_TSTRING) || (luaV_tostring(L, o))) + +#define tonumber(o, n) (ttype(o) == LUA_TNUMBER || (((o) = luaV_tonumber(o, n)) != NULL)) + +#define equalobj(L, o1, o2) (ttype(o1) == ttype(o2) && luaV_equalval(L, o1, o2)) + +LUAI_FUNC int luaV_strcmp(const TString* ls, const TString* rs); +LUAI_FUNC int luaV_lessthan(lua_State* L, const TValue* l, const TValue* r); +LUAI_FUNC int luaV_lessequal(lua_State* L, const TValue* l, const TValue* r); +LUAI_FUNC int luaV_equalval(lua_State* L, const TValue* t1, const TValue* t2); + +template +void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); + +LUAI_FUNC void luaV_dolen(lua_State* L, StkId ra, const TValue* rb); +LUAI_FUNC const TValue* luaV_tonumber(const TValue* obj, TValue* n); +LUAI_FUNC const LUA_VECTOR_TYPE* luaV_tovector(const TValue* obj); +LUAI_FUNC int luaV_tostring(lua_State* L, StkId obj); +LUAI_FUNC void luaV_gettable(lua_State* L, const TValue* t, TValue* key, StkId val); +LUAI_FUNC void luaV_settable(lua_State* L, const TValue* t, TValue* key, StkId val); +LUAI_FUNC void luaV_concat(lua_State* L, int total, int last); +LUAI_FUNC void luaV_getimport(lua_State* L, LuaTable* env, TValue* k, StkId res, uint32_t id, bool propagatenil); +LUAI_FUNC void luaV_prepareFORN(lua_State* L, StkId plimit, StkId pstep, StkId pinit); +LUAI_FUNC void luaV_callTM(lua_State* L, int nparams, int res); +LUAI_FUNC void luaV_tryfuncTM(lua_State* L, StkId func); + +LUAI_FUNC void luau_execute(lua_State* L); +LUAI_FUNC void luau_finishop(lua_State* L); +LUAI_FUNC int luau_precall(lua_State* L, struct lua_TValue* func, int nresults); +LUAI_FUNC void luau_pushhandlerci(lua_State* L, StkId funcslot, int errfunc, int nresults); +LUAI_FUNC void luau_poscall(lua_State* L, StkId first); +LUAI_FUNC void luau_pospcallsuccess(lua_State* L); +LUAI_FUNC void luau_callhook(lua_State* L, lua_Hook hook, void* userdata); diff --git a/External/luau/VM/src/lvmexecute.cpp b/External/luau/VM/src/lvmexecute.cpp new file mode 100644 index 00000000..11493d33 --- /dev/null +++ b/External/luau/VM/src/lvmexecute.cpp @@ -0,0 +1,4003 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lclass.h" +#include "lvm.h" + +#include "lstate.h" +#include "ltable.h" +#include "lfunc.h" +#include "lstring.h" +#include "lvector.h" +#include "lgc.h" +#include "lmem.h" +#include "ldebug.h" +#include "ldo.h" +#include "lbuiltins.h" +#include "lnumutils.h" +#include "lbytecode.h" + +#include + +LUAU_FASTFLAGVARIABLE(DebugLuauUserDefinedClassesRuntime) +LUAU_FASTFLAGVARIABLE(LuauCallFeedback) +LUAU_FASTFLAGVARIABLE(LuauPromoteProto) +LUAU_FASTFLAGVARIABLE(LuauBackedgeHeapCheck) +LUAU_FLAGVERSION(LuauBackedgeHeapCheck, 2) +LUAU_FASTFLAG(LuauFastpcall) +LUAU_FASTFLAGVARIABLE(LuauFastpcallInterrupt) + +// Disable c99-designator to avoid the warning in computed goto dispatch table +#ifdef __clang__ +#if __has_warning("-Wc99-designator") +#pragma clang diagnostic ignored "-Wc99-designator" +#endif +#endif + +// When working with VM code, pay attention to these rules for correctness: +// 1. Many external Lua functions can fail; for them to fail and be able to generate a proper stack, we need to copy pc to L->ci->savedpc before the +// call +// 2. Many external Lua functions can reallocate the stack. This invalidates stack pointers in VM C stack frame, most importantly base, but also +// ra/rb/rc! +// 3. VM_PROTECT macro saves savedpc and restores base for you; most external calls need to be wrapped into that. However, it does NOT restore +// ra/rb/rc! +// 4. When copying an object to any existing object as a field, generally speaking you need to call luaC_barrier! Be careful with all setobj calls +// 5. To make 4 easier to follow, please use setobj2s for copies to stack, setobj2t for writes to tables, and setobj for other copies. +// 6. You can define HARDSTACKTESTS in luaconf.h which will aggressively realloc stack; with address sanitizer this should be effective at finding +// stack corruption bugs +// 7. Many external Lua functions can call GC! GC will *not* traverse pointers to new objects that aren't reachable from Lua root. Be careful when +// creating new Lua objects, store them to stack soon. + +// When calling luau_callTM, we usually push the arguments to the top of the stack. +// This is safe to do for complicated reasons: +// - stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack) +// - stack reallocation copies values past stack_last + +// All external function calls that can cause stack realloc or Lua calls have to be wrapped in VM_PROTECT +// This makes sure that we save the pc (in case the Lua call needs to generate a backtrace) before the call, +// and restores the stack pointer after in case stack gets reallocated +// Should only be used on the slow paths. +#define VM_PROTECT(x) \ + { \ + L->ci->savedpc = pc; \ + { \ + x; \ + }; \ + base = L->base; \ + } + +// To avoid VM_PROTECT(luaC_checkGC(L)) overhead for cases where GC step is not needed +#define VM_CHECK_GC(x) \ + { \ + if (luaC_needsGC(L)) \ + { \ + L->ci->savedpc = pc; \ + luaC_step(L, true); \ + base = L->base; \ + } \ + } + +// Some external functions can cause an error, but never reallocate the stack; for these, VM_PROTECT_PC() is +// a cheaper version of VM_PROTECT that can be called before the external call. +#define VM_PROTECT_PC() L->ci->savedpc = pc +#define VM_ASSERT_PC(pc) \ + LUAU_ASSERT(unsigned(pc - L->ci->p->code) < unsigned(L->ci->p->sizecode)); + +#define VM_REG(i) (LUAU_ASSERT(unsigned(i) < unsigned(L->top - base)), &base[i]) +#define VM_KV(i) (LUAU_ASSERT(unsigned(i) < unsigned(L->ci->p->sizek)), &k[i]) +#define VM_UV(i) (LUAU_ASSERT(unsigned(i) < unsigned(cl->nupvalues)), &cl->l.uprefs[i]) + +#define VM_PATCH_OP(pc, op) *const_cast(pc) = (uint8_t(op) | (0xffffff00u & *(pc))) +#define VM_PATCH_C(pc, slot) *const_cast(pc) = ((uint8_t(slot) << 24) | (0x00ffffffu & *(pc))) +#define VM_PATCH_E(pc, slot) *const_cast(pc) = ((uint32_t(slot) << 8) | (0x000000ffu & *(pc))) +#define VM_PATCH_AUX(pc, slot) *const_cast(pc) = uint32_t(slot) +#define VM_PATCH_AUX_SLOT(pc, k, slot) *const_cast(pc) = ((k) | (uint32_t(slot) << 16)) + +#define VM_INTERRUPT() \ + { \ + void (*interrupt)(lua_State*, int) = L->global->cb.interrupt; \ + if (LUAU_UNLIKELY(!!interrupt)) \ + { /* the interrupt hook is called right before we advance pc */ \ + VM_PROTECT(L->ci->savedpc++; interrupt(L, -1)); \ + if (L->status != 0) \ + { \ + L->ci->savedpc--; \ + goto exit; \ + } \ + } \ + } + +#define VM_DISPATCH_OP(op) &&CASE_##op + +#define VM_DISPATCH_TABLE() \ + VM_DISPATCH_OP(LOP_NOP), VM_DISPATCH_OP(LOP_BREAK), VM_DISPATCH_OP(LOP_LOADNIL), VM_DISPATCH_OP(LOP_LOADB), VM_DISPATCH_OP(LOP_LOADN), \ + VM_DISPATCH_OP(LOP_LOADK), VM_DISPATCH_OP(LOP_MOVE), VM_DISPATCH_OP(LOP_GETGLOBAL), VM_DISPATCH_OP(LOP_SETGLOBAL), \ + VM_DISPATCH_OP(LOP_GETUPVAL), VM_DISPATCH_OP(LOP_SETUPVAL), VM_DISPATCH_OP(LOP_CLOSEUPVALS), VM_DISPATCH_OP(LOP_GETIMPORT), \ + VM_DISPATCH_OP(LOP_GETTABLE), VM_DISPATCH_OP(LOP_SETTABLE), VM_DISPATCH_OP(LOP_GETTABLEKS), VM_DISPATCH_OP(LOP_SETTABLEKS), \ + VM_DISPATCH_OP(LOP_GETTABLEN), VM_DISPATCH_OP(LOP_SETTABLEN), VM_DISPATCH_OP(LOP_NEWCLOSURE), VM_DISPATCH_OP(LOP_NAMECALL), \ + VM_DISPATCH_OP(LOP_CALL), VM_DISPATCH_OP(LOP_RETURN), VM_DISPATCH_OP(LOP_JUMP), VM_DISPATCH_OP(LOP_JUMPBACK), VM_DISPATCH_OP(LOP_JUMPIF), \ + VM_DISPATCH_OP(LOP_JUMPIFNOT), VM_DISPATCH_OP(LOP_JUMPIFEQ), VM_DISPATCH_OP(LOP_JUMPIFLE), VM_DISPATCH_OP(LOP_JUMPIFLT), \ + VM_DISPATCH_OP(LOP_JUMPIFNOTEQ), VM_DISPATCH_OP(LOP_JUMPIFNOTLE), VM_DISPATCH_OP(LOP_JUMPIFNOTLT), VM_DISPATCH_OP(LOP_ADD), \ + VM_DISPATCH_OP(LOP_SUB), VM_DISPATCH_OP(LOP_MUL), VM_DISPATCH_OP(LOP_DIV), VM_DISPATCH_OP(LOP_MOD), VM_DISPATCH_OP(LOP_POW), \ + VM_DISPATCH_OP(LOP_ADDK), VM_DISPATCH_OP(LOP_SUBK), VM_DISPATCH_OP(LOP_MULK), VM_DISPATCH_OP(LOP_DIVK), VM_DISPATCH_OP(LOP_MODK), \ + VM_DISPATCH_OP(LOP_POWK), VM_DISPATCH_OP(LOP_AND), VM_DISPATCH_OP(LOP_OR), VM_DISPATCH_OP(LOP_ANDK), VM_DISPATCH_OP(LOP_ORK), \ + VM_DISPATCH_OP(LOP_CONCAT), VM_DISPATCH_OP(LOP_NOT), VM_DISPATCH_OP(LOP_MINUS), VM_DISPATCH_OP(LOP_LENGTH), VM_DISPATCH_OP(LOP_NEWTABLE), \ + VM_DISPATCH_OP(LOP_DUPTABLE), VM_DISPATCH_OP(LOP_SETLIST), VM_DISPATCH_OP(LOP_FORNPREP), VM_DISPATCH_OP(LOP_FORNLOOP), \ + VM_DISPATCH_OP(LOP_FORGLOOP), VM_DISPATCH_OP(LOP_FORGPREP_INEXT), VM_DISPATCH_OP(LOP_FASTCALL3), VM_DISPATCH_OP(LOP_FORGPREP_NEXT), \ + VM_DISPATCH_OP(LOP_NATIVECALL), VM_DISPATCH_OP(LOP_GETVARARGS), VM_DISPATCH_OP(LOP_DUPCLOSURE), VM_DISPATCH_OP(LOP_PREPVARARGS), \ + VM_DISPATCH_OP(LOP_LOADKX), VM_DISPATCH_OP(LOP_JUMPX), VM_DISPATCH_OP(LOP_FASTCALL), VM_DISPATCH_OP(LOP_COVERAGE), \ + VM_DISPATCH_OP(LOP_CAPTURE), VM_DISPATCH_OP(LOP_SUBRK), VM_DISPATCH_OP(LOP_DIVRK), VM_DISPATCH_OP(LOP_FASTCALL1), \ + VM_DISPATCH_OP(LOP_FASTCALL2), VM_DISPATCH_OP(LOP_FASTCALL2K), VM_DISPATCH_OP(LOP_FORGPREP), VM_DISPATCH_OP(LOP_JUMPXEQKNIL), \ + VM_DISPATCH_OP(LOP_JUMPXEQKB), VM_DISPATCH_OP(LOP_JUMPXEQKN), VM_DISPATCH_OP(LOP_JUMPXEQKS), VM_DISPATCH_OP(LOP_IDIV), \ + VM_DISPATCH_OP(LOP_IDIVK), VM_DISPATCH_OP(LOP_GETUDATAKS), VM_DISPATCH_OP(LOP_SETUDATAKS), VM_DISPATCH_OP(LOP_NAMECALLUDATA), \ + VM_DISPATCH_OP(LOP_NEWCLASSMEMBER), VM_DISPATCH_OP(LOP_CALLFB), VM_DISPATCH_OP(LOP_CMPPROTO), VM_DISPATCH_OP(LOP_FASTPCALL), \ + VM_DISPATCH_OP(LOP_NEWCLASS), + +#if defined(__GNUC__) || defined(__clang__) +#define VM_USE_CGOTO 1 +#else +#define VM_USE_CGOTO 0 +#endif + +/** + * These macros help dispatching Luau opcodes using either case + * statements or computed goto. + * VM_CASE(op) Generates either a case statement or a label + * VM_NEXT() fetch a byte and dispatch or jump to the beginning of the switch statement + * VM_CONTINUE() Use an opcode override to dispatch with computed goto or + * switch statement to skip a LOP_BREAK instruction. + */ +#if VM_USE_CGOTO +#define VM_CASE(op) CASE_##op: +#define VM_NEXT() goto*(SingleStep ? &&dispatch : kDispatchTable[LUAU_INSN_OP(*pc)]) +#define VM_CONTINUE(op) goto* kDispatchTable[uint8_t(op)] +#else +#define VM_CASE(op) case op: +#define VM_NEXT() goto dispatch +#define VM_CONTINUE(op) \ + dispatchOp = uint8_t(op); \ + goto dispatchContinue +#endif + +// Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost. +#define VM_HAS_NATIVE 1 + +LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata) +{ + ptrdiff_t base = savestack(L, L->base); + ptrdiff_t top = savestack(L, L->top); + ptrdiff_t ci_top = savestack(L, L->ci->top); + int status = L->status; + + // if the hook is called externally on a paused thread, we need to make sure the paused thread can emit Luau calls + if (status == LUA_YIELD || status == LUA_BREAK) + { + L->status = 0; + L->base = L->ci->base; + } + + Closure* cl = clvalue(L->ci->func); + + // note: the pc expectations of the hook are matching the general "pc points to next instruction" + // however, for the hook to be able to continue execution from the same point, this is called with savedpc at the *current* instruction + // this needs to be called before luaD_checkstack in case it fails to reallocate stack + const Instruction* oldsavedpc = L->ci->savedpc; + + if (L->ci->savedpc && L->ci->savedpc != L->ci->p->code + L->ci->p->sizecode) + L->ci->savedpc++; + + luaD_checkstack(L, LUA_MINSTACK); // ensure minimum stack size + L->ci->top = L->top + LUA_MINSTACK; + LUAU_ASSERT(L->ci->top <= L->stack_last); + + lua_Debug ar; + ar.currentline = + cl->isC ? -1 : luaG_getline(L->ci->p, pcRel(L->ci->savedpc, L->ci->p)); + ar.userdata = userdata; + + hook(L, &ar); + + L->ci->savedpc = oldsavedpc; + + L->ci->top = restorestack(L, ci_top); + L->top = restorestack(L, top); + + // note that we only restore the paused state if the hook hasn't yielded by itself + if (status == LUA_YIELD && L->status != LUA_YIELD) + { + L->status = LUA_YIELD; + L->base = restorestack(L, base); + } + else if (status == LUA_BREAK) + { + LUAU_ASSERT(L->status != LUA_BREAK); // hook shouldn't break again + + L->status = LUA_BREAK; + L->base = restorestack(L, base); + } +} + +inline bool luau_skipstep(uint8_t op) +{ + return op == LOP_PREPVARARGS || op == LOP_BREAK; +} + +static LUAU_NOINLINE void luau_setupcci(lua_State* L, int nresults, StkId fun) +{ + CallInfo* ci = incr_ci(L); + + ci->func = fun; + ci->p = getproto(clvalue(fun)); + ci->base = fun + 1; + ci->top = L->top + LUA_MINSTACK; + ci->savedpc = NULL; + ci->flags = 0; + ci->nresults = nresults; + + L->base = fun + 1; + + luaD_checkstackfornewci(L, LUA_MINSTACK); + + LUAU_ASSERT(ci->top <= L->stack_last); + LUAU_ASSERT(ttisfunction(ci->func)); +} + +template +static void luau_execute(lua_State* L) +{ +#if VM_USE_CGOTO + static const void* kDispatchTable[256] = {VM_DISPATCH_TABLE()}; +#endif + + // the critical interpreter state, stored in locals for performance + // the hope is that these map to registers without spilling (which is not true for x86 :/) + Closure* cl; + StkId base; + TValue* k; + const Instruction* pc; + + // In debug builds, compilers will often layout each variable in its own stack slot + // This can considerably increase the stack frame of the interpreter loop and cause C stack overflows under the LUAI_MAXCCALLS limit + // By defining shared variables here, we force the stack slot reuse for these variables across the interpreter loop +#if defined(LUAU_ASSERTENABLED) + + Instruction insn; + StkId ra; + StkId rb; + StkId rc; + +#define VM_CASE_INSTRUCTION +#define VM_CASE_STKID + +#else + +#define VM_CASE_INSTRUCTION Instruction +#define VM_CASE_STKID StkId + +#endif + + LUAU_ASSERT(isLua(L->ci)); + LUAU_ASSERT(L->isactive); + LUAU_ASSERT(!isblack(obj2gco(L))); // we don't use luaC_threadbarrier because active threads never turn black + +#if VM_HAS_NATIVE + if ((L->ci->flags & LUA_CALLINFO_NATIVE) && !SingleStep) + { + Proto* p = L->ci->p; + LUAU_ASSERT(p->execdata); + + if (L->global->ecb.enter(L, p) == 0) + return; + } + +reentry: +#endif + + LUAU_ASSERT(isLua(L->ci)); + LUAU_ASSERT(L->ci->p != nullptr); + + pc = L->ci->savedpc; + cl = clvalue(L->ci->func); + base = L->base; + k = L->ci->p->k; + + VM_NEXT(); // starts the interpreter "loop" + + { + dispatch: + // Note: this code doesn't always execute! on some platforms we use computed goto which bypasses all of this unless we run in single-step mode + // Therefore only ever put assertions here. + LUAU_ASSERT(base == L->base && L->base == L->ci->base); + LUAU_ASSERT(base <= L->top && L->top <= L->stack + L->stacksize); + + // ... and singlestep logic :) + if (SingleStep) + { + if (L->global->cb.debugstep && !luau_skipstep(LUAU_INSN_OP(*pc))) + { + VM_PROTECT(luau_callhook(L, L->global->cb.debugstep, NULL)); + + // allow debugstep hook to put thread into error/yield state + if (L->status != 0) + goto exit; + } + +#if VM_USE_CGOTO + VM_CONTINUE(LUAU_INSN_OP(*pc)); +#endif + } + +#if !VM_USE_CGOTO + size_t dispatchOp = LUAU_INSN_OP(*pc); + + dispatchContinue: + switch (dispatchOp) +#endif + { + VM_CASE(LOP_NOP) + { + VM_CASE_INSTRUCTION insn = *pc++; + LUAU_ASSERT(insn == 0); + VM_NEXT(); + } + + VM_CASE(LOP_LOADNIL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + setnilvalue(ra); + VM_NEXT(); + } + + VM_CASE(LOP_LOADB) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + setbvalue(ra, LUAU_INSN_B(insn)); + + pc += LUAU_INSN_C(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_LOADN) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + setnvalue(ra, LUAU_INSN_D(insn)); + VM_NEXT(); + } + + VM_CASE(LOP_LOADK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_D(insn)); + + setobj2s(L, ra, kv); + VM_NEXT(); + } + + VM_CASE(LOP_MOVE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + + setobj2s(L, ra, rb); + VM_NEXT(); + } + + VM_CASE(LOP_GETGLOBAL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + LUAU_ASSERT(ttisstring(kv)); + + // fast-path: value is in expected slot + LuaTable* h = cl->env; + int slot = LUAU_INSN_C(insn) & h->nodemask8; + LuaNode* n = &h->node[slot]; + + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv)) && !ttisnil(gval(n))) + { + setobj2s(L, ra, gval(n)); + VM_NEXT(); + } + else + { + // slow-path, may invoke Lua calls via __index metamethod + TValue g; + sethvalue(L, &g, h); + L->cachedslot = slot; + VM_PROTECT(luaV_gettable(L, &g, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + } + + VM_CASE(LOP_SETGLOBAL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + LUAU_ASSERT(ttisstring(kv)); + + // fast-path: value is in expected slot + LuaTable* h = cl->env; + int slot = LUAU_INSN_C(insn) & h->nodemask8; + LuaNode* n = &h->node[slot]; + + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)) && !h->readonly)) + { + setobj2t(L, gval(n), ra); + luaC_barriert(L, h, ra); + VM_NEXT(); + } + else + { + // slow-path, may invoke Lua calls via __newindex metamethod + TValue g; + sethvalue(L, &g, h); + L->cachedslot = slot; + VM_PROTECT(luaV_settable(L, &g, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + } + + VM_CASE(LOP_GETUPVAL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* ur = VM_UV(LUAU_INSN_B(insn)); + TValue* v = ttisupval(ur) ? upvalue(ur)->v : ur; + + setobj2s(L, ra, v); + VM_NEXT(); + } + + VM_CASE(LOP_SETUPVAL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* ur = VM_UV(LUAU_INSN_B(insn)); + UpVal* uv = upvalue(ur); + + setobj(L, uv->v, ra); + luaC_barrier(L, uv, ra); + VM_NEXT(); + } + + VM_CASE(LOP_CLOSEUPVALS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + if (L->openupval && L->openupval->v >= ra) + luaF_close(L, ra); + VM_NEXT(); + } + + VM_CASE(LOP_GETIMPORT) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_D(insn)); + + // fast-path: import resolution was successful and closure environment is "safe" for import + if (!ttisnil(kv) && cl->env->safeenv) + { + setobj2s(L, ra, kv); + pc++; // skip over AUX + VM_NEXT(); + } + else + { + uint32_t aux = *pc++; + + VM_PROTECT(luaV_getimport(L, cl->env, k, ra, aux, /* propagatenil= */ false)); + VM_NEXT(); + } + } + + VM_CASE(LOP_GETTABLEKS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + LUAU_ASSERT(ttisstring(kv)); + + // fast-path: built-in table + if (LUAU_LIKELY(ttistable(rb))) + { + LuaTable* h = hvalue(rb); + + int slot = LUAU_INSN_C(insn) & h->nodemask8; + LuaNode* n = &h->node[slot]; + + // fast-path: value is in expected slot + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)))) + { + setobj2s(L, ra, gval(n)); + VM_NEXT(); + } + else if (!h->metatable) + { + // fast-path: value is not in expected slot, but the table lookup doesn't involve metatable + const TValue* res = luaH_getstr(h, tsvalue(kv)); + + if (res != luaO_nilobject) + { + int cachedslot = gval2slot(h, res); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, cachedslot); + } + + setobj2s(L, ra, res); + VM_NEXT(); + } + else + { + // slow-path, may invoke Lua calls via __index metamethod + L->cachedslot = slot; + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + } + else + { + // fast-path: registered direct field handler + if (ttisuserdata(rb)) + { + LuaTable* dispatch = L->global->udatadirectfields[uvalue(rb)->tag]; + if (dispatch) + { + int slot = LUAU_INSN_C(insn) & dispatch->nodemask8; + LuaNode* n = &dispatch->node[slot]; + +#if LUA_VECTOR_DOUBLE == 1 + DirectFieldResult dfr{L, ra}; + void* resultarg = 𝔡 +#else + void* resultarg = ra; +#endif + + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)))) + { + lua_UserdataDirectFieldGet fn = reinterpret_cast(pvalue(gval(n))); + fn(uvalue(rb)->data, resultarg); + VM_NEXT(); + } + + const TValue* fptr = luaH_getstr(dispatch, tsvalue(kv)); + if (!ttisnil(fptr)) + { + // cache slot for future lookups + VM_PATCH_C(pc - 2, gval2slot(dispatch, fptr)); + lua_UserdataDirectFieldGet fn = reinterpret_cast(pvalue(fptr)); + fn(uvalue(rb)->data, resultarg); + VM_NEXT(); + } + } + + // fall through to slow path + } + + // fast-path: user data with C __index TM + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = fasttm(L, uvalue(rb)->metatable, TM_INDEX)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top = top + 3; + + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + else if (ttisvector(rb)) + { + // fast-path: quick case-insensitive comparison with "X"/"Y"/"Z" + const char* name = getstr(tsvalue(kv)); + int ic = (name[0] | ' ') - 'x'; + +#if LUA_VECTOR_SIZE == 4 + // 'w' is before 'x' in ascii, so ic is -1 when indexing with 'w' + if (ic == -1) + ic = 3; +#endif + + if (unsigned(ic) < LUA_VECTOR_SIZE && name[1] == '\0') + { + const LUA_VECTOR_TYPE* v = vvalue(rb); // silences ubsan when indexing v[] + setnvalue(ra, v[ic]); + VM_NEXT(); + } + + fn = fasttm(L, L->global->mt[LUA_TVECTOR], TM_INDEX); + + if (fn && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top = top + 3; + + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + + // fall through to slow path + } + else if (LUAU_UNLIKELY(FFlag::DebugLuauUserDefinedClassesRuntime && ttisobject(rb))) + { + // fast-path: the "hash line" is an offset that points + // to the class member with the same name. + uint8_t slot = LUAU_INSN_C(insn); + LuauObject* inst = objectvalue(rb); + if (LUAU_LIKELY(slot < inst->lclass->numberofallmembers && tsvalue(kv) == inst->lclass->offsettomember[slot])) + { + setobj2s(L, ra, luaR_lookupmemberatoffset(inst, slot)); + VM_NEXT(); + } + // slow-er path: the slot mismatched so we fall back to looking up the offset from the string. + else + { + const TValue* offset = luaH_getstr(inst->lclass->memberstooffset, tsvalue(kv)); + if (ttisnil(offset)) + luaG_missingmembererror(L, rb, kv); + const uint32_t offsetnum = uint32_t(nvalue(offset)); + setobj2s(L, ra, luaR_lookupmemberatoffset(inst, offsetnum)); + VM_PATCH_C(pc - 2, offsetnum); + VM_NEXT(); + } + } + + // fall through to slow path + } + + // slow-path, may invoke Lua calls via __index metamethod + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + VM_NEXT(); + } + + VM_CASE(LOP_SETTABLEKS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + LUAU_ASSERT(ttisstring(kv)); + + // fast-path: built-in table + if (LUAU_LIKELY(ttistable(rb))) + { + LuaTable* h = hvalue(rb); + + int slot = LUAU_INSN_C(insn) & h->nodemask8; + LuaNode* n = &h->node[slot]; + + // fast-path: value is in expected slot + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)) && !h->readonly)) + { + setobj2t(L, gval(n), ra); + luaC_barriert(L, h, ra); + VM_NEXT(); + } + else if (fastnotm(h->metatable, TM_NEWINDEX) && !h->readonly) + { + VM_PROTECT_PC(); // set may fail + + TValue* res = luaH_setstr(L, h, tsvalue(kv)); + int cachedslot = gval2slot(h, res); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, cachedslot); + setobj2t(L, res, ra); + luaC_barriert(L, h, ra); + VM_NEXT(); + } + else + { + // slow-path, may invoke Lua calls via __newindex metamethod + L->cachedslot = slot; + VM_PROTECT(luaV_settable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + } + else + { + // fast-path: user data with C __newindex TM + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = fasttm(L, uvalue(rb)->metatable, TM_NEWINDEX)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 4 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + setobj2s(L, top + 3, ra); + L->top = top + 4; + + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_callTM(L, 3, -1)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + VM_NEXT(); + } + else + { + // slow-path, may invoke Lua calls via __newindex metamethod + VM_PROTECT(luaV_settable(L, rb, kv, ra)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_GETTABLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path: array lookup + if (ttistable(rb) && ttisnumber(rc)) + { + LuaTable* h = hvalue(rb); + + double indexd = nvalue(rc); + int index = int(indexd); + + // index has to be an exact integer and in-bounds for the array portion + if (LUAU_LIKELY(unsigned(index) - 1 < unsigned(h->sizearray) && !h->metatable && double(index) == indexd)) + { + setobj2s(L, ra, &h->array[unsigned(index - 1)]); + VM_NEXT(); + } + + // fall through to slow path + } + + // slow-path: handles out of bounds array lookups, non-integer numeric keys, non-array table lookup, __index MT calls + VM_PROTECT(luaV_gettable(L, rb, rc, ra)); + VM_NEXT(); + } + + VM_CASE(LOP_SETTABLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path: array assign + if (ttistable(rb) && ttisnumber(rc)) + { + LuaTable* h = hvalue(rb); + + double indexd = nvalue(rc); + int index = int(indexd); + + // index has to be an exact integer and in-bounds for the array portion + if (LUAU_LIKELY(unsigned(index) - 1 < unsigned(h->sizearray) && !h->metatable && !h->readonly && double(index) == indexd)) + { + setobj2t(L, &h->array[unsigned(index - 1)], ra); + luaC_barriert(L, h, ra); + VM_NEXT(); + } + + // fall through to slow path + } + + // slow-path: handles out of bounds array assignments, non-integer numeric keys, non-array table access, __newindex MT calls + VM_PROTECT(luaV_settable(L, rb, rc, ra)); + VM_NEXT(); + } + + VM_CASE(LOP_GETTABLEN) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + int c = LUAU_INSN_C(insn); + + // fast-path: array lookup + if (ttistable(rb)) + { + LuaTable* h = hvalue(rb); + + if (LUAU_LIKELY(unsigned(c) < unsigned(h->sizearray) && !h->metatable)) + { + setobj2s(L, ra, &h->array[c]); + VM_NEXT(); + } + + // fall through to slow path + } + + // slow-path: handles out of bounds array lookups + TValue n; + setnvalue(&n, c + 1); + VM_PROTECT(luaV_gettable(L, rb, &n, ra)); + VM_NEXT(); + } + + VM_CASE(LOP_SETTABLEN) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + int c = LUAU_INSN_C(insn); + + // fast-path: array assign + if (ttistable(rb)) + { + LuaTable* h = hvalue(rb); + + if (LUAU_LIKELY(unsigned(c) < unsigned(h->sizearray) && !h->metatable && !h->readonly)) + { + setobj2t(L, &h->array[c], ra); + luaC_barriert(L, h, ra); + VM_NEXT(); + } + + // fall through to slow path + } + + // slow-path: handles out of bounds array lookups + TValue n; + setnvalue(&n, c + 1); + VM_PROTECT(luaV_settable(L, rb, &n, ra)); + VM_NEXT(); + } + + VM_CASE(LOP_NEWCLOSURE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + Proto* pv = L->ci->p->p[LUAU_INSN_D(insn)]; + LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < unsigned(L->ci->p->sizep)); + + VM_PROTECT_PC(); // luaF_newLclosure may fail due to OOM + + // note: we save closure to stack early in case the code below wants to capture it by value + Closure* ncl = luaF_newLclosure(L, pv->nups, cl->env, pv); + setclvalue(L, ra, ncl); + + for (int ui = 0; ui < pv->nups; ++ui) + { + Instruction uinsn = *pc++; + LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); + + switch (LUAU_INSN_A(uinsn)) + { + case LCT_VAL: + setobj(L, &ncl->l.uprefs[ui], VM_REG(LUAU_INSN_B(uinsn))); + break; + + case LCT_REF: + setupvalue(L, &ncl->l.uprefs[ui], luaF_findupval(L, VM_REG(LUAU_INSN_B(uinsn)))); + break; + + case LCT_UPVAL: + setobj(L, &ncl->l.uprefs[ui], VM_UV(LUAU_INSN_B(uinsn))); + break; + + default: + LUAU_ASSERT(!"Unknown upvalue capture type"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks + } + } + + VM_PROTECT(luaC_checkGC(L)); + VM_NEXT(); + } + + VM_CASE(LOP_NAMECALL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + LUAU_ASSERT(ttisstring(kv)); + + if (LUAU_LIKELY(ttistable(rb))) + { + LuaTable* h = hvalue(rb); + // note: we can't use nodemask8 here because we need to query the main position of the table, and 8-bit nodemask8 only works + // for predictive lookups + LuaNode* n = &h->node[tsvalue(kv)->hash & (sizenode(h) - 1)]; + + const TValue* mt = 0; + const LuaNode* mtn = 0; + + // fast-path: key is in the table in expected slot + if (ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n))) + { + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, gval(n)); + } + // fast-path: key is absent from the base, table has an __index table, and it has the result in the expected slot + else if (gnext(n) == 0 && (mt = fasttm(L, hvalue(rb)->metatable, TM_INDEX)) && ttistable(mt) && + (mtn = &hvalue(mt)->node[LUAU_INSN_C(insn) & hvalue(mt)->nodemask8]) && ttisstring(gkey(mtn)) && + tsvalue(gkey(mtn)) == tsvalue(kv) && !ttisnil(gval(mtn))) + { + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, gval(mtn)); + } + else + { + // slow-path: handles full table lookup + setobj2s(L, ra + 1, rb); + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + if (ttisnil(ra)) + luaG_methoderror(L, ra + 1, tsvalue(kv)); + } + } + else + { + LuaTable* mt = ttisuserdata(rb) ? uvalue(rb)->metatable : L->global->mt[ttype(rb)]; + const TValue* tmi = 0; + + // fast-path: metatable with __namecall + if (const TValue* fn = fasttm(L, mt, TM_NAMECALL)) + { + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, fn); + + L->namecall = tsvalue(kv); + } + else if ((tmi = fasttm(L, mt, TM_INDEX)) && ttistable(tmi)) + { + LuaTable* h = hvalue(tmi); + int slot = LUAU_INSN_C(insn) & h->nodemask8; + LuaNode* n = &h->node[slot]; + + // fast-path: metatable with __index that has method in expected slot + if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)))) + { + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, gval(n)); + } + else + { + // slow-path: handles slot mismatch + setobj2s(L, ra + 1, rb); + L->cachedslot = slot; + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + if (ttisnil(ra)) + luaG_methoderror(L, ra + 1, tsvalue(kv)); + } + } + else if (LUAU_UNLIKELY(FFlag::DebugLuauUserDefinedClassesRuntime && ttisobject(rb))) + { + uint8_t slot = LUAU_INSN_C(insn); + LuauObject* inst = objectvalue(rb); + if (slot < inst->lclass->numberofallmembers && tsvalue(kv) == inst->lclass->offsettomember[slot]) + { + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, luaR_lookupmemberatoffset(inst, slot)); + } + // slow-er path: try to fetch the field manually. + else + { + const TValue* offset = luaH_getstr(inst->lclass->memberstooffset, tsvalue(kv)); + if (ttisnil(offset)) + luaG_missingmembererror(L, rb, kv); + const uint32_t offsetnum = uint32_t(nvalue(offset)); + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, luaR_lookupmemberatoffset(inst, offsetnum)); + VM_PATCH_C(pc - 2, offsetnum); + } + } + else + { + // slow-path: handles non-table __index + setobj2s(L, ra + 1, rb); + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + if (ttisnil(ra)) + luaG_methoderror(L, ra + 1, tsvalue(kv)); + } + } + + if (LUAU_UNLIKELY(FFlag::LuauCallFeedback)) + { + VM_NEXT(); + } + else + { + // intentional fallthrough to CALL + LUAU_ASSERT(LUAU_INSN_OP(*pc) == LOP_CALL); + } + } + + VM_CASE(LOP_CALL) + { + VM_INTERRUPT(); + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + + StkId argtop = L->top; + argtop = (nparams == LUA_MULTRET) ? argtop : ra + 1 + nparams; + + if (LUAU_UNLIKELY(!ttisfunction(ra))) + { + // slow-path: not a function call + VM_PROTECT_PC(); // luaV_tryfuncTM may fail + + luaV_tryfuncTM(L, ra); + argtop++; // __call adds an extra self + } + + Closure* ccl = clvalue(ra); + L->ci->savedpc = pc; + + CallInfo* ci = incr_ci(L); + ci->func = ra; + ci->p = getproto(ccl); + ci->base = ra + 1; + ci->top = argtop + ccl->stacksize; // note: technically UB since we haven't reallocated the stack yet + ci->savedpc = NULL; + ci->flags = 0; + ci->nresults = nresults; + + L->base = ci->base; + L->top = argtop; + + // note: this reallocs stack, but we don't need to VM_PROTECT this + // this is because we're going to modify base/savedpc manually anyhow + // crucially, we can't use ra/argtop after this line + luaD_checkstackfornewci(L, ccl->stacksize); + + LUAU_ASSERT(ci->top <= L->stack_last); + + if (!ccl->isC) + { + Proto* p = ccl->l.p; + + // fill unused parameters with nil + StkId argi = L->top; + StkId argend = L->base + p->numparams; + while (argi < argend) + setnilvalue(argi++); // complete missing arguments + L->top = p->is_vararg ? argi : ci->top; + + // reentry + // codeentry may point to NATIVECALL instruction when proto is compiled to native code + // this will result in execution continuing in native code, and is equivalent to if (p->execdata) but has no additional overhead + // note that p->codeentry may point *outside* of p->code..p->code+p->sizecode, but that pointer never gets saved to savedpc. + pc = SingleStep ? p->code : p->codeentry; + cl = ccl; + base = L->base; + k = p->k; + VM_NEXT(); + } + else + { + lua_CFunction func = ccl->c.f; + int n = func(L); + + // yield + if (n < 0) + goto exit; + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + // copy return values into parent stack (but only up to nresults!), fill the rest with nil + // note: in MULTRET context nresults starts as -1 so i != 0 condition never activates intentionally + StkId res = ci->func; + StkId vali = L->top - n; + StkId valend = L->top; + + int i; + for (i = nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = (nresults == LUA_MULTRET) ? res : cip->top; + + base = L->base; // stack may have been reallocated, so we need to refresh base ptr + VM_NEXT(); + } + } + + VM_CASE(LOP_CALLFB) + { + VM_INTERRUPT(); + VM_CASE_INSTRUCTION insn = *pc++; + Instruction feedback_slot = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + + StkId argtop = L->top; + argtop = (nparams == LUA_MULTRET) ? argtop : ra + 1 + nparams; + + // slow-path: not a function call + if (LUAU_UNLIKELY(!ttisfunction(ra))) + { + if (feedback_slot != LUAU_INSN_FBSLOT_SEALED) + VM_PATCH_AUX(pc - 1, LUAU_INSN_FBSLOT_SEALED); + + VM_PROTECT_PC(); // luaV_tryfuncTM may fail + + luaV_tryfuncTM(L, ra); + argtop++; // __call adds an extra self + } + + Closure* ccl = clvalue(ra); + L->ci->savedpc = pc; + + CallInfo* ci = incr_ci(L); + ci->func = ra; + ci->p = getproto(ccl); + ci->base = ra + 1; + ci->top = argtop + ccl->stacksize; // note: technically UB since we haven't reallocated the stack yet + ci->savedpc = NULL; + ci->flags = 0; + ci->nresults = nresults; + + L->base = ci->base; + L->top = argtop; + + // note: this reallocs stack, but we don't need to VM_PROTECT this + // this is because we're going to modify base/savedpc manually anyhow + // crucially, we can't use ra/argtop after this line + luaD_checkstackfornewci(L, ccl->stacksize); + + LUAU_ASSERT(ci->top <= L->stack_last); + + if (!ccl->isC) + { + Proto* p = ccl->l.p; + + if (feedback_slot != LUAU_INSN_FBSLOT_SEALED) + { + if (!luaF_recordhit(L, cl, ccl, feedback_slot)) + VM_PATCH_AUX(pc - 1, LUAU_INSN_FBSLOT_SEALED); + } + + // fill unused parameters with nil + StkId argi = L->top; + StkId argend = L->base + p->numparams; + while (argi < argend) + setnilvalue(argi++); // complete missing arguments + L->top = p->is_vararg ? argi : ci->top; + + // reentry + // codeentry may point to NATIVECALL instruction when proto is compiled to native code + // this will result in execution continuing in native code, and is equivalent to if (p->execdata) but has no additional overhead + // note that p->codeentry may point *outside* of p->code..p->code+p->sizecode, but that pointer never gets saved to savedpc. + pc = SingleStep ? p->code : p->codeentry; + cl = ccl; + base = L->base; + k = p->k; + VM_NEXT(); + } + else + { + if (feedback_slot != LUAU_INSN_FBSLOT_SEALED) + VM_PATCH_AUX(pc - 1, LUAU_INSN_FBSLOT_SEALED); + + lua_CFunction func = ccl->c.f; + int n = func(L); + + // yield + if (n < 0) + goto exit; + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + // copy return values into parent stack (but only up to nresults!), fill the rest with nil + // note: in MULTRET context nresults starts as -1 so i != 0 condition never activates intentionally + StkId res = ci->func; + StkId vali = L->top - n; + StkId valend = L->top; + + int i; + for (i = nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = (nresults == LUA_MULTRET) ? res : cip->top; + + base = L->base; // stack may have been reallocated, so we need to refresh base ptr + VM_NEXT(); + } + } + + VM_CASE(LOP_RETURN) + { + VM_INTERRUPT(); + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = &base[LUAU_INSN_A(insn)]; // note: this can point to L->top if b == LUA_MULTRET making VM_REG unsafe to use + int b = LUAU_INSN_B(insn) - 1; + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + StkId res = ci->func; // note: we assume CALL always puts func+args and expects results to start at func + + StkId vali = ra; + StkId valend = + (b == LUA_MULTRET) ? L->top : ra + b; // copy as much as possible for MULTRET calls, and only as much as needed otherwise + + int nresults = ci->nresults; + + // copy return values into parent stack (but only up to nresults!), fill the rest with nil + // note: in MULTRET context nresults starts as -1 so i != 0 condition never activates intentionally + int i; + for (i = nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = (nresults == LUA_MULTRET) ? res : cip->top; + + // we're done! + if (LUAU_UNLIKELY(ci->flags & LUA_CALLINFO_RETURN)) + { + goto exit; + } + + LUAU_ASSERT(isLua(L->ci)); + + Closure* nextcl = clvalue(cip->func); + LUAU_ASSERT(cip->p != nullptr); + Proto* nextproto = cip->p; + +#if VM_HAS_NATIVE + if (LUAU_UNLIKELY((cip->flags & LUA_CALLINFO_NATIVE) && !SingleStep)) + { + if (L->global->ecb.enter(L, nextproto) == 1) + goto reentry; + else + goto exit; + } +#endif + + // reentry + pc = cip->savedpc; + cl = nextcl; + base = L->base; + k = nextproto->k; + VM_NEXT(); + } + + VM_CASE(LOP_JUMP) + { + VM_CASE_INSTRUCTION insn = *pc++; + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPIF) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + pc += l_isfalse(ra) ? 0 : LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPIFNOT) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + pc += l_isfalse(ra) ? LUAU_INSN_D(insn) : 0; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPIFEQ) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // Note that all jumps below jump by 1 in the "false" case to skip over aux + if (ttype(ra) == ttype(rb)) + { + switch (ttype(ra)) + { + case LUA_TNIL: + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TBOOLEAN: + pc += bvalue(ra) == bvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TLIGHTUSERDATA: + pc += (pvalue(ra) == pvalue(rb) && lightuserdatatag(ra) == lightuserdatatag(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TNUMBER: + pc += nvalue(ra) == nvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TVECTOR: + pc += luai_veceq(vvalue(ra), vvalue(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TSTRING: + case LUA_TFUNCTION: + case LUA_TTHREAD: + case LUA_TBUFFER: + pc += gcvalue(ra) == gcvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TTABLE: + // fast-path: same metatable, no EQ metamethod + if (hvalue(ra)->metatable == hvalue(rb)->metatable) + { + const TValue* fn = fasttm(L, hvalue(ra)->metatable, TM_EQ); + + if (!fn) + { + pc += hvalue(ra) == hvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + // slow path after switch() + break; + + case LUA_TUSERDATA: + // fast-path: same metatable, no EQ metamethod or C metamethod + if (uvalue(ra)->metatable == uvalue(rb)->metatable) + { + const TValue* fn = fasttm(L, uvalue(ra)->metatable, TM_EQ); + + if (!fn) + { + pc += uvalue(ra) == uvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else if (ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, ra); + setobj2s(L, top + 2, rb); + int res = int(top - base); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, res)); + pc += !l_isfalse(&base[res]) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + // slow path after switch() + break; + + // Class objects are only ever physically equal, so check + // for pointer equality. + case LUA_TCLASS: + pc += classvalue(ra) == classvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + break; + + case LUA_TOBJECT: + // For now, hit the slow path after the switch (we may + // need to invoke metamethods). + break; + + case LUA_TINTEGER: + pc += lvalue(ra) == lvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + default: + LUAU_ASSERT(!"Unknown value type"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks + } + + // slow-path: tables with metatables and userdata values + // note that we don't have a fast path for userdata values without metatables, since that's very rare + int res; + VM_PROTECT(res = luaV_equalval(L, ra, rb)); + + pc += (res == 1) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + pc += 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_JUMPIFNOTEQ) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // Note that all jumps below jump by 1 in the "true" case to skip over aux + if (ttype(ra) == ttype(rb)) + { + switch (ttype(ra)) + { + case LUA_TNIL: + pc += 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TBOOLEAN: + pc += bvalue(ra) != bvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TLIGHTUSERDATA: + pc += (pvalue(ra) != pvalue(rb) || lightuserdatatag(ra) != lightuserdatatag(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TNUMBER: + pc += nvalue(ra) != nvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TVECTOR: + pc += !luai_veceq(vvalue(ra), vvalue(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TSTRING: + case LUA_TFUNCTION: + case LUA_TTHREAD: + case LUA_TBUFFER: + pc += gcvalue(ra) != gcvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + case LUA_TTABLE: + // fast-path: same metatable, no EQ metamethod + if (hvalue(ra)->metatable == hvalue(rb)->metatable) + { + const TValue* fn = fasttm(L, hvalue(ra)->metatable, TM_EQ); + + if (!fn) + { + pc += hvalue(ra) != hvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + // slow path after switch() + break; + + case LUA_TUSERDATA: + // fast-path: same metatable, no EQ metamethod or C metamethod + if (uvalue(ra)->metatable == uvalue(rb)->metatable) + { + const TValue* fn = fasttm(L, uvalue(ra)->metatable, TM_EQ); + + if (!fn) + { + pc += uvalue(ra) != uvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else if (ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, ra); + setobj2s(L, top + 2, rb); + int res = int(top - base); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, res)); + pc += l_isfalse(&base[res]) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + // slow path after switch() + break; + + // Class objects are only ever physically equal, so check + // for pointer inequality. + case LUA_TCLASS: + pc += classvalue(ra) != classvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + break; + + case LUA_TOBJECT: + // For now, hit the slow path after the switch (we may + // need to invoke metamethods). + break; + + case LUA_TINTEGER: + pc += lvalue(ra) != lvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + + default: + LUAU_ASSERT(!"Unknown value type"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks + } + + // slow-path: tables with metatables and userdata values + // note that we don't have a fast path for userdata values without metatables, since that's very rare + int res; + VM_PROTECT(res = luaV_equalval(L, ra, rb)); + + pc += (res == 0) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_JUMPIFLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // fast-path: number + // Note that all jumps below jump by 1 in the "false" case to skip over aux + if (LUAU_LIKELY(ttisnumber(ra) && ttisnumber(rb))) + { + pc += nvalue(ra) <= nvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + // fast-path: string + else if (ttisstring(ra) && ttisstring(rb)) + { + pc += luaV_strcmp(tsvalue(ra), tsvalue(rb)) <= 0 ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + int res; + VM_PROTECT(res = luaV_lessequal(L, ra, rb)); + + pc += (res == 1) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_JUMPIFNOTLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // fast-path: number + // Note that all jumps below jump by 1 in the "true" case to skip over aux + if (LUAU_LIKELY(ttisnumber(ra) && ttisnumber(rb))) + { + pc += !(nvalue(ra) <= nvalue(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + // fast-path: string + else if (ttisstring(ra) && ttisstring(rb)) + { + pc += !(luaV_strcmp(tsvalue(ra), tsvalue(rb)) <= 0) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + int res; + VM_PROTECT(res = luaV_lessequal(L, ra, rb)); + + pc += (res == 0) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_JUMPIFLT) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // fast-path: number + // Note that all jumps below jump by 1 in the "false" case to skip over aux + if (LUAU_LIKELY(ttisnumber(ra) && ttisnumber(rb))) + { + pc += nvalue(ra) < nvalue(rb) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + // fast-path: string + else if (ttisstring(ra) && ttisstring(rb)) + { + pc += luaV_strcmp(tsvalue(ra), tsvalue(rb)) < 0 ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + int res; + VM_PROTECT(res = luaV_lessthan(L, ra, rb)); + + pc += (res == 1) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_JUMPIFNOTLT) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(aux); + + // fast-path: number + // Note that all jumps below jump by 1 in the "true" case to skip over aux + if (LUAU_LIKELY(ttisnumber(ra) && ttisnumber(rb))) + { + pc += !(nvalue(ra) < nvalue(rb)) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + // fast-path: string + else if (ttisstring(ra) && ttisstring(rb)) + { + pc += !(luaV_strcmp(tsvalue(ra), tsvalue(rb)) < 0) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + int res; + VM_PROTECT(res = luaV_lessthan(L, ra, rb)); + + pc += (res == 0) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_ADD) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb) && ttisnumber(rc))) + { + setnvalue(ra, nvalue(rb) + nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisvector(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb[0] + vc[0], vb[1] + vc[1], vb[2] + vc[2], vb[3] + vc[3]); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_ADD)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, rc); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_SUB) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb) && ttisnumber(rc))) + { + setnvalue(ra, nvalue(rb) - nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisvector(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb[0] - vc[0], vb[1] - vc[1], vb[2] - vc[2], vb[3] - vc[3]); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_SUB)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, rc); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_MUL) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb) && ttisnumber(rc))) + { + setnvalue(ra, nvalue(rb) * nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisnumber(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE vc = cast_to(LUA_VECTOR_TYPE, nvalue(rc)); + setvvalue(L, ra, vb[0] * vc, vb[1] * vc, vb[2] * vc, vb[3] * vc); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisvector(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb[0] * vc[0], vb[1] * vc[1], vb[2] * vc[2], vb[3] * vc[3]); + VM_NEXT(); + } + else if (ttisnumber(rb) && ttisvector(rc)) + { + LUA_VECTOR_TYPE vb = cast_to(LUA_VECTOR_TYPE, nvalue(rb)); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb * vc[0], vb * vc[1], vb * vc[2], vb * vc[3]); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + StkId rbc = ttisnumber(rb) ? rc : rb; + const TValue* fn = 0; + if (ttisuserdata(rbc) && (fn = luaT_gettmbyobj(L, rbc, TM_MUL)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, rc); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_DIV) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb) && ttisnumber(rc))) + { + setnvalue(ra, nvalue(rb) / nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisnumber(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE vc = cast_to(LUA_VECTOR_TYPE, nvalue(rc)); + setvvalue(L, ra, vb[0] / vc, vb[1] / vc, vb[2] / vc, vb[3] / vc); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisvector(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb[0] / vc[0], vb[1] / vc[1], vb[2] / vc[2], vb[3] / vc[3]); + VM_NEXT(); + } + else if (ttisnumber(rb) && ttisvector(rc)) + { + LUA_VECTOR_TYPE vb = cast_to(LUA_VECTOR_TYPE, nvalue(rb)); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, vb / vc[0], vb / vc[1], vb / vc[2], vb / vc[3]); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + StkId rbc = ttisnumber(rb) ? rc : rb; + const TValue* fn = 0; + if (ttisuserdata(rbc) && (fn = luaT_gettmbyobj(L, rbc, TM_DIV)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, rc); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_IDIV) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb) && ttisnumber(rc))) + { + setnvalue(ra, luai_numidiv(nvalue(rb), nvalue(rc))); + VM_NEXT(); + } + else if (ttisvector(rb) && ttisnumber(rc)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE vc = cast_to(LUA_VECTOR_TYPE, nvalue(rc)); + setvvalue( + L, + ra, + LUA_VECTOR_TYPE(luai_numidiv(vb[0], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[1], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[2], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[3], vc)) + ); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + StkId rbc = ttisnumber(rb) ? rc : rb; + const TValue* fn = 0; + if (ttisuserdata(rbc) && (fn = luaT_gettmbyobj(L, rbc, TM_IDIV)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, rc); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_MOD) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb) && ttisnumber(rc)) + { + double nb = nvalue(rb); + double nc = nvalue(rc); + setnvalue(ra, luai_nummod(nb, nc)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + + VM_CASE(LOP_POW) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb) && ttisnumber(rc)) + { + setnvalue(ra, pow(nvalue(rb), nvalue(rc))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rc)); + VM_NEXT(); + } + } + + VM_CASE(LOP_ADDK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb)) + { + setnvalue(ra, nvalue(rb) + nvalue(kv)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + + VM_CASE(LOP_SUBK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb)) + { + setnvalue(ra, nvalue(rb) - nvalue(kv)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + + VM_CASE(LOP_MULK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb))) + { + setnvalue(ra, nvalue(rb) * nvalue(kv)); + VM_NEXT(); + } + else if (ttisvector(rb)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE vc = cast_to(LUA_VECTOR_TYPE, nvalue(kv)); + setvvalue(L, ra, vb[0] * vc, vb[1] * vc, vb[2] * vc, vb[3] * vc); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_MUL)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_DIVK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb))) + { + setnvalue(ra, nvalue(rb) / nvalue(kv)); + VM_NEXT(); + } + else if (ttisvector(rb)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE nc = cast_to(LUA_VECTOR_TYPE, nvalue(kv)); + setvvalue(L, ra, vb[0] / nc, vb[1] / nc, vb[2] / nc, vb[3] / nc); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_DIV)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_IDIVK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb))) + { + setnvalue(ra, luai_numidiv(nvalue(rb), nvalue(kv))); + VM_NEXT(); + } + else if (ttisvector(rb)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + LUA_VECTOR_TYPE vc = cast_to(LUA_VECTOR_TYPE, nvalue(kv)); + setvvalue( + L, + ra, + LUA_VECTOR_TYPE(luai_numidiv(vb[0], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[1], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[2], vc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[3], vc)) + ); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_IDIV)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top = top + 3; + + VM_PROTECT(luaV_callTM(L, 2, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_MODK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb)) + { + double nb = nvalue(rb); + double nk = nvalue(kv); + setnvalue(ra, luai_nummod(nb, nk)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + + VM_CASE(LOP_POWK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rb)) + { + double nb = nvalue(rb); + double nk = nvalue(kv); + + // pow is very slow so we specialize this for ^2, ^0.5 and ^3 + double r = (nk == 2.0) ? nb * nb : (nk == 0.5) ? sqrt(nb) : (nk == 3.0) ? nb * nb * nb : pow(nb, nk); + + setnvalue(ra, r); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, kv)); + VM_NEXT(); + } + } + + VM_CASE(LOP_AND) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + setobj2s(L, ra, l_isfalse(rb) ? rb : rc); + VM_NEXT(); + } + + VM_CASE(LOP_OR) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + setobj2s(L, ra, l_isfalse(rb) ? rc : rb); + VM_NEXT(); + } + + VM_CASE(LOP_ANDK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + setobj2s(L, ra, l_isfalse(rb) ? rb : kv); + VM_NEXT(); + } + + VM_CASE(LOP_ORK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + TValue* kv = VM_KV(LUAU_INSN_C(insn)); + + setobj2s(L, ra, l_isfalse(rb) ? kv : rb); + VM_NEXT(); + } + + VM_CASE(LOP_CONCAT) + { + VM_CASE_INSTRUCTION insn = *pc++; + int b = LUAU_INSN_B(insn); + int c = LUAU_INSN_C(insn); + + // This call may realloc the stack! So we need to query args further down + VM_PROTECT(luaV_concat(L, c - b + 1, c)); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + setobj2s(L, ra, base + b); + VM_PROTECT(luaC_checkGC(L)); + VM_NEXT(); + } + + VM_CASE(LOP_NOT) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + + int res = l_isfalse(rb); + setbvalue(ra, res); + VM_NEXT(); + } + + VM_CASE(LOP_MINUS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rb))) + { + setnvalue(ra, -nvalue(rb)); + VM_NEXT(); + } + else if (ttisvector(rb)) + { + const LUA_VECTOR_TYPE* vb = vvalue(rb); + setvvalue(L, ra, -vb[0], -vb[1], -vb[2], -vb[3]); + VM_NEXT(); + } + else + { + // fast-path for userdata with C functions + const TValue* fn = 0; + if (ttisuserdata(rb) && (fn = luaT_gettmbyobj(L, rb, TM_UNM)) && ttisfunction(fn) && clvalue(fn)->isC) + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 2 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, fn); + setobj2s(L, top + 1, rb); + L->top = top + 2; + + VM_PROTECT(luaV_callTM(L, 1, LUAU_INSN_A(insn))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, rb, rb)); + VM_NEXT(); + } + } + } + + VM_CASE(LOP_LENGTH) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + + // fast-path #1: tables + if (LUAU_LIKELY(ttistable(rb))) + { + LuaTable* h = hvalue(rb); + + if (fastnotm(h->metatable, TM_LEN)) + { + setnvalue(ra, cast_num(luaH_getn(h))); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_dolen(L, ra, rb)); + VM_NEXT(); + } + } + // fast-path #2: strings (not very important but easy to do) + else if (ttisstring(rb)) + { + TString* ts = tsvalue(rb); + setnvalue(ra, cast_num(ts->len)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_dolen(L, ra, rb)); + VM_NEXT(); + } + } + + VM_CASE(LOP_NEWTABLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + int b = LUAU_INSN_B(insn); + uint32_t aux = *pc++; + + VM_PROTECT_PC(); // luaH_new may fail due to OOM + + sethvalue(L, ra, luaH_new(L, aux, b == 0 ? 0 : (1 << (b - 1)))); + VM_PROTECT(luaC_checkGC(L)); + VM_NEXT(); + } + + VM_CASE(LOP_DUPTABLE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_D(insn)); + + VM_PROTECT_PC(); // luaH_clone may fail due to OOM + + sethvalue(L, ra, luaH_clone(L, hvalue(kv))); + VM_PROTECT(luaC_checkGC(L)); + VM_NEXT(); + } + + VM_CASE(LOP_SETLIST) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = &base[LUAU_INSN_B(insn)]; // note: this can point to L->top if c == LUA_MULTRET making VM_REG unsafe to use + int c = LUAU_INSN_C(insn) - 1; + uint32_t index = *pc++; + + if (c == LUA_MULTRET) + { + c = int(L->top - rb); + L->top = L->ci->top; + } + + LuaTable* h = hvalue(ra); + + // TODO: we really don't need this anymore + if (!ttistable(ra)) + return; // temporary workaround to weaken a rather powerful exploitation primitive in case of a MITM attack on bytecode + + int last = index + c - 1; + if (last > h->sizearray) + { + VM_PROTECT_PC(); // luaH_resizearray may fail due to OOM + + luaH_resizearray(L, h, last); + } + + TValue* array = h->array; + + for (int i = 0; i < c; ++i) + setobj2t(L, &array[index + i - 1], rb + i); + + luaC_barrierfast(L, h); + VM_NEXT(); + } + + VM_CASE(LOP_FORNPREP) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + if (!ttisnumber(ra + 0) || !ttisnumber(ra + 1) || !ttisnumber(ra + 2)) + { + // slow-path: can convert arguments to numbers and trigger Lua errors + // Note: this doesn't reallocate stack so we don't need to recompute ra/base + VM_PROTECT_PC(); + + luaV_prepareFORN(L, ra + 0, ra + 1, ra + 2); + } + + double limit = nvalue(ra + 0); + double step = nvalue(ra + 1); + double idx = nvalue(ra + 2); + + // Note: make sure the loop condition is exactly the same between this and LOP_FORNLOOP so that we handle NaN/etc. consistently + pc += (step > 0 ? idx <= limit : limit <= idx) ? 0 : LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_FORNLOOP) + { + VM_INTERRUPT(); + if (FFlag::LuauBackedgeHeapCheck) + VM_CHECK_GC(L); + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + LUAU_ASSERT(ttisnumber(ra + 0) && ttisnumber(ra + 1) && ttisnumber(ra + 2)); + + double limit = nvalue(ra + 0); + double step = nvalue(ra + 1); + double idx = nvalue(ra + 2) + step; + + setnvalue(ra + 2, idx); + + // Note: make sure the loop condition is exactly the same between this and LOP_FORNPREP so that we handle NaN/etc. consistently + if (step > 0 ? idx <= limit : limit <= idx) + { + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // fallthrough to exit + VM_NEXT(); + } + } + + VM_CASE(LOP_FORGPREP) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + if (FFlag::DebugLuauUserDefinedClassesRuntime) + { + // If this is a function it will be called + // during FORGLOOP + if (!ttisfunction(ra)) + { + LuaTable* mt = ttistable(ra) ? hvalue(ra)->metatable : ttisuserdata(ra) ? uvalue(ra)->metatable : cast_to(LuaTable*, NULL); + const TValue* fn = fasttm(L, mt, TM_ITER); + + if (LUAU_UNLIKELY(fn == NULL && ttisobject(ra))) + { + fn = luaT_gettmbyobj(L, ra, TM_ITER); + // if the metamethod is not present, error. + if (ttisnil(fn)) + { + VM_PROTECT_PC(); + luaG_typeerror(L, ra, "iterate over"); + } + } + + if (fn) + { + setobj2s(L, ra + 1, ra); + setobj2s(L, ra, fn); + + L->top = ra + 2; // func + self arg + LUAU_ASSERT(L->top <= L->stack_last); + + VM_PROTECT(luaD_call(L, ra, 3)); + L->top = L->ci->top; + + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + + // protect against __iter returning nil, since nil is used as a marker for builtin iteration in FORGLOOP + if (ttisnil(ra)) + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "call"); + } + } + else if (fasttm(L, mt, TM_CALL)) + { + // table or userdata with __call, will be called during FORGLOOP + // TODO: we might be able to stop supporting this depending on whether it's used in practice + } + else if (ttistable(ra)) + { + // set up registers for builtin iteration + setobj2s(L, ra + 1, ra); + setpvalue(ra + 2, reinterpret_cast(uintptr_t(0)), LU_TAG_ITERATOR); + setnilvalue(ra); + } + else + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "iterate over"); + } + } + } + else + { + + if (ttisfunction(ra)) + { + // will be called during FORGLOOP + } + else + { + LuaTable* mt = ttistable(ra) ? hvalue(ra)->metatable : ttisuserdata(ra) ? uvalue(ra)->metatable : cast_to(LuaTable*, NULL); + + if (const TValue* fn = fasttm(L, mt, TM_ITER)) + { + setobj2s(L, ra + 1, ra); + setobj2s(L, ra, fn); + + L->top = ra + 2; // func + self arg + LUAU_ASSERT(L->top <= L->stack_last); + + VM_PROTECT(luaD_call(L, ra, 3)); + L->top = L->ci->top; + + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + + // protect against __iter returning nil, since nil is used as a marker for builtin iteration in FORGLOOP + if (ttisnil(ra)) + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "call"); + } + } + else if (fasttm(L, mt, TM_CALL)) + { + // table or userdata with __call, will be called during FORGLOOP + // TODO: we might be able to stop supporting this depending on whether it's used in practice + } + else if (ttistable(ra)) + { + // set up registers for builtin iteration + setobj2s(L, ra + 1, ra); + setpvalue(ra + 2, reinterpret_cast(uintptr_t(0)), LU_TAG_ITERATOR); + setnilvalue(ra); + } + else + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "iterate over"); + } + } + } + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_FORGLOOP) + { + VM_INTERRUPT(); + if (FFlag::LuauBackedgeHeapCheck) + VM_CHECK_GC(L); + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + uint32_t aux = *pc; + + // fast-path: builtin table iteration + // note: ra=nil guarantees ra+1=table and ra+2=userdata because of the setup by FORGPREP* opcodes + // TODO: remove the table check per guarantee above + if (ttisnil(ra) && ttistable(ra + 1)) + { + LuaTable* h = hvalue(ra + 1); + int index = int(reinterpret_cast(pvalue(ra + 2))); + + int sizearray = h->sizearray; + + // clear extra variables since we might have more than two + // note: while aux encodes ipairs bit, when set we always use 2 variables, so it's safe to check this via a signed comparison + if (LUAU_UNLIKELY(int(aux) > 2)) + for (int i = 2; i < int(aux); ++i) + setnilvalue(ra + 3 + i); + + // terminate ipairs-style traversal early when encountering nil + if (int(aux) < 0 && (unsigned(index) >= unsigned(sizearray) || ttisnil(&h->array[index]))) + { + pc++; + VM_NEXT(); + } + + // first we advance index through the array portion + while (unsigned(index) < unsigned(sizearray)) + { + TValue* e = &h->array[index]; + + if (!ttisnil(e)) + { + setpvalue(ra + 2, reinterpret_cast(uintptr_t(index + 1)), LU_TAG_ITERATOR); + setnvalue(ra + 3, double(index + 1)); + setobj2s(L, ra + 4, e); + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + index++; + } + + int sizenode = 1 << h->lsizenode; + + // then we advance index through the hash portion + while (unsigned(index - sizearray) < unsigned(sizenode)) + { + LuaNode* n = &h->node[index - sizearray]; + + if (!ttisnil(gval(n))) + { + setpvalue(ra + 2, reinterpret_cast(uintptr_t(index + 1)), LU_TAG_ITERATOR); + getnodekey(L, ra + 3, n); + setobj2s(L, ra + 4, gval(n)); + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + index++; + } + + // fallthrough to exit + pc++; + VM_NEXT(); + } + else + { + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + setobj2s(L, ra + 3 + 2, ra + 2); + setobj2s(L, ra + 3 + 1, ra + 1); + setobj2s(L, ra + 3, ra); + + L->top = ra + 3 + 3; // func + 2 args (state and index) + LUAU_ASSERT(L->top <= L->stack_last); + + bool yielded; + VM_PROTECT(yielded = luaD_performcally(L, ra + 3, uint8_t(aux))); + + if (yielded) + goto exit; + + L->top = L->ci->top; + + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + + // copy first variable back into the iteration index + setobj2s(L, ra + 2, ra + 3); + + // note that we need to increment pc by 1 to exit the loop since we need to skip over aux + pc += ttisnil(ra + 3) ? 1 : LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + } + + VM_CASE(LOP_FORGPREP_INEXT) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + // fast-path: ipairs/inext + if (cl->env->safeenv && ttistable(ra + 1) && ttisnumber(ra + 2) && nvalue(ra + 2) == 0.0) + { + setnilvalue(ra); + // ra+1 is already the table + setpvalue(ra + 2, reinterpret_cast(uintptr_t(0)), LU_TAG_ITERATOR); + } + else if (!ttisfunction(ra)) + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "iterate over"); + } + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_FORGPREP_NEXT) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + // fast-path: pairs/next + if (cl->env->safeenv && ttistable(ra + 1) && ttisnil(ra + 2)) + { + setnilvalue(ra); + // ra+1 is already the table + setpvalue(ra + 2, reinterpret_cast(uintptr_t(0)), LU_TAG_ITERATOR); + } + else if (!ttisfunction(ra)) + { + VM_PROTECT_PC(); // next call always errors + luaG_typeerror(L, ra, "iterate over"); + } + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_NATIVECALL) + { + Proto* p = L->ci->p; + LUAU_ASSERT(p->execdata); + + CallInfo* ci = L->ci; + + if (FFlag::LuauFastpcall) + ci->flags |= LUA_CALLINFO_NATIVE; + else + ci->flags = LUA_CALLINFO_NATIVE; + + ci->savedpc = p->code; + +#if VM_HAS_NATIVE + if (L->global->ecb.enter(L, p) == 1) + goto reentry; + else + goto exit; +#else + LUAU_ASSERT(!"Opcode is only valid when VM_HAS_NATIVE is defined"); + LUAU_UNREACHABLE(); +#endif + } + + VM_CASE(LOP_GETVARARGS) + { + VM_CASE_INSTRUCTION insn = *pc++; + int b = LUAU_INSN_B(insn) - 1; + int n = cast_int(base - L->ci->func) - L->ci->p->numparams - 1; + + if (b == LUA_MULTRET) + { + VM_PROTECT(luaD_checkstack(L, n)); + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); // previous call may change the stack + + for (int j = 0; j < n; j++) + setobj2s(L, ra + j, base - n + j); + + L->top = ra + n; + VM_NEXT(); + } + else + { + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + for (int j = 0; j < b && j < n; j++) + setobj2s(L, ra + j, base - n + j); + for (int j = n; j < b; j++) + setnilvalue(ra + j); + VM_NEXT(); + } + } + + VM_CASE(LOP_DUPCLOSURE) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_D(insn)); + + Closure* kcl = clvalue(kv); + + VM_PROTECT_PC(); // luaF_newLclosure may fail due to OOM + + // clone closure if the environment is not shared + // note: we save closure to stack early in case the code below wants to capture it by value + Closure* ncl = + (kcl->env == cl->env) ? kcl : luaF_newLclosure(L, kcl->nupvalues, cl->env, getproto(kcl)); + setclvalue(L, ra, ncl); + + // this loop does three things: + // - if the closure was created anew, it just fills it with upvalues + // - if the closure from the constant table is used, it fills it with upvalues so that it can be shared in the future + // - if the closure is reused, it checks if the reuse is safe via rawequal, and falls back to duplicating the closure + // normally this would use two separate loops, for reuse check and upvalue setup, but MSVC codegen goes crazy if you do that + for (int ui = 0; ui < kcl->nupvalues; ++ui) + { + Instruction uinsn = pc[ui]; + LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); + LUAU_ASSERT(LUAU_INSN_A(uinsn) == LCT_VAL || LUAU_INSN_A(uinsn) == LCT_UPVAL); + + TValue* uv = (LUAU_INSN_A(uinsn) == LCT_VAL) ? VM_REG(LUAU_INSN_B(uinsn)) : VM_UV(LUAU_INSN_B(uinsn)); + + // check if the existing closure is safe to reuse + if (ncl == kcl && luaO_rawequalObj(&ncl->l.uprefs[ui], uv)) + continue; + + // lazily clone the closure and update the upvalues + if (ncl == kcl && kcl->preload == 0) + { + ncl = luaF_newLclosure(L, kcl->nupvalues, cl->env, getproto(kcl)); + setclvalue(L, ra, ncl); + + ui = -1; // restart the loop to fill all upvalues + continue; + } + + // this updates a newly created closure, or an existing closure created during preload, in which case we need a barrier + setobj(L, &ncl->l.uprefs[ui], uv); + luaC_barrier(L, ncl, uv); + } + + // this is a noop if ncl is newly created or shared successfully, but it has to run after the closure is preloaded for the first time + ncl->preload = 0; + + if (kcl != ncl) + VM_PROTECT(luaC_checkGC(L)); + + pc += kcl->nupvalues; + VM_NEXT(); + } + + VM_CASE(LOP_PREPVARARGS) + { + VM_CASE_INSTRUCTION insn = *pc++; + int numparams = LUAU_INSN_A(insn); + + // all fixed parameters are copied after the top so we need more stack space + VM_PROTECT(luaD_checkstack(L, cl->stacksize + numparams)); + + // the caller must have filled extra fixed arguments with nil + LUAU_ASSERT(cast_int(L->top - base) >= numparams); + + // move fixed parameters to final position + StkId fixed = base; // first fixed argument + base = L->top; // final position of first argument + + for (int i = 0; i < numparams; ++i) + { + setobj2s(L, base + i, fixed + i); + setnilvalue(fixed + i); + } + + // rewire our stack frame to point to the new base + L->ci->base = base; + L->ci->top = base + cl->stacksize; + + L->base = base; + L->top = L->ci->top; + VM_NEXT(); + } + VM_CASE(LOP_JUMPBACK) + { + VM_INTERRUPT(); + if (FFlag::LuauBackedgeHeapCheck) + VM_CHECK_GC(L); + VM_CASE_INSTRUCTION insn = *pc++; + + pc += LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_LOADKX) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + + setobj2s(L, ra, kv); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPX) + { + VM_INTERRUPT(); + if (FFlag::LuauBackedgeHeapCheck) + VM_CHECK_GC(L); + VM_CASE_INSTRUCTION insn = *pc++; + + pc += LUAU_INSN_E(insn); + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_FASTCALL) + { + VM_CASE_INSTRUCTION insn = *pc++; + int bfid = LUAU_INSN_A(insn); + int skip = LUAU_INSN_C(insn); + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = LUAU_INSN_B(call) - 1; + int nresults = LUAU_INSN_C(call) - 1; + + nparams = (nparams == LUA_MULTRET) ? int(L->top - ra - 1) : nparams; + + luau_FastFunction f = luauF_table[bfid]; + LUAU_ASSERT(f); + + if (cl->env->safeenv) + { + VM_PROTECT_PC(); // f may fail due to OOM + + int n = f(L, ra, ra + 1, nresults, ra + 2, nparams); + + if (n >= 0) + { + // when nresults != MULTRET, L->top might be pointing to the middle of stack frame if nparams is equal to MULTRET + // instead of restoring L->top to L->ci->top if nparams is MULTRET, we do it unconditionally to skip an extra check + L->top = (nresults == LUA_MULTRET) ? ra + n : L->ci->top; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + + VM_CASE(LOP_COVERAGE) + { + VM_CASE_INSTRUCTION insn = *pc++; + int hits = LUAU_INSN_E(insn); + + // update hits with saturated add and patch the instruction in place + hits = (hits < (1 << 23) - 1) ? hits + 1 : hits; + VM_PATCH_E(pc - 1, hits); + + VM_NEXT(); + } + + VM_CASE(LOP_CAPTURE) + { + LUAU_ASSERT(!"CAPTURE is a pseudo-opcode and must be executed as part of NEWCLOSURE"); + LUAU_UNREACHABLE(); + } + + VM_CASE(LOP_SUBRK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rc)) + { + setnvalue(ra, nvalue(kv) - nvalue(rc)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, kv, rc)); + VM_NEXT(); + } + } + + VM_CASE(LOP_DIVRK) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_B(insn)); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rc))) + { + setnvalue(ra, nvalue(kv) / nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rc)) + { + LUA_VECTOR_TYPE nb = cast_to(LUA_VECTOR_TYPE, nvalue(kv)); + const LUA_VECTOR_TYPE* vc = vvalue(rc); + setvvalue(L, ra, nb / vc[0], nb / vc[1], nb / vc[2], nb / vc[3]); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarithimpl(L, ra, kv, rc)); + VM_NEXT(); + } + } + + VM_CASE(LOP_FASTCALL1) + { + VM_CASE_INSTRUCTION insn = *pc++; + int bfid = LUAU_INSN_A(insn); + TValue* arg = VM_REG(LUAU_INSN_B(insn)); + int skip = LUAU_INSN_C(insn); + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = 1; + int nresults = LUAU_INSN_C(call) - 1; + + luau_FastFunction f = luauF_table[bfid]; + LUAU_ASSERT(f); + + if (cl->env->safeenv) + { + VM_PROTECT_PC(); // f may fail due to OOM + + int n = f(L, ra, arg, nresults, NULL, nparams); + + if (n >= 0) + { + if (nresults == LUA_MULTRET) + L->top = ra + n; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + + VM_CASE(LOP_FASTCALL2) + { + VM_CASE_INSTRUCTION insn = *pc++; + int bfid = LUAU_INSN_A(insn); + int skip = LUAU_INSN_C(insn) - 1; + uint32_t aux = *pc++; + TValue* arg1 = VM_REG(LUAU_INSN_B(insn)); + TValue* arg2 = VM_REG(aux); + + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = 2; + int nresults = LUAU_INSN_C(call) - 1; + + luau_FastFunction f = luauF_table[bfid]; + LUAU_ASSERT(f); + + if (cl->env->safeenv) + { + VM_PROTECT_PC(); // f may fail due to OOM + + int n = f(L, ra, arg1, nresults, arg2, nparams); + + if (n >= 0) + { + if (nresults == LUA_MULTRET) + L->top = ra + n; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + + VM_CASE(LOP_FASTCALL2K) + { + VM_CASE_INSTRUCTION insn = *pc++; + int bfid = LUAU_INSN_A(insn); + int skip = LUAU_INSN_C(insn) - 1; + uint32_t aux = *pc++; + TValue* arg1 = VM_REG(LUAU_INSN_B(insn)); + TValue* arg2 = VM_KV(aux); + + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = 2; + int nresults = LUAU_INSN_C(call) - 1; + + luau_FastFunction f = luauF_table[bfid]; + LUAU_ASSERT(f); + + if (cl->env->safeenv) + { + VM_PROTECT_PC(); // f may fail due to OOM + + int n = f(L, ra, arg1, nresults, arg2, nparams); + + if (n >= 0) + { + if (nresults == LUA_MULTRET) + L->top = ra + n; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + + VM_CASE(LOP_FASTCALL3) + { + VM_CASE_INSTRUCTION insn = *pc++; + int bfid = LUAU_INSN_A(insn); + int skip = LUAU_INSN_C(insn) - 1; + uint32_t aux = *pc++; + TValue* arg1 = VM_REG(LUAU_INSN_B(insn)); + TValue* arg2 = VM_REG(LUAU_INSN_AUX_A(aux)); + TValue* arg3 = VM_REG(LUAU_INSN_AUX_B(aux)); + + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = 3; + int nresults = LUAU_INSN_C(call) - 1; + + luau_FastFunction f = luauF_table[bfid]; + LUAU_ASSERT(f); + + if (cl->env->safeenv) + { + VM_PROTECT_PC(); // f may fail due to OOM + + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 2 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top, arg2); + setobj2s(L, top + 1, arg3); + + int n = f(L, ra, arg1, nresults, top, nparams); + + if (n >= 0) + { + if (nresults == LUA_MULTRET) + L->top = ra + n; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + else + { + // continue execution through the fallback code + VM_NEXT(); + } + } + + VM_CASE(LOP_BREAK) + { + LUAU_ASSERT(L->ci->p->debuginsn); + + uint8_t op = L->ci->p->debuginsn[unsigned(pc - L->ci->p->code)]; + LUAU_ASSERT(op != LOP_BREAK); + + if (L->global->cb.debugbreak) + { + VM_PROTECT(luau_callhook(L, L->global->cb.debugbreak, NULL)); + + // allow debugbreak hook to put thread into error/yield state + if (L->status != 0) + goto exit; + } + + VM_CONTINUE(op); + } + + VM_CASE(LOP_JUMPXEQKNIL) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + static_assert(LUA_TNIL == 0, "we expect type-1 to be negative iff type is nil"); + // condition is equivalent to: int(ttisnil(ra)) != LUAU_INSN_AUX_NOT(aux) + pc += int((ttype(ra) - 1) ^ aux) < 0 ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPXEQKB) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + + pc += int(ttisboolean(ra) && bvalue(ra) == int(LUAU_INSN_AUX_KB(aux))) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPXEQKN) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_AUX_KV(aux)); + LUAU_ASSERT(ttisnumber(kv)); + +#if defined(__aarch64__) + // On several ARM chips (Apple M1/M2, Neoverse N1), comparing the result of a floating-point comparison is expensive, and a branch + // is much cheaper; on some 32-bit ARM chips (Cortex A53) the performance is about the same so we prefer less branchy variant there + if (LUAU_INSN_AUX_NOT(aux)) + pc += !(ttisnumber(ra) && nvalue(ra) == nvalue(kv)) ? LUAU_INSN_D(insn) : 1; + else + pc += (ttisnumber(ra) && nvalue(ra) == nvalue(kv)) ? LUAU_INSN_D(insn) : 1; +#else + pc += int(ttisnumber(ra) && nvalue(ra) == nvalue(kv)) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; +#endif + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_JUMPXEQKS) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_AUX_KV(aux)); + LUAU_ASSERT(ttisstring(kv)); + + pc += int(ttisstring(ra) && gcvalue(ra) == gcvalue(kv)) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_GETUDATAKS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + uint32_t kidx = LUAU_INSN_AUX_KV16(aux); + TValue* kv = VM_KV(kidx); + + if (LUAU_LIKELY(ttisuserdata(rb))) + { + int utag = uvalue(rb)->tag; + lua_UdataDirectAccessData& udatadirect = L->global->udatadirect[utag]; + lua_UserdataDirectAccess onudataindex = udatadirect.index; + TValue* tm = &udatadirect.indextm; + + if (LUAU_LIKELY(onudataindex != nullptr && !ttisnil(tm))) + { + void* udata = uvalue(rb)->data; + + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 3 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, tm); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + L->top += 3; + + L->ci->savedpc = pc; + + ++L->nCcalls; + + if (L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + luau_setupcci(L, 1, top); + + uint16_t cachedslot = LUAU_INSN_AUX_SLOT(aux); + onudataindex(L, udata, tsvalue(kv)->atom, &cachedslot, utag); + + // update cached slot if instruction didn't deoptimize + if (cachedslot != LUAU_INSN_AUX_SLOT(aux) && LUAU_INSN_OP(*(pc - 2)) == LOP_GETUDATAKS) + VM_PATCH_AUX_SLOT(pc - 1, kidx, cachedslot); + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + L->ci = cip; + L->base = cip->base; + --L->nCcalls; + + // stack may have been reallocated, so we need to refresh base ptr + base = L->base; + ra = VM_REG(LUAU_INSN_A(insn)); + + // grab result while L->top is still pointed to the previous function frame + setobj2s(L, ra, L->top - 1); + + // then update top + L->top = cip->top; + + VM_NEXT(); + } + } + + // Slow path - backpatch and dispatch to regular table access + VM_PATCH_OP(pc - 2, LOP_GETTABLEKS); + VM_PATCH_AUX_SLOT(pc - 1, kidx, 0); + + pc -= 2; + VM_CONTINUE(LOP_GETTABLEKS); + } + + VM_CASE(LOP_SETUDATAKS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + uint32_t kidx = LUAU_INSN_AUX_KV16(aux); + TValue* kv = VM_KV(kidx); + + if (LUAU_LIKELY(ttisuserdata(rb))) + { + int utag = uvalue(rb)->tag; + lua_UdataDirectAccessData& udatadirect = L->global->udatadirect[utag]; + lua_UserdataDirectAccess onudatanewindex = udatadirect.newindex; + TValue* tm = &udatadirect.newindextm; + + if (LUAU_LIKELY(onudatanewindex != nullptr && !ttisnil(tm))) + { + void* udata = uvalue(rb)->data; + + // note: it's safe to push arguments past top for complicated reasons (see top of the file) + LUAU_ASSERT(L->top + 4 < L->stack + L->stacksize); + StkId top = L->top; + setobj2s(L, top + 0, tm); + setobj2s(L, top + 1, rb); + setobj2s(L, top + 2, kv); + setobj2s(L, top + 3, ra); + L->top += 4; + + L->ci->savedpc = pc; + + ++L->nCcalls; + + if (L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + luau_setupcci(L, 0, top); + + uint16_t cachedslot = LUAU_INSN_AUX_SLOT(aux); + onudatanewindex(L, udata, tsvalue(kv)->atom, &cachedslot, utag); + + // update cached slot if instruction didn't deoptimize + if (cachedslot != LUAU_INSN_AUX_SLOT(aux) && LUAU_INSN_OP(*(pc - 2)) == LOP_SETUDATAKS) + VM_PATCH_AUX_SLOT(pc - 1, kidx, cachedslot); + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + L->ci = cip; + L->base = cip->base; + L->top = cip->top; + --L->nCcalls; + + // stack may have been reallocated, so we need to refresh base ptr + base = L->base; + + VM_NEXT(); + } + } + + // Slow path - backpatch and dispatch to regular table access + VM_PATCH_OP(pc - 2, LOP_SETTABLEKS); + VM_PATCH_AUX_SLOT(pc - 1, kidx, 0); + + pc -= 2; + VM_CONTINUE(LOP_SETTABLEKS); + } + + VM_CASE(LOP_NAMECALLUDATA) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + VM_CASE_STKID rb = VM_REG(LUAU_INSN_B(insn)); + uint32_t aux = *pc++; + uint32_t kidx = LUAU_INSN_AUX_KV16(aux); + TValue* kv = VM_KV(kidx); + + if (LUAU_LIKELY(ttisuserdata(rb))) + { + int utag = uvalue(rb)->tag; + lua_UdataDirectAccessData& udatadirect = L->global->udatadirect[utag]; + lua_UserdataDirectNamecall onudatanamecall = udatadirect.namecall; + TValue* tm = &udatadirect.namecalltm; + + if (LUAU_LIKELY(onudatanamecall != nullptr && !ttisnil(tm))) + { + void* udata = uvalue(rb)->data; + + // note: order of copies allows rb to alias ra+1 or ra + setobj2s(L, ra + 1, rb); + setobj2s(L, ra, tm); + const Instruction* ncslot = pc - 1; + + LUAU_ASSERT(LUAU_INSN_OP(*pc) == LOP_CALL || LUAU_INSN_OP(*pc) == LOP_CALLFB); + insn = *pc++; + if (FFlag::LuauCallFeedback && LUAU_INSN_OP(insn) == LOP_CALLFB) + pc++; + + StkId callRa = VM_REG(LUAU_INSN_A(insn)); + LUAU_ASSERT(callRa == ra); + + // first half of OP_CALL + int nparams = LUAU_INSN_B(insn) - 1; + int nresults = LUAU_INSN_C(insn) - 1; + + L->ci->savedpc = pc; + L->namecall = tsvalue(kv); + L->top = (nparams == LUA_MULTRET) ? L->top : ra + 1 + nparams; + + // note: namecalls do not increase C call number and allow yielding + + luau_setupcci(L, nresults, ra); + + LUAU_ASSERT(tsvalue(kv)->atom >= 0); + + uint16_t cachedslot = LUAU_INSN_AUX_SLOT(aux); + int results = onudatanamecall(L, udata, tsvalue(kv)->atom, &cachedslot, utag); + + // update cached slot if instruction didn't deoptimize + if (cachedslot != LUAU_INSN_AUX_SLOT(aux) && LUAU_INSN_OP(*(ncslot - 1)) == LOP_NAMECALLUDATA) + VM_PATCH_AUX_SLOT(ncslot, kidx, cachedslot); + + // yield + if (results < 0) + return; + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + StkId res = ci->func; + StkId vali = L->top - results; + StkId valend = L->top; + + int i; + for (i = nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + L->ci = cip; + L->base = cip->base; + L->top = (nresults == LUA_MULTRET) ? res : cip->top; + + // stack may have been reallocated, so we need to refresh base ptr + base = L->base; + + VM_NEXT(); + } + } + + // Slow path - backpatch and dispatch to regular namecall + VM_PATCH_OP(pc - 2, LOP_NAMECALL); + VM_PATCH_AUX_SLOT(pc - 1, kidx, 0); + + pc -= 2; + VM_CONTINUE(LOP_NAMECALL); + } + + VM_CASE(LOP_NEWCLASSMEMBER) + { + VM_CASE_INSTRUCTION insn = *pc++; + uint32_t aux = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + TValue* membername = VM_KV(aux); + LUAU_ASSERT(ttisstring(membername)); + LUAU_ASSERT(LUAU_INSN_B(insn) == 0); + VM_CASE_STKID rc = VM_REG(LUAU_INSN_C(insn)); + VM_PROTECT_PC(); + luaR_addclassmember(L, classvalue(ra), tsvalue(membername), rc); + VM_NEXT(); + } + + VM_CASE(LOP_CMPPROTO) + { + Instruction insn = *pc++; + uint32_t funid = *pc++; + StkId ra = VM_REG(LUAU_INSN_A(insn)); + + if (LUAU_UNLIKELY(!ttisfunction(ra))) + { + pc += LUAU_INSN_D(insn) - 1; + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + Closure* ccl = clvalue(ra); + if (ccl->isC || ccl->l.p->funid != funid) + pc += LUAU_INSN_D(insn) - 1; + + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + VM_CASE(LOP_FASTPCALL) + { + VM_CASE_INSTRUCTION insn = *pc++; + + // even with compiler flag enabled, runtime can be safely disabled and will execute the fallback + if (!FFlag::LuauFastpcall) + VM_NEXT(); + + if (FFlag::LuauFastpcallInterrupt) + VM_INTERRUPT(); + + int pfid = LUAU_INSN_A(insn); + int skip = LUAU_INSN_C(insn); + VM_ASSERT_PC(pc + skip); + + Instruction call = pc[skip]; + LUAU_ASSERT(LUAU_INSN_OP(call) == LOP_CALL); + + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(call)); + + int nparams = LUAU_INSN_B(call) - 1; + int nresults = LUAU_INSN_C(call) - 1; + + nparams = (nparams == LUA_MULTRET) ? int(L->top - ra - 1) : nparams; + + // fast protected calls are only supported in safe environments and in yieldable contexts + if (LUAU_UNLIKELY(!cl->env->safeenv) || L->nCcalls > L->baseCcalls) + VM_NEXT(); + + int errfunc = luauPF_table[pfid](L, ra, nparams); + + if (errfunc < 0) + VM_NEXT(); + + L->ci->savedpc = pc + skip + 1; // return skips the fallback path + + L->top = ra + 1 + nparams; + + // note: creating a call frame can invalidate 'ra' and other StkId + luau_pushhandlerci(L, ra, errfunc, nresults); + + // prepare target call, note that we request fewer results from the target as pcalls provide 'status' + StkId callerfunc = L->ci->base + errfunc; + int calleeresults = (nresults > 0) ? nresults - 1 : nresults; + int pr = luau_precall(L, callerfunc, calleeresults); + + if (LUAU_LIKELY(pr == PCRLUA)) + { + // target Luau function must return so that C continuation can be processed + L->ci->flags |= LUA_CALLINFO_RETURN; + + Closure* fcl = clvalue(L->ci->func); + Proto* p = L->ci->p; + + // reentry into the call (see LOP_CALL for description of how native calls are handled with 'codeentry') + pc = SingleStep ? p->code : p->codeentry; + cl = fcl; + base = L->base; + k = p->k; + VM_NEXT(); + } + else if (pr == PCRC) + { + luau_pospcallsuccess(L); + base = L->base; + + pc += skip + 1; // skip instructions that compute function as well as CALL + VM_ASSERT_PC(pc); + VM_NEXT(); + } + + LUAU_ASSERT(pr == PCRYIELD); + goto exit; + } + + VM_CASE(LOP_NEWCLASS) + { + VM_CASE_INSTRUCTION insn = *pc++; + VM_CASE_STKID ra = VM_REG(LUAU_INSN_A(insn)); + uint8_t super = LUAU_INSN_B(insn); + + // Load and clone class object from constant table using offset in aux + uint32_t aux = *pc++; + TValue* kv = VM_KV(aux); + + VM_PROTECT_PC(); + LuauClass* newcls = luaR_cloneclass(L, classvalue(kv)); + setclassvalue(L, ra, newcls); + newcls->isopen = (LUAU_INSN_C(insn) & 0x1u) != 0; // bottom bit of C is the isopen flag + + if (super != 0xff) + { + VM_CASE_STKID rb = VM_REG(super); + + if (LUAU_UNLIKELY(!ttisclass(rb))) + luaG_typeerror(L, rb, "extend"); + + luaR_inheritclass(L, newcls, classvalue(rb)); + } + + VM_NEXT(); + } + +#if !VM_USE_CGOTO + default: + LUAU_ASSERT(!"Unknown opcode"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks +#endif + } + } + +exit:; +} + +void luau_execute(lua_State* L) +{ + if (L->singlestep) + luau_execute(L); + else + luau_execute(L); +} + +void luau_finishop(lua_State* L) +{ + CallInfo* ci = L->ci; + ci->flags &= ~LUA_CALLINFO_OPYIELD; + + StkId base = L->base; + + const Instruction* pc = ci->savedpc; + Instruction insn = *(pc - 1); // the interrupted instruction + + switch (LUAU_INSN_OP(insn)) + { + case LOP_FORGLOOP: + { + StkId ra = VM_REG(LUAU_INSN_A(insn)); + + // copy first variable back into the iteration index + setobj2s(L, ra + 2, ra + 3); + + // note that we need to increment pc by 1 to exit the loop since we need to skip over aux + pc += ttisnil(ra + 3) ? 1 : LUAU_INSN_D(insn); + VM_ASSERT_PC(pc); + break; + } + default: + LUAU_ASSERT(!"Unknown opcode"); + LUAU_UNREACHABLE(); + } + + L->ci->savedpc = pc; +} + +int luau_precall(lua_State* L, StkId func, int nresults) +{ + if (!ttisfunction(func)) + { + luaV_tryfuncTM(L, func); + // L->top is incremented by tryfuncTM + } + + Closure* ccl = clvalue(func); + + CallInfo* ci = incr_ci(L); + ci->func = func; + ci->p = getproto(ccl); + ci->base = func + 1; + ci->top = L->top + ccl->stacksize; + ci->savedpc = NULL; + ci->flags = 0; + ci->nresults = nresults; + + L->base = ci->base; + // Note: L->top is assigned externally + + luaD_checkstackfornewci(L, ccl->stacksize); + LUAU_ASSERT(ci->top <= L->stack_last); + + if (!ccl->isC) + { + Proto* p = ccl->l.p; + + // fill unused parameters with nil + StkId argi = L->top; + StkId argend = L->base + p->numparams; + while (argi < argend) + setnilvalue(argi++); // complete missing arguments + L->top = p->is_vararg ? argi : ci->top; + + ci->savedpc = p->code; + +#if VM_HAS_NATIVE + if (p->exectarget != 0 && p->execdata) + ci->flags = LUA_CALLINFO_NATIVE; +#endif + + return PCRLUA; + } + else + { + lua_CFunction func = ccl->c.f; + int n = func(L); + + // yield + if (n < 0) + return PCRYIELD; + + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + // copy return values into parent stack (but only up to nresults!), fill the rest with nil + // TODO: it might be worthwhile to handle the case when nresults==b explicitly? + StkId res = ci->func; + StkId vali = L->top - n; + StkId valend = L->top; + + int i; + for (i = nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = res; + + return PCRC; + } +} + +void luau_pushhandlerci(lua_State* L, StkId funcslot, int errfunc, int nresults) +{ + Closure* ccl = clvalue(funcslot); + LUAU_ASSERT(ccl->isC && ccl->c.cont); + + CallInfo* ci = incr_ci(L); + ci->func = funcslot; + ci->p = nullptr; + ci->base = funcslot + 1; + ci->top = L->top + ccl->stacksize; + ci->flags = LUA_CALLINFO_HANDLE | LUA_CALLINFO_PCALL; + ci->nresults = nresults; + ci->errfunc = errfunc; + + // Note: L->top is assigned externally + + luaD_checkstackfornewci(L, int(ccl->stacksize)); + LUAU_ASSERT(ci->top <= L->stack_last); +} + +void luau_poscall(lua_State* L, StkId first) +{ + // finish interrupted execution of `OP_CALL' + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + + // copy return values into parent stack (but only up to nresults!), fill the rest with nil + // TODO: it might be worthwhile to handle the case when nresults==b explicitly? + StkId res = ci->func; + StkId vali = first; + StkId valend = L->top; + + int i; + for (i = ci->nresults; i != 0 && vali < valend; i--) + setobj2s(L, res++, vali++); + while (i-- > 0) + setnilvalue(res++); + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = (ci->nresults == LUA_MULTRET) ? res : cip->top; +} + +void luau_pospcallsuccess(lua_State* L) +{ + // finish interrupted execution of a fast protected call + // ci is our callinfo, cip is our parent + CallInfo* ci = L->ci; + CallInfo* cip = ci - 1; + int calleeresults = (ci->nresults > 0) ? ci->nresults - 1 : ci->nresults; + + // return 'status' value in-place + StkId res = ci->func; + setbvalue(res++, 1); + + // for functions with an error handler (e.g. xpcall), shift results one left over it + if (ci->errfunc != 0) + { + StkId vali = L->base + ci->errfunc; + StkId valend = (calleeresults == LUA_MULTRET) ? L->top : (vali + calleeresults); + while (vali < valend) + setobj2s(L, res++, vali++); + } + else + { + res += (calleeresults == LUA_MULTRET) ? int(L->top - L->base) : calleeresults; + } + + // pop the stack frame + L->ci = cip; + L->base = cip->base; + L->top = (calleeresults == LUA_MULTRET) ? res : cip->top; +} diff --git a/External/luau/VM/src/lvmload.cpp b/External/luau/VM/src/lvmload.cpp new file mode 100644 index 00000000..4fa87cee --- /dev/null +++ b/External/luau/VM/src/lvmload.cpp @@ -0,0 +1,875 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lclass.h" +#include "lvm.h" + +#include "lstate.h" +#include "ltable.h" +#include "lfunc.h" +#include "lobject.h" +#include "lstring.h" +#include "lvector.h" + +#include "lgc.h" +#include "lmem.h" +#include "lbytecode.h" +#include "lapi.h" + +#include + +LUAU_FASTFLAG(LuauCallFeedback) +LUAU_FASTFLAGVARIABLE(LuauCostModel) +LUAU_FASTFLAGVARIABLE(LuauLoadRemapOptionalUserdata) + +template +struct TempBuffer +{ + lua_State* L; + T* data; + size_t count; + + TempBuffer() + : L(NULL) + , data(NULL) + , count(0) + { + } + + TempBuffer(const TempBuffer&) = delete; + TempBuffer(TempBuffer&&) = delete; + + TempBuffer& operator=(const TempBuffer&) = delete; + TempBuffer& operator=(TempBuffer&&) = delete; + + ~TempBuffer() noexcept + { + if (data) + luaM_freearray(L, data, count, T, 0); + } + + void allocate(lua_State* L, size_t count) + { + LUAU_ASSERT(this->L == nullptr); + this->L = L; + this->data = luaM_newarray(L, count, T, 0); + this->count = count; + } + + T& operator[](size_t index) + { + LUAU_ASSERT(index < count); + return data[index]; + } +}; + +struct ScopedSetGCThreshold +{ +public: + ScopedSetGCThreshold(global_State* global, size_t newThreshold) noexcept + : global{global} + { + originalThreshold = global->GCthreshold; + global->GCthreshold = newThreshold; + } + + ScopedSetGCThreshold(const ScopedSetGCThreshold&) = delete; + ScopedSetGCThreshold(ScopedSetGCThreshold&&) = delete; + + ScopedSetGCThreshold& operator=(const ScopedSetGCThreshold&) = delete; + ScopedSetGCThreshold& operator=(ScopedSetGCThreshold&&) = delete; + + ~ScopedSetGCThreshold() noexcept + { + global->GCthreshold = originalThreshold; + } + +private: + global_State* global = nullptr; + size_t originalThreshold = 0; +}; + +void luaV_getimport(lua_State* L, LuaTable* env, TValue* k, StkId res, uint32_t id, bool propagatenil) +{ + int count = id >> 30; + LUAU_ASSERT(count > 0); + + int id0 = int(id >> 20) & 1023; + int id1 = int(id >> 10) & 1023; + int id2 = int(id) & 1023; + + // after the first call to luaV_gettable, res may be invalid, and env may (sometimes) be garbage collected + // we take care to not use env again and to restore res before every consecutive use + ptrdiff_t resp = savestack(L, res); + + // global lookup for id0 + TValue g; + sethvalue(L, &g, env); + luaV_gettable(L, &g, &k[id0], res); + + // table lookup for id1 + if (count < 2) + return; + + res = restorestack(L, resp); + if (!propagatenil || !ttisnil(res)) + luaV_gettable(L, res, &k[id1], res); + + // table lookup for id2 + if (count < 3) + return; + + res = restorestack(L, resp); + if (!propagatenil || !ttisnil(res)) + luaV_gettable(L, res, &k[id2], res); +} + +template +static T read(const char* data, size_t size, size_t& offset) +{ + T result; + memcpy(&result, data + offset, sizeof(T)); + offset += sizeof(T); + + return result; +} + +static unsigned int readVarInt(const char* data, size_t size, size_t& offset) +{ + unsigned int result = 0; + unsigned int shift = 0; + + uint8_t byte; + + do + { + byte = read(data, size, offset); + result |= (byte & 127) << shift; + shift += 7; + } while (byte & 128); + + return result; +} + +static uint64_t readVarInt64(const char* data, size_t size, size_t& offset) +{ + uint64_t result = 0; + unsigned int shift = 0; + + uint8_t byte; + + do + { + byte = read(data, size, offset); + result |= ((uint64_t)(byte & 127)) << shift; + shift += 7; + } while (byte & 128); + + return result; +} + +static TString* readString(TempBuffer& strings, const char* data, size_t size, size_t& offset) +{ + unsigned int id = readVarInt(data, size, offset); + + return id == 0 ? NULL : strings[id - 1]; +} + +static void resolveImportSafe(lua_State* L, LuaTable* env, TValue* k, uint32_t id) +{ + struct ResolveImport + { + TValue* k; + uint32_t id; + + static void run(lua_State* L, void* ud) + { + ResolveImport* self = static_cast(ud); + + // note: we call getimport with nil propagation which means that accesses to table chains like A.B.C will resolve in nil + // this is technically not necessary but it reduces the number of exceptions when loading scripts that rely on getfenv/setfenv for global + // injection + // allocate a stack slot so that we can do table lookups + luaD_checkstack(L, 1); + setnilvalue(L->top); + L->top++; + + luaV_getimport(L, L->gt, self->k, L->top - 1, self->id, /* propagatenil= */ true); + } + }; + + ResolveImport ri = {k, id}; + if (L->gt->safeenv) + { + // luaD_pcall will make sure that if any C/Lua calls during import resolution fail, the thread state is restored back + int oldTop = lua_gettop(L); + int status = luaD_pcall(L, &ResolveImport::run, &ri, savestack(L, L->top), 0); + LUAU_ASSERT(oldTop + 1 == lua_gettop(L)); // if an error occurred, luaD_pcall saves it on stack + + if (status != 0) + { + // replace error object with nil + setnilvalue(L->top - 1); + } + } + else + { + setnilvalue(L->top); + L->top++; + } +} + +static uint8_t remapUserdataType(uint8_t type, uint8_t* userdataRemapping, uint32_t count) +{ + LUAU_ASSERT(FFlag::LuauLoadRemapOptionalUserdata); + uint8_t tag = type & ~LBC_TYPE_OPTIONAL_BIT; + uint8_t optional = type & LBC_TYPE_OPTIONAL_BIT; + uint32_t index = uint32_t(tag - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + return userdataRemapping[index] | optional; + + return type; +} + +static void remapUserdataTypes(char* data, size_t size, uint8_t* userdataRemapping, uint32_t count) +{ + size_t offset = 0; + + uint32_t typeSize = readVarInt(data, size, offset); + uint32_t upvalCount = readVarInt(data, size, offset); + uint32_t localCount = readVarInt(data, size, offset); + + if (typeSize != 0) + { + uint8_t* types = (uint8_t*)data + offset; + + // Skip two bytes of function type introduction + for (uint32_t i = 2; i < typeSize; i++) + { + if (FFlag::LuauLoadRemapOptionalUserdata) + { + types[i] = remapUserdataType(types[i], userdataRemapping, count); + } + else + { + uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + types[i] = userdataRemapping[index]; + } + } + + offset += typeSize; + } + + if (upvalCount != 0) + { + uint8_t* types = (uint8_t*)data + offset; + + for (uint32_t i = 0; i < upvalCount; i++) + { + if (FFlag::LuauLoadRemapOptionalUserdata) + { + types[i] = remapUserdataType(types[i], userdataRemapping, count); + } + else + { + uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + types[i] = userdataRemapping[index]; + } + } + + offset += upvalCount; + } + + if (localCount != 0) + { + for (uint32_t i = 0; i < localCount; i++) + { + if (FFlag::LuauLoadRemapOptionalUserdata) + { + data[offset] = char(remapUserdataType(uint8_t(data[offset]), userdataRemapping, count)); + } + else + { + uint32_t index = uint32_t(data[offset] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + data[offset] = userdataRemapping[index]; + } + + offset += 2; + readVarInt(data, size, offset); + readVarInt(data, size, offset); + } + } + + LUAU_ASSERT(offset == size); +} + +static int loadsafe( + lua_State* L, + TempBuffer& strings, + TempBuffer& protos, + const char* chunkname, + const char* data, + size_t size, + int env +) +{ + size_t offset = 0; + + uint8_t version = read(data, size, offset); + + // 0 means the rest of the bytecode is the error message + if (version == 0) + { + char chunkbuf[LUA_IDSIZE]; + const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname)); + lua_pushfstring(L, "%s%.*s", chunkid, int(size - offset), data + offset); + return 1; + } + + if ((version < LBC_VERSION_MIN || version > LBC_VERSION_MAX) && version != LBC_VERSION_CLASSES) + { + char chunkbuf[LUA_IDSIZE]; + const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname)); + lua_pushfstring(L, "%s: bytecode version mismatch (expected [%d..%d], got %d)", chunkid, LBC_VERSION_MIN, LBC_VERSION_MAX, version); + return 1; + } + + uint8_t typesversion = 0; + + if (version >= 4) + { + typesversion = read(data, size, offset); + + if (typesversion < LBC_TYPE_VERSION_MIN || typesversion > LBC_TYPE_VERSION_MAX) + { + char chunkbuf[LUA_IDSIZE]; + const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname)); + lua_pushfstring( + L, "%s: bytecode type version mismatch (expected [%d..%d], got %d)", chunkid, LBC_TYPE_VERSION_MIN, LBC_TYPE_VERSION_MAX, typesversion + ); + return 1; + } + } + + // env is 0 for current environment and a stack index otherwise + LuaTable* envt = (env == 0) ? L->gt : hvalue(luaA_toobject(L, env)); + + TString* source = luaS_new(L, chunkname); + + // string table + unsigned int stringCount = readVarInt(data, size, offset); + strings.allocate(L, stringCount); + + for (unsigned int i = 0; i < stringCount; ++i) + { + unsigned int length = readVarInt(data, size, offset); + + strings[i] = luaS_newlstr(L, data + offset, length); + offset += length; + } + + // userdata type remapping table + // for unknown userdata types, the entry will remap to common 'userdata' type + const uint32_t userdataTypeLimit = LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE; + uint8_t userdataRemapping[userdataTypeLimit]; + + if (typesversion == 3) + { + memset(userdataRemapping, LBC_TYPE_USERDATA, userdataTypeLimit); + + uint8_t index = read(data, size, offset); + + while (index != 0) + { + TString* name = readString(strings, data, size, offset); + + if (uint32_t(index - 1) < userdataTypeLimit) + { + if (auto cb = L->global->ecb.gettypemapping) + userdataRemapping[index - 1] = cb(L, getstr(name), name->len); + } + + index = read(data, size, offset); + } + } + + // proto table + unsigned int protoCount = readVarInt(data, size, offset); + protos.allocate(L, protoCount); + + for (unsigned int i = 0; i < protoCount; ++i) + { + uint32_t protoSize = 0; + if (version >= 12) + protoSize = readVarInt(data, size, offset); + size_t protoStartOffset = offset; + Proto* p = luaF_newproto(L); + p->source = source; + p->bytecodeid = int(i); + p->funid = L->global->lastprotoid == 0 ? 0 : L->global->lastprotoid++; + + p->maxstacksize = read(data, size, offset); + p->numparams = read(data, size, offset); + p->nups = read(data, size, offset); + p->is_vararg = read(data, size, offset); + + if (version >= 4) + { + p->flags = read(data, size, offset); + + if (typesversion == 1) + { + uint32_t typesize = readVarInt(data, size, offset); + + if (typesize) + { + uint8_t* types = (uint8_t*)data + offset; + + LUAU_ASSERT(typesize == unsigned(2 + p->numparams)); + LUAU_ASSERT(types[0] == LBC_TYPE_FUNCTION); + LUAU_ASSERT(types[1] == p->numparams); + + // transform v1 into v2 format + int headersize = typesize > 127 ? 4 : 3; + + p->typeinfo = luaM_newarray(L, headersize + typesize, uint8_t, p->memcat); + p->sizetypeinfo = headersize + typesize; + + if (headersize == 4) + { + p->typeinfo[0] = (typesize & 127) | (1 << 7); + p->typeinfo[1] = typesize >> 7; + p->typeinfo[2] = 0; + p->typeinfo[3] = 0; + } + else + { + p->typeinfo[0] = uint8_t(typesize); + p->typeinfo[1] = 0; + p->typeinfo[2] = 0; + } + + memcpy(p->typeinfo + headersize, types, typesize); + } + + offset += typesize; + } + else if (typesversion == 2 || typesversion == 3) + { + uint32_t typesize = readVarInt(data, size, offset); + + if (typesize) + { + uint8_t* types = (uint8_t*)data + offset; + + p->typeinfo = luaM_newarray(L, typesize, uint8_t, p->memcat); + p->sizetypeinfo = typesize; + memcpy(p->typeinfo, types, typesize); + offset += typesize; + + if (typesversion == 3) + { + remapUserdataTypes((char*)(uint8_t*)p->typeinfo, p->sizetypeinfo, userdataRemapping, userdataTypeLimit); + } + } + } + } + + const int sizecode = readVarInt(data, size, offset); + p->code = luaM_newarray(L, sizecode, Instruction, p->memcat); + p->sizecode = sizecode; + + for (int j = 0; j < p->sizecode; ++j) + p->code[j] = read(data, size, offset); + + p->codeentry = p->code; + + const int sizek = readVarInt(data, size, offset); + p->k = luaM_newarray(L, sizek, TValue, p->memcat); + p->sizek = sizek; + + // Initialize the constants to nil to ensure they have a valid state + // in the event that some operation in the following loop fails with + // an exception. + for (int j = 0; j < p->sizek; ++j) + { + setnilvalue(&p->k[j]); + } + + for (int j = 0; j < p->sizek; ++j) + { + switch (read(data, size, offset)) + { + case LBC_CONSTANT_NIL: + // All constants have already been pre-initialized to nil + break; + + case LBC_CONSTANT_BOOLEAN: + { + uint8_t v = read(data, size, offset); + setbvalue(&p->k[j], v); + break; + } + + case LBC_CONSTANT_NUMBER: + { + double v = read(data, size, offset); + setnvalue(&p->k[j], v); + break; + } + + case LBC_CONSTANT_VECTOR: + { + float x = read(data, size, offset); + float y = read(data, size, offset); + float z = read(data, size, offset); + float w = read(data, size, offset); + (void)w; + setvvalue(L, &p->k[j], x, y, z, w); + break; + } + + case LBC_CONSTANT_VECTORD: + { + double x = read(data, size, offset); + double y = read(data, size, offset); + double z = read(data, size, offset); + double w = read(data, size, offset); + (void)w; + setvvalue(L, &p->k[j], x, y, z, w); + break; + } + + case LBC_CONSTANT_STRING: + { + TString* v = readString(strings, data, size, offset); + setsvalue(L, &p->k[j], v); + break; + } + + case LBC_CONSTANT_IMPORT: + { + uint32_t iid = read(data, size, offset); + resolveImportSafe(L, envt, p->k, iid); + setobj(L, &p->k[j], L->top - 1); + L->top--; + break; + } + + case LBC_CONSTANT_TABLE: + { + int keys = readVarInt(data, size, offset); + LuaTable* h = luaH_new(L, 0, keys); + for (int i = 0; i < keys; ++i) + { + int key = readVarInt(data, size, offset); + TValue* val = luaH_set(L, h, &p->k[key]); + setnvalue(val, 0.0); + } + sethvalue(L, &p->k[j], h); + break; + } + + case LBC_CONSTANT_TABLE_WITH_CONSTANTS: + { + uint32_t keys = readVarInt(data, size, offset); + LuaTable* h = luaH_new(L, 0, keys); + + TempBuffer nilKeys; + nilKeys.allocate(L, keys); + size_t nilKeysSize = 0; + + for (uint32_t i = 0; i < keys; ++i) + { + int32_t key = readVarInt(data, size, offset); + TValue* val = luaH_set(L, h, &p->k[key]); + int32_t constantIdx = read(data, size, offset); + if (constantIdx >= 0) + { + TValue* constant = &p->k[constantIdx]; + if (ttisnil(constant)) + { + nilKeys[nilKeysSize++] = key; + } + else + { + setobj2t(L, val, constant); + luaC_barriert(L, h, constant); + continue; + } + } + setnvalue(val, 0.0); + } + + for (size_t idx = 0; idx < nilKeysSize; idx++) + { + int32_t key = nilKeys[idx]; + TValue* val = luaH_set(L, h, &p->k[key]); + setnilvalue(val); + } + + sethvalue(L, &p->k[j], h); + break; + } + + case LBC_CONSTANT_CLOSURE: + { + uint32_t fid = readVarInt(data, size, offset); + Closure* cl = luaF_newLclosure(L, protos[fid]->nups, envt, protos[fid]); + cl->preload = (cl->nupvalues > 0); + setclvalue(L, &p->k[j], cl); + break; + } + + case LBC_CONSTANT_CLASS_SHAPE: + { + uint32_t cnid = readVarInt(data, size, offset); + TValue* classname = &p->k[cnid]; + LUAU_ASSERT(ttisstring(classname)); + uint32_t numProperties = readVarInt(data, size, offset); + uint32_t numMethods = readVarInt(data, size, offset); + uint32_t numMembers = numMethods + numProperties; + TString** offsetToMember = luaM_newarray(L, numMembers, TString*, L->activememcat); + LuaTable* membersToOffset = luaH_new(L, 0, numMembers); + + for (uint32_t idx = 0; idx < numMembers; idx++) + { + uint32_t mid = readVarInt(data, size, offset); + TValue* memberName = &p->k[mid]; + LUAU_ASSERT(ttisstring(memberName)); + offsetToMember[idx] = tsvalue(memberName); + TValue* val = luaH_setstr(L, membersToOffset, tsvalue(memberName)); + setnvalue(val, idx); + } + + membersToOffset->readonly = true; + + LuauClass* lco = luaR_newclass(L, tsvalue(classname), membersToOffset, offsetToMember, numProperties, numMethods, envt); + setclassvalue(L, &p->k[j], lco); + break; + } + + case LBC_CONSTANT_INTEGER: + { + bool isNegative = read(data, size, offset); + uint64_t magnitude = readVarInt64(data, size, offset); + setlvalue(&p->k[j], isNegative ? (int64_t)(~magnitude + 1) : (int64_t)magnitude); + break; + } + + default: + LUAU_ASSERT(!"Unexpected constant kind"); + } + } + + for (Instruction* instruction = p->code; instruction < p->code + p->sizecode;) + { + int targetOp = -1; + + switch (LUAU_INSN_OP(*instruction)) + { + case LOP_GETTABLEKS: + targetOp = LOP_GETUDATAKS; + break; + + case LOP_SETTABLEKS: + targetOp = LOP_SETUDATAKS; + break; + + case LOP_NAMECALL: + targetOp = LOP_NAMECALLUDATA; + break; + } + + if (targetOp != -1) + { + LUAU_ASSERT(instruction[1] < uint32_t(sizek)); + + // We take over the upper 16 bits of AUX - so no constants with big indices. + if (instruction[1] < 0x10000) + { + TValue* k = &p->k[instruction[1]]; + TString* s = tsvalue(k); + + luaS_updateatom(L, s); + + if (s->atom >= 0) + *instruction = (*instruction & 0xffffff00) | targetOp; + } + } + + instruction += Luau::getOpLength(LuauOpcode(LUAU_INSN_OP(*instruction))); + } + + const int sizep = readVarInt(data, size, offset); + p->p = luaM_newarray(L, sizep, Proto*, p->memcat); + p->sizep = sizep; + + for (int j = 0; j < p->sizep; ++j) + { + uint32_t fid = readVarInt(data, size, offset); + p->p[j] = protos[fid]; + } + + p->linedefined = readVarInt(data, size, offset); + p->debugname = readString(strings, data, size, offset); + + uint8_t lineinfo = read(data, size, offset); + + if (lineinfo) + { + p->linegaplog2 = read(data, size, offset); + + int intervals = ((p->sizecode - 1) >> p->linegaplog2) + 1; + int absoffset = (p->sizecode + 3) & ~3; + + const int sizelineinfo = absoffset + intervals * sizeof(int); + p->lineinfo = luaM_newarray(L, sizelineinfo, uint8_t, p->memcat); + p->sizelineinfo = sizelineinfo; + + p->abslineinfo = (int*)(p->lineinfo + absoffset); + + uint8_t lastoffset = 0; + for (int j = 0; j < p->sizecode; ++j) + { + lastoffset += read(data, size, offset); + p->lineinfo[j] = lastoffset; + } + + int lastline = 0; + for (int j = 0; j < intervals; ++j) + { + lastline += read(data, size, offset); + p->abslineinfo[j] = lastline; + } + } + + uint8_t debuginfo = read(data, size, offset); + + if (debuginfo) + { + const int sizelocvars = readVarInt(data, size, offset); + p->locvars = luaM_newarray(L, sizelocvars, LocVar, p->memcat); + p->sizelocvars = sizelocvars; + + for (int j = 0; j < p->sizelocvars; ++j) + { + p->locvars[j].varname = readString(strings, data, size, offset); + p->locvars[j].startpc = readVarInt(data, size, offset); + p->locvars[j].endpc = readVarInt(data, size, offset); + p->locvars[j].reg = read(data, size, offset); + } + + const int sizeupvalues = readVarInt(data, size, offset); + LUAU_ASSERT(sizeupvalues == p->nups); + + p->upvalues = luaM_newarray(L, sizeupvalues, TString*, p->memcat); + p->sizeupvalues = sizeupvalues; + + for (int j = 0; j < p->sizeupvalues; ++j) + { + p->upvalues[j] = readString(strings, data, size, offset); + } + } + + if (version >= 11) + { + p->feedbackvecsize = readVarInt(data, size, offset); + + if (p->feedbackvecsize > 0) + { + p->feedbackvec = luaM_newarray(L, p->feedbackvecsize, FeedbackVectorSlot, p->memcat); + } + for (uint32_t j = 0; j < p->feedbackvecsize; j++) + { + uint8_t slottype = read(data, size, offset); + LUAU_ASSERT(slottype == LFT_CALLTARGET); + FeedbackVectorSlot& slot = p->feedbackvec[j]; + slot.kind = static_cast(slottype); + slot.call_target.pc = readVarInt(data, size, offset); + slot.call_target.proto = 0; + slot.call_target.hits = 0; + } + } + + if (version >= 12) + { + if ((p->flags & LPF_INLINABLE) != 0) + p->cost = readVarInt64(data, size, offset); + } + + if (version >= 12) + { + // Potentially skipping unknown data at the end of Proto. + offset = protoStartOffset + protoSize; + } + + protos[i] = p; + } + + // "main" proto is pushed to Lua stack + uint32_t mainid = readVarInt(data, size, offset); + Proto* main = protos[mainid]; + + luaC_threadbarrier(L); + + Closure* cl = luaF_newLclosure(L, 0, envt, main); + setclvalue(L, L->top, cl); + incr_top(L); + + return 0; +} + +int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size, int env) +{ + // we will allocate a fair amount of memory so check GC before we do + luaC_checkGC(L); + + // pause GC for the duration of deserialization - some objects we're creating aren't rooted + const ScopedSetGCThreshold pauseGC{L->global, SIZE_MAX}; + + struct LoadContext + { + TempBuffer strings; + TempBuffer protos; + const char* chunkname; + const char* data; + size_t size; + int env; + + int result; + + static void run(lua_State* L, void* ud) + { + LoadContext* ctx = (LoadContext*)ud; + + ctx->result = loadsafe(L, ctx->strings, ctx->protos, ctx->chunkname, ctx->data, ctx->size, ctx->env); + } + } ctx = { + {}, + {}, + chunkname, + data, + size, + env, + }; + + int status = luaD_rawrunprotected(L, &LoadContext::run, &ctx); + + // load can either succeed or get an OOM error, any other errors should be handled internally + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); + + if (status == LUA_ERRMEM) + { + lua_pushstring(L, LUA_MEMERRMSG); // out-of-memory error message doesn't require an allocation + return 1; + } + + return ctx.result; +} diff --git a/External/luau/VM/src/lvmutils.cpp b/External/luau/VM/src/lvmutils.cpp new file mode 100644 index 00000000..6f350da4 --- /dev/null +++ b/External/luau/VM/src/lvmutils.cpp @@ -0,0 +1,837 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details +#include "lclass.h" +#include "lfunc.h" +#include "lvm.h" + +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lvector.h" +#include "lgc.h" +#include "ldo.h" +#include "lnumutils.h" + +#include + +// limit for table tag-method chains (to avoid loops) +#define MAXTAGLOOP 100 + +LUAU_FASTFLAG(LuauFrozenMetaButterfly) +LUAU_FASTFLAG(DebugLuauUserDefinedClassesRuntime) +LUAU_FASTFLAG(LuauPromoteProto) +LUAU_FASTFLAGVARIABLE(LuauCallLuauTm) + +const TValue* luaV_tonumber(const TValue* obj, TValue* n) +{ + double num; + if (ttisnumber(obj)) + return obj; + if (ttisstring(obj) && luaO_str2d(svalue(obj), &num)) + { + setnvalue(n, num); + return n; + } + else + return NULL; +} + +int luaV_tostring(lua_State* L, StkId obj) +{ + if (!ttisnumber(obj)) + return 0; + else + { + char s[LUAI_MAXNUM2STR]; + double n = nvalue(obj); + char* e = luai_num2str(s, n); + LUAU_ASSERT(e < s + sizeof(s)); + setsvalue(L, obj, luaS_newlstr(L, s, e - s)); + return 1; + } +} + +const LUA_VECTOR_TYPE* luaV_tovector(const TValue* obj) +{ + if (ttisvector(obj)) + return vvalue(obj); + + return nullptr; +} + +static LUAU_FORCEINLINE void callTMluau(lua_State* L, StkId top, Closure* ccl, int nresults) +{ + if (++L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + ptrdiff_t funcoffset = savestack(L, top); + Proto* p = getproto(ccl); + + CallInfo* ci = incr_ci(L); + ci->func = top; + ci->p = p; + ci->base = top + 1; + ci->top = L->top + ccl->stacksize; + ci->savedpc = p->code; + ci->flags = LUA_CALLINFO_RETURN; + ci->nresults = nresults; + + L->base = ci->base; + + luaD_checkstackfornewci(L, ccl->stacksize); // clobbers 'top' + LUAU_ASSERT(ci->top <= L->stack_last); + + StkId argi = L->top; + StkId argend = L->base + p->numparams; + while (argi < argend) + setnilvalue(argi++); + L->top = p->is_vararg ? argi : ci->top; + + if (p->exectarget != 0 && p->execdata) + ci->flags |= LUA_CALLINFO_NATIVE; + + luau_execute(L); + + // resume_continue is not handled here as metamethods are not yieldable + + L->top = restorestack(L, funcoffset) + nresults; + L->nCcalls--; +} + +static StkId callTMres(lua_State* L, StkId res, const TValue* f, const TValue* p1, const TValue* p2) +{ + ptrdiff_t result = savestack(L, res); + // using stack room beyond top is technically safe here, but for very complicated reasons: + // * The stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated + // * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua + // stack and checkstack may invalidate those pointers + // * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack + // * during stack reallocation all of the allocated stack is copied (even beyond stack_last) so these + // values will be preserved even if they go past stack_last + if (FFlag::LuauCallLuauTm) + { + StkId top = L->top; + LUAU_ASSERT((top + 3) < (L->stack + L->stacksize)); + setobj2s(L, top, f); // push function + setobj2s(L, top + 1, p1); // 1st argument + setobj2s(L, top + 2, p2); // 2nd argument + + // fast-path for Luau to Luau calls + if (L->isactive && isLua(L->ci) && ttisfunction(top) && !clvalue(top)->isC) + { + L->top += 3; + callTMluau(L, top, clvalue(top), 1); + } + else + { + luaD_checkstack(L, 3); + StkId func = L->top; + L->top += 3; + luaD_call(L, func, 1); + } + } + else + { + LUAU_ASSERT((L->top + 3) < (L->stack + L->stacksize)); + setobj2s(L, L->top, f); // push function + setobj2s(L, L->top + 1, p1); // 1st argument + setobj2s(L, L->top + 2, p2); // 2nd argument + luaD_checkstack(L, 3); + L->top += 3; + luaD_call(L, L->top - 3, 1); + } + + res = restorestack(L, result); + L->top--; + setobj2s(L, res, L->top); + return res; +} + +// There is only one call location for this function, but inlining it has been measured to have a negative effect on luaV_settable +static LUAU_NOINLINE void callTM(lua_State* L, const TValue* f, const TValue* p1, const TValue* p2, const TValue* p3) +{ + // using stack room beyond top is technically safe here, but for very complicated reasons: + // * The stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated + // * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua + // stack and checkstack may invalidate those pointers + // * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack + // * during stack reallocation all of the allocated stack is copied (even beyond stack_last) so these + // values will be preserved even if they go past stack_last + if (FFlag::LuauCallLuauTm) + { + StkId top = L->top; + LUAU_ASSERT((top + 4) < (L->stack + L->stacksize)); + setobj2s(L, top, f); // push function + setobj2s(L, top + 1, p1); // 1st argument + setobj2s(L, top + 2, p2); // 2nd argument + setobj2s(L, top + 3, p3); // 3th argument + + // fast-path for Luau to Luau calls + if (L->isactive && isLua(L->ci) && ttisfunction(top) && !clvalue(top)->isC) + { + L->top += 4; + callTMluau(L, top, clvalue(top), 0); + } + else + { + luaD_checkstack(L, 4); + StkId func = L->top; + L->top += 4; + luaD_call(L, func, 0); + } + } + else + { + LUAU_ASSERT((L->top + 4) < (L->stack + L->stacksize)); + setobj2s(L, L->top, f); // push function + setobj2s(L, L->top + 1, p1); // 1st argument + setobj2s(L, L->top + 2, p2); // 2nd argument + setobj2s(L, L->top + 3, p3); // 3th argument + luaD_checkstack(L, 4); + L->top += 4; + luaD_call(L, L->top - 4, 0); + } +} + +void luaV_gettable(lua_State* L, const TValue* t, TValue* key, StkId val) +{ + int loop; + for (loop = 0; loop < MAXTAGLOOP; loop++) + { + const TValue* tm; + if (ttistable(t)) + { // `t' is a table? + LuaTable* h = hvalue(t); + + const TValue* res = luaH_get(h, key); // do a primitive get + + if (res != luaO_nilobject) + L->cachedslot = gval2slot(h, res); // remember slot to accelerate future lookups + + if (FFlag::LuauFrozenMetaButterfly) + { + if (LUAU_LIKELY(!ttisnil(res))) + { + setobj2s(L, val, res); + return; + } + + if ((tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) + { + setnilvalue(val); + return; + } + } + else + { + if (!ttisnil(res) // result is no nil? + || (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) + { // or no TM? + setobj2s(L, val, res); + return; + } + } + + // t isn't a table, so see if it has an INDEX meta-method to look up the key with + } + else if (LUAU_UNLIKELY(FFlag::DebugLuauUserDefinedClassesRuntime && ttisobject(t))) + { + LuauObject* inst = objectvalue(t); + const TValue* offsettval = luaH_get(inst->lclass->memberstooffset, key); + + // Class instances throw if you try to access a member that is not + // present. + if (ttisnil(offsettval)) + luaG_missingmembererror(L, t, key); + + const uint32_t offset = uint32_t(nvalue(offsettval)); + setobj2s(L, val, luaR_lookupmemberatoffset(inst, offset)); + return; + } + else if (LUAU_UNLIKELY(FFlag::DebugLuauUserDefinedClassesRuntime && ttisclass(t))) + { + LuauClass* lco = classvalue(t); + const TValue* res = luaH_get(lco->memberstooffset, key); + + // Class objects throw if you try to access a member that is not + // present. + if (ttisnil(res)) + luaG_missingmembererror(L, t, key); + + const uint32_t offset = uint32_t(nvalue(res)); + LUAU_ASSERT(offset < lco->numberofallmembers); + + // This is the case where we try to access an instance member on a + // class object, for example: + // + // class Box + // public item + // function print(self) print(self.item) end + // end + // + // local _ = Box.item + // + if (offset < lco->numberofinstancemembers) + luaG_missingmembererror(L, t, key); + + setobj2s(L, val, &lco->staticmembers[offset - lco->numberofinstancemembers]); + return; + } + else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX))) + luaG_indexerror(L, t, key); + if (ttisfunction(tm)) + { + callTMres(L, val, tm, t, key); + return; + } + t = tm; // else repeat with `tm' + } + luaG_runerror(L, "'__index' chain too long; possible loop"); +} + +void luaV_settable(lua_State* L, const TValue* t, TValue* key, StkId val) +{ + int loop; + TValue temp; + for (loop = 0; loop < MAXTAGLOOP; loop++) + { + const TValue* tm; + if (ttistable(t)) + { // `t' is a table? + LuaTable* h = hvalue(t); + + const TValue* oldval = luaH_get(h, key); + + // should we assign the key? (if key is valid or __newindex is not set) + if (!ttisnil(oldval) || (tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL) + { + if (h->readonly) + luaG_readonlyerror(L); + + // luaH_set would work but would repeat the lookup so we use luaH_setslot that can reuse oldval if it's safe + TValue* newval = luaH_setslot(L, h, oldval, key); + + L->cachedslot = gval2slot(h, newval); // remember slot to accelerate future lookups + + setobj2t(L, newval, val); + luaC_barriert(L, h, val); + return; + } + + // fallthrough to metamethod + } + else if (LUAU_UNLIKELY(FFlag::DebugLuauUserDefinedClassesRuntime && ttisobject(t))) + { + LuauObject* inst = objectvalue(t); + const TValue* offset = luaH_get(inst->lclass->memberstooffset, key); + if (ttisnil(offset)) + luaG_missingmembererror(L, t, key); + const uint32_t offsetnum = uint32_t(nvalue(offset)); + LUAU_ASSERT(offsetnum < inst->lclass->numberofallmembers); + if (offsetnum >= inst->lclass->numberofinstancemembers) + luaG_indexerror(L, t, key); + setobj2class(L, &inst->members[offsetnum], val); + luaC_barrier(L, inst, val); + return; + } + else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_NEWINDEX))) + luaG_indexerror(L, t, key); + + if (ttisfunction(tm)) + { + callTM(L, tm, t, key, val); + return; + } + // else repeat with `tm' + setobj(L, &temp, tm); // avoid pointing inside table (may rehash) + t = &temp; + } + luaG_runerror(L, "'__newindex' chain too long; possible loop"); +} + +static int call_binTM(lua_State* L, const TValue* p1, const TValue* p2, StkId res, TMS event) +{ + const TValue* tm = luaT_gettmbyobj(L, p1, event); // try first operand + if (ttisnil(tm)) + tm = luaT_gettmbyobj(L, p2, event); // try second operand + if (ttisnil(tm)) + return 0; + callTMres(L, res, tm, p1, p2); + return 1; +} + +static const TValue* get_compTM(lua_State* L, LuaTable* mt1, LuaTable* mt2, TMS event) +{ + const TValue* tm1 = fasttm(L, mt1, event); + const TValue* tm2; + if (tm1 == NULL) + return NULL; // no metamethod + if (mt1 == mt2) + return tm1; // same metatables => same metamethods + tm2 = fasttm(L, mt2, event); + if (tm2 == NULL) + return NULL; // no metamethod + if (luaO_rawequalObj(tm1, tm2)) // same metamethods? + return tm1; + return NULL; +} + +static int call_orderTM(lua_State* L, const TValue* p1, const TValue* p2, TMS event, bool error = false) +{ + const TValue* tm1 = luaT_gettmbyobj(L, p1, event); + const TValue* tm2; + if (ttisnil(tm1)) + { + if (error) + luaG_ordererror(L, p1, p2, event); + return -1; // no metamethod? + } + tm2 = luaT_gettmbyobj(L, p2, event); + if (!luaO_rawequalObj(tm1, tm2)) // different metamethods? + { + if (error) + luaG_ordererror(L, p1, p2, event); + return -1; + } + callTMres(L, L->top, tm1, p1, p2); + return !l_isfalse(L->top); +} + +int luaV_strcmp(const TString* ls, const TString* rs) +{ + if (ls == rs) + return 0; + + const char* l = getstr(ls); + const char* r = getstr(rs); + + // always safe to read one character because even empty strings are nul terminated + if (*l != *r) + return uint8_t(*l) - uint8_t(*r); + + size_t ll = ls->len; + size_t lr = rs->len; + size_t lmin = ll < lr ? ll : lr; + + int res = memcmp(l, r, lmin); + if (res != 0) + return res; + + return ll == lr ? 0 : ll < lr ? -1 : 1; +} + +int luaV_lessthan(lua_State* L, const TValue* l, const TValue* r) +{ + if (LUAU_UNLIKELY(ttype(l) != ttype(r))) + luaG_ordererror(L, l, r, TM_LT); + else if (LUAU_LIKELY(ttisnumber(l))) + return luai_numlt(nvalue(l), nvalue(r)); + else if (ttisstring(l)) + return luaV_strcmp(tsvalue(l), tsvalue(r)) < 0; + else + return call_orderTM(L, l, r, TM_LT, /* error= */ true); +} + +int luaV_lessequal(lua_State* L, const TValue* l, const TValue* r) +{ + int res; + if (ttype(l) != ttype(r)) + luaG_ordererror(L, l, r, TM_LE); + else if (ttisnumber(l)) + return luai_numle(nvalue(l), nvalue(r)); + else if (ttisstring(l)) + return luaV_strcmp(tsvalue(l), tsvalue(r)) <= 0; + else if ((res = call_orderTM(L, l, r, TM_LE)) != -1) // first try `le' + return res; + else if ((res = call_orderTM(L, r, l, TM_LT)) == -1) // error if not `lt' + luaG_ordererror(L, l, r, TM_LE); + return !res; +} + +int luaV_equalval(lua_State* L, const TValue* t1, const TValue* t2) +{ + const TValue* tm; + LUAU_ASSERT(ttype(t1) == ttype(t2)); + switch (ttype(t1)) + { + case LUA_TNIL: + return 1; + case LUA_TNUMBER: + return luai_numeq(nvalue(t1), nvalue(t2)); + case LUA_TINTEGER: + return luai_inteq(lvalue(t1), lvalue(t2)); + case LUA_TVECTOR: + return luai_veceq(vvalue(t1), vvalue(t2)); + case LUA_TBOOLEAN: + return bvalue(t1) == bvalue(t2); // true must be 1 !! + case LUA_TLIGHTUSERDATA: + return pvalue(t1) == pvalue(t2) && lightuserdatatag(t1) == lightuserdatatag(t2); + case LUA_TUSERDATA: + { + tm = get_compTM(L, uvalue(t1)->metatable, uvalue(t2)->metatable, TM_EQ); + if (!tm) + return uvalue(t1) == uvalue(t2); + break; // will try TM + } + case LUA_TCLASS: + return classvalue(t1) == classvalue(t2); + case LUA_TOBJECT: + { + // We follow the same rules as metatables. + tm = get_compTM(L, objectvalue(t1)->lclass->instancemetatable, objectvalue(t2)->lclass->instancemetatable, TM_EQ); + if (!tm) + return objectvalue(t1) == objectvalue(t2); + break; // will try TM + } + case LUA_TTABLE: + { + tm = get_compTM(L, hvalue(t1)->metatable, hvalue(t2)->metatable, TM_EQ); + if (!tm) + return hvalue(t1) == hvalue(t2); + break; // will try TM + } + default: + return gcvalue(t1) == gcvalue(t2); + } + callTMres(L, L->top, tm, t1, t2); // call TM + return !l_isfalse(L->top); +} + +void luaV_concat(lua_State* L, int total, int last) +{ + do + { + StkId top = L->base + last + 1; + int n = 2; // number of elements handled in this pass (at least 2) + if (!(ttisstring(top - 2) || ttisnumber(top - 2)) || !tostring(L, top - 1)) + { + if (!call_binTM(L, top - 2, top - 1, top - 2, TM_CONCAT)) + luaG_concaterror(L, top - 2, top - 1); + } + else if (tsvalue(top - 1)->len == 0) // second op is empty? + (void)tostring(L, top - 2); // result is first op (as string) + else + { + // at least two string values; get as many as possible + size_t tl = tsvalue(top - 1)->len; + char* buffer; + int i; + // collect total length + for (n = 1; n < total && tostring(L, top - n - 1); n++) + { + size_t l = tsvalue(top - n - 1)->len; + if (l > MAXSSIZE - tl) + luaG_runerror(L, "string length overflow"); + tl += l; + } + + char buf[LUA_BUFFERSIZE]; + TString* ts = nullptr; + + if (tl < LUA_BUFFERSIZE) + { + buffer = buf; + } + else + { + ts = luaS_bufstart(L, tl); + buffer = ts->data; + } + + tl = 0; + for (i = n; i > 0; i--) + { // concat all strings + size_t l = tsvalue(top - i)->len; + memcpy(buffer + tl, svalue(top - i), l); + tl += l; + } + + if (tl < LUA_BUFFERSIZE) + { + setsvalue(L, top - n, luaS_newlstr(L, buffer, tl)); + } + else + { + setsvalue(L, top - n, luaS_buffinish(L, ts)); + } + } + total -= n - 1; // got `n' strings to create 1 new + last -= n - 1; + } while (total > 1); // repeat until only 1 result left +} + +template +void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc) +{ + TValue tempb, tempc; + const TValue *b, *c; + + // vector operations that we support: + // v+v v-v -v (add/sub/neg) + // v*v s*v v*s (mul) + // v/v s/v v/s (div) + // v//v s//v v//s (floor div) + const LUA_VECTOR_TYPE* vb = ttisvector(rb) ? vvalue(rb) : nullptr; + const LUA_VECTOR_TYPE* vc = ttisvector(rc) ? vvalue(rc) : nullptr; + + if (vb && vc) + { + switch (op) + { + case TM_ADD: + setvvalue(L, ra, vb[0] + vc[0], vb[1] + vc[1], vb[2] + vc[2], vb[3] + vc[3]); + return; + case TM_SUB: + setvvalue(L, ra, vb[0] - vc[0], vb[1] - vc[1], vb[2] - vc[2], vb[3] - vc[3]); + return; + case TM_MUL: + setvvalue(L, ra, vb[0] * vc[0], vb[1] * vc[1], vb[2] * vc[2], vb[3] * vc[3]); + return; + case TM_DIV: + setvvalue(L, ra, vb[0] / vc[0], vb[1] / vc[1], vb[2] / vc[2], vb[3] / vc[3]); + return; + case TM_IDIV: + setvvalue( + L, + ra, + LUA_VECTOR_TYPE(luai_numidiv(vb[0], vc[0])), + LUA_VECTOR_TYPE(luai_numidiv(vb[1], vc[1])), + LUA_VECTOR_TYPE(luai_numidiv(vb[2], vc[2])), + LUA_VECTOR_TYPE(luai_numidiv(vb[3], vc[3])) + ); + return; + case TM_UNM: + setvvalue(L, ra, -vb[0], -vb[1], -vb[2], -vb[3]); + return; + default: + break; + } + } + else if (vb) + { + c = ttisnumber(rc) ? rc : luaV_tonumber(rc, &tempc); + + if (c) + { + LUA_VECTOR_TYPE nc = cast_to(LUA_VECTOR_TYPE, nvalue(c)); + + switch (op) + { + case TM_MUL: + setvvalue(L, ra, vb[0] * nc, vb[1] * nc, vb[2] * nc, vb[3] * nc); + return; + case TM_DIV: + setvvalue(L, ra, vb[0] / nc, vb[1] / nc, vb[2] / nc, vb[3] / nc); + return; + case TM_IDIV: + setvvalue( + L, + ra, + LUA_VECTOR_TYPE(luai_numidiv(vb[0], nc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[1], nc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[2], nc)), + LUA_VECTOR_TYPE(luai_numidiv(vb[3], nc)) + ); + return; + default: + break; + } + } + } + else if (vc) + { + b = ttisnumber(rb) ? rb : luaV_tonumber(rb, &tempb); + + if (b) + { + LUA_VECTOR_TYPE nb = cast_to(LUA_VECTOR_TYPE, nvalue(b)); + + switch (op) + { + case TM_MUL: + setvvalue(L, ra, nb * vc[0], nb * vc[1], nb * vc[2], nb * vc[3]); + return; + case TM_DIV: + setvvalue(L, ra, nb / vc[0], nb / vc[1], nb / vc[2], nb / vc[3]); + return; + case TM_IDIV: + setvvalue( + L, + ra, + LUA_VECTOR_TYPE(luai_numidiv(nb, vc[0])), + LUA_VECTOR_TYPE(luai_numidiv(nb, vc[1])), + LUA_VECTOR_TYPE(luai_numidiv(nb, vc[2])), + LUA_VECTOR_TYPE(luai_numidiv(nb, vc[3])) + ); + return; + default: + break; + } + } + } + + if ((b = luaV_tonumber(rb, &tempb)) != NULL && (c = luaV_tonumber(rc, &tempc)) != NULL) + { + double nb = nvalue(b), nc = nvalue(c); + + switch (op) + { + case TM_ADD: + setnvalue(ra, luai_numadd(nb, nc)); + break; + case TM_SUB: + setnvalue(ra, luai_numsub(nb, nc)); + break; + case TM_MUL: + setnvalue(ra, luai_nummul(nb, nc)); + break; + case TM_DIV: + setnvalue(ra, luai_numdiv(nb, nc)); + break; + case TM_IDIV: + setnvalue(ra, luai_numidiv(nb, nc)); + break; + case TM_MOD: + setnvalue(ra, luai_nummod(nb, nc)); + break; + case TM_POW: + setnvalue(ra, luai_numpow(nb, nc)); + break; + case TM_UNM: + setnvalue(ra, luai_numunm(nb)); + break; + default: + LUAU_ASSERT(0); + break; + } + } + else + { + if (!call_binTM(L, rb, rc, ra, op)) + { + luaG_aritherror(L, rb, rc, op); + } + } +} + +// instantiate private template implementation for external callers +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); +template void luaV_doarithimpl(lua_State* L, StkId ra, const TValue* rb, const TValue* rc); + +void luaV_dolen(lua_State* L, StkId ra, const TValue* rb) +{ + const TValue* tm = NULL; + switch (ttype(rb)) + { + case LUA_TTABLE: + { + LuaTable* h = hvalue(rb); + if ((tm = fasttm(L, h->metatable, TM_LEN)) == NULL) + { + setnvalue(ra, cast_num(luaH_getn(h))); + return; + } + break; + } + case LUA_TSTRING: + { + TString* ts = tsvalue(rb); + setnvalue(ra, cast_num(ts->len)); + return; + } + default: + tm = luaT_gettmbyobj(L, rb, TM_LEN); + } + + if (ttisnil(tm)) + luaG_typeerror(L, rb, "get length of"); + + StkId res = callTMres(L, ra, tm, rb, luaO_nilobject); + if (!ttisnumber(res)) + luaG_runerror(L, "'__len' must return a number"); // note, we can't access rb since stack may have been reallocated +} + +LUAU_NOINLINE void luaV_prepareFORN(lua_State* L, StkId plimit, StkId pstep, StkId pinit) +{ + if (!ttisnumber(pinit) && !luaV_tonumber(pinit, pinit)) + luaG_forerror(L, pinit, "initial value"); + if (!ttisnumber(plimit) && !luaV_tonumber(plimit, plimit)) + luaG_forerror(L, plimit, "limit"); + if (!ttisnumber(pstep) && !luaV_tonumber(pstep, pstep)) + luaG_forerror(L, pstep, "step"); +} + +// calls a C function f with no yielding support; optionally save one resulting value to the res register +// the function and arguments have to already be pushed to L->top +LUAU_NOINLINE void luaV_callTM(lua_State* L, int nparams, int res) +{ + ++L->nCcalls; + + if (L->nCcalls >= LUAI_MAXCCALLS) + luaD_checkCstack(L); + + luaD_checkstack(L, LUA_MINSTACK); + + StkId top = L->top; + StkId fun = top - nparams - 1; + + CallInfo* ci = incr_ci(L); + ci->func = fun; + ci->p = nullptr; + ci->base = fun + 1; + ci->top = top + LUA_MINSTACK; + ci->savedpc = NULL; + ci->flags = 0; + ci->nresults = (res >= 0); + LUAU_ASSERT(ci->top <= L->stack_last); + + LUAU_ASSERT(ttisfunction(ci->func)); + LUAU_ASSERT(clvalue(ci->func)->isC); + + L->base = fun + 1; + LUAU_ASSERT(L->top == L->base + nparams); + + lua_CFunction func = clvalue(fun)->c.f; + int n = func(L); + LUAU_ASSERT(n >= 0); // yields should have been blocked by nCcalls + + // ci is our callinfo, cip is our parent + // note that we read L->ci again since it may have been reallocated by the call + CallInfo* cip = L->ci - 1; + + // copy return value into parent stack + if (res >= 0) + { + if (n > 0) + { + setobj2s(L, &cip->base[res], L->top - n); + } + else + { + setnilvalue(&cip->base[res]); + } + } + + L->ci = cip; + L->base = cip->base; + L->top = cip->top; + + --L->nCcalls; +} + +LUAU_NOINLINE void luaV_tryfuncTM(lua_State* L, StkId func) +{ + const TValue* tm = luaT_gettmbyobj(L, func, TM_CALL); + if (!ttisfunction(tm)) + luaG_typeerror(L, func, "call"); + for (StkId p = L->top; p > func; p--) // open space for metamethod + setobj2s(L, p, p - 1); + L->top++; // stack space pre-allocated by the caller + setobj2s(L, func, tm); // tag method is the new function to be called +} diff --git a/External/luau/lua_LICENSE.txt b/External/luau/lua_LICENSE.txt new file mode 100644 index 00000000..2302dc3c --- /dev/null +++ b/External/luau/lua_LICENSE.txt @@ -0,0 +1,19 @@ +Copyright © 1994–2019 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/External/manifest.json b/External/manifest.json index e44c4180..ea5db8f3 100644 --- a/External/manifest.json +++ b/External/manifest.json @@ -257,5 +257,140 @@ "bcdec.h": "134520764d96f70a27db814173616c89ad85db95fbd3328417c7a8c77e7ca189", "LICENSE.txt": "2414c782587f6e96661cb0d19d752432ddecfcc1a221b4cb12bc2253a54dc5ac" } + }, + "luau": { + "repository": "https://github.com/luau-lang/luau", + "commit": "2d88f9b5facaa9e4ec91247b39ae8251bd49a244", + "files": { + "Ast/include/Luau/Allocator.h": "9e16a4f0ff19ad34008573256df2ace81abb5df42772103a8e2e4452620e14cc", + "Ast/include/Luau/Ast.h": "98fc52d27142f86a58a0fd9a3ed401b63e5a8f9f1d46e05b7637394b2b034795", + "Ast/include/Luau/Confusables.h": "27cfab506d1b64a1874148f2e4ccfd075b019e4db6b93ab90499a75e71908ad2", + "Ast/include/Luau/Cst.h": "d0490a5e626d75008ed8ea738058bbd37e224000f4de3403e314f4334a493d75", + "Ast/include/Luau/Lexer.h": "ae7dcc3e73c31c29b6e4e2731d6a55319621753b92b876e4d15c62837942c0d4", + "Ast/include/Luau/Location.h": "08c54c08419430e78ab6f4b455d1ff85a324e66e9e2a4fa133e56ae7e00f9bd4", + "Ast/include/Luau/ParseOptions.h": "793d224a51e6bdc31580847f9d75a64525bb55431acbd9f5b974ff2b7c96dd44", + "Ast/include/Luau/ParseResult.h": "59c715ee6a25dd9e2a95ea2bd86f17fef84b3d706c470a242960edf8a9b2e70e", + "Ast/include/Luau/Parser.h": "7d54a7d0edc26be161ea0964b4ad2d0f3a1b9f2995b7b6e66591463cfbc142e0", + "Ast/include/Luau/PrettyPrinter.h": "54aca99b47af0d9d3781dd3b786fbf88ba2f2af3f37f456221a76d8cd21d139e", + "Ast/src/Allocator.cpp": "47bc5f01ad11bc5e0b8010e7122b0ba2e0ba1abcffb9e222cdbe7ceb6752bc9b", + "Ast/src/Ast.cpp": "e41c4f812353691cfd9257cdff31cf3c4d875e2c558f7e6eaa8d60b885c71d6a", + "Ast/src/Confusables.cpp": "bb11e83335ff1897828315178c7b18104b2a23474b202d235c0932458f0ac6a7", + "Ast/src/Cst.cpp": "a3d57a57615ba5b2c9df36ab42a8e1a9ebb3e65c9b8c371f9a0027b64261177a", + "Ast/src/Lexer.cpp": "9f9ed71bf7a9eaf79226afe6881ac614f956a11898cb206f34bef3d8b3a492ce", + "Ast/src/Location.cpp": "7275e3f9290ab8b1ed7233d625ebb9961a3df2139702b3fb90efd2c4989f9b64", + "Ast/src/Parser.cpp": "fb909e76c152a6a5fefdbf5847123d54ff11a21cc4873ecdc1074c9dbc267d48", + "Ast/src/PrettyPrinter.cpp": "11d16fe06b79bd02ad98d358e641ada3b5b023313252b2f8b6dc7f5527b944ce", + "Bytecode/include/Luau/BytecodeBuilder.h": "c07fbeb01cf27f6ec29c74e60dc5748b6f0ea757d2ed03578c96d1bc27843a7c", + "Bytecode/include/Luau/BytecodeCallInliner.h": "400d3065478a942a42b28e5c46d8702e8700300bb57a86db819840e3d45f9d50", + "Bytecode/include/Luau/BytecodeDump.h": "9e7a37565a10e3a8c412d7f28767e54956cd780c7a5e9fa16c362039ca7b1fc4", + "Bytecode/include/Luau/BytecodeGraph.h": "8133f817671d04f5562106fa6a417ea9c67950d723b9c209d461419d6a3ec727", + "Bytecode/include/Luau/BytecodeOps.h": "36bacbb78f5dd29fffa18000a00d4af1fcf941980bcbdeefe99aa98ad9fcab96", + "Bytecode/include/Luau/BytecodeValidation.h": "2f89105890165f65fe1e78811309182703a32e7f952aec0d07403b0cd8e4b4fa", + "Bytecode/include/Luau/Sccp.h": "a6c6cdf340dd1ff1c3cdf11b03f11135004e71a54dae78d0ceee1f36f1422708", + "Bytecode/src/BytecodeBuilder.cpp": "9623f2aa9c5c5b1a41875a317b90df29de7346c76c3cfbcf3eb6f856c9b17a35", + "Bytecode/src/BytecodeDump.cpp": "a4e9955f5a8df7176f26aff4f877bda242ddf2765275ad6a1ffd5bf0edd33207", + "Bytecode/src/BytecodeGraph.cpp": "cd9b217f85dee6502a3314f1004ec09ef5eca004b4555754cf7d6f1914ca05c0", + "Bytecode/src/BytecodeGraphParser.h": "8cebeb656256c57c3cc8320048ba4249ab068bc97915747eed4c5e2f79d6c4dc", + "Bytecode/src/BytecodeGraphSerializer.h": "f542a1c9e8670296ee48b5a3bf05cbda81dddf48a72538f1738bd5835dfd84c1", + "Bytecode/src/Sccp.cpp": "97c29d2347e7085c0f97dd2dba80d5b04806a9e1d6fb61f8c6515f849d8e0c09", + "Common/include/Luau/Bytecode.h": "3f7bee53df5c382aa21d60196adc167ece6fd4256981f323e1b253307a277c0b", + "Common/include/Luau/BytecodeUtils.h": "eff2984b00e90aa7e29db5480122bcdc974df18d4ba64d55ca4d41f5773ac7b9", + "Common/include/Luau/BytecodeWire.h": "ed568671c4d33aa54a217bdd42ec834df452fbb202a8d2f61ca4051ce67feb5e", + "Common/include/Luau/Common.h": "2b306eb67011df26aef6528ec8da9ae6117b0c5c478a698d5174102c08214755", + "Common/include/Luau/DenseHash.h": "3172a7f4945cae559e452ab3387aab9302cd1ac8a5e9bd3803067612e14f72f3", + "Common/include/Luau/ExperimentalFlags.h": "bfddb24784ae7d88673267b58e2ac379fa7dcba7622ef99eac950840e415dd80", + "Common/include/Luau/HashUtil.h": "b33dbf412bbc4db38101604e92c546a6efe4e1063a3a16f94258b7b6c20ea356", + "Common/include/Luau/InsertionOrderedMap.h": "6a0fa6ca37c72b110b4b185fa26b6808579f59a6e885d622f6d969cdf35c48ec", + "Common/include/Luau/ScopedSeenSet.h": "674311bcaea32976c080d105a8f59e9c72389a2a4b05a20507cb688614289019", + "Common/include/Luau/SmallVector.h": "97216a5d211190f6f31e1ed28144908506cc67bdcebfe7b958a00fcf590147e9", + "Common/include/Luau/StringUtils.h": "5014d742c5efe6b8324aa8c4a0dabea319c8c905a99f96282aca0beaa404975f", + "Common/include/Luau/TimeTrace.h": "11c7e8b48e6d2417786ffa5ae492932601d4d11aad1732baa87627e0ff04452b", + "Common/include/Luau/Variant.h": "3b3da1f01559d9a8ba3dbe6f0fdbde154b23f1476ed0ec929e3188019553b53c", + "Common/include/Luau/VecDeque.h": "5d6b930c594bacaa55bee5279951a2e9f269535cf950fbb9c69d72be9d0d42de", + "Common/src/BytecodeWire.cpp": "1f6a963756f2e36e521da51b4b4e8bae509e738166d62e1e23e4d75dd333a56c", + "Common/src/StringUtils.cpp": "3a02c47c377998cca93d5224e38472320625e250c35d93ae8f9fe81d47aca8ee", + "Common/src/TimeTrace.cpp": "5a75ce96d13a30b172ba407e285970f5632ffd61bf152da5bff9bc593fc90caf", + "Compiler/include/Luau/Compiler.h": "8e5b57bbb1c18a244a368c794a5e4513b9dfc11ff7d1350c0f8da7d61f117d47", + "Compiler/include/luacode.h": "64dac57eb94a4c7860390e00ddb16f8706c578ec02094ab94bf0c5c4bdb96fa4", + "Compiler/src/BuiltinFolding.cpp": "d5a8a44150dc1e57287075d6ad3d902d948816f824f8536f013248466f7bc47b", + "Compiler/src/BuiltinFolding.h": "36acc91091653808458dacafb932200770ade7d21686f5d085f7ea5c8ee40a7f", + "Compiler/src/Builtins.cpp": "11fbb9bedd7a1c672181a368831081c212d17f0b5e06240449979b0b4d757964", + "Compiler/src/Builtins.h": "fa5f8471e7f72560f89f13b1b61563e00bf9a707bf9cf5603bff74079fd51538", + "Compiler/src/Compiler.cpp": "9e41532e52548d6518168296b0aaaa2a9ae8bc57edbad85d730f63f8bbecce9a", + "Compiler/src/ConstantFolding.cpp": "43ad6fede2efe5891f47a182b78d3704b19bcaf36e255a3dd7ca69dde42e6fe3", + "Compiler/src/ConstantFolding.h": "f0b3d5e7fa919e33dcc10ebb8e0e9903d11985671e12c8af8494e2c069af723a", + "Compiler/src/CostModel.cpp": "36871229af086e13b23610aca803af5a310cc5e56cc6289c5ecff86f26530073", + "Compiler/src/CostModel.h": "09bb2dd1e2ed0a0e6c54b6149eaae10fe4358e71f61e5b260b53b3ce57a87100", + "Compiler/src/TableShape.cpp": "ae068e94ceed15602e5094525795a72a49438f06964ec8ba84e9b2faedc5e44f", + "Compiler/src/TableShape.h": "07817d16a81478e163e34bea16c5810904690c4407e434218a9875ce7f7849e5", + "Compiler/src/Types.cpp": "b5bc7aa91c0c8664de76db458b41c2912388f1ac10a47f175602d753cbaa1ca1", + "Compiler/src/Types.h": "182cdedc7a39649e585dc72b30752f5f07076795d3d5b3fa7220122b7120d4ef", + "Compiler/src/Utils.h": "01ba0ab151d5730c7822412c898ffa720a81b6f7441a5d66274beff4a474e7d2", + "Compiler/src/ValueTracking.cpp": "bd520c5c2763a645ada6b4c4da13b4fc11022c17781baf88a6b6b023e217207b", + "Compiler/src/ValueTracking.h": "0c8d7d5c9405eb51aa4d57cb1e8e13d99fa8df0bdff427fffc9378486044a459", + "Compiler/src/lcode.cpp": "b843c2b6c8efc9646ceee6f0ae4f606178d98eacf2c10a068287de48f5f488b5", + "LICENSE.txt": "1ceedfcc375343e3ec8e56861fa73ba3bd186ed178d5d000546447d358d38949", + "VM/include/lua.h": "d037b86599da4bdc51b7ddb3471f77d54bcb5209b810375772bb4dbe4a1b2b8b", + "VM/include/luaconf.h": "8b83ad672b597f97b42567e1f4b97b05f45a453c3d4a964adc910f80180a9022", + "VM/include/lualib.h": "f951f5ef55ab78a1d17a7ce457fd8c1a560b0c09f443a63d4b5b181c906a354a", + "VM/src/lapi.cpp": "265ce5d9180085de3d3942374fb1d79100650c8397175e8c4500b24aec8fcb6d", + "VM/src/lapi.h": "4c5c17e92e45d0c14a1b74c5b7eb47a2afdc361b853daa3e396ec73bcf372c2c", + "VM/src/laux.cpp": "ab9b21c981d3764769f20921b8ad168e20b64826dc8627eaaeeaf501f5ec7dad", + "VM/src/lbaselib.cpp": "fce2d874679ebf9931d25d4a8be8265fbf872ce74301a85b456f4d0f1826409e", + "VM/src/lbitlib.cpp": "90a4c6c2ef63b1565342151a7f196e1410ce4aa6ae565672a5f07c3e5175a3ad", + "VM/src/lbuffer.cpp": "ad79012e1433cd4b43bca6ce2733ac920cc76d3e77c190c057f83e3f7b6696e1", + "VM/src/lbuffer.h": "c494c949aaefde1f5d57145d90e4f32ba4b0a9543bd8f1b2d1619c6dfd11a939", + "VM/src/lbuflib.cpp": "1ba25e1140240bac7bebb93a894d7855a94a955e63097ad55bd93f92953f14ac", + "VM/src/lbuiltins.cpp": "a99aadc2f206df6377360791dc10bf23b9dabe3c57ad7235509406a0b6be33ac", + "VM/src/lbuiltins.h": "e7da571b784027e475238d43df08c3158c1c0f28469a8f9eef4e41de57969736", + "VM/src/lbytecode.h": "ef29914a98325d2348f940f992b9ba1455838c7402d3cdb24da8f7313ba1eb5e", + "VM/src/lclass.cpp": "fe9abad544bdd3441b2bb3a1e42d751fcb5904d77ed8c9d95309c04757625631", + "VM/src/lclass.h": "46008dbf7856a54a10aa9fb6110b20aa498e6420dd955466a7513baab8ae3f3e", + "VM/src/lclasslib.cpp": "b2839864e7f15a40da16768a5780b631b0028fb4006cc8e2e860607e88e7ef7b", + "VM/src/lcommon.h": "d500caa74c70e556f691a5b5fab6af9db58630909b6756eebdb7e8d39612dd6b", + "VM/src/lcorolib.cpp": "32087aa5234bca5b8440d9d13dc9fcb755a5cbf0b1ebdcc0bc7bae0c9929207a", + "VM/src/ldblib.cpp": "b12d9f1b4cda9582e1bdb7ae7cb52dcfc1586ae1513fe0da1ba4e3fdd9e4d1df", + "VM/src/ldebug.cpp": "ac403cec88a4c0ed525767d2826d6502c538c0e650481a547cc566e3f25e45cd", + "VM/src/ldebug.h": "d5f4d724ae3ba074962915cd3195f92f02b28c1c233f70f273091bb6c1784a25", + "VM/src/ldo.cpp": "980ba70630e202f413f3b0663fe31aded191ba9c06cc33050a4a7e87d3a6ab21", + "VM/src/ldo.h": "28135b06c5a200e58e33609547c01d8b4ecea1a394dd0b7a9b41409e6add0e11", + "VM/src/lfunc.cpp": "3e7cd3ac802494abc285a09fb25ae38a9a92ece24215a2edc5e5392392bbbf32", + "VM/src/lfunc.h": "953aa2643985c97cac4a2ae2bdc09b234c1011b8827471796a47f153145dc1d7", + "VM/src/lgc.cpp": "fe4f646d10b3d9a890256f16d93d52ae5f2b9302afb97aff12ceb21c3c8d8415", + "VM/src/lgc.h": "1e8923b26c1cb1b3c34a2ab3830b7d344bb83fce7c33dc90865b2b33099a60f6", + "VM/src/lgcdebug.cpp": "0d1d774c584a74334f3ccd4c8b63cf926adf1141e6d28cd917cfe92bbbff547d", + "VM/src/linit.cpp": "26784d13def229bfe8b6d4c7d0755b4b8a9ec069bd87d9f15980c2a72b6c40fa", + "VM/src/lintlib.cpp": "0b3d50e68ea7665234bda44d931940edfb23171c24467e01eb16702f031f2edb", + "VM/src/lmathlib.cpp": "a78b3b624fef350f78d2a415dd2aa29ad46f31e45d1586fabd8f2d9e88110845", + "VM/src/lmem.cpp": "02ea9e6cd7f191726418aaf600c9e29c2d7b39316dae5bbd96a4ddfdf106d07a", + "VM/src/lmem.h": "a3aac0753903ed71abc286e9b81f603af11bf5b4901dcfaf7ac7da98f769bb37", + "VM/src/lnumprint.cpp": "c6001abe411f06fa1036a5a01ddc824657f2a01e4ff1b8c5213509acfad1368f", + "VM/src/lnumutils.h": "c1ecd03514976290fe86f72dd8c95da91678187b0fa60f849b4a614610f79616", + "VM/src/lobject.cpp": "a3fc6b61e79c3adf8084ec9ace67ce0a3507bc98b263ff033d7eb89174962d16", + "VM/src/lobject.h": "61363b11385dbb9dfecdeb0fba083650ce32393b16c20121b43b5e1c6240e6df", + "VM/src/loslib.cpp": "0e88d5dc2cfb6b29c07d98454bce1312f479e519072401214fda724f32cedff2", + "VM/src/lperf.cpp": "6effaa71b16a7cdc1576062533e8f524f34ad30de64194fd106ae373800783b9", + "VM/src/lstate.cpp": "ff3119870f7b0da66793cb88a9e0192e5d7e19b3431321a9c433180c3504ae68", + "VM/src/lstate.h": "ce6ee680bcd5b439e8e267709b37ceefb39ee317ffbc283bf77deb024e5d0254", + "VM/src/lstring.cpp": "c3cd0c52a48f4d2f0c2c6a0ff9d5961432a1e3b42fab9be2de89b32caf99d0e2", + "VM/src/lstring.h": "e2eb97d428caa84b7590737a919810ef836f06a24da25390877dc95b9f495f0a", + "VM/src/lstrlib.cpp": "6d3a17a8af6f60f1f744c1fa5c453871124cceb034fb5ef130467054eca7227a", + "VM/src/ltable.cpp": "4779653bbecf06263c6c9b66a89f52a96af938d4a99e31d28a79f95c2071cb2a", + "VM/src/ltable.h": "90ed3ad38d1cac28482087ae834dac120d02e072bca12193622a7603b1f93f9f", + "VM/src/ltablib.cpp": "3bdaf0b7025dc0153f106f2dfb00ead0d6aafd7dd3df4a52b473b0b4778b0eac", + "VM/src/ltm.cpp": "e590f9221a3f402943296167d4d6005e8e24f5aa1d24c7257c817b25d3e274b2", + "VM/src/ltm.h": "817c62bb01d3f04c6b217cdc24295209fc4784461139e8dff2d71046d6eff265", + "VM/src/ludata.cpp": "929d73a728fc13a59aa6a08a7bec970b408ecf4a3b5f45dbd9f92f7e7c7816be", + "VM/src/ludata.h": "2c6707c17ef9decfdebc7dc6167c1d7659d226f6186494798de181bc667c094e", + "VM/src/lutf8lib.cpp": "f8a4051ec967fa2ea24c72e3b331b1f608594f85852a3630b6bf71bed59029e1", + "VM/src/lveclib.cpp": "1a7c758b5f26f8c0e75419bbe9e8cc8a5e8f2b17771c3b6ced04e514910ce300", + "VM/src/lvector.cpp": "33612d191c1457c027b7b0ce3e126e71640abfe06ab1c1cd178a027224cc0cf7", + "VM/src/lvector.h": "6885616089ade2f1e4c144d04365ec2960624371fc6a641cb45e2d5f30f9bc50", + "VM/src/lvm.h": "acecceacd51769778ea36856df46ee973d9847c617e4736ee62036aa83591526", + "VM/src/lvmexecute.cpp": "ede508b83178d06c4beff03570612000d36e1b179211d52b95227f26f85d4ed8", + "VM/src/lvmload.cpp": "520799daed20c7d825476f9b4e97202f18cf65beb13bb3466da4ae3e9a395150", + "VM/src/lvmutils.cpp": "8758be8c575f1773d429d5236c6fc125c358659910fdf8a521bca59a830420e3", + "lua_LICENSE.txt": "1d913b8bad19567e42303dfbc18e9c1e937c3c3f9ad64ae994f1bd18704dd020" + } } } diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index 7f74641b..9cc43909 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -16,6 +16,7 @@ #include "Extension/Settings/replay_drafts.h" #include "Engine/Game/World/client_state.h" #include "Extension/HallOfMeat/hall_of_meat.h" +#include "Extension/Plugins/plugin_runtime.h" #include "Extension/RoadRash/road_rash.h" #include "Extension/BoardBreak/board_break.h" #include "Extension/BoardBreak/board_break_audio.h" @@ -1096,6 +1097,10 @@ void update_model(std::uintptr_t client, TickState& frame) { dingosdk::drone::outfit::on_client_tick(); dingosdk::board_break::on_client_tick(client); dingosdk::grind_exit_control::on_client_tick(client); + { + DINGO_PROFILE_ZONE("tick/plugins"); + dingosdk::plugins::on_client_tick(); + } if (!has_request && now < r.next_model && state == r.previous_state) return; r.next_model = now + 500; DINGO_PROFILE_ZONE("tick/update_model/world model (500 ms)"); @@ -1104,6 +1109,7 @@ void update_model(std::uintptr_t client, TickState& frame) { const auto& current_level = description.lm_level.empty() ? description.level : description.lm_level; dingosdk::live_mods::set_current_level(current_level); dingosdk::hall_of_meat::set_level(current_level); + dingosdk::plugins::set_level(current_level); dingosdk::replay_drafts::on_client_tick(r.base, state); // A live mod apply hands over the destinations its mods declare. A map // being played that is no longer among them (its mod disabled or deleted) diff --git a/Runtime/console_bridge.cpp b/Runtime/console_bridge.cpp index 10b8ebc3..1ba6e0a3 100644 --- a/Runtime/console_bridge.cpp +++ b/Runtime/console_bridge.cpp @@ -1,5 +1,6 @@ #include "runtime_internal.h" #include "Extension/Console/commands.h" +#include "Extension/Plugins/plugin_runtime.h" #include "Extension/Progression/mission_progression_override.h" #include "Extension/Settings/gameplay_settings_override.h" #include "Extension/Settings/named_settings.h" @@ -95,6 +96,8 @@ std::string apply_console_named_setting(std::string_view name, std::string_view } void execute_console_command(const std::string& command) { try { + // A plugin's command never shadows one of ReSkate's: the host refuses those names. + if (dingosdk::plugins::run_console(command)) return; const dingosdk::console::Output output{[](const std::string& line) { console_line(line); }, {}, {}}; dingosdk::console::game_commands().execute(command, console_model_snapshot(), output); } catch (const std::exception& error) { diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 9b2e836c..394a63fa 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -30,6 +30,7 @@ #include "Extension/Assets/map_download.h" #include "Extension/Boot/offline_boot.h" #include "Extension/HallOfMeat/hall_of_meat.h" +#include "Extension/Plugins/plugin_runtime.h" #include "Extension/RoadRash/road_rash.h" #include "Extension/BoardBreak/board_break.h" #include "Extension/Skater/grind_exit_control.h" @@ -444,6 +445,8 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { const bool camera_hook = dingosdk::start_camera_observer(r.base); record("{\"event\":\"camera_observer_initialized\",\"active\":" + std::string(camera_hook ? "true" : "false") + "}"); (void)dingosdk::start_no_bail(r.base); + const bool plugins = dingosdk::plugins::start(); + record("{\"event\":\"plugins_initialized\",\"active\":" + std::string(plugins ? "true" : "false") + "}"); const bool meat = dingosdk::hall_of_meat::start(r.base); record("{\"event\":\"hall_of_meat_initialized\",\"active\":" + std::string(meat ? "true" : "false") + "}"); const bool rash = dingosdk::road_rash::start(r.base); diff --git a/cmake/Core.cmake b/cmake/Core.cmake index b0db0e8e..aee3d7f5 100644 --- a/cmake/Core.cmake +++ b/cmake/Core.cmake @@ -134,3 +134,16 @@ endif() # World layers read from the installed level data, cached per game build. add_library(dingosdk_world_layer_scan STATIC Engine/Vfs/world_layer_scan.cpp) target_link_libraries(dingosdk_world_layer_scan PUBLIC dingosdk_json PRIVATE dingosdk_game_archives dingosdk_content_cache) + +# Plugins (Extension/Plugins): Luau scripts in mods, each in its own sandboxed VM. The host, its +# VMs and the API; finding plugins among the mods needs the mod list, so it is Windows only for now. +add_library(dingosdk_plugins STATIC + Extension/Plugins/plugin_manifest.cpp + Extension/Plugins/plugin_vm.cpp + Extension/Plugins/plugin_api.cpp + Extension/Plugins/plugin_host.cpp) +target_link_libraries(dingosdk_plugins PUBLIC dingosdk_json dingosdk_console_core PRIVATE dingosdk_luau) +if(WIN32) + target_sources(dingosdk_plugins PRIVATE Extension/Plugins/plugin_discovery.cpp) + target_link_libraries(dingosdk_plugins PUBLIC dingosdk_mod_list) +endif() diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 378c9a2f..afe8d9f5 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -43,6 +43,19 @@ add_library(dingosdk_zstd STATIC ${zstd_sources}) target_include_directories(dingosdk_zstd SYSTEM PUBLIC External/zstd/lib) target_compile_definitions(dingosdk_zstd PRIVATE ZSTD_LEGACY_SUPPORT=0 ZSTD_DISABLE_ASM) set_target_properties(dingosdk_zstd PROPERTIES FOLDER "Dependencies") +# Luau (External/luau): the VM and compiler plugins run on (Extension/Plugins). Only these five +# parts: no CodeGen (native code from scripts), no Analysis, no CLI or Require. +file(GLOB dingosdk_luau_sources CONFIGURE_DEPENDS External/luau/Common/src/*.cpp External/luau/Ast/src/*.cpp + External/luau/Bytecode/src/*.cpp External/luau/Compiler/src/*.cpp External/luau/VM/src/*.cpp) +add_library(dingosdk_luau STATIC ${dingosdk_luau_sources}) +target_include_directories(dingosdk_luau SYSTEM PUBLIC External/luau/Common/include External/luau/Ast/include + External/luau/Bytecode/include External/luau/Compiler/include External/luau/VM/include) +target_include_directories(dingosdk_luau PRIVATE External/luau/VM/src External/luau/Bytecode/src) +target_compile_features(dingosdk_luau PRIVATE cxx_std_17) +if(MSVC) + target_compile_definitions(dingosdk_luau PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() +set_target_properties(dingosdk_luau PROPERTIES FOLDER "Dependencies") add_library(dingosdk_sqlite STATIC External/sqlite/sqlite3.c) target_include_directories(dingosdk_sqlite SYSTEM PUBLIC External/sqlite) target_compile_definitions(dingosdk_sqlite PRIVATE SQLITE_THREADSAFE=1 SQLITE_OMIT_LOAD_EXTENSION SQLITE_DQS=0 SQLITE_DEFAULT_MEMSTATUS=0) diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index e6256e6a..2abeef33 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -122,6 +122,8 @@ add_library(dingosdk_runtime SHARED Extension/Scripting/lua_startup.cpp Extension/Scripting/lua_native_block.cpp Extension/Scripting/custom_script_loader.cpp + Extension/Plugins/plugin_runtime.cpp + Extension/Plugins/plugin_commands.cpp Extension/Assets/morph_memory_pool.cpp Extension/Assets/native_render_resource_pool.cpp Extension/World/native_entity_pages.cpp @@ -246,7 +248,7 @@ add_library(dingosdk_runtime SHARED target_link_libraries(dingosdk_runtime PRIVATE dingosdk_logging dingosdk_profiler dingosdk_supported_build dingosdk_overlay dingosdk_console_core dingosdk_steam_restart_guard dingosdk_startup_interventions dingosdk_fast_travel_unlock dingosdk_local_profile dingosdk_initfs dingosdk_mods dingosdk_mod_list dingosdk_launcher_support dingosdk_miniz shell32 dingosdk_startup_window dingosdk_custom_scripts dingosdk_world_layer_scan bcrypt - dingosdk_https winhttp ws2_32 xaudio2 ole32 dingosdk_game_archives dingosdk_word_filter) + dingosdk_https winhttp ws2_32 xaudio2 ole32 dingosdk_game_archives dingosdk_word_filter dingosdk_plugins) add_library(dingosdk_custom_level_manifest STATIC Engine/Game/World/custom_level_manifest.cpp)