From faf00f6bbc68e92b37eaf8a75e9681bb9a11b663 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Fri, 10 Jul 2026 19:00:01 +0000 Subject: [PATCH 1/5] Allow ruleset tables to be passed to legacy functions. --- VM/include/llruleset_builder.h | 7 ++++ VM/src/llruleset_builder.cpp | 34 +++++++++++++++++ tests/SLConformance.test.cpp | 28 ++++++++++++++ tests/conformance/ruleset_coerce.lua | 57 ++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 tests/conformance/ruleset_coerce.lua diff --git a/VM/include/llruleset_builder.h b/VM/include/llruleset_builder.h index 6d082e92..a85eea1d 100644 --- a/VM/include/llruleset_builder.h +++ b/VM/include/llruleset_builder.h @@ -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. diff --git a/VM/src/llruleset_builder.cpp b/VM/src/llruleset_builder.cpp index 318b89a4..9626806d 100644 --- a/VM/src/llruleset_builder.cpp +++ b/VM/src/llruleset_builder.cpp @@ -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, diff --git a/tests/SLConformance.test.cpp b/tests/SLConformance.test.cpp index 099a0134..d13afbd6 100644 --- a/tests/SLConformance.test.cpp +++ b/tests/SLConformance.test.cpp @@ -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(); diff --git a/tests/conformance/ruleset_coerce.lua b/tests/conformance/ruleset_coerce.lua new file mode 100644 index 00000000..999ffacc --- /dev/null +++ b/tests/conformance/ruleset_coerce.lua @@ -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" From 8dcc9d13ef6f218983c306a73c10a48d52c40af8 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 13 Jul 2026 20:20:51 +0000 Subject: [PATCH 2/5] Update autobuild package. --- autobuild.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autobuild.xml b/autobuild.xml index 6981a6f1..d7274c14 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -102,11 +102,11 @@ archive hash - f8949fca1ee107be2b878f38b916d7fe254be03f + c9928095cf40377e901458f5240305a7af2c554d hash_algorithm sha1 url - https://github.com/secondlife/lsl-definitions/releases/download/v0.6.11/lsl_definitions-0.6.11-common-28811066278.tar.zst + https://github.com/secondlife/lsl-definitions/releases/download/v0.6.12/lsl_definitions-0.6.12-common-29281156609.tar.zst name common @@ -119,7 +119,7 @@ copyright Copyright (c) 2026, Linden Research, Inc. version - 0.6.11 + 0.6.12 use_scm_version true name From b82b1e5439425250c771f4389788bb4e08708230 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 13 Jul 2026 20:33:05 +0000 Subject: [PATCH 3/5] update builtins. --- builtins.txt | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/builtins.txt b/builtins.txt index d0b9b038..0a24e07a 100644 --- a/builtins.txt +++ b/builtins.txt @@ -47,7 +47,7 @@ void llDeleteCharacter( ) key llDeleteKeyValue( string k ) list llDeleteSubList( list src, integer start_index, integer end_index ) string llDeleteSubString( string src, integer start_index, integer end_index ) -integer llDerezObject( key id, integer flag ) +integer llDerezObject( key id, integer flags ) void llDetachFromAvatar( ) list llDetectedDamage( integer number ) vector llDetectedGrab( integer number ) @@ -67,12 +67,12 @@ vector llDetectedTouchST( integer index ) vector llDetectedTouchUV( integer index ) integer llDetectedType( integer number ) vector llDetectedVel( integer number ) -void llDialog( key avatar, string message, list buttons, integer channel ) +void llDialog( key agent, string msg, list buttons, integer channel ) void llDie( ) string llDumpList2String( list src, string separator ) integer llEdgeOfWorld( vector pos, vector dir ) void llEjectFromLand( key avatar ) -void llEmail( string address, string subject, string message ) +void llEmail( string address, string subject, string msg ) string llEscapeURL( string url ) rotation llEuler2Rot( vector v ) void llEvade( key target, list options ) @@ -92,7 +92,7 @@ list llGetAgentList( integer scope, list options ) vector llGetAgentSize( key avatar ) float llGetAlpha( integer face ) float llGetAndResetTime( ) -string llGetAnimation( key id ) +string llGetAnimation( key avatar ) list llGetAnimationList( key avatar ) string llGetAnimationOverride( string anim_state ) integer llGetAttached( ) @@ -127,11 +127,11 @@ float llGetHealth( key id ) string llGetInventoryAcquireTime( string item ) key llGetInventoryCreator( string item ) string llGetInventoryDesc( string item ) -key llGetInventoryKey( string name ) -string llGetInventoryName( integer type, integer number ) +key llGetInventoryKey( string item ) +string llGetInventoryName( integer type, integer index ) integer llGetInventoryNumber( integer type ) -integer llGetInventoryPermMask( string item, integer category ) -integer llGetInventoryType( string name ) +integer llGetInventoryPermMask( string item, integer group ) +integer llGetInventoryType( string item ) key llGetKey( ) key llGetLandOwnerAt( vector pos ) key llGetLinkKey( integer link ) @@ -164,7 +164,7 @@ list llGetObjectDetails( key id, list params ) key llGetObjectLinkKey( key object_id, integer link ) float llGetObjectMass( key id ) string llGetObjectName( ) -integer llGetObjectPermMask( integer category ) +integer llGetObjectPermMask( integer group ) integer llGetObjectPrimCount( key prim ) vector llGetOmega( ) key llGetOwner( ) @@ -225,11 +225,11 @@ string llGetUsername( key id ) vector llGetVel( ) list llGetVisualParams( key agentid, list params ) float llGetWallclock( ) -integer llGiveAgentInventory( key agent, string folder, list inventory, list options ) -void llGiveInventory( key destination, string inventory ) -void llGiveInventoryList( key target, string folder, list inventory ) +integer llGiveAgentInventory( key agent, string folder, list items, list options ) +void llGiveInventory( key target, string item ) +void llGiveInventoryList( key target, string folder, list items ) integer llGiveMoney( key destination, integer amount ) -void llGodLikeRezObject( key inventory, vector pos ) +void llGodLikeRezObject( key id, vector pos ) float llGround( vector offset ) vector llGroundContour( vector offset ) vector llGroundNormal( vector offset ) @@ -239,8 +239,8 @@ string llHMAC( string private_key, string msg, string algorithm ) key llHTTPRequest( string url, list parameters, string body ) void llHTTPResponse( key request_id, integer status, string body ) integer llHash( string val ) -string llInsertString( string dst, integer pos, string src ) -void llInstantMessage( key user, string message ) +string llInsertString( string dst, integer index, string src ) +void llInstantMessage( key agent, string msg ) string llIntegerToBase64( integer number ) integer llIsFriend( key agent_id ) integer llIsLinkGLTFMaterial( integer link, integer face ) @@ -347,8 +347,8 @@ void llReleaseControls( ) void llReleaseURL( string url ) void llRemoteDataReply( key channel, key message_id, string sdata, integer idata ) void llRemoteDataSetRegion( ) -void llRemoteLoadScript( key target, string name, integer running, integer start_param ) -void llRemoteLoadScriptPin( key target, string name, integer pin, integer running, integer start_param ) +void llRemoteLoadScript( key target, string script, integer running, integer start_param ) +void llRemoteLoadScriptPin( key target, string script, integer pin, integer running, integer start_param ) void llRemoveFromLandBanList( key avatar ) void llRemoveFromLandPassList( key avatar ) void llRemoveInventory( string item ) @@ -359,7 +359,7 @@ string llReplaceSubString( string src, string pattern, string replacement_patter key llRequestAgentData( key id, integer data ) key llRequestDisplayName( key id ) void llRequestExperiencePermissions( key agent, string name ) -key llRequestInventoryData( string name ) +key llRequestInventoryData( string item ) void llRequestPermissions( key agent, integer permissions ) key llRequestSecureURL( ) key llRequestSimulatorData( string region, integer data ) @@ -369,14 +369,14 @@ key llRequestUsername( key id ) void llResetAnimationOverride( string anim_state ) void llResetLandBanList( ) void llResetLandPassList( ) -void llResetOtherScript( string name ) +void llResetOtherScript( string script ) void llResetScript( ) void llResetTime( ) integer llReturnObjectsByID( list objects ) integer llReturnObjectsByOwner( key owner, integer scope ) -void llRezAtRoot( string inventory, vector position, vector velocity, rotation rot, integer param ) -void llRezObject( string inventory, vector pos, vector vel, rotation rot, integer param ) -key llRezObjectWithParams( string inventory, list params ) +void llRezAtRoot( string item, vector pos, vector vel, rotation rot, integer start_param ) +void llRezObject( string item, vector pos, vector vel, rotation rot, integer start_param ) +key llRezObjectWithParams( string item, list options ) float llRot2Angle( rotation rot ) vector llRot2Axis( rotation rot ) vector llRot2Euler( rotation quat ) @@ -420,7 +420,7 @@ void llSetForce( vector force, integer is_local ) void llSetForceAndTorque( vector force, vector torque, integer is_local ) integer llSetGroundTexture( list changes ) void llSetHoverHeight( float height, integer water, float tau ) -void llSetInventoryPermMask( string item, integer category, integer value ) +void llSetInventoryPermMask( string item, integer group, integer flags ) void llSetKeyframedMotion( list keyframes, list options ) void llSetLinkAlpha( integer link, float alpha, integer face ) void llSetLinkCamera( integer link, vector eye, vector at ) @@ -437,7 +437,7 @@ void llSetLocalRot( rotation rot ) integer llSetMemoryLimit( integer limit ) void llSetObjectDesc( string description ) void llSetObjectName( string name ) -void llSetObjectPermMask( integer mask, integer value ) +void llSetObjectPermMask( integer group, integer flags ) integer llSetParcelForSale( integer ForSale, list Options ) void llSetParcelMusicURL( string url ) void llSetPayPrice( integer price, list quick_pay_buttons ) @@ -451,7 +451,7 @@ void llSetRemoteScriptAccessPin( integer pin ) void llSetRenderMaterial( string material, integer face ) void llSetRot( rotation rot ) void llSetScale( vector size ) -void llSetScriptState( string name, integer running ) +void llSetScriptState( string script, integer running ) void llSetSitText( string text ) void llSetSoundQueueing( integer queue ) void llSetSoundRadius( float radius ) @@ -496,15 +496,15 @@ float llTan( float theta ) integer llTarget( vector position, float range ) void llTargetOmega( vector axis, float spinrate, float gain ) void llTargetRemove( integer handle ) -void llTargetedEmail( integer target, string subject, string message ) +void llTargetedEmail( integer target, string subject, string msg ) void llTeleportAgent( key agent, string landmark, vector position, vector look_at ) void llTeleportAgentGlobalCoords( key agent, vector global_coordinates, vector region_coordinates, vector look_at ) void llTeleportAgentHome( key avatar ) -void llTextBox( key avatar, string message, integer channel ) +void llTextBox( key agent, string msg, integer channel ) string llToLower( string src ) string llToUpper( string src ) key llTransferLindenDollars( key destination, integer amount ) -integer llTransferOwnership( key agent_id, integer flags, list options ) +integer llTransferOwnership( key agent, integer flags, list options ) void llTriggerSound( string sound, float volume ) void llTriggerSoundLimited( string sound, float volume, vector top_north_east, vector bottom_south_west ) void llUnSit( key id ) @@ -1555,14 +1555,14 @@ const rotation ZERO_ROTATION = <0.0, 0.0, 0.0, 1.0> const vector ZERO_VECTOR = <0.0, 0.0, 0.0> event at_rot_target( integer handle, rotation targetrot, rotation ourrot ) event at_target( integer tnum, vector targetpos, vector ourpos ) -event attach( key id ) -event changed( integer change ) +event attach( key avatar ) +event changed( integer changes ) event collision( integer num_detected ) event collision_end( integer num_detected ) event collision_start( integer num_detected ) event control( key id, integer level, integer edge ) event dataserver( key queryid, string data ) -event email( string time, string address, string subject, string message, integer num_left ) +event email( string time, string address, string subject, string msg, integer num_left ) event experience_permissions( key agent_id ) event experience_permissions_denied( key agent_id, integer reason ) event final_damage( integer num_detected ) @@ -1574,7 +1574,7 @@ event land_collision_end( vector pos ) event land_collision_start( vector pos ) event link_message( integer sender_num, integer num, string str, key id ) event linkset_data( integer action, string name, string value ) -event listen( integer channel, string name, key id, string message ) +event listen( integer channel, string name, key id, string msg ) event money( key id, integer amount ) event moving_end( ) event moving_start( ) From 832c647071bed1ea6a15e44f1b229b574a1815ef Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 13 Jul 2026 21:24:51 +0000 Subject: [PATCH 4/5] address some feedback. --- VM/src/llruleset_builder.cpp | 91 ++++++++++--------- tests/SLConformance.test.cpp | 33 +------ .../ruleset_builder_collection.lua | 12 +++ tests/conformance/ruleset_coerce.lua | 57 ------------ 4 files changed, 61 insertions(+), 132 deletions(-) delete mode 100644 tests/conformance/ruleset_coerce.lua diff --git a/VM/src/llruleset_builder.cpp b/VM/src/llruleset_builder.cpp index 9626806d..650a51a1 100644 --- a/VM/src/llruleset_builder.cpp +++ b/VM/src/llruleset_builder.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "lua.h" #include "lcommon.h" @@ -11,6 +12,11 @@ #include "llsl.h" #include "llruleset_builder.h" +#include "lapi.h" // luaA_toobject, luaA_pushobject +#include "lobject.h" // TValue, hvalue, ttisnil, ttistable, ttisboolean, ttisnumber, bvalue, nvalue +#include "ltable.h" // LuaTable, luaH_getnum, luaH_getstr, luaH_getn +#include "lstring.h" // luaS_new + struct RulesetBuilderDef { std::vector descs; // sorted by tag; .name points into string literals @@ -56,13 +62,17 @@ namespace int slua_ruleset_serialize_string_csv(lua_State* L, int list, int idx, const RulesetParamDescriptor& desc) { - int n = (int)lua_objlen(L, -1); + LuaTable* t = hvalue(luaA_toobject(L, -1)); + int n = luaH_getn(t); if (n > 0) { std::string csv; for (int j = 1; j <= n; ++j) { - lua_rawgeti(L, -1, j); + const TValue* elem = luaH_getnum(t, j); + if (ttisnil(elem)) + continue; + luaA_pushobject(L, elem); if (slua_ruleset_to_string(L)) { size_t len = 0; @@ -105,19 +115,21 @@ namespace { // Collect keys for deterministic ordering. std::vector keys; - lua_pushnil(L); - while (lua_next(L, -2)) + for (int index = 0; (index = lua_rawiter(L, -1, index)) >= 0;) { 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 + lua_pop(L, 2); } std::sort(keys.begin(), keys.end()); + LuaTable* t = hvalue(luaA_toobject(L, -1)); for (const auto& key : keys) { - lua_pushlstring(L, key.c_str(), key.size()); - lua_rawget(L, -2); // push value + const TValue* val = luaH_getstr(t, luaS_new(L, key.c_str())); + if (ttisnil(val)) + continue; + luaA_pushobject(L, val); if (!slua_ruleset_to_string(L)) continue; lua_pushinteger(L, desc.tag); @@ -136,10 +148,14 @@ namespace int slua_ruleset_serialize_string_multi(lua_State* L, int list, int idx, const RulesetParamDescriptor& desc) { - int n = (int)lua_objlen(L, -1); + LuaTable* t = hvalue(luaA_toobject(L, -1)); + int n = luaH_getn(t); for (int j = 1; j <= n; ++j) { - lua_rawgeti(L, -1, j); + const TValue* elem = luaH_getnum(t, j); + if (ttisnil(elem)) + continue; + luaA_pushobject(L, elem); if (slua_ruleset_to_string(L)) { lua_pushinteger(L, desc.tag); @@ -192,21 +208,20 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe return; // empty list // Phase 1: accumulate flag bits per backing field. + LuaTable* params_h = hvalue(luaA_toobject(L, params_idx)); std::unordered_map flag_set_bits; // field_tag -> bits to OR in std::unordered_map flag_clear_bits; // field_tag -> bits to AND out for (const auto& fdesc : def->flag_descs) { - lua_rawgetfield(L, params_idx, fdesc.name); - if (!lua_isnil(L, -1)) + const TValue* val = luaH_getstr(params_h, luaS_new(L, fdesc.name)); + if (!ttisnil(val)) { - bool set = lua_isboolean(L, -1) ? (bool)lua_toboolean(L, -1) - : (lua_tointeger(L, -1) != 0); + bool set = ttisboolean(val) ? (bool)bvalue(val) : (nvalue(val) != 0.0); if (set) flag_set_bits[fdesc.field_tag] |= fdesc.mask; else flag_clear_bits[fdesc.field_tag] |= fdesc.mask; } - lua_pop(L, 1); } // Phase 2: emit tag/value pairs in tag order. @@ -215,12 +230,10 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe 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); + const TValue* val = luaH_getstr(params_h, luaS_new(L, desc.name)); + has_raw = !ttisnil(val); + if (has_raw && ttisnumber(val)) + raw_int = (int)nvalue(val); // Integer fields that back flags: merge accumulated bits. auto set_it = flag_set_bits.find(desc.tag); @@ -231,7 +244,6 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe int set_mask = (set_it != flag_set_bits.end()) ? set_it->second : 0; int clear_mask = (clear_it != flag_clear_bits.end()) ? clear_it->second : 0; int merged = (raw_int | set_mask) & ~clear_mask; - if (has_raw) lua_pop(L, 1); lua_pushinteger(L, desc.tag); lua_rawseti(L, list, ++idx); lua_pushinteger(L, merged); @@ -243,12 +255,9 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe // Encode as 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); + if (!has_raw || !ttistable(val)) continue; - } - + luaA_pushobject(L, val); idx = slua_ruleset_serialize_string_csv(L, list, idx, desc); continue; } @@ -257,11 +266,9 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe // 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); + if (!has_raw || !ttistable(val)) continue; - } + luaA_pushobject(L, val); idx = slua_ruleset_serialize_string_map(L, list, idx, desc); continue; } @@ -270,11 +277,9 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe // 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); + if (!has_raw || !ttistable(val)) continue; - } + luaA_pushobject(L, val); idx = slua_ruleset_serialize_string_multi(L, list, idx, desc); continue; } @@ -286,13 +291,11 @@ void slua_ruleset_serialize(lua_State* L, int params_idx, const RulesetBuilderDe 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)); + if (desc.semantic == 'b' && ttisboolean(val)) + lua_pushinteger(L, bvalue(val)); else - lua_pushvalue(L, -1); + luaA_pushobject(L, val); lua_rawseti(L, list, ++idx); - - lua_pop(L, 1); } } @@ -307,19 +310,17 @@ bool slua_ruleset_coerce(lua_State* L, int params_idx, const RulesetBuilderDef* 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); + LuaTable* h = hvalue(luaA_toobject(L, params_idx)); + bool has_first = !ttisnil(luaH_getnum(h, 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); + bool has_keys = (lua_rawiter(L, params_idx, 0) >= 0); if (!has_keys) return false; // Empty table, already a valid empty list - lua_pop(L, 2); // Pop key and value + lua_pop(L, 2); // Pop key and value pushed by lua_rawiter // Serialize dict to flat list (pushes new table on stack) slua_ruleset_serialize(L, params_idx, def); diff --git a/tests/SLConformance.test.cpp b/tests/SLConformance.test.cpp index d13afbd6..280baba0 100644 --- a/tests/SLConformance.test.cpp +++ b/tests/SLConformance.test.cpp @@ -1555,10 +1555,11 @@ TEST_CASE("ruleset_builder_collection") }(); runConformance("ruleset_builder_collection.lua", nullptr, [](lua_State* L) { - // Mock apply function: serialize params and store result as _captured_rules. + // Mock apply function: coerce params (dict->list if needed) and store as _captured_rules. auto apply_fn = [](lua_State* L) -> int { const auto* def = (const RulesetBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1)); - slua_ruleset_serialize(L, 1, def); + slua_ruleset_coerce(L, 1, def); + lua_pushvalue(L, 1); lua_setglobal(L, "_captured_rules"); return 0; }; @@ -1566,32 +1567,4 @@ 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(); diff --git a/tests/conformance/ruleset_builder_collection.lua b/tests/conformance/ruleset_builder_collection.lua index 18f428b8..5d7e7fce 100644 --- a/tests/conformance/ruleset_builder_collection.lua +++ b/tests/conformance/ruleset_builder_collection.lua @@ -61,4 +61,16 @@ check( {5, "https://x.com", 7, "K", "V", 9, "hello"} ) +-- ─── Coercion: dict vs sequential list ─────────────────────────────────────── + +-- Dict input is coerced to a flat tag/value list. +check({label = "solo"}, {9, "solo"}) +check({whitelist = {"a", "b"}, label = "hi"}, {5, "a,b", 9, "hi"}) + +-- Sequential list (has integer key 1) passes through unchanged. +check({5, "prebuilt", 9, "label"}, {5, "prebuilt", 9, "label"}) + +-- Empty table passes through unchanged. +check({}, {}) + return "OK" diff --git a/tests/conformance/ruleset_coerce.lua b/tests/conformance/ruleset_coerce.lua deleted file mode 100644 index 999ffacc..00000000 --- a/tests/conformance/ruleset_coerce.lua +++ /dev/null @@ -1,57 +0,0 @@ --- 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" From 7a5646503b3573e000bee505e4c33109d25326ec Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 14 Jul 2026 15:48:20 +0000 Subject: [PATCH 5/5] kill some comments --- VM/src/llruleset_builder.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/VM/src/llruleset_builder.cpp b/VM/src/llruleset_builder.cpp index 650a51a1..9f58e762 100644 --- a/VM/src/llruleset_builder.cpp +++ b/VM/src/llruleset_builder.cpp @@ -12,10 +12,10 @@ #include "llsl.h" #include "llruleset_builder.h" -#include "lapi.h" // luaA_toobject, luaA_pushobject -#include "lobject.h" // TValue, hvalue, ttisnil, ttistable, ttisboolean, ttisnumber, bvalue, nvalue -#include "ltable.h" // LuaTable, luaH_getnum, luaH_getstr, luaH_getn -#include "lstring.h" // luaS_new +#include "lapi.h" +#include "lobject.h" +#include "ltable.h" +#include "lstring.h" struct RulesetBuilderDef {