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
60 changes: 60 additions & 0 deletions VM/include/llfluent_builder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#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; // scalar: 'i'=integer 'f'=float 's'=string 'v'=vector 'r'=rotation 'b'=boolean 'a'=asset 'k'=key
// collection: 'C'=string-csv ({string} array → escaped comma-joined string, one tag/value pair)
// 'M'=string-map ({[string]:string} table → one tag/key/value triple per entry)
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
);
301 changes: 301 additions & 0 deletions VM/src/llfluent_builder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
#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;
}

// 'C': string-csv — value is a Lua array of strings.
// Encode as backslash-escaped comma-joined string; emit one tag/value pair.
if (desc.semantic == 'C')
{
if (!has_raw || !lua_istable(L, -1))
{
if (has_raw) lua_pop(L, 1);
continue;
}
int n = (int)lua_objlen(L, -1);
if (n > 0)
{
std::string csv;
for (int j = 1; j <= n; ++j)
{
lua_rawgeti(L, -1, j);
size_t len = 0;
const char* s = lua_tolstring(L, -1, &len);
if (s)
{
if (j > 1) csv += ',';
for (size_t k = 0; k < len; ++k)
{
if (s[k] == '\\' || s[k] == ',') csv += '\\';
csv += s[k];
}
}
lua_pop(L, 1);
}
lua_pop(L, 1); // pop the array
lua_pushinteger(L, desc.tag);
lua_rawseti(L, list, ++idx);
lua_pushlstring(L, csv.c_str(), csv.size());
lua_rawseti(L, list, ++idx);
}
else
{
lua_pop(L, 1); // pop empty array, emit nothing
}
continue;
}

// 'M': string-map — value is a Lua string-keyed table.
// Emit one tag/key/value triple per entry, in sorted key order.
if (desc.semantic == 'M')
{
if (!has_raw || !lua_istable(L, -1))
{
if (has_raw) lua_pop(L, 1);
continue;
}
// Collect keys for deterministic ordering.
std::vector<std::string> keys;
lua_pushnil(L);
while (lua_next(L, -2))
{
if (lua_type(L, -2) == LUA_TSTRING)
keys.push_back(lua_tostring(L, -2));
lua_pop(L, 1); // pop value, keep key for next iteration
}
std::sort(keys.begin(), keys.end());

for (const auto& key : keys)
{
lua_pushlstring(L, key.c_str(), key.size());
lua_rawget(L, -2); // push value
int vtype = lua_type(L, -1);
// Skip non-primitive types that have no useful string representation.
if (vtype == LUA_TNIL || vtype == LUA_TTABLE || vtype == LUA_TFUNCTION)
{
lua_pop(L, 1);
continue;
}
// luaL_tolstring handles string, number, boolean, vector, and
// quaternions (via __tostring metamethod). It pushes the result
// on top, leaving the original value beneath it.
luaL_tolstring(L, -1, nullptr);
lua_remove(L, -2); // drop original, keep the string
lua_pushinteger(L, desc.tag);
lua_rawseti(L, list, ++idx);
lua_pushlstring(L, key.c_str(), key.size());
lua_rawseti(L, list, ++idx);
lua_rawseti(L, list, ++idx); // value
}
lua_pop(L, 1); // pop the table
continue;
}

// 'N': string-multi — value is a Lua array of strings.
// Emit one tag/value pair per element, preserving order.
if (desc.semantic == 'N')
{
if (!has_raw || !lua_istable(L, -1))
{
if (has_raw) lua_pop(L, 1);
continue;
}
int n = (int)lua_objlen(L, -1);
for (int j = 1; j <= n; ++j)
{
lua_rawgeti(L, -1, j);
if (lua_type(L, -1) == LUA_TSTRING)
{
lua_pushinteger(L, desc.tag);
lua_rawseti(L, list, ++idx);
lua_rawseti(L, list, ++idx);
}
else
{
lua_pop(L, 1); // skip non-string values
}
}
lua_pop(L, 1); // pop the array
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
Loading
Loading