LSL-PyOptimizer/lslopt/lslfuncopt.py

551 lines
22 KiB
Python

# (C) Copyright 2015-2024 Sei Lisa. All rights reserved.
#
# This file is part of LSL PyOptimizer.
#
# LSL PyOptimizer is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# LSL PyOptimizer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LSL PyOptimizer. If not, see <http://www.gnu.org/licenses/>.
# Optimize calls to LSL library functions and parameters where possible
# This is dependent on the LSL function library.
from lslopt import lslcommon
from lslopt.lslcommon import Key, Vector, Quaternion, nr
from lslopt import lslfuncs
from strutil import unicode
SensorFunctions = {'llSensor', 'llSensorRepeat'}
# not sure about llRemoteDataReply but let's fall on the safe side
NoKeyOptimizationFunctions = {'llMessageLinked', 'llRemoteDataReply'}
def OptimizeArgs(node, sym):
"""Transform function arguments to shorter equivalents where possible."""
assert node.nt == 'FNCALL'
params = node.ch
name = node.name
if 'Loc' in sym:
# This is a UDF. We can't do anything for these.
return
if name in SensorFunctions:
# The cutoff value is at a bit less than 3.1275 for some reason,
# but we use 3.14159.
if (params[4].nt == 'CONST' and params[4].t == 'float'
and params[4].value > 3.14159):
params[4].value = 4.0
types = sym['ParamTypes']
if name not in NoKeyOptimizationFunctions:
# Transform invalid/null keys to "" with the exceptions above,
# e.g. llGetOwnerKey(NULL_KEY) -> llGetOwnerKey("")
for i in range(len(types)):
if types[i] == 'key':
if params[i].nt == 'CONST':
if not lslfuncs.cond(Key(params[i].value)):
params[i].value = u""
params[i].type = 'string'
# llGetObjectDetails return types
# Last: 49 (OBJECT_TEXT_ALPHA).
objDetailsTypes = 'issvrvkkkiiififfffkiiiiiiffkiviiksiisiiiiiifssivvf'
# llGetParcelDetails return types
# Last: 13 (PARCEL_DETAILS_SCRIPT_DANGER)
parcelDetailsTypes = 'sskkikiiivviii'
parcelMediaQTypes = \
{ 4: 'k' # PARCEL_MEDIA_COMMAND_TEXTURE
, 5: 's' # PARCEL_MEDIA_COMMAND_URL
, 10: 's' # PARCEL_MEDIA_COMMAND_TYPE
, 11: 'ii' # PARCEL_MEDIA_COMMAND_SIZE
, 12: 's' # PARCEL_MEDIA_COMMAND_DESC
, 13: 'f' # PARCEL_MEDIA_COMMAND_LOOP
}
# llGetPrimitiveParams and llGetLinkPrimitiveParams return types
primParamsTypes = \
{ # 0 (unassigned)
1: 'i*' # 1=PRIM_TYPE_LEGACY
, 2: 'i' # 2=PRIM_MATERIAL
, 3: 'i' # 3=PRIM_PHYSICS
, 4: 'i' # 4=PRIM_TEMP_ON_REZ
, 5: 'i' # 5=PRIM_PHANTOM
, 6: 'v' # 6=PRIM_POSITION
, 7: 'v' # 7=PRIM_SIZE
, 8: 'r' # 8=PRIM_ROTATION
, 9: 'i*' # 9=PRIM_TYPE
# 10 to 16: unassigned
, 17: 'svvf' # 17=PRIM_TEXTURE
, 18: 'vf' # 18=PRIM_COLOR
, 19: 'ii' # 19=PRIM_BUMP_SHINY
, 20: 'i' # 20=PRIM_FULLBRIGHT
, 21: 'iiffffv' # 21=PRIM_FLEXIBLE
, 22: 'i' # 22=PRIM_TEXGEN
, 23: 'ivfff' # 23=PRIM_POINT_LIGHT
# 24 :unassigned
, 25: 'f' # 25=PRIM_GLOW
, 26: 'svf' # 26=PRIM_TEXT
, 27: 's' # 27=PRIM_NAME
, 28: 's' # 28=PRIM_DESC
, 29: 'r' # 29=PRIM_ROT_LOCAL
, 30: 'i' # 30=PRIM_PHYSICS_SHAPE_TYPE
# 31: unassigned
, 32: 'vff' # 32=PRIM_OMEGA
, 33: 'v' # 33=PRIM_POS_LOCAL
, 34: '' # 34=PRIM_LINK_TARGET
, 35: 'v' # 35=PRIM_SLICE
, 36: 'svvfvii' # 36=PRIM_SPECULAR
, 37: 'svvf' # 37=PRIM_NORMAL
, 38: 'ii' # 38=PRIM_ALPHA_MODE
, 39: 'i' # 39=PRIM_ALLOW_UNSIT
, 40: 'i' # 40=PRIM_SCRIPTED_SIT_ONLY
, 41: 'ivv' # 41=PRIM_SIT_TARGET
, 42: 'sfff' # 42=PRIM_PROJECTOR
, 43: 'i' # 43=PRIM_CLICK_ACTION
, 44: 'iffi' # 44=PRIM_REFLECTION_PROBE
# GLTF parameters admit an empty string in any of the places. We're not
# prepared to deal with that.
# , 45: 'svvf' # 45=PRIM_GLTF_NORMAL
# , 46: 'svvfv' # 46=PRIM_GLTF_EMISSIVE
# , 47: 'svvfff' # 47=PRIM_GLTF_METALLIC_ROUGHNESS
# , 48: 'svvfvfifi' # 48=PRIM_GLTF_BASE_COLOR
, 45: '*' # 45=PRIM_GLTF_NORMAL
, 46: '*' # 46=PRIM_GLTF_EMISSIVE
, 47: '*' # 47=PRIM_GLTF_METALLIC_ROUGHNESS
, 48: '*' # 48=PRIM_GLTF_BASE_COLOR
, 49: 's' # 49=PRIM_RENDER_MATERIAL
, 50: 'i' # 50=PRIM_SIT_FLAGS
}
# llGetPrimitiveParams parameters with arguments. F=face, L=link.
primParamsArgs = \
{ 17: 'F' # 17=PRIM_TEXTURE
, 18: 'F' # 18=PRIM_COLOR
, 19: 'F' # 19=PRIM_BUMP_SHINY
, 20: 'F' # 20=PRIM_FULLBRIGHT
, 22: 'F' # PRIM_TEXGEN
, 25: 'F' # PRIM_GLOW
, 34: 'L' # PRIM_LINK_TARGET
, 36: 'F' # PRIM_SPECULAR
, 37: 'F' # PRIM_NORMAL
, 38: 'F' # PRIM_ALPHA_MODE
, 45: 'F' # PRIM_GLTF_NORMAL
, 46: 'F' # PRIM_GLTF_EMISSIVE
, 47: 'F' # PRIM_GLTF_METALLIC_ROUGHNESS
, 48: 'F' # PRIM_GLTF_BASE_COLOR
, 49: 'F' # PRIM_RENDER_MATERIAL
}
# llGetPrimMediaParams and llGetLinkMedia return types
# Last: 14 (PRIM_MEDIA_PERMS_CONTROL)
primMediaTypes = 'iissiiiiiiiisii'
# llGetEnvironment return types (not to be confused with llGetEnv)
getEnvTypes = \
{ 15: 'fff'
, 0: 'v'
, 1: 'iii'
, 2: 'vfffvvvi'
, 4: 'fff'
, 5: 'f'
, 6: 'ff'
, 9: 'rffivvv'
, 13: 'f'
, 14: 'rfvivvv'
, 10: 'fff'
, 11: 'fff'
, 8: 'vvv'
, 100: 'f'
, 101: 'vff'
, 102: 'ff'
, 103: 'ii'
, 104: 'v'
, 105: 'ff'
, 106: 'vv'
, 200: 'iif'
}
# Types for llGetVisualDetails parameters (anything not in this list is
# returned as an empty string)
visualDetailsTypes = \
{ 33: 'f', 'height': 'f'
, 38: 'f', 'torso_length': 'f'
, 80: 'f', 'male': 'f'
, 198: 'f', 'heel_height': 'f'
, 503: 'f', 'platform_height': 'f'
, 616: 'f', 'shoe_height': 'f'
, 692: 'f', 'leg_length': 'f'
, 693: 'f', 'arm_length': 'f'
, 756: 'f', 'neck_length': 'f'
, 814: 'f', 'waist_height': 'f'
, 842: 'f', 'hip_length': 'f'
, 11001: 'f', 'hover': 'f'
}
# Compatibility: list extraction function / input type (by type's initial),
# e.g. 'si' means llList2String can extract an integer.
listCompat = {'ss', 'sk', 'si', 'sf', 'sv', 'sr', 'ks', 'kk',
'is', 'ii', 'if', 'fs', 'fi', 'ff', 'vv', 'rr'}
defaultListVals = {'llList2Integer':0, 'llList2Float':0.0,
'llList2String':u'',
# llList2Key is set programmatically in FuncOptSetup
#'llList2Key':Key(u''),
'llList2Vector':Vector((0.,0.,0.)),
'llList2Rot':Quaternion((0.,0.,0.,1.))}
# Auxiliary function for llDumpList2String optimization
def CastDL2S(self, node, index):
"""Cast a list element to string, wrapping it in a list if it's a vector or
rotation.
"""
elem = self.GetListNodeElement(node, index)
assert elem is not False
if type(elem) != nr:
elem = nr(nt='CONST', t=lslcommon.PythonType2LSL[type(elem)], SEF=True,
value=elem)
if elem.t in {'vector', 'rotation'}:
return self.Cast(self.Cast(elem, 'list'), 'string')
return self.Cast(elem, 'string')
# Quick hack to work around lack of cached per-node ContainsFunctions info
def FnFree(self, node):
if node.nt == 'FNCALL':
return False
if node.nt in {'CONST', 'IDENT', 'FLD'}:
return True
return all(FnFree(self, node.ch[i]) for i in range(len(node.ch)))
# The 'self' parameter here is the constant folding object.
def OptimizeFunc(self, parent, index):
"""Look for possible optimizations taking advantage of the specific LSL
library function semantics.
"""
node = parent[index]
assert node.nt == 'FNCALL'
name = node.name
child = node.ch
if self.optlistlength and name == 'llGetListLength':
# Convert llGetListLength(expr) to (expr != [])
node = nr(nt='CONST', t='list', value=[], SEF=True)
parent[index] = node = nr(nt='!=', t='integer',
ch=[child[0], node], SEF=child[0].SEF)
if name == 'llDumpList2String':
assert child[0].t == 'list'
if (child[1].nt == 'CONST'
and child[1].t in {'string', 'key'}
and child[1].value == u""
):
# Convert llDumpList2String(expr, "") to (string)(expr)
node.nt = 'CAST'
del child[1]
del node.name
return
list_len = self.GetListNodeLength(child[0])
if list_len is not False and list_len == 1 and child[1].SEF:
# A single-element list can always be transformed regardless of
# the presence of function calls with side effects
parent[index] = CastDL2S(self, child[0], 0)
return
if node.SEF:
# Attempt to convert the function call into a sum of strings when
# possible and productive.
if list_len is False:
# Can't identify the length, which means we can't optimize.
return
if list_len == 0:
# Empty list -> empty string, no matter the separator
# (remember we're SEF).
parent[index] = nr(nt='CONST', t='string', value=u'', SEF=True)
return
# Only optimize if the second param is a very simple expression,
# otherwise the sums can get large.
if child[1].nt in {'CONST', 'IDENT'}:
threshold = 10
else:
return
# Apply a threshold for optimizing as a sum.
if list_len > threshold:
return
elems = [self.GetListNodeElement(child[0], i)
for i in range(list_len)]
# Don't optimize if an element can't be extracted or is a list
if any(i is False or type(i) == nr and i.t == 'list'
for i in elems):
return
# We reorder list constructors as right-to-left sums. When an
# element contains function calls, it will generate a savepoint,
# but with that strategy, the maximum extra stack size at the time
# of each savepoint is 1.
# If the first element has a function call, we may end up causing
# more memory usage, because in a list constructor, the first
# element has no stack to save; however, if any elements past the
# third have function calls at the same time, the memory we add
# will be compensated by the memory we save, because the 3rd
# element has 2 elements in the stack, therefore reducing it to 1
# is a save; similarly, any elements past the 3rd containing
# function calls cause bigger and bigger saves.
# Since we're also eliminating the llDumpList2String function call,
# that may count for the extra stack element added. Therefore, we
# disable this condition and optimize unconditionally.
#if (child[0].nt in ('LIST', 'CONST') and list_len >= 3
# and type(elems[0]) == nr and not FnFree(self, elems[0])
# and all(type(i) != nr or FnFree(self, i) for i in elems[2:])
# ):
# return
# Optimize to a sum of strings, right-to-left to save stack.
i = list_len - 1
newnode = CastDL2S(self, child[0], i)
while i > 0:
i -= 1
newnode = nr(nt='+', t='string', SEF=True,
ch=[CastDL2S(self, child[0], i),
nr(nt='+', t='string', SEF=True,
ch=[self.Cast(child[1], 'string').copy(), newnode]
)
])
parent[index] = newnode
# Re-fold
self.FoldTree(parent, index)
return
if (name in {'llList2String', 'llList2Key', 'llList2Integer',
'llList2Float', 'llList2Vector', 'llList2Rot'}
and child[1].nt == 'CONST'
):
# 2nd arg to llList2XXXX must be integer
assert child[1].t == 'integer'
listarg = child[0]
idx = child[1].value
value = self.GetListNodeElement(listarg, idx)
tvalue = self.TypeFromNodeOrConst(value)
const = self.ConstFromNodeOrConst(value)
if const is not False and node.SEF:
# Managed to get a constant from a list, even if the
# list wasn't constant. Handle the type conversion.
if (node.t[0] + tvalue[0]) in listCompat:
const = lslfuncs.InternalTypecast(const,
lslcommon.LSLType2Python[node.t],
InList=True, f32=True)
else:
const = defaultListVals[name]
parent[index] = nr(nt='CONST', t=node.t, value=const, SEF=True)
return
if listarg.nt == 'FNCALL':
if listarg.name == 'llGetObjectDetails':
# make it the list argument of llGetObjectDetails
listarg = listarg.ch[1]
value = self.GetListNodeElement(listarg, idx)
tvalue = self.TypeFromNodeOrConst(value)
const = self.ConstFromNodeOrConst(value)
if type(const) == int and self.GetListNodeLength(listarg) == 1:
# Some of these can be handled with a typecast to string.
if name == 'llList2String':
# turn the node into a cast of arg 0 to string
node.nt = 'CAST'
del child[1]
del node.name
return
# The other ones that support cast to string then to
# the final type in some cases (depending on the
# list type, which we know) are key/int/float.
finaltype = objDetailsTypes[const:const+1]
if (name == 'llList2Key' # checked via listCompat
or (name == 'llList2Integer'
and finaltype in {'s', 'i'}) # won't work for floats
or (name == 'llList2Float'
and finaltype in {'s', 'i'}) # won't work for floats
) and (node.t[0] + finaltype) in listCompat:
# -> (key)((string)llGetObjectDetails...)
# or (integer)((string)llGetObjectDetails...)
node.nt = 'CAST'
del child[1]
del node.name
child[0] = self.Cast(child[0], 'string')
return
# Check for type incompatibility or index out of range
# and replace node with a constant if that's the case
if (value is False
or type(const) == int
and (node.t[0] + objDetailsTypes[const])
not in listCompat
) and node.SEF:
parent[index] = nr(nt='CONST', t=node.t,
value=defaultListVals[name], SEF=True)
elif listarg.name in {'llGetPrimitiveParams',
'llGetLinkPrimitiveParams'}:
# We're going to work with the primitive params list.
listarg = listarg.ch[
0 if listarg.name == 'llGetPrimitiveParams'
else 1]
length = self.GetListNodeLength(listarg)
if length is not False:
# Construct a list (string) of return types.
# A '*' in the list means the type can't be
# determined past this point (used with PRIM_TYPE).
i = 0
returntypes = ''
while i < length:
param = self.GetListNodeElement(listarg, i)
param = self.ConstFromNodeOrConst(param)
if (param is False
or type(param) != int
# Parameters with arguments have
# side effects (errors).
# We could check whether there's a face
# argument and the face is 0, which is
# guaranteed to exist, but it's not worth
# the effort.
or param in primParamsArgs
or param not in primParamsTypes
):
# Can't process this list.
returntypes = '!'
break
returntypes += primParamsTypes[param]
i += 1
if returntypes != '!':
if (len(returntypes) == 1
and returntypes != '*'
and idx in {0, -1}
):
if name == 'llList2String':
node.nt = 'CAST'
del child[1]
del node.name
return
if ((name == 'llList2Key'
or name == 'llList2Integer'
and returntypes in {'s', 'i'}
or name == 'llList2Float'
and returntypes in {'s', 'i'}
)
and (node.t[0] + returntypes)
in listCompat
):
node.nt = 'CAST'
del child[1]
del node.name
child[0] = nr(nt='CAST', t='string',
ch=[child[0]], SEF=child[0].SEF)
return
# The position of parameters past the first asterisk can't
# be determined, so we only consider parameters before it.
asteriskPos = returntypes.find('*')
if (asteriskPos == -1
or 0 <= idx < asteriskPos
or asteriskPos - len(returntypes) < idx < 0
):
# Check for type incompatibility or index
# out of range.
if idx < 0:
# s[-1:0] doesn't return the last char
# so we make it positive to ensure correctness
idx += len(returntypes)
if ((node.t[0] + returntypes[idx:idx+1])
not in listCompat
and node.SEF):
parent[index] = nr(nt='CONST', t=node.t,
value=defaultListVals[name], SEF=True)
return
del returntypes
elif listarg.name == 'llGetParcelDetails':
listarg = listarg.ch[1] # 0 is vector pos
# TODO: llList2XXX(llGetParcelDetails)
elif listarg.name == 'llParcelMediaQuery':
listarg = listarg.ch[0]
# TODO: llList2XXX(llParcelMediaQuery)
elif listarg.name in {'llGetPrimMediaParams', 'llGetLinkMedia'}:
listarg = listarg.ch[
1 if listarg.name == 'llGetPrimMediaParams'
else 2]
# TODO: llList2XXX(llGetPrimMediaXXX)
elif listarg.name == 'llGetEnvironment':
listarg = listarg.ch[1] # 0 is vector pos
# TODO: llList2XXX(llGetEnvironment)
elif listarg.name == 'llGetVisualParams':
listarg = listarg.ch[1] # 0 is id
# TODO: llList2XXX(llGetVisualParams)
del listarg, idx, value, tvalue, const
return
if name == 'llDialog':
if self.GetListNodeLength(child[2]) == 1:
button = self.ConstFromNodeOrConst(
self.GetListNodeElement(child[2], 0))
if type(button) == unicode and button == u'OK':
# remove the element, as 'OK' is the default button in SL
child[2] = nr(nt='CONST', t='list', value=[], SEF=True)
return
if (name == 'llDeleteSubList'
or name == 'llListReplaceList' and child[1].nt == 'CONST'
and not child[1].value
):
# llDeleteSubList(x, 0, -1) -> [] if x is SEF
# llListReplaceList(x, [], 0, -1) -> [] if x is SEF
if (child[0].SEF
and child[-2].nt == 'CONST' and child[-1].nt == 'CONST'
and child[-2].value == 0 and child[-1].value == -1
):
parent[index] = nr(nt='CONST', t='list', value=[], SEF=True)
return
if name == 'llFrand':
# We can set a range when the input is known to be positive or negative
argmin = self.getMin(child[0])
argmax = self.getMax(child[0])
if argmin is not None and argmin >= 0:
# Positive argument
node.min = 0.
if argmax is not None:
node.max = lslfuncs.F32(argmax * 0.9999999403953552)
elif argmax is not None and argmax <= 0:
# Negative argument
node.max = 0.
if argmin is not None:
node.min = lslfuncs.F32(argmin * 0.9999999403953552)
def FuncOptSetup():
# Patch the default values list for LSO
if lslcommon.LSO:
defaultListVals['llList2Key'] = Key(lslfuncs.NULL_KEY)
else:
defaultListVals['llList2Key'] = Key(u"")