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..9f58e762 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" +#include "lobject.h" +#include "ltable.h" +#include "lstring.h" + 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,16 +291,46 @@ 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); } } +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). + 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. + 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 pushed by lua_rawiter + + // 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/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 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( ) diff --git a/tests/SLConformance.test.cpp b/tests/SLConformance.test.cpp index 099a0134..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; }; 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"