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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ __pycache__
/fuzz/corpus/json/*
!/fuzz/corpus/json/*.json
*.code-workspace
*.tar.bz2
2 changes: 2 additions & 0 deletions Sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions VM/include/llfluent_builder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once
#include <stddef.h>

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
);
185 changes: 185 additions & 0 deletions VM/src/llfluent_builder.cpp
Original file line number Diff line number Diff line change
@@ -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 <string>
#include <vector>
#include <unordered_map>
#include <algorithm>

struct FluentBuilderDef
{
std::vector<FluentParamDescriptor> descs; // sorted by tag
std::vector<std::string> names; // storage for descriptor name strings
std::unordered_map<std::string, int> name_to_index;

std::vector<FluentFlagDescriptor> flag_descs;
std::vector<std::string> flag_names;
std::unordered_map<std::string, int> 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<size_t> 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<FluentParamDescriptor> sorted_descs(count);
std::vector<std::string> 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<int, int> 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);
}
2 changes: 1 addition & 1 deletion build-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
5 changes: 5 additions & 0 deletions init_debian_buster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
53 changes: 53 additions & 0 deletions tests/SLConformance.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Loading
Loading