Skip to content
Open
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
7 changes: 7 additions & 0 deletions VM/include/llruleset_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ void ruleset_builder_def_add_flags(
// Flag boolean properties are merged into their backing integer field before emission.
void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDef* def);

// Coerce a dict-style params table to a flat rules list in-place.
// If the value at params_idx is a dict (table with no integer key 1), serializes it
// using slua_ruleset_serialize and replaces the stack slot with the result.
// If the value is already a sequential list, nil, or non-table, leaves it unchanged.
// Returns true if coercion was performed, false otherwise.
bool slua_ruleset_coerce(lua_State* L, int params_idx, const RulesetBuilderDef* 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.
Expand Down
34 changes: 34 additions & 0 deletions VM/src/llruleset_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,40 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe
}
}

bool slua_ruleset_coerce(lua_State* L, int params_idx, const RulesetBuilderDef* def)
{
// Convert relative index to absolute
if (params_idx < 0 && params_idx > LUA_REGISTRYINDEX)
params_idx = lua_gettop(L) + params_idx + 1;

// Not a table? Leave unchanged.
if (!lua_istable(L, params_idx))
return false;

// Check if this is a dict (no integer key 1) vs sequential list (has key 1).
lua_rawgeti(L, params_idx, 1);
bool has_first = !lua_isnil(L, -1);
lua_pop(L, 1);

if (has_first)
return false; // Sequential list, leave unchanged

// Check if table has any keys at all.
lua_pushnil(L);
bool has_keys = (lua_next(L, params_idx) != 0);
if (!has_keys)
return false; // Empty table, already a valid empty list
lua_pop(L, 2); // Pop key and value

// Serialize dict to flat list (pushes new table on stack)
slua_ruleset_serialize(L, params_idx, def);

// Replace original table with serialized list
lua_replace(L, params_idx);

return true;
}

void slua_register_ruleset_fn(
lua_State* L,
const char* module_name,
Expand Down
28 changes: 28 additions & 0 deletions tests/SLConformance.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1566,4 +1566,32 @@ TEST_CASE("ruleset_builder_collection")
});
}

TEST_CASE("slua_ruleset_coerce")
{
// Simple descriptor table for testing coercion.
static const RulesetParamDescriptor kDescs[] = {
{"alpha", 'f', 1},
{"name", 's', 2},
{"count", 'i', 3},
};
static RulesetBuilderDef* s_def = []() {
return ruleset_builder_def_build(kDescs, std::size(kDescs));
}();

runConformance("ruleset_coerce.lua", nullptr, [](lua_State* L) {
// Test function: coerces input, returns (did_coerce, result).
auto coerce_test = [](lua_State* L) -> int {
const auto* def = (const RulesetBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1));
lua_settop(L, 1); // Ensure exactly one argument
bool did_coerce = slua_ruleset_coerce(L, 1, def);
lua_pushboolean(L, did_coerce);
lua_insert(L, 1); // [did_coerce, coerced_or_original]
return 2;
};
lua_pushlightuserdata(L, (void*)s_def);
lua_pushcclosure(L, coerce_test, nullptr, 1);
lua_setglobal(L, "test_coerce");
});
}

TEST_SUITE_END();
57 changes: 57 additions & 0 deletions tests/conformance/ruleset_coerce.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- Conformance tests for slua_ruleset_coerce().
--
-- The C++ harness registers test_coerce(value) which calls slua_ruleset_coerce
-- on the input and returns (did_coerce, result).
-- Descriptor table: alpha='f'/1, name='s'/2, count='i'/3

-- ─── Dict tables should be coerced ───────────────────────────────────────────

-- Basic dict with multiple fields
local did, result = test_coerce({alpha = 0.5, name = "test", count = 42})
assert(did == true, "dict should be coerced")
assert(type(result) == "table", "result should be a table")
-- Verify serialized output: tags in order 1, 2, 3
assert(result[1] == 1, "first tag should be alpha (1)")
assert(result[2] == 0.5, "alpha value")
assert(result[3] == 2, "second tag should be name (2)")
assert(result[4] == "test", "name value")
assert(result[5] == 3, "third tag should be count (3)")
assert(result[6] == 42, "count value")

-- Single field dict
local did2, result2 = test_coerce({name = "solo"})
assert(did2 == true, "single-field dict should be coerced")
assert(result2[1] == 2 and result2[2] == "solo", "single field serialized correctly")

-- ─── Sequential lists should pass through unchanged ──────────────────────────

-- Flat rules list (has integer key 1)
local did3, result3 = test_coerce({1, 0.5, 2, "test"})
assert(did3 == false, "sequential list should not be coerced")
assert(result3[1] == 1, "list unchanged")
assert(result3[2] == 0.5, "list unchanged")
assert(result3[3] == 2, "list unchanged")
assert(result3[4] == "test", "list unchanged")

-- ─── Empty table should pass through unchanged ───────────────────────────────

local did4, result4 = test_coerce({})
assert(did4 == false, "empty table should not be coerced")
assert(type(result4) == "table", "empty table passed through")
assert(next(result4) == nil, "table is still empty")

-- ─── Non-table values should pass through unchanged ──────────────────────────

local did5, result5 = test_coerce(nil)
assert(did5 == false, "nil should not be coerced")
assert(result5 == nil, "nil passed through")

local did6, result6 = test_coerce(123)
assert(did6 == false, "number should not be coerced")
assert(result6 == 123, "number passed through")

local did7, result7 = test_coerce("hello")
assert(did7 == false, "string should not be coerced")
assert(result7 == "hello", "string passed through")

return "OK"
Loading