From fef56ab7ac2a5d8bb5acf0d64b72c3e30bc06cc0 Mon Sep 17 00:00:00 2001 From: Harold Cindy <120691094+HaroldCindy@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:52:02 -0700 Subject: [PATCH 1/5] Improve forward and backward-compatibility for serialized scripts Intended to make it much less of a pain in the butt to do non-breaking upgrades to the serialization format without totally hosing everything. Accordingly, we add some test fixtures with serialized versions of the state of a set of scripts, to ensure that future versions of SLua are still able to load them. This also meant dragging some more stuff from `server` into this repo. Hooray. --- ARES.bt | 178 ++- Bytecode/include/Luau/BytecodeHeader.h | 42 + Bytecode/src/BytecodeHeader.cpp | 70 + CLI/src/Compile.cpp | 1 + CLI/src/Harness.cpp | 136 +- CLI/src/Repl.cpp | 3 +- CMakeLists.txt | 10 +- .../include/Luau/ByteStream.h | 51 +- Compiler/include/Luau/Compiler.h | 12 + Compiler/include/Luau/LSLCompiler.h | 19 +- Compiler/include/luacode.h | 3 + Compiler/src/Compiler.cpp | 21 + Compiler/src/LSLCompiler.cpp | 46 +- Compiler/src/lcode.cpp | 35 + Executor/include/Luau/Executor.h | 59 +- Executor/include/Luau/Script.h | 82 +- Executor/src/Executor.cpp | 32 +- Executor/src/Logging.cpp | 8 + Executor/src/Script.cpp | 290 ++-- LSLBuiltins/include/Luau/LSLBuiltins.h | 147 +- LSLBuiltins/src/LSLBuiltins.cpp | 206 +-- Makefile | 47 +- Sources.cmake | 6 +- VM/include/llsl.h | 2 + VM/include/lua.h | 26 +- VM/src/ares.cpp | 398 +++-- VM/src/ares.h | 33 +- VM/src/llsl.cpp | 39 + autobuild.xml | 24 +- build-cmd.sh | 7 +- builtins.txt | 66 +- tests/Conformance.test.cpp | 82 +- tests/LSLCompiler.test.cpp | 44 + tests/SLExecutor.test.cpp | 1343 +++++++++++------ tests/SLExecutorFixture.h | 376 +++++ tests/SLGoldenFixtures.test.cpp | 373 +++++ .../fixtures/exec/between-handlers.lsl | 16 + .../fixtures/exec/between-handlers.lua | 40 + .../fixtures/exec/errored-handler.lua | 8 + .../fixtures/exec/errored-main.lsl | 69 + ...exec2.0-ares6.0-between-handlers.lsl.sluac | Bin 0 -> 401 bytes ...exec2.0-ares6.0-between-handlers.lsl.state | Bin 0 -> 587 bytes ...exec2.0-ares6.0-between-handlers.lua.sluac | Bin 0 -> 1569 bytes ...exec2.0-ares6.0-between-handlers.lua.state | Bin 0 -> 1459 bytes .../exec2.0-ares6.0-errored-handler.lua.sluac | Bin 0 -> 290 bytes .../exec2.0-ares6.0-errored-handler.lua.state | Bin 0 -> 948 bytes .../exec2.0-ares6.0-errored-main.lsl.sluac | Bin 0 -> 49557 bytes .../exec2.0-ares6.0-errored-main.lsl.state | Bin 0 -> 86538 bytes ...2.0-ares6.0-state-change-pending.lsl.sluac | Bin 0 -> 532 bytes ...2.0-ares6.0-state-change-pending.lsl.state | Bin 0 -> 668 bytes .../exec2.0-ares6.0-yielded-handler.lsl.sluac | Bin 0 -> 400 bytes .../exec2.0-ares6.0-yielded-handler.lsl.state | Bin 0 -> 658 bytes .../exec2.0-ares6.0-yielded-handler.lua.sluac | Bin 0 -> 430 bytes .../exec2.0-ares6.0-yielded-handler.lua.state | Bin 0 -> 1149 bytes .../exec2.0-ares6.0-yielded-main.lua.sluac | Bin 0 -> 307 bytes .../exec2.0-ares6.0-yielded-main.lua.state | Bin 0 -> 725 bytes .../fixtures/exec/state-change-pending.lsl | 23 + .../fixtures/exec/yielded-handler.lsl | 19 + .../fixtures/exec/yielded-handler.lua | 11 + .../fixtures/exec/yielded-main.lua | 9 + 60 files changed, 3440 insertions(+), 1072 deletions(-) create mode 100644 Bytecode/include/Luau/BytecodeHeader.h create mode 100644 Bytecode/src/BytecodeHeader.cpp rename {Executor => Common}/include/Luau/ByteStream.h (63%) create mode 100644 tests/SLExecutorFixture.h create mode 100644 tests/SLGoldenFixtures.test.cpp create mode 100644 tests/conformance/fixtures/exec/between-handlers.lsl create mode 100644 tests/conformance/fixtures/exec/between-handlers.lua create mode 100644 tests/conformance/fixtures/exec/errored-handler.lua create mode 100644 tests/conformance/fixtures/exec/errored-main.lsl create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-between-handlers.lsl.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-between-handlers.lsl.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-between-handlers.lua.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-between-handlers.lua.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-errored-handler.lua.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-errored-handler.lua.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-errored-main.lsl.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-errored-main.lsl.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-state-change-pending.lsl.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-state-change-pending.lsl.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-handler.lsl.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-handler.lsl.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-handler.lua.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-handler.lua.state create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-main.lua.sluac create mode 100644 tests/conformance/fixtures/exec/exec2.0-ares6.0-yielded-main.lua.state create mode 100644 tests/conformance/fixtures/exec/state-change-pending.lsl create mode 100644 tests/conformance/fixtures/exec/yielded-handler.lsl create mode 100644 tests/conformance/fixtures/exec/yielded-handler.lua create mode 100644 tests/conformance/fixtures/exec/yielded-main.lua diff --git a/ARES.bt b/ARES.bt index b5b6dc4a..929f2ab7 100644 --- a/ARES.bt +++ b/ARES.bt @@ -4,11 +4,12 @@ // File: ARES.bt // Authors: Harold Cindy // Version: -// Purpose: dissecting serialized Ares chunks -// Category: -// File Mask: *.ares -// ID Bytes: 41 52 45 53 -// History: +// Purpose: dissecting serialized Ares chunks, bare or inside an +// Executor state payload +// Category: +// File Mask: *.ares,*.state +// ID Bytes: 41 52 45 53, 45 58 45 43 +// History: //------------------------------------------------ // This file originally based on Eris' FILEFORMAT file @@ -21,6 +22,7 @@ typedef uint32 uint32_t; typedef uint64 uint64_t; typedef int16 int16_t; typedef int32 int32_t; +typedef int64 int64_t; // Note that in the context of Ares size_t is _always_ 64-bit! typedef uint64 size_t; typedef size_t ProtoPtr; @@ -103,6 +105,7 @@ struct PermKey; struct Upvaldesc; struct LocVar; struct CallInfo; +struct ExecString; // From LuauBytecode.bt @@ -500,13 +503,26 @@ local uint64 refNum = 0; // tracks where we saw each reference local uint64 refPositions[0xFFFF] = {0}; +/* Closes a length-prefixed record: anything left before record_end is test + * padding or fields a newer minor appended, both of which the reader skips. + * Declarations leak into the calling struct. */ +void ParseRecordEnd(int64 record_end) { + if (FTell() < record_end) + uchar appended[record_end - FTell()] ; + Assert(FTell() == record_end); +} + typedef struct { char sig[4] ; /* Header signature for rudimentary validation */ Assert(sig == "ARES"); - uint32_t version; - /* Version 4 moved the type tags off of lua_Type and onto AresType, so - * older blobs cannot be parsed with this template. */ - Assert(version >= 4); + uint32_t major; + uint32_t minor; + /* Mirrors ARES_FORMAT_MAJOR in VM/include/lua.h. Any minor under it + * parses: fields are only ever appended inside records, and each record's + * length word steps over what this template doesn't know. */ + Assert(major == 6); + uint32_t record_len; + local int64 record_end = FTell() + record_len; uint8_t sizeof_number; /* sizeof(lua_Number) to check type compatibility */ lua_Number test; /* -1.234567890 to check representation compatibility */ uint8_t sizeof_int; /* sizeof(int) in persisted data */ @@ -515,11 +531,12 @@ typedef struct { /* Note that the last two fields determine the size of the int and size_t * fields in the following definitions. We write each value in the native * "size" and check for truncation when reading, if necessary. */ + /* Reserved when the header is written and patched in after the root + * object, see p_header_refcount() in ares.cpp. */ + uint32_t final_refcount; + ParseRecordEnd(record_end); } Header ; -// Define this up here so structs can reference the version -Header header; /* The header used for basic validation. */ - // GC object header with memcat. // Declarations leak into the calling struct. void ParseGCHeader() { @@ -554,9 +571,10 @@ typedef struct { case ARES_T_VECTOR: // storing a reference to a reference? no. case ARES_T_REFERENCE: - // permanent never writes to the ref table, what would be the point? - case ARES_T_PERMANENT: break; + // Permanents take a number too: persist_keyed() allocates it before + // the permanents table gets a look, and u_permanent() reserves it + // before reading the key, so the key's own objects number after it. default: { ourRef = ++refNum; // track where we saw this so we can show the address of what's @@ -565,6 +583,23 @@ typedef struct { } } + // Mirrors type_is_framed() in ares.cpp: the VM-shaped bodies carry a + // length word so a reader can step over fields it doesn't know. + local int64 record_end = -1; + switch(type) { + case ARES_T_TABLE: + case ARES_T_FUNCTION: + case ARES_T_USERDATA: + case ARES_T_THREAD: + case ARES_T_PROTO: + case ARES_T_UPVAL: + case ARES_T_CLASS: + case ARES_T_OBJECT: + uint32_t record_len ; + record_end = FTell() + record_len; + break; + } + switch(type) { case ARES_T_NIL: break; @@ -611,22 +646,29 @@ typedef struct { uint32 reference ; /* The index the object was referenced with */ ourRef = reference; break; + case ARES_T_CLASS: + case ARES_T_OBJECT: + /* Framed, but ares.cpp has no body for these yet. */ + Assert(0); default: Assert(0); } + + if (record_end >= 0) + ParseRecordEnd(record_end); } Object; typedef struct { ParseGCHeader(); size_t length; /* The length of the string */ char str[length]; /* The actual string (not always null terminated) */ -} String ; +} String ; typedef struct { ParseGCHeader(); size_t length; /* The length of the buffer */ char data[length]; /* The actual buffer data */ -} Buffer ; +} Buffer ; struct Table { ParseGCHeader(); @@ -782,19 +824,20 @@ struct Proto { struct Thread { ParseGCHeader(); Object env; - int stacksize; /* The overall size of the stack filled with objects, - * including all stack frames. */ + uint32_t stacksize; /* Allocated stack slots, restored as-is */ size_t top; /* top = L->top - L->stack; */ Object stack[top]; /* All stack values, bottom up */ AresStatus status; /* current thread status (ok, yield) */ uint8_t activememcat ; - size_t errfunc; /* NOT USED current error handling function (stack index) */ Object namecall; /* The pending namecall string, nil for none */ - int32_t num_cis; /* number of callinfo frames */ - /* The CallInfo stack, starting with base_ci */ + uint32_t size_ci; /* Allocated callinfo slots, never below BASIC_CI_SIZE */ + uint32_t num_cis; /* number of callinfo frames */ + /* The CallInfo stack, starting with base_ci. Each frame is its own record. */ struct CallInfo { + uint32_t record_len ; + local int64 record_end = FTell() + record_len; size_t func; /* func = ci->func - thread->stack */ size_t top; /* top = ci->top - thread->stack */ size_t base; /* base = ci->base - thread-stack */ @@ -806,22 +849,14 @@ struct Thread { int yield_point; int savedpc; /* savedpc = ci->u.l.savedpc - ci_func(ci)->p->code */ } else if (ci_kind == ERIS_CI_KIND_C) { - //uint8_t status; - //if (callstatus & (CIST_YPCALL | CIST_YIELDED)) { - // int32_t ctx; /* context info. in case of yields */ - // Object k; /* C function, callback for resuming */ - //} + int32_t errfunc; /* pcall's error handler, 1-based from ci->base, 0 for none */ Object function; } else { Assert(ci_kind == ERIS_CI_KIND_NONE); } + ParseRecordEnd(record_end); } ci[num_cis] ; - if (status == ARES_S_YIELD) { - // size_t extra; /* value of thread->ci->extra, which is the original - // * value of thread->ci->func */ - } - while (TRUE) { struct OpenUpval { size_t idx; /* stack index of the value + 1; 0 if end of list */ @@ -841,4 +876,83 @@ struct PermKey { * value in the permanents table when unpersisting has the correct type. */ }; -Object rootobj; /* The root object that was persisted. */ +typedef struct { + refNum = 0; + Header header; + Object rootobj; /* The root object that was persisted. */ + /* The same check the reader makes in u_finish() */ + if (refNum != header.final_refcount) + { + Printf("refNum != header.final_refcount, %d != %d\n", refNum, header.final_refcount); + Assert(0); + } +} AresStream; + +/* Length-prefixed string as written by ByteWriter::writeString */ +typedef struct { + uint32_t length; + char str[length]; +} ExecString ; + +/* The wrapper Script::serializeState() puts around an Ares blob, see + * Executor/src/Script.cpp. Fields only ever get appended inside the two + * sections, so a newer minor parses. */ +typedef struct { + /* The base class's fingerprint, covering the core section. Mirrors + * kScriptStateFingerprint in Executor/include/Luau/Script.h. */ + char tag[4] ; + Assert(tag == "EXEC"); + uint32_t major; + uint32_t minor; + Assert(major == 2); + /* The concrete Script subclass's fingerprint (EXEC again for the base + * class), covering the extra section at the end. */ + char class_tag[4] ; + uint32_t class_major; + uint32_t class_minor; + + struct CoreSection { + uint32_t record_len ; + local int64 record_end = FTell() + record_len; + float sleep; + uint32_t memory_limit; + uint8_t fault_kind; + ExecString fault_string; + ExecString extended_fault_string; + uint8_t main_function_complete; + uint8_t is_lsl; + uint32_t api_version; + /* The indra registers, opaque bitfields in the asset header's mask + * space (bit n-1 is builtins.txt event n). */ + int32_t current_state; + int32_t next_state; + uint64_t current_handler; + uint64_t sticky_handler; + uint64_t current_events; + uint64_t event_handlers; + ParseRecordEnd(record_end); + } core ; + + uint32_t ares_length; + local int64 ares_end = FTell() + ares_length; + AresStream ares; + Assert(FTell() == ares_end); + + /* Subclass state, versioned by class_major/class_minor. The base Script + * writes nothing here, so the layout is whatever serializeExtra() chose. */ + struct ExtraSection { + uint32_t record_len ; + if (record_len > 0) + uchar data[record_len]; + } extra; +} ExecState; + +local string magic = ReadString(0, 4); +if (magic == "EXEC") + ExecState exec; +else if (magic == "ARES") + AresStream ares; +else + Assert(0); +/* Both readers refuse trailing bytes */ +Assert(FEof()); diff --git a/Bytecode/include/Luau/BytecodeHeader.h b/Bytecode/include/Luau/BytecodeHeader.h new file mode 100644 index 00000000..0ac9aac8 --- /dev/null +++ b/Bytecode/include/Luau/BytecodeHeader.h @@ -0,0 +1,42 @@ +// ServerLua: the asset format, a BytecodeHeader followed by Luau bytecode. +#pragma once + +#include "Luau/ByteStream.h" + +#include +#include +#include + +namespace Luau +{ + +// What a host stores in front of compiled bytecode in a script asset. Carries +// what has to be known about the script before an image exists, so nothing is +// rediscovered from the VM. The codec is declared just below. +struct BytecodeHeader +{ + bool isLSL = false; + uint32_t apiVersion = 0; + // Per-state LSL handler masks, indexed by state number, bit (event index - 1) + // with the index being the event's position in builtins.txt (see + // LSLBuiltins.h). Empty for SLua. + std::vector stateHandlerMasks; + // Bytes charged per script for the bytecode, so we can swap bytecode behind + // people's backs without moving reported memory. Zero charges the real length. + uint32_t chargedBytecodeSize = 0; +}; + +// Numbered above the header versions the server wrote privately before this +// codec existed, so the two never read as each other in a log line. Fields +// only ever get appended inside the section, so any minor under the major +// parses and the reader defaults what an older writer left out. +constexpr StateFingerprint kBytecodeHeaderFingerprint{{'L', 'U', 'A', 'U'}, 6, 0}; + +// Appends the header to `out`; the raw bytecode follows it +void writeBytecodeHeader(std::string& out, const BytecodeHeader& header); + +// Parses the header off the front of an asset. `bytecode_start` is where the +// raw bytecode begins. False for a wrong tag, a different major, or truncation. +bool readBytecodeHeader(const char* data, size_t len, BytecodeHeader& header, size_t& bytecode_start); + +} // namespace Luau diff --git a/Bytecode/src/BytecodeHeader.cpp b/Bytecode/src/BytecodeHeader.cpp new file mode 100644 index 00000000..0e8b9a48 --- /dev/null +++ b/Bytecode/src/BytecodeHeader.cpp @@ -0,0 +1,70 @@ +// ServerLua: asset header codec, see `BytecodeHeader` in BytecodeHeader.h +#include "Luau/BytecodeHeader.h" + +#include + +namespace Luau +{ + +void writeBytecodeHeader(std::string& out, const BytecodeHeader& header) +{ + ByteWriter writer{out}; + writer.writeBytes(kBytecodeHeaderFingerprint.tag, sizeof(kBytecodeHeaderFingerprint.tag)); + writer.writeU32(kBytecodeHeaderFingerprint.major); + writer.writeU32(kBytecodeHeaderFingerprint.minor); + + size_t section = writer.beginSection(); + writer.writeU8((uint8_t)header.isLSL); + writer.writeU32(header.apiVersion); + writer.writeU32((uint32_t)header.stateHandlerMasks.size()); + for (uint64_t mask : header.stateHandlerMasks) + writer.writeU64(mask); + writer.writeU32(header.chargedBytecodeSize); + // New fields go here, and bump kBytecodeHeaderFingerprint.minor + writer.endSection(section); +} + +bool readBytecodeHeader(const char* data, size_t len, BytecodeHeader& header, size_t& bytecode_start) +{ + if (data == nullptr) + return false; + + ByteReader reader{data, len}; + + char tag[sizeof(kBytecodeHeaderFingerprint.tag)]; + if (!reader.readBytes(tag, sizeof(tag)) || memcmp(tag, kBytecodeHeaderFingerprint.tag, sizeof(tag)) != 0) + return false; + + uint32_t major = 0; + uint32_t minor = 0; + if (!reader.readU32(major) || !reader.readU32(minor) || major != kBytecodeHeaderFingerprint.major) + return false; + + ByteReader section{nullptr, 0}; + if (!reader.readSection(section)) + return false; + + uint8_t is_lsl = 0; + uint32_t num_states = 0; + if (!section.readU8(is_lsl) || !section.readU32(header.apiVersion) || !section.readU32(num_states)) + return false; + // Eight bytes each, so a count the section can't hold is a corrupt header + // rather than something to allocate for + if (num_states > section.remaining / sizeof(uint64_t)) + return false; + header.isLSL = is_lsl != 0; + header.stateHandlerMasks.resize(num_states); + for (uint64_t& mask : header.stateHandlerMasks) + { + if (!section.readU64(mask)) + return false; + } + if (!section.readU32(header.chargedBytecodeSize)) + return false; + // Fields appended after 6.0 are read here only if the section has them + + bytecode_start = len - reader.remaining; + return true; +} + +} // namespace Luau diff --git a/CLI/src/Compile.cpp b/CLI/src/Compile.cpp index 1c2a9f7c..5ef578c6 100644 --- a/CLI/src/Compile.cpp +++ b/CLI/src/Compile.cpp @@ -2,6 +2,7 @@ #include "Luau/CodeGenOptions.h" #include "lua.h" #include "lualib.h" +#include "luacode.h" #include "Luau/CodeGen.h" #include "Luau/Compiler.h" diff --git a/CLI/src/Harness.cpp b/CLI/src/Harness.cpp index b8da56b0..dca79148 100644 --- a/CLI/src/Harness.cpp +++ b/CLI/src/Harness.cpp @@ -3,14 +3,15 @@ // scheduling overhead that a bare REPL never exercises can be measured. #include "lua.h" #include "lualib.h" +#include "luacode.h" #include "llsl.h" #include "Luau/Common.h" #include "Luau/Compiler.h" +#include "Luau/ParseResult.h" #include "Luau/Executor.h" #include "Luau/FileUtils.h" #include "Luau/Flags.h" -#include "Luau/LSLBuiltins.h" #include "Luau/Script.h" #ifdef LUAU_USE_TAILSLIDE @@ -87,17 +88,15 @@ static double script_clock(lua_State* L) static void log_to_stderr(LogLevel level, const char* source, const char* message) { - static const char* level_names[] = {"DEBUG", "INFO", "WARN"}; + static const char* level_names[] = {"DEBUG", "INFO", "WARN", "ERROR"}; fprintf(stderr, "[%s] %s: %s\n", level_names[(int)level], source, message); } // Drives the script through run windows until it completes, faults, or -// refuses. `lsl_state` is left at whatever state it ended in so a later -// dispatch lands in the right handler table. -static RunResult run_to_completion(Script& script, double quanta, bool is_lsl, double& accum_sleep, size_t& slices, int& lsl_state) +// refuses, following the sim's driver: finish whatever is in flight, run a +// pending state_exit, commit the state change, run state_entry. +static RunResult run_to_completion(Script& script, double quanta, double& accum_sleep, size_t& slices) { - // state_entry is implicit in Lua, but LSL needs it specifically dispatched. - bool dispatch_state_entry = is_lsl; RunResult result; for (;;) { @@ -109,32 +108,40 @@ static RunResult run_to_completion(Script& script, double quanta, bool is_lsl, d script.setSleep(0.0f); } + int event = Luau::LSLEvent::None; + if (!script.isHandlerActive()) { - RunWindow window(script, quanta); - if (dispatch_state_entry) - { - result = script.callEventHandler(lsl_state, "state_entry", nullptr); - dispatch_state_entry = false; - } + // The engine only pends state_exit when the departing state handles it + if (script.isStateChangePending() && !(script.getCurrentEvents() & Luau::LSLEventBit::StateExit)) + script.nextState(); + + if (script.getCurrentEvents() & Luau::LSLEventBit::StateExit) + event = Luau::LSLEvent::StateExit; + else if (script.getCurrentEvents() & Luau::LSLEventBit::StateEntry) + event = Luau::LSLEvent::StateEntry; else + break; + + // A state without the handler just drops the event + const uint64_t event_bit = Luau::lslEventBit(event); + if (!(script.getEventHandlers() & event_bit)) { - result = script.resumeEventHandler(); + script.setCurrentEvents(script.getCurrentEvents() & ~event_bit); + continue; } } - ++slices; - if (result.status == HandlerRunStatus::Preempted) - continue; - if (result.status == HandlerRunStatus::StateChange) { - // TODO: Do state_exit too... meh. - lsl_state = result.newState; - dispatch_state_entry = true; - continue; + RunWindow window(script, quanta); + if (event != Luau::LSLEvent::None) + result = script.callEventHandler(event, nullptr); + else + result = script.resumeEventHandler(); } + ++slices; - // Anything else means we're done. - break; + if (result.status != HandlerRunStatus::Preempted && result.status != HandlerRunStatus::Ok && result.status != HandlerRunStatus::StateChange) + break; } // Bank sleep the final slice left behind @@ -234,7 +241,7 @@ static void print_window_timing(const char* label, const WindowTiming& timing) // Opens and closes `count` windows twice over: once empty, so the installer // and GC bookkeeping are all that's measured, then once dispatching a // handler each time. The difference is the Lua dispatch cost. -static int run_window_bench(Script& script, double quanta, size_t count, int lsl_state) +static int run_window_bench(Script& script, double quanta, size_t count) { WindowTiming empty = time_windows(count, [&]() { RunWindow window(script, quanta); @@ -252,10 +259,7 @@ static int run_window_bench(Script& script, double quanta, size_t count, int lsl if (resuming) result = script.resumeEventHandler(); else - script.callEventHandler(lsl_state, "touch_start", [](lua_State* L, void *ctx) - { - lua_pushnumber(L, 0); - }); + result = script.callEventHandler(Luau::LSLEvent::MovingStart, nullptr); resuming = result.status == HandlerRunStatus::Preempted; return resuming || result.status == HandlerRunStatus::Ok; }); @@ -269,11 +273,11 @@ static int run_window_bench(Script& script, double quanta, size_t count, int lsl resuming = result.status == HandlerRunStatus::Preempted; } - // A missing state_entry is fine for a normal run, but here it means the + // A missing handler is fine for a normal run, but here it means the // user's script has nothing to dispatch. if (result.status == HandlerRunStatus::NotRun) { - fprintf(stderr, "Error: script has no touch_start handler to benchmark\n"); + fprintf(stderr, "Error: script has no moving_start handler to benchmark\n"); return 1; } return report_failure(script, result); @@ -289,7 +293,7 @@ static void displayHelp(const char* argv0) printf("Options:\n"); printf(" --quanta=: time slice per run window (default 200)\n"); printf(" --window-bench=: after the script completes, open and close n run\n" - " windows, empty and then dispatching its touch_start handler,\n" + " windows, empty and then dispatching its moving_start handler,\n" " to measure the per-window overhead\n"); printf(" --fire-lead=: how early the threaded or signal installer puts the\n" " interrupt handler in ahead of the deadline (default: per policy)\n"); @@ -420,25 +424,45 @@ int main(int argc, char** argv) return 1; } - const bool is_lsl = strstr(script_path, ".lsl") != nullptr; - std::string bytecode; - if (is_lsl) + // Only picks which compiler to run; everything downstream reads the flavor + // off the asset header instead. + const bool compile_as_lsl = strstr(script_path, ".lsl") != nullptr; + const uint32_t api_version = 0; + // What a host would have stored: the header, then the bytecode. A failed + // compile throws instead, so nothing that isn't an asset reaches the engine. + std::string asset; + try { + if (compile_as_lsl) + { #ifdef LUAU_USE_TAILSLIDE - bytecode = compileLSL(*source); + asset = compileLSLAssetOrThrow(*source, api_version); #else - fprintf(stderr, "No LSL support, do a Tailslide-enabled build\n"); - return 1; + fprintf(stderr, "No LSL support, do a Tailslide-enabled build\n"); + return 1; #endif + } + else + { + Luau::CompileOptions copts = {}; + copts.optimizationLevel = optimization_level; + copts.debugLevel = 1; + copts.typeInfoLevel = 1; + copts.libraryMemberConstantCb = &luauSL_lookup_constant_cb; + asset = Luau::compileAssetOrThrow(*source, api_version, copts); + } } - else + catch (Luau::ParseErrors& e) { - Luau::CompileOptions copts = {}; - copts.optimizationLevel = optimization_level; - copts.debugLevel = 1; - copts.typeInfoLevel = 1; - copts.libraryMemberConstantCb = &luauSL_lookup_constant_cb; - bytecode = Luau::compile(*source, copts); + fprintf(stderr, "Compile error:\n"); + for (const Luau::ParseError& error : e.getErrors()) + fprintf(stderr, "%d: %s\n", error.getLocation().begin.line + 1, error.what()); + return 1; + } + catch (Luau::CompileError& e) + { + fprintf(stderr, "Compile error:\n%d: %s\n", e.getLocation().begin.line + 1, e.what()); + return 1; } HostCallbacks callbacks; @@ -455,19 +479,16 @@ int main(int argc, char** argv) Provisioner<> provisioner(callbacks); ImageConfig image_config; - image_config.bytecode = bytecode.data(); - image_config.bytecodeSize = bytecode.size(); - image_config.chargedBytecodeSize = bytecode.size(); - image_config.isLSL = is_lsl; - image_config.chunkname = is_lsl ? "=lsl_script" : "=lua_script"; + image_config.asset = asset.data(); + image_config.assetSize = asset.size(); image_config.name = script_path; - // Built stepwise rather than through provisionScript() so compile and - // load errors can be printed. - std::shared_ptr image = provisioner.buildImage(provisioner.createEnvironment(is_lsl, 0), image_config); + // Built stepwise rather than through provisionScript() so load errors can + // be printed. + std::shared_ptr image = provisioner.buildImage(provisioner.createEnvironment(compile_as_lsl, api_version), image_config); if (image == nullptr || !image->isValid()) { - fprintf(stderr, "Compile error:\n%s\n", image != nullptr ? image->getError().c_str() : "image build refused"); + fprintf(stderr, "Load error:\n%s\n", image != nullptr ? image->getError().c_str() : "image build refused"); return 1; } @@ -480,7 +501,7 @@ int main(int argc, char** argv) return 1; } - // For LSL this also runs the constructor + // Stages the main function (LSL's constructor) as the first handler if (!script->loadDefaultState()) { fprintf(stderr, "Load error: %s\n", script->getExtendedFaultString().empty() ? script->getFaultString().c_str() : script->getExtendedFaultString().c_str()); @@ -490,15 +511,14 @@ int main(int argc, char** argv) double quanta = quanta_usec * 1e-6; double accum_sleep = 0.0; size_t slices = 0; - int lsl_state = 0; double start = lua_clock(); - RunResult result = run_to_completion(*script, quanta, is_lsl, accum_sleep, slices, lsl_state); + RunResult result = run_to_completion(*script, quanta, accum_sleep, slices); double runtime = lua_clock() - start; fprintf(stderr, "Runtime: %f, Accum. Sleep: %f, Time Slices: %zu\n", runtime, accum_sleep, slices); int exit_code = report_failure(*script, result); if (exit_code == 0 && window_bench > 0) - exit_code = run_window_bench(*script, quanta, window_bench, lsl_state); + exit_code = run_window_bench(*script, quanta, window_bench); print_watchdog_stats(provisioner); return exit_code; diff --git a/CLI/src/Repl.cpp b/CLI/src/Repl.cpp index b3f834fe..62c37777 100644 --- a/CLI/src/Repl.cpp +++ b/CLI/src/Repl.cpp @@ -5,10 +5,10 @@ #include "Luau/Common.h" #include "lua.h" #include "lualib.h" +#include "luacode.h" #include "Luau/CodeGen.h" #include "Luau/Compiler.h" -#include "Luau/LSLBuiltins.h" #include "Luau/Parser.h" #include "Luau/TimeTrace.h" #include "Luau/Counters.h" @@ -43,6 +43,7 @@ #endif #include "llsl.h" +#include "Luau/LSLBuiltins.h" #ifdef LUAU_USE_TAILSLIDE #include "Luau/LSLCompiler.h" #endif diff --git a/CMakeLists.txt b/CMakeLists.txt index fc5e21a8..57f0d4a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,7 @@ target_link_libraries(Luau.CLI.lib PUBLIC Luau.LSLBuiltins) target_compile_features(Luau.LSLBuiltins PUBLIC cxx_std_17) target_include_directories(Luau.LSLBuiltins PUBLIC LSLBuiltins/include) target_include_directories(Luau.LSLBuiltins PRIVATE ${CMAKE_BINARY_DIR}) # For builtins_embedded.h -target_link_libraries(Luau.LSLBuiltins PRIVATE Luau.Common Luau.VM Luau.Compiler) +target_link_libraries(Luau.LSLBuiltins PUBLIC Luau.Common) target_compile_features(Luau.Ast PUBLIC cxx_std_17) target_include_directories(Luau.Ast PUBLIC Ast/include) @@ -154,7 +154,7 @@ target_link_libraries(Luau.Inliner PUBLIC Luau.Bytecode) target_compile_features(Luau.Compiler PUBLIC cxx_std_17) target_include_directories(Luau.Compiler PUBLIC Compiler/include "${PACKAGE_INCLUDE_DIR}") -target_link_libraries(Luau.Compiler PUBLIC Luau.Ast Luau.Bytecode) +target_link_libraries(Luau.Compiler PUBLIC Luau.Ast Luau.Bytecode Luau.LSLBuiltins) target_compile_features(Luau.Config PUBLIC cxx_std_17) target_include_directories(Luau.Config PUBLIC Config/include) @@ -173,12 +173,12 @@ target_link_libraries(Luau.CodeGen PUBLIC Luau.Common) target_compile_features(Luau.VM PRIVATE cxx_std_17) target_include_directories(Luau.VM PUBLIC VM/include "${PACKAGE_INCLUDE_DIR}") -target_link_libraries(Luau.VM PUBLIC Luau.Common) +target_link_libraries(Luau.VM PUBLIC Luau.Common Luau.LSLBuiltins) # ServerLua: per-script execution engine shared with the script host target_compile_features(Luau.Executor PUBLIC cxx_std_17) target_include_directories(Luau.Executor PUBLIC Executor/include) -target_link_libraries(Luau.Executor PUBLIC Luau.VM) +target_link_libraries(Luau.Executor PUBLIC Luau.VM Luau.Bytecode Luau.LSLBuiltins) target_link_libraries(Luau.Executor PRIVATE Luau.VM.Internals) target_compile_features(Luau.Require PUBLIC cxx_std_17) @@ -321,7 +321,7 @@ if(CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin|iOS") target_link_libraries(osthreads INTERFACE "-lpthread") endif () -# ServerLua: We use threads internally, library consumers need pthreads if applciable. +# ServerLua: We use threads internally, library consumers need pthreads if applicable. target_link_libraries(Luau.Executor PUBLIC osthreads) # ServerLua: POSIX timers live in librt on glibc < 2.34, a stub elsewhere if(CMAKE_SYSTEM_NAME MATCHES "Linux") diff --git a/Executor/include/Luau/ByteStream.h b/Common/include/Luau/ByteStream.h similarity index 63% rename from Executor/include/Luau/ByteStream.h rename to Common/include/Luau/ByteStream.h index 81d68e2d..75f91716 100644 --- a/Executor/include/Luau/ByteStream.h +++ b/Common/include/Luau/ByteStream.h @@ -1,6 +1,7 @@ // ServerLua: little-endian bytestream primitives for the wrappers we place -// around ares-serialized state. I regret some of my design decisions here -// and this probably should not need to be part of the public API. +// around ares-serialized state and in front of bytecode. I regret some of my +// design decisions here and this probably should not need to be part of the +// public API. #pragma once #include @@ -9,8 +10,20 @@ namespace Luau { -namespace Executor + +// Identifies which class a persisted payload belongs to, and which layout it +// used. A reader refuses a different `major` and accepts any `minor`: fields +// are only ever appended inside length-prefixed sections, so a newer minor's +// extra bytes are skipped. Bump `minor` for an append, `major` for anything +// that can't be expressed as one. +struct StateFingerprint { + // A FOURCC, so the head of a payload reads as text in a dump + char tag[4]; + uint32_t major; + uint32_t minor; +}; +static_assert(sizeof(StateFingerprint::tag) == 4); struct ByteWriter { @@ -56,6 +69,23 @@ struct ByteWriter } void writeString(const std::string& value) { writeString(value.data(), value.size()); } + + // Length-prefixed section. Reserve the length, write the body, then patch + // it in. Readers skip whatever they don't understand at the end of a + // section, so fields may be appended to one without breaking older readers. + size_t beginSection() + { + size_t at = out.size(); + writeU32(0); + return at; + } + + void endSection(size_t at) + { + uint32_t len = (uint32_t)(out.size() - at - 4); + for (int i = 0; i < 4; ++i) + out[at + i] = (char)(uint8_t)(len >> (i * 8)); + } }; struct ByteReader @@ -131,7 +161,20 @@ struct ByteReader remaining -= len; return true; } + + // Hands back a reader bounded to the next section and steps over it + bool readSection(ByteReader& section) + { + uint32_t len; + if (!readU32(len) || len > remaining) + return false; + section = ByteReader{data, len}; + data += len; + remaining -= len; + return true; + } + + bool atEnd() const { return remaining == 0; } }; -} // namespace Executor } // namespace Luau diff --git a/Compiler/include/Luau/Compiler.h b/Compiler/include/Luau/Compiler.h index ae2afd0f..4ea89015 100644 --- a/Compiler/include/Luau/Compiler.h +++ b/Compiler/include/Luau/Compiler.h @@ -5,6 +5,7 @@ #include "Luau/Location.h" #include "Luau/StringUtils.h" #include "Luau/Common.h" +#include "Luau/BytecodeHeader.h" namespace Luau { @@ -104,6 +105,17 @@ std::string compile( BytecodeEncoder* encoder = nullptr ); +// ServerLua: compiles a complete asset, a BytecodeHeader followed by the +// bytecode. Throws on errors rather than encoding one in the return value: +// nothing stores a failed compile, so there is no asset shape to put it in. +std::string compileAssetOrThrow( + const std::string& source, + uint32_t apiVersion = 0, + 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); diff --git a/Compiler/include/Luau/LSLCompiler.h b/Compiler/include/Luau/LSLCompiler.h index c95f3198..90aea52f 100644 --- a/Compiler/include/Luau/LSLCompiler.h +++ b/Compiler/include/Luau/LSLCompiler.h @@ -52,6 +52,11 @@ class LuauResourceVisitor : public ASTVisitor { LuauSymbolMap *_mSymData = nullptr; uint32_t _mTopFuncID = 0; uint32_t _mTopStateID = 0; + // One handled-events mask per state, indexed by state number + std::vector _mStateMasks; + +public: + const std::vector &getStateMasks() const { return _mStateMasks; } }; @@ -142,7 +147,17 @@ class LuauVisitor : public ASTVisitor } -void compileLSLOrThrow(Luau::BytecodeBuilder &bcb, const std::string &source); -std::string compileLSL(const std::string &source); +// What the compiler learned about a script beyond its bytecode +struct LSLScriptInfo { + // One mask per state, indexed by state number. Bit (index - 1) is set for + // each event the state handles, index being the event's 1-based position + // in builtins.txt, which Tailslide records on the event symbol. + std::vector stateHandlerMasks; +}; + +void compileLSLOrThrow(Luau::BytecodeBuilder &bcb, const std::string &source, LSLScriptInfo *info = nullptr); +std::string compileLSL(const std::string &source, LSLScriptInfo *info = nullptr); + +std::string compileLSLAssetOrThrow(const std::string &source, uint32_t apiVersion = 0); #endif // LUAU_LSLCOMPILER_H diff --git a/Compiler/include/luacode.h b/Compiler/include/luacode.h index 3eb1915c..e004bee4 100644 --- a/Compiler/include/luacode.h +++ b/Compiler/include/luacode.h @@ -87,3 +87,6 @@ LUACODE_API void luau_set_compile_constant_vector(lua_CompileConstant* constant, 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); +// ServerLua: libraryMemberConstantCb that folds the LSL constants from the loaded builtins.txt +LUACODE_API void luauSL_lookup_constant_cb(const char* library, const char* member, lua_CompileConstant* constant); + diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index b165593d..75ce72a1 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -5374,6 +5374,27 @@ std::string compile(const std::string& source, const CompileOptions& options, co } } +// ServerLua: asset-returning counterpart to compileOrThrow() +std::string compileAssetOrThrow( + const std::string& source, + uint32_t apiVersion, + const CompileOptions& options, + const ParseOptions& parseOptions, + BytecodeEncoder* encoder +) +{ + BytecodeBuilder bcb(encoder); + compileOrThrow(bcb, source, options, parseOptions); + + BytecodeHeader header; + header.apiVersion = apiVersion; + + std::string asset; + writeBytecodeHeader(asset, header); + asset += bcb.getBytecode(); + return asset; +} + void setCompileConstantNil(CompileConstant* constant) { Compile::Constant* target = reinterpret_cast(constant); diff --git a/Compiler/src/LSLCompiler.cpp b/Compiler/src/LSLCompiler.cpp index e4b9b480..ff063dc5 100644 --- a/Compiler/src/LSLCompiler.cpp +++ b/Compiler/src/LSLCompiler.cpp @@ -3,6 +3,7 @@ #include "Luau/LSLCompiler.h" #include "Luau/Compiler.h" +#include "Luau/LSLBuiltins.h" #include "Luau/ParseResult.h" #include @@ -17,6 +18,9 @@ constexpr uint32_t kMaxLocalCount = 200; constexpr uint32_t kMaxStringImportRef = 1024; // static const uint32_t kMaxInstructionCount = 1'000'000'000; +// Masks are 64 bits wide, so an event index past this can't be represented +constexpr int kMaxEventIndex = 64; + static Luau::Location convertLoc(TailslideLType *loc) { if (loc == nullptr) @@ -52,11 +56,27 @@ bool LuauResourceVisitor::visit(LSLState *state) if (_mTopStateID >= INT16_MAX) throw Luau::CompileError(convertLoc(state->getLoc()), "Too many states"); getSymbolData(state->getSymbol())->index = (int16_t)_mTopStateID++; + _mStateMasks.push_back(0); return true; } bool LuauResourceVisitor::visit(LSLEventHandler *handler) { handleFuncLike(handler); + + // Parented to a node list which is parented to the state, same as the + // name mangling + auto *state_sym = handler->getParent()->getParent()->getSymbol(); + LUAU_ASSERT(state_sym != nullptr); + // Tailslide numbered the event by its position in builtins.txt + const char *event_name = handler->getSymbol()->getName(); + int event_index = handler->getSymbol()->getEventIndex(); + if (event_index <= 0 || event_index > kMaxEventIndex) + throw Luau::CompileError(convertLoc(handler->getLoc()), Luau::format("Event '%s' has no usable index", event_name).c_str()); + // The runtime dispatches by the same number, so the two had better agree + // on which builtins.txt they read + if (Luau::lslEventIndex(event_name) != event_index) + throw Luau::CompileError(convertLoc(handler->getLoc()), Luau::format("Event '%s' is numbered %d by the compiler but %d by the runtime", event_name, event_index, Luau::lslEventIndex(event_name)).c_str()); + _mStateMasks[getSymbolData(state_sym)->index] |= (uint64_t)1 << (event_index - 1); return false; } @@ -2168,7 +2188,7 @@ void LuauVisitor::patchJumpOrThrow(size_t jumpLabel, size_t targetLabel) } -void compileLSLOrThrow(Luau::BytecodeBuilder &bcb, const std::string &source) +void compileLSLOrThrow(Luau::BytecodeBuilder &bcb, const std::string &source, LSLScriptInfo *info) { thread_local bool builtins_initialized = false; if (!builtins_initialized) { @@ -2260,14 +2280,17 @@ void compileLSLOrThrow(Luau::BytecodeBuilder &bcb, const std::string &source) LuauVisitor luauVisitor(&bcb, symbol_map); script->visit(&luauVisitor); + + if (info != nullptr) + info->stateHandlerMasks = luauResourceVisitor.getStateMasks(); } -std::string compileLSL(const std::string &source) +std::string compileLSL(const std::string &source, LSLScriptInfo *info) { Luau::BytecodeBuilder bcb; try { - compileLSLOrThrow(bcb, source); + compileLSLOrThrow(bcb, source, info); return bcb.getBytecode(); } catch (Luau::ParseErrors &e) @@ -2285,6 +2308,23 @@ std::string compileLSL(const std::string &source) } } +std::string compileLSLAssetOrThrow(const std::string &source, uint32_t apiVersion) +{ + Luau::BytecodeBuilder bcb; + LSLScriptInfo info; + compileLSLOrThrow(bcb, source, &info); + + Luau::BytecodeHeader header; + header.isLSL = true; + header.apiVersion = apiVersion; + header.stateHandlerMasks = std::move(info.stateHandlerMasks); + + std::string asset; + Luau::writeBytecodeHeader(asset, header); + asset += bcb.getBytecode(); + return asset; +} + char* luau_lsl_compile(const char* source, size_t size, size_t* outsize, bool *is_error) { *outsize = 0; diff --git a/Compiler/src/lcode.cpp b/Compiler/src/lcode.cpp index b3327cb3..008ac09e 100644 --- a/Compiler/src/lcode.cpp +++ b/Compiler/src/lcode.cpp @@ -2,6 +2,7 @@ #include "luacode.h" #include "Luau/Compiler.h" +#include "Luau/LSLBuiltins.h" // ServerLua #include @@ -62,3 +63,37 @@ void luau_set_compile_constant_string(lua_CompileConstant* constant, const char* { Luau::setCompileConstantString(constant, s, l); } + +// ServerLua: constant folder hook for the LSL builtins +void luauSL_lookup_constant_cb(const char* library, const char* member, lua_CompileConstant* constant) +{ + // We only touch _globals_ + if (library != nullptr) + return; + + const Luau::SLConstant* sl_constant = Luau::luauSL_find_constant(member); + if (sl_constant == nullptr) + return; + + switch (sl_constant->type) + { + case Luau::SLConstantType::String: + luau_set_compile_constant_string(constant, sl_constant->valueString, sl_constant->stringLength); + break; + case Luau::SLConstantType::Integer: + luau_set_compile_constant_number(constant, (double)sl_constant->valueInteger); + break; + case Luau::SLConstantType::Float: + luau_set_compile_constant_number(constant, sl_constant->valueNumber); + break; + case Luau::SLConstantType::Vector: + { + const auto& vec = sl_constant->valueVector; + luau_set_compile_constant_vector(constant, vec[0], vec[1], vec[2], 0.0f); + break; + } + default: + // Can't set these as compile-time constants. + break; + } +} diff --git a/Executor/include/Luau/Executor.h b/Executor/include/Luau/Executor.h index 243fd6cd..9036033a 100644 --- a/Executor/include/Luau/Executor.h +++ b/Executor/include/Luau/Executor.h @@ -2,11 +2,15 @@ #pragma once #include +#include #include #include +#include #include "lua.h" +#include "Luau/BytecodeHeader.h" + namespace Luau { namespace Executor @@ -19,14 +23,6 @@ constexpr int kUserMemcat = LUA_FIRST_USER_MEMCAT; // Default (and normal maximum) per-script memory limit constexpr int kDefaultMemoryLimit = 1024 * 128; -// Identifies which class a persisted payload belongs to, and which layout it -// used. Bump `version` whenever that class's fields change. -struct StateFingerprint -{ - char tag[4]; - uint32_t version; -}; - // Log levels for LogCallback enum class LogLevel : uint8_t { @@ -39,17 +35,11 @@ enum class LogLevel : uint8_t // Parameters that define the sealed image consumed by buildImage() struct ImageConfig { - // Luau bytecode with any host asset header already stripped off - const char* bytecode = nullptr; - size_t bytecodeSize = 0; - bool isLSL = false; - uint32_t apiVersion = 0; - // We want to leave open the possibility that we can change bytecode behind - // people's backs for upgrading reasons. Keep around the amount we want to - // actually "charge" them for the bytecode size for memory accounting purposes - // so this doesn't break scripts. - size_t chargedBytecodeSize = 0; - const char* chunkname = "=lua_script"; + // The stored asset: a BytecodeHeader followed by the Luau bytecode. The + // flavor, API version and charged size all come off the header, so there + // is only ever one source for them. + const char* asset = nullptr; + size_t assetSize = 0; // Identifier used in build log messages. Probably an asset UUID. const char* name = ""; }; @@ -88,6 +78,7 @@ inline LogCallback& logCallback() void logDebug(const char* source, const char* fmt, ...) LUA_PRINTF_ATTR(2, 3); void logInfo(const char* source, const char* fmt, ...) LUA_PRINTF_ATTR(2, 3); void logWarn(const char* source, const char* fmt, ...) LUA_PRINTF_ATTR(2, 3); +void logError(const char* source, const char* fmt, ...) LUA_PRINTF_ATTR(2, 3); // Monotonic seconds for the per-safepoint elapsed check, which is the one // place clock cost shows up in throughput. Nothing else reads it. @@ -361,6 +352,10 @@ class IImage virtual bool isLSL() const = 0; virtual uint32_t getAPIVersion() const = 0; + // The asset's per-state handler masks, for deciding what to dispatch. + // Empty for SLua, which registers its handlers at runtime instead. + virtual const std::vector& getStateHandlerMasks() const = 0; + // Forks off an instance using the forkserver, either with the default // state blob, or a provided one if we're resuming. virtual Instance forkInstance(lua_SLRuntimeState* owner, const std::string* blob = nullptr) = 0; @@ -394,8 +389,9 @@ class Image : public IImage const std::string& getName() const override { return mName; } IEnvironment& getEnvironment() const override { return *mEnvironment; } IProvisioner& getProvisioner() const override { return mEnvironment->getProvisioner(); } - bool isLSL() const override { return mIsLSL; } - uint32_t getAPIVersion() const override { return mAPIVersion; } + bool isLSL() const override { return mHeader.isLSL; } + uint32_t getAPIVersion() const override { return mHeader.apiVersion; } + const std::vector& getStateHandlerMasks() const override { return mHeader.stateHandlerMasks; } Instance forkInstance(lua_SLRuntimeState* owner, const std::string* blob) override; bool serializeInstance(const Instance& instance, std::string& out) override; @@ -417,8 +413,8 @@ class Image : public IImage // Keeps the environment alive for as long as the image exists std::shared_ptr mEnvironment; - bool mIsLSL = false; - uint32_t mAPIVersion = 0; + // Parsed off the front of the asset by build() + BytecodeHeader mHeader; // Ares forkserver thread holding the pristine post-load snapshot, // anchored in the environment's registry @@ -493,11 +489,6 @@ class Provisioner : public IProvisioner logWarn(config.name ? config.name : "", "Refusing to build image in an environment provisioned elsewhere"); return nullptr; } - if (environment->isLSL() != config.isLSL || environment->getAPIVersion() != config.apiVersion) - { - logWarn(config.name ? config.name : "", "Refusing to build image in an environment of a different flavor"); - return nullptr; - } std::shared_ptr image = makeImage(std::move(environment), config); image->build(config); @@ -523,7 +514,17 @@ class Provisioner : public IProvisioner // Convenience method so you don't have to do the above manually. std::shared_ptr