diff --git a/.gitignore b/.gitignore index 4f34a306..cba1cf26 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ __pycache__ /fuzz/corpus/json/* !/fuzz/corpus/json/*.json *.code-workspace +*.tar.bz2 diff --git a/Sources.cmake b/Sources.cmake index 7736c0a9..81ea185e 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -429,6 +429,8 @@ target_sources(Luau.VM PRIVATE VM/src/llprim.cpp VM/src/llprim.h VM/src/llprim_set_primitive_params.inl + VM/src/llfluent_builder.cpp + VM/include/llfluent_builder.h VM/src/lyieldable.cpp VM/src/lstrbuf.cpp VM/src/lyieldstrlib.h diff --git a/VM/include/llfluent_builder.h b/VM/include/llfluent_builder.h new file mode 100644 index 00000000..0990b799 --- /dev/null +++ b/VM/include/llfluent_builder.h @@ -0,0 +1,58 @@ +#pragma once +#include + +struct lua_State; +typedef int (*lua_CFunction)(lua_State* L); + +// Default link target: apply to the prim running the script. +static const int SLUA_LINK_THIS = -4; + +struct FluentParamDescriptor +{ + const char* name; // effective property name (pretty alias or strict fallback) + char semantic; // 'i','f','s','v','r','b','a','k' + int tag; // PSYS_* constant integer value +}; + +struct FluentFlagDescriptor +{ + const char* name; // boolean property name, e.g. "color_interp" + int mask; // bitmask, e.g. 0x01 + int field_tag; // tag of the integer field holding the bits, e.g. 0 for "flags" +}; + +// Opaque handle — definition lives in llfluent_builder.cpp. +struct FluentBuilderDef; + +// Build a FluentBuilderDef from an array of descriptors. +// Deep-copies all strings. Caller owns the returned pointer (process lifetime expected). +// Descriptors are sorted by tag internally; caller order does not matter. +FluentBuilderDef* fluent_builder_def_build( + const FluentParamDescriptor* descs, + size_t count +); + +// Attach flag-bit boolean properties to an existing def. +// Each descriptor maps a property name to a bitmask within the integer field at field_tag. +// Deep-copies all strings. Call after fluent_builder_def_build(). +void fluent_builder_def_add_flags( + FluentBuilderDef* def, + const FluentFlagDescriptor* descs, + size_t count +); + +// Serialize a params table into a flat tag/value rules list and push it onto the stack. +// params_idx is the stack index of the params table (may be nil — emits an empty list). +// Flag boolean properties are merged into their backing integer field before emission. +void slua_fluent_serialize(lua_State* L, int params_idx, const FluentBuilderDef* def); + +// Register fn as module_name.fn_name in L's globals, with def stored as upvalue 1. +// Creates the module table if it does not yet exist; adds to it if it does. +// Sets the module table readonly after each call. +void slua_register_fluent_fn( + lua_State* L, + const char* module_name, + const char* fn_name, + lua_CFunction fn, + const FluentBuilderDef* def +); diff --git a/VM/src/llfluent_builder.cpp b/VM/src/llfluent_builder.cpp new file mode 100644 index 00000000..4289a7d0 --- /dev/null +++ b/VM/src/llfluent_builder.cpp @@ -0,0 +1,185 @@ +#define llfluent_builder_c + +#include "lua.h" +#include "lcommon.h" +#include "lualib.h" +#include "llsl.h" +#include "llfluent_builder.h" + +#include +#include +#include +#include + +struct FluentBuilderDef +{ + std::vector descs; // sorted by tag + std::vector names; // storage for descriptor name strings + std::unordered_map name_to_index; + + std::vector flag_descs; + std::vector flag_names; + std::unordered_map flag_name_to_index; +}; + +FluentBuilderDef* fluent_builder_def_build( + const FluentParamDescriptor* descs, + size_t count +) +{ + auto* def = new FluentBuilderDef; + + // Copy descriptors and deep-copy name strings. + def->descs.resize(count); + def->names.resize(count); + for (size_t i = 0; i < count; ++i) + { + def->names[i] = descs[i].name; + def->descs[i] = descs[i]; + def->descs[i].name = def->names[i].c_str(); + } + + // Sort by tag for deterministic serialization order. + // names and descs stay in sync via index sort. + std::vector order(count); + for (size_t i = 0; i < count; ++i) order[i] = i; + std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { + return def->descs[a].tag < def->descs[b].tag; + }); + + std::vector sorted_descs(count); + std::vector sorted_names(count); + for (size_t i = 0; i < count; ++i) + { + sorted_names[i] = std::move(def->names[order[i]]); + sorted_descs[i] = def->descs[order[i]]; + sorted_descs[i].name = sorted_names[i].c_str(); + } + def->descs = std::move(sorted_descs); + def->names = std::move(sorted_names); + + // Build name-to-index lookup. + for (int i = 0; i < (int)count; ++i) + def->name_to_index[def->descs[i].name] = i; + + return def; +} + +void fluent_builder_def_add_flags( + FluentBuilderDef* def, + const FluentFlagDescriptor* descs, + size_t count +) +{ + def->flag_descs.resize(count); + def->flag_names.resize(count); + for (size_t i = 0; i < count; ++i) + { + def->flag_names[i] = descs[i].name; + def->flag_descs[i] = descs[i]; + def->flag_descs[i].name = def->flag_names[i].c_str(); + def->flag_name_to_index[def->flag_names[i]] = (int)i; + } +} + +void slua_fluent_serialize(lua_State* L, int params_idx, const FluentBuilderDef* def) +{ + lua_newtable(L); + int list = lua_gettop(L); + int idx = 0; + + if (lua_isnoneornil(L, params_idx)) + return; // empty list + + // Phase 1: accumulate flag bits per backing field. + std::unordered_map flag_accumulator; // field_tag -> OR'd mask + for (const auto& fdesc : def->flag_descs) + { + lua_rawgetfield(L, params_idx, fdesc.name); + if (!lua_isnil(L, -1)) + { + bool set = lua_isboolean(L, -1) ? (bool)lua_toboolean(L, -1) + : (lua_tointeger(L, -1) != 0); + if (set) + flag_accumulator[fdesc.field_tag] |= fdesc.mask; + } + lua_pop(L, 1); + } + + // Phase 2: emit tag/value pairs in tag order. + for (const auto& desc : def->descs) + { + int raw_int = 0; + bool has_raw = false; + + lua_rawgetfield(L, params_idx, desc.name); + has_raw = !lua_isnil(L, -1); + if (has_raw && lua_isnumber(L, -1)) + raw_int = (int)lua_tointeger(L, -1); + if (!has_raw) + lua_pop(L, 1); + + // Integer fields that back flags: merge accumulated bits. + auto flag_it = flag_accumulator.find(desc.tag); + if (desc.semantic == 'i' && flag_it != flag_accumulator.end()) + { + int merged = raw_int | flag_it->second; + if (has_raw) lua_pop(L, 1); + if (merged != 0) + { + lua_pushinteger(L, desc.tag); + lua_rawseti(L, list, ++idx); + lua_pushinteger(L, merged); + lua_rawseti(L, list, ++idx); + } + continue; + } + + if (!has_raw) + continue; + + // Append tag then value. + lua_pushinteger(L, desc.tag); + lua_rawseti(L, list, ++idx); + + if (desc.semantic == 'b' && lua_isboolean(L, -1)) + lua_pushinteger(L, lua_toboolean(L, -1)); + else + lua_pushvalue(L, -1); + lua_rawseti(L, list, ++idx); + + lua_pop(L, 1); + } +} + +void slua_register_fluent_fn( + lua_State* L, + const char* module_name, + const char* fn_name, + lua_CFunction fn, + const FluentBuilderDef* def +) +{ + int top = lua_gettop(L); + + lua_getglobal(L, module_name); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + lua_newtable(L); + } + else + { + lua_setreadonly(L, -1, false); + } + int module_idx = lua_gettop(L); + + lua_pushlightuserdata(L, (void*)def); + lua_pushcclosurek(L, fn, fn_name, 1, nullptr); + lua_setfield(L, module_idx, fn_name); + + lua_setreadonly(L, module_idx, true); + lua_setglobal(L, module_name); + + LUAU_ASSERT(lua_gettop(L) == top); +} diff --git a/build-cmd.sh b/build-cmd.sh index 3146ed2f..df076335 100755 --- a/build-cmd.sh +++ b/build-cmd.sh @@ -37,7 +37,7 @@ mkdir -p "$stage/lib/release" pushd "$top" pushd "VM/include" - cp -v lua.h luaconf.h lualib.h llsl.h "$stage/include/luau/" + cp -v lua.h luaconf.h lualib.h llsl.h llfluent_builder.h "$stage/include/luau/" popd pushd "Compiler/include" cp -v luacode.h "$stage/include/luau/" diff --git a/init_debian_buster.sh b/init_debian_buster.sh index fe7736e6..bba6b3b6 100755 --- a/init_debian_buster.sh +++ b/init_debian_buster.sh @@ -29,10 +29,15 @@ apt-get install -y gcc-multilib g++-multilib cmake # Finally, autobuild pip3 --no-cache-dir install pydot==1.4.2 pyzstd==0.15.10 autobuild +update-alternatives --install /usr/bin/clang clang /usr/bin/clang-11 100 +update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-11 100 update-alternatives --install /usr/bin/cc cc /usr/bin/clang-11 100 update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-11 100 +update-alternatives --install /usr/bin/python python /usr/bin/python3 100 cd /root git clone https://github.com/secondlife/build-variables.git cd build-variables echo 'export AUTOBUILD_VARIABLES_FILE=/root/build-variables/variables' >> ~/.bashrc + +git config --global --add safe.directory $(pwd) diff --git a/tests/SLConformance.test.cpp b/tests/SLConformance.test.cpp index e52fd325..e16cde9f 100644 --- a/tests/SLConformance.test.cpp +++ b/tests/SLConformance.test.cpp @@ -3,6 +3,7 @@ #include "lualib.h" #include "luacode.h" #include "luacodegen.h" +#include "llfluent_builder.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/DenseHash.h" @@ -1486,4 +1487,56 @@ TEST_CASE("Memory hygiene") runConformance("memory_hygiene.lua"); } +TEST_CASE("llparticle") +{ + static const FluentParamDescriptor kDescs[] = { + {"flags", 'i', 0}, + {"color_begin", 'v', 1}, + {"alpha_begin", 'f', 2}, + {"burst_rate", 'f', 13}, + }; + static const FluentFlagDescriptor kFlagDescs[] = { + {"color_interp", 0x1, 0}, + {"scale_interp", 0x2, 0}, + {"bounce", 0x4, 0}, + {"emissive", 0x100, 0}, + }; + static FluentBuilderDef* s_def = []() { + auto* d = fluent_builder_def_build(kDescs, std::size(kDescs)); + fluent_builder_def_add_flags(d, kFlagDescs, std::size(kFlagDescs)); + return d; + }(); + + runConformance("llprim_particle.lua", nullptr, [](lua_State* L) { + // Mock ll.LinkParticleSystem so the dispatch wrapper can be exercised. + static const luaL_Reg test_ll_lib[] = { + {"LinkParticleSystem", [](lua_State* L) -> int { + luaL_checkinteger(L, 1); + luaL_checktype(L, 2, LUA_TTABLE); + lua_pushvalue(L, 1); + lua_setglobal(L, "_captured_link"); + lua_pushvalue(L, 2); + lua_setglobal(L, "_captured_rules"); + return 0; + }}, + {nullptr, nullptr} + }; + luaL_register_noclobber(L, LUA_LLLIBNAME, test_ll_lib); + + auto particle_system = [](lua_State* L) -> int { + const auto* def = (const FluentBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1)); + int link = lua_isnoneornil(L, 2) ? SLUA_LINK_THIS : luaL_checkinteger(L, 2); + slua_fluent_serialize(L, 1, def); + int rules_idx = lua_gettop(L); + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll"); + lua_rawgetfield(L, -1, "LinkParticleSystem"); + lua_pushinteger(L, link); + lua_pushvalue(L, rules_idx); + lua_call(L, 2, 0); + return 0; + }; + slua_register_fluent_fn(L, "llprim", "ParticleSystem", particle_system, s_def); + }); +} + TEST_SUITE_END(); diff --git a/tests/conformance/llprim_particle.lua b/tests/conformance/llprim_particle.lua new file mode 100644 index 00000000..dbe52d96 --- /dev/null +++ b/tests/conformance/llprim_particle.lua @@ -0,0 +1,75 @@ +-- Conformance test for llprim.ParticleSystem(). +-- The C++ test harness registers a mock ll.LinkParticleSystem that stores +-- captured_link and _captured_rules as globals, and registers llparticle +-- with a subset of particle descriptors: +-- flags 'i' tag 0 (backing field for flag bits) +-- color_begin 'v' tag 1 +-- alpha_begin 'f' tag 2 +-- burst_rate 'f' tag 13 +-- Flag descriptors (all backed by tag 0): +-- color_interp 0x001 +-- scale_interp 0x002 +-- bounce 0x004 +-- emissive 0x100 + +-- Helper: call and return captured state. +local function dispatch(params, link) + if link ~= nil then + llprim.ParticleSystem(params, link) + else + llprim.ParticleSystem(params) + end + return _captured_link, _captured_rules +end + +-- Test 1: nil params emits empty rules list, link defaults to LINK_THIS. +local link, rules = dispatch(nil) +assert(link == LINK_THIS, "nil params: link should be LINK_THIS") +assert(type(rules) == "table", "nil params: rules should be a table") +assert(#rules == 0, "nil params: rules should be empty") + +-- Test 2: explicit link number is forwarded. +link, rules = dispatch(nil, 3) +assert(link == 3, "explicit link=3 should be forwarded") + +-- Test 3: scalar float property is serialized with its tag. +link, rules = dispatch({ burst_rate = 2.5 }) +assert(#rules == 2, "burst_rate: expected 2 elements") +assert(rules[1] == PSYS_SRC_BURST_RATE, "burst_rate: tag should be PSYS_SRC_BURST_RATE") +assert(rules[2] == 2.5, "burst_rate: value should be 2.5") + +-- Test 4: vector property is serialized correctly. +local col = vector(1, 0.5, 0.25) +link, rules = dispatch({ color_begin = col }) +assert(#rules == 2, "color_begin: expected 2 elements") +assert(rules[1] == PSYS_PART_START_COLOR, "color_begin: tag should be PSYS_PART_START_COLOR") +assert(rules[2] == col, "color_begin: value should be the vector") + +-- Test 5: single flag boolean is merged into the flags integer field. +link, rules = dispatch({ color_interp = true }) +assert(#rules == 2, "color_interp: expected 2 elements (flags field only)") +assert(rules[1] == PSYS_PART_FLAGS, "color_interp: tag should be PSYS_PART_FLAGS") +assert(rules[2] == PSYS_PART_INTERP_COLOR_MASK, "color_interp: value should be PSYS_PART_INTERP_COLOR_MASK") + +-- Test 6: multiple flag booleans accumulate into a single flags field entry. +link, rules = dispatch({ color_interp = true, bounce = true }) +assert(#rules == 2, "two flags: should still be one flags field entry") +assert(rules[1] == PSYS_PART_FLAGS, "two flags: tag should be PSYS_PART_FLAGS") +assert(rules[2] == bit32.bor(PSYS_PART_INTERP_COLOR_MASK, PSYS_PART_BOUNCE_MASK), + "two flags: both bits should be set") + +-- Test 7: raw flags integer and boolean flag properties are merged together. +link, rules = dispatch({ flags = PSYS_PART_EMISSIVE_MASK, bounce = true }) +assert(#rules == 2, "raw+boolean flags: should be one flags field entry") +assert(rules[1] == PSYS_PART_FLAGS, "raw+boolean flags: tag should be PSYS_PART_FLAGS") +assert(rules[2] == bit32.bor(PSYS_PART_EMISSIVE_MASK, PSYS_PART_BOUNCE_MASK), + "raw+boolean flags: both bits should be set") + +-- Test 8: multiple params serialize in ascending tag order. +link, rules = dispatch({ burst_rate = 1.0, color_begin = vector(1, 0, 0) }) +-- color_begin tag=1, burst_rate tag=13 — tag 1 must come first. +assert(#rules == 4, "two params: expected 4 elements") +assert(rules[1] == PSYS_PART_START_COLOR, "tag order: color_begin should come before burst_rate") +assert(rules[3] == PSYS_SRC_BURST_RATE, "tag order: burst_rate should be second") + +return "OK"