def LastPassPostOrder(self, parent, index): node = parent[index] nt = node.nt child = node.ch if nt == 'FNDEF': if hasattr(node, 'scope') and self.BadStCh: # There is at least one bad state change statement in the # function (must be the result of optimization). # Insert dummy IF(1){...} statement covering the whole function # (if it returs a value, insert a return statement too). child[0] = nr(nt='{}', t=None, scope=len(self.symtab), ch=[ nr(nt='IF', t=None, ch=[ nr(nt='CONST', t='integer', value=1, SEF=True), child[0] ]) ]) self.symtab.append({}) child = node.ch if node.t is not None: # Inserting a state switch in a function that returns a # value must count as one of the dumbest things to do. # We do as best as we can: add a return statement with the # default value for the type. child[0].ch.append( nr(nt='RETURN', t=None, ch=[ nr(nt='CONST', t=node.t, SEF=True, value=lslcommon.LSLTypeDefaults[node.t]) ])) del self.BadStCh return if nt == 'EXPR': self.inExpr = False if nt == '{}': self.scopeStack.pop() if nt == 'WHILE' or nt == 'DO' or nt == 'FOR': self.prevStmt = self.lastStmt self.lastStmt = rec(node=node, parent=parent, index=index)
def Cast(self, value, newtype): """Return a CAST node if the types are not equal, otherwise the value unchanged. """ if value.t == newtype: return value ret = nr(nt='CAST', t=newtype, ch=[value], SEF=value.SEF) if value.SEF: ret.SEF = True if hasattr(value, 'X'): ret.X = value.X return ret
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')
def RecurseSingleStatement(self, parent, index, scope): # Synthesize a block node whose child is the statement. newscope = self.newSymtab() node = nr(nt='{}', t=None, scope=newscope, ch=[parent[index]], SEF=parent[index].SEF) # Recurse into that node, so that any additions are made right there. self.RecurseStatement(node.ch, 0, newscope) # If it's no longer a single statement, promote it to a block. if len(node.ch) != 1: parent[index] = node else: # The new level won't be necessary. We can't delete the symbol # table, though, because that shifts any new symbol tables. assert not self.symtab[newscope] parent[index] = node.ch[0]
def inline(self, tree, symtab): self.tree = tree self.symtab = symtab self.symCtr = {} self.expanding = [] for i in range(len(tree)): if tree[i].nt == 'STDEF': for j in range(len(tree[i].ch)): # for each event in the state self.RecurseStatement(tree[i].ch[j].ch, 0, tree[i].ch[j].ch[0].scope) elif (tree[i].nt == 'FNDEF' and not symtab[tree[i].scope][tree[i].name].get( 'Inline', False)): # Must be an UDF self.RecurseStatement(tree[i].ch, 0, tree[i].ch[0].scope) # Remove all inline function definitions for i in range(len(tree)): if (tree[i].nt == 'FNDEF' and symtab[tree[i].scope][tree[i].name].get( 'Inline', False)): tree[i] = nr(nt='LAMBDA', t=None)
def LastPassPreOrder(self, parent, index): node = parent[index] nt = node.nt child = node.ch if (nt != '{}' and nt != ';' and nt != 'LAMBDA' and nt != '@' and not self.inExpr): # Node that generates code. # EXPR counts as a single statement for JUMP purposes. self.prevStmt = self.lastStmt self.lastStmt = rec(node=node, parent=parent, index=index) # These are all the labels that label the current statement self.lastLabels = self.curLabels self.curLabels = set() if nt == 'EXPR': self.inExpr = True return if (self.optlistadd and not self.globalmode and (nt == 'CONST' and node.t == 'list' or nt == 'LIST' or nt == '+' and child[0].t == 'list' and (child[1].nt == 'CONST' and child[1].t == 'list' or child[1].nt == 'LIST'))): # Perform these transformations if the list is SEF: # [a, b, ...] -> (list)a + b... # ListExpr + [a, b, ..] -> ListExpr + a + b... # (ListExpr doesn't need to be SEF for this to work) # This transformation makes it difficult to handle lists during # optimization, that's why it's done in a separate pass. # Make listnode be the LIST or CONST element. listnode = child[1] if nt == '+' else node # left is the leftmost element in the addition. # left = None means the first element of the list must be # turned into a cast within the loop. left = child[0] if nt == '+' else None if listnode.SEF: for elem in (listnode.value if listnode.nt == 'CONST' else listnode.ch): elemnode = nr(nt='CONST', t=lslcommon.PythonType2LSL[type(elem)], value=elem, SEF=True) if listnode.nt == 'CONST' else elem left = (self.Cast(elemnode, 'list') if left is None else nr(nt='+', t='list', SEF=True, ch=[left, elemnode ])) del elemnode if left is not None: # it's none for empty lists parent[index] = left # Do another pass on the result self.RecursiveLastPass(parent, index) return del listnode, left if nt == 'FNDEF': # StChAreBad will be True if this is a user-defined function, # where state changes are considered bad. # BadStCh will be True if at least one state change statement # is found while monitoring state changes. self.subinfo['StChAreBad'] = hasattr(node, 'scope') self.BadStCh = False return if nt == 'IF': if len(child) == 2: # Don't monitor the children. self.subinfo['StChAreBad'] = False return if nt == 'DO': self.subinfo['StChAreBad'] = False return if nt == 'FOR': self.subinfo['StChAreBad'] = False return if nt == 'WHILE': self.subinfo['StChAreBad'] = False return if nt == 'STSW': if self.subinfo['StChAreBad']: # Found one. self.BadStCh = True return if nt == 'FNCALL': # lslrenamer can benefit from a list of used library functions, # so provide it. if 'Loc' not in self.symtab[0][node.name]: # system library function self.usedlibfuncs.add(node.name) if nt == 'JUMP': if self.lastLabels: # This jump is labeled. The jumps that go to any of its labels # might be changeable to the destination of this jump, if the # scope of the destination is visible from those jumps, and the # jump eliminated. # TODO: We need the positions of these jumps for this to work. # We could perhaps change 'ref' in the symbol to a list instead # of a counter, pointing to the jumps. pass if nt == '@': self.curLabels.add(node) if self.lastStmt.node.nt == 'JUMP': jump = self.lastStmt.node if jump.scope == node.scope and jump.name == node.name: # Remove the jump self.lastStmt.parent[self.lastStmt.index] = nr(nt=';') assert self.symtab[node.scope][node.name]['ref'] > 0 self.symtab[node.scope][node.name]['ref'] -= 1 if nt == '{}': self.scopeStack.append(node.scope)
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'), 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' \ and 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.nt == 'FNCALL' and 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 < 0 or param >= len(primParamsTypes) or primParamsTypes[param] is False): # 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 if (returntypes.find('*') == -1 or idx >= 0 and idx < returntypes.find('*') or idx < 0 and idx > returntypes.rfind('*') - len(returntypes)): # Check for type incompatibility or index # out of range. if idx < 0: # s[-1:0] doesn't return the last char # so we have to compensate 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 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
def GetFuncCopy(self, node, scope=0): """Get a copy of the function's body Replaces 'return expr' with assignment+jump, 'return' with jump, and existing labels with fresh labels. Also creates new symtabs for locals and adjusts scopes of symbols. """ nt = node.nt if nt == 'FNDEF': # We're at the top level. Check return type and create a label, # then recurse into the block. assert node.ch[0].nt == '{}' copy = self.GetFuncCopy(node.ch[0], node.ch[0].scope) assert copy.nt == '{}' self.FixJumps(copy) return copy if nt == '{}': copy = node.copy() copy.scope = self.newSymtab() copy.ch = [] for i in node.ch: copy.ch.append(self.GetFuncCopy(i, node.scope)) if i.nt == 'DECL': self.symtab[copy.scope][i.name] = \ self.symtab[i.scope][i.name].copy() self.symtab[node.scope][i.name]['NewSymbolScope'] = \ copy.scope copy.ch[-1].scope = copy.scope return copy if nt == '@': copy = node.copy() oldscope = node.scope oldname = node.name copy.name = self.newId('lbl', scope, { 'Kind': 'l', 'Scope': scope, 'ref': 0 }) copy.scope = scope self.symtab[oldscope][oldname]['NewSymbolName'] = copy.name self.symtab[oldscope][oldname]['NewSymbolScope'] = scope return copy if nt == 'RETURN': newnode = nr(nt='JUMP', t=None, name=self.retlabel, scope=self.retlscope) self.symtab[self.retlscope][self.retlabel]['ref'] += 1 if node.ch: # Returns a value. Wrap in {} and add an assignment. # BUG: We don't honour ExplicitCast here. newnode = nr(nt='{}', t=None, scope=self.newSymtab(), ch=[ nr(nt='EXPR', t=self.rettype, ch=[ nr(nt='=', t=self.rettype, ch=[ nr(nt='IDENT', t=node.ch[0].t, name=self.retvar, scope=self.retscope), self.GetFuncCopy(node.ch[0]) ]) ]), newnode ]) return newnode if nt == 'IDENT': copy = node.copy() if 'NewSymbolScope' in self.symtab[node.scope][node.name]: copy.scope = \ self.symtab[node.scope][node.name]['NewSymbolScope'] return copy if not node.ch: return node.copy() copy = node.copy() copy.ch = [] for i in node.ch: copy.ch.append(self.GetFuncCopy(i, scope)) return copy
def RecurseStatement(self, parent, index, scope): node = parent[index] nt = node.nt child = node.ch fns = None if nt in SINGLE_OPT_EXPR_CHILD_NODES: if child: fns = self.RecurseExpression(child, 0, scope) if nt == 'EXPR' and not node.ch: del parent[index] else: return elif nt == '{}': i = -len(child) while i: self.RecurseStatement(child, len(child) + i, node.scope) i += 1 elif nt == 'IF': fns = self.RecurseExpression(child, 0, scope) self.RecurseSingleStatement(child, 1, scope) if len(child) > 2: self.RecurseSingleStatement(child, 2, scope) # Loop handling is tricky. # # Consider this: # # integer f() # { # llOwnerSay("body of f"); # return 1; # } # # while (f()) # llOwnerSay("doing stuff"); # # In order to execute it every loop iteration, the while() loop must be # converted to an if+jump: # # integer ___ret__00001; # @___lbl__00001; # { # llOwnerSay("body_of_f"); # __ret__00001 = 1; # } # if (___ret__00001) # { # llOwnerSay("doing stuff"); # jump ___lbl__00001; # } # # The for loop is similar, but the initializer and iterator must be # expanded as well, to convert it to a while loop. When expanding the # iterator, care must be taken to avoid name clashes. For example: # # for (i = 0; f(i); i++) # { # integer i; # } # # should be expanded to: # # i = 0; # @loop_label; # integer ___ret__00001; # { # llOwnerSay("body of f"); # ___ret__00001 = 1, # } # if (___ret__00001) # { # { # integer i; # } # i++; # } # # The extra {} inside the 'if' are needed to protect the iterator from # being affected by the variables defined in the inner block. # # Do loops are different: # # do # llOwnerSay("doing stuff"); # while (f()); # # is converted to: # # integer ___ret__00001; # do # { # llOwnerSay("doing stuff"); # { # llOwnerSay("body_of_f"); # ___ret__00001 = 1; # } # } # while (___ret__00001); # elif nt == 'DO': self.RecurseSingleStatement(child, 0, scope) fns = self.RecurseExpression(child, 1, scope) if fns: # Need to do some plumbing to move the bodies into the loop i = 0 while i < len(fns): if fns[i].nt != 'DECL': assert fns[i].nt in ('{}', '@') # All bodies must be moved inside the loop if child[0].nt != '{}': # Needs wrapping now child[0] = nr(nt='{}', t=None, ch=[child[0]], scope=self.newSymtab()) child[0].ch.append(fns.pop(i)) else: i += 1 elif nt == 'WHILE': # Convert to if() lbl = self.newId('whl', scope, { 'Kind': 'l', 'Scope': scope, 'ref': 1 }) parent.insert(index, nr(nt='@', t=None, name=lbl, scope=scope)) index += 1 node.nt = 'IF' if child[1].nt != '{}': # Needs wrapping now child[1] = nr(nt='{}', t=None, ch=[child[1]], scope=self.newSymtab()) child[1].ch.append(nr(nt='JUMP', t=None, name=lbl, scope=scope)) fns = self.RecurseExpression(child, 0, scope) self.RecurseSingleStatement(child, 1, scope) elif nt == 'FOR': assert child[0].nt == 'EXPRLIST' assert child[2].nt == 'EXPRLIST' for i in child[0].ch: parent.insert(index, nr(nt='EXPR', t=i.t, ch=[i])) fns = self.RecurseExpression(parent, index, scope) parent[index:index] = fns index += 1 + len(fns) lbl = self.newId('for', scope, { 'Kind': 'l', 'Scope': scope, 'ref': 1 }) parent.insert(index, nr(nt='@', t=None, name=lbl, scope=scope)) index += 1 node.nt = 'IF' if child[3].nt != '{}': # Needs wrapping now child[3] = nr(nt='{}', t=None, ch=[child[3]], scope=self.newSymtab()) # Needs another wrapping if iterator is not empty if child[2].ch: child[3] = nr(nt='{}', t=None, ch=[child[3]], scope=self.newSymtab()) for i in child[2].ch: child[3].ch.append(nr(nt='EXPR', t=i.t, ch=[i])) del child[2] del child[0] child[1].ch.append(nr(nt='JUMP', t=None, name=lbl, scope=scope)) fns = self.RecurseExpression(child, 0, scope) self.RecurseSingleStatement(child, 1, scope) else: assert False, u"Unexpected node type: %s" % nt.decode('utf8') if fns: parent[index:index] = fns
def ConvertFunction(self, parent, index, scope): node = parent[index] fns = [] for i in range(len(node.ch)): fns.extend(self.RecurseExpression(node.ch, i, scope)) fnsym = self.symtab[0][node.name] rettype = fnsym['Type'] self.rettype = rettype retvar = None if rettype is not None: # Returns a value. Create a local variable at the starting level. retvar = self.newId('ret', scope, { 'Kind': 'v', 'Scope': scope, 'Type': rettype }) # Add the declaration to the list of statements fns.append(nr(nt='DECL', t=rettype, name=retvar, scope=scope)) # Begin expansion if node.name in self.expanding: raise EExpansionLoop() outer = None if fnsym['ParamNames']: # Add a new block + symbols + assignments for parameter values pscope = self.newSymtab() outer = nr(nt='{}', t=None, scope=pscope, ch=[]) origpscope = self.tree[fnsym['Loc']].pscope for i in range(len(fnsym['ParamNames'])): # Add parameter assignments and symbol table entries pname = fnsym['ParamNames'][i] ptype = fnsym['ParamTypes'][i] value = node.ch[i] self.symtab[pscope][pname] = { 'Kind': 'v', 'Type': ptype, 'Scope': pscope } self.symtab[origpscope][pname]['NewSymbolScope'] = pscope # BUG: We don't honour ExplicitCast here. outer.ch.append( nr(nt='DECL', t=ptype, name=pname, scope=pscope, ch=[value])) self.expanding.append(node.name) self.retvar = retvar self.retscope = scope self.retlscope = scope retlabel = self.newId('rtl', scope, { 'Kind': 'l', 'Scope': scope, 'ref': 0 }) self.retlabel = retlabel # Get a copy of the function blk = [self.GetFuncCopy(self.tree[fnsym['Loc']])] if outer: outer.ch.extend(blk) blk = [outer] retused = self.symtab[scope][retlabel]['ref'] != 0 self.RecurseStatement(blk, 0, scope) # recursively expand functions # Add return label if used, otherwise remove it from the symbol table if retused: blk.append(nr(nt='@', name=retlabel, scope=scope)) else: del self.symtab[scope][retlabel] self.expanding.pop() # End expansion fns.extend(blk) if rettype is None: del parent[index] else: parent[index] = nr(nt='IDENT', t=rettype, name=retvar, scope=scope) return fns
def CleanNode(self, curnode): """Recursively checks if the children are used, deleting those that are not. """ if curnode.ch is None or (curnode.nt == 'DECL' and curnode.scope == 0): return # NOTE: Should not depend on 'Loc', since the nodes that are the # destination of 'Loc' are renumbered as we delete stuff from globals. index = int(curnode.nt in self.assign_ops) # don't recurse into lvalues while index < len(curnode.ch): node = curnode.ch[index] if not hasattr(node, 'X'): if curnode.ch[index].nt == 'JUMP': # Decrease label reference count scope = curnode.ch[index].scope name = curnode.ch[index].name assert self.symtab[scope][name]['ref'] > 0 self.symtab[scope][name]['ref'] -= 1 del curnode.ch[index] continue nt = node.nt if nt == 'DECL': if self.OKtoRemoveSymbol(node): if not node.ch or node.ch[0].SEF: del curnode.ch[index] continue node = curnode.ch[index] = nr( nt='EXPR', t=node.t, ch=[self.Cast(node.ch[0], node.t)]) elif nt == 'FLD': sym = self.OKtoRemoveSymbol(node.ch[0]) if sym: value = sym['W'] # Mark as executed, so it isn't optimized out. value.X = True fieldidx = 'xyzs'.index(node.fld) if value.nt == 'CONST': value = value.value[fieldidx] value = nr(nt='CONST', X=True, SEF=True, t=self.PythonType2LSL[type(value)], value=value) value = self.Cast(value, 'float') SEF = True else: # assumed VECTOR or ROTATION per OKtoRemoveSymbol SEF = value.SEF value = self.Cast(value.ch[fieldidx], 'float') # Replace it node = curnode.ch[index] = value node.SEF = SEF elif nt == 'IDENT': sym = self.OKtoRemoveSymbol(node) if sym: # Mark as executed, so it isn't optimized out. # Make shallow copy. # TODO: Should the copy be a deep copy? assert sym.get('W', False) is not False new = sym['W'].copy() if hasattr(new, 'orig'): del new.orig new.X = True # this part makes no sense? #new.SEF = sym['W'].SEF if new.t != node.t: new = self.Cast(new, node.t) curnode.ch[index] = node = new # Delete orig if present, as we've eliminated the original #if hasattr(sym['W'], 'orig'): # del sym['W'].orig elif nt in self.assign_ops: ident = node.ch[0] if ident.nt == 'FLD': ident = ident.ch[0] sym = self.OKtoRemoveSymbol(ident) if sym: node = curnode.ch[index] = self.Cast(node.ch[1], node.t) elif nt in ('IF', 'WHILE', 'DO', 'FOR'): # If the mandatory statement is to be removed, replace it # with a ; to prevent leaving the statement empty. child = node.ch idx = 3 if nt == 'FOR' else 0 if nt == 'DO' else 1 if not hasattr(child[idx], 'X'): child[idx] = nr(nt=';', t=None, X=True, SEF=True) if nt == 'DO' and not hasattr(child[1], 'X'): # Mandatory condition but not executed - replace child[1] = nr(nt='CONST', X=True, SEF=True, t='integer', value=0) self.CleanNode(node) index += 1
def MarkReferences(self, node): """Marks each node it passes through as executed (X), and each variable as read (R) (with count) and/or written (W) (with node where it is, or False if written more than once) as appropriate. Traces execution to determine if any part of the code is never executed. """ # The 'X' key, when present, indicates whether a node is executed. # Its value means whether this instruction will proceed to the next # (True: it will; False: it won't). if hasattr(node, 'X'): return node.X # branch already analyzed nt = node.nt child = node.ch # Control flow statements if nt == 'STSW': node.X = False # Executed but path-breaking. sym = self.symtab[0][node.name] if not hasattr(self.tree[sym['Loc']], 'X'): self.MarkReferences(self.tree[sym['Loc']]) return False if nt == 'JUMP': node.X = False # Executed but path-breaking. sym = self.symtab[node.scope][node.name] if 'R' in sym: sym['R'] += 1 else: sym['R'] = 1 return False if nt == 'RETURN': node.X = False # Executed but path-breaking. if child: self.MarkReferences(child[0]) return False if nt == 'IF': # "When you get to a fork in the road, take it." node.X = None # provisional value, refined later self.MarkReferences(child[0]) condnode = child[0] if condnode.nt == 'CONST': if lslfuncs.cond(condnode.value): # TRUE - 'then' branch always executed. node.X = self.MarkReferences(child[1]) return node.X elif len(child) == 3: # FALSE - 'else' branch always executed. node.X = self.MarkReferences(child[2]) return node.X # else fall through else: cont = self.MarkReferences(child[1]) if len(child) == 3: if not cont: cont = self.MarkReferences(child[2]) node.X = cont return cont self.MarkReferences(child[2]) node.X = True return True if nt == 'WHILE': node.X = None # provisional value, refined later self.MarkReferences(child[0]) if child[0].nt == 'CONST': if lslfuncs.cond(child[0].value): # Infinite loop - unless it returns, it stops # execution. But it is executed itself. self.MarkReferences(child[1]) node.X = False return node.X # else the inside isn't executed at all, so don't mark it else: self.MarkReferences(child[1]) node.X = True return True if nt == 'DO': node.X = None # provisional value, refined later if not self.MarkReferences(child[0]): node.X = False return False self.MarkReferences(child[1]) # It proceeds to the next statement unless it's an infinite loop node.X = not (child[1].nt == 'CONST' and lslfuncs.cond(child[1].value)) return node.X if nt == 'FOR': node.X = None # provisional value, refined later self.MarkReferences(child[0]) self.MarkReferences(child[1]) if child[1].nt == 'CONST': if lslfuncs.cond(child[1].value): # Infinite loop - unless it returns, it stops # execution. But it is executed itself. node.X = False self.MarkReferences(child[3]) self.MarkReferences(child[2]) # this can't stop execution return node.X # else the body and the iterator aren't executed at all, so # don't mark them node.X = True else: node.X = True self.MarkReferences(child[3]) self.MarkReferences(child[2]) # Mark the EXPRLIST as always executed, but not the subexpressions. # That forces the EXPRLIST (which is a syntactic requirement) to be # kept, while still simplifying the contents properly. child[2].X = True return True if nt == '{}': # Go through each statement in turn. If one stops execution, # continue reading until either we find a used label (and resume # execution) or reach the end (and return False). Otherwise return # True. continues = True node.X = None # provisional for stmt in child: if continues or stmt.nt == '@': continues = self.MarkReferences(stmt) node.X = continues return continues if nt == 'FNCALL': node.X = None # provisional sym = self.symtab[0][node.name] fdef = self.tree[sym['Loc']] if 'Loc' in sym else None for idx in xrange(len(child) - 1, -1, -1): # Each element is a "write" on the callee's parameter. # E.g. f(integer a, integer b) { f(2,3); } means 2, 3 are # writes to a and b. # This has been eliminated, as it causes more trouble than # it fixes. self.MarkReferences(child[idx]) if fdef is not None: psym = self.symtab[fdef.pscope][fdef.pnames[idx]] #if 'W' in psym: # psym['W'] = False #else: # psym['W'] = child[idx] psym['W'] = False if 'Loc' in sym: if not hasattr(self.tree[sym['Loc']], 'X'): self.MarkReferences(self.tree[sym['Loc']]) node.X = self.tree[sym['Loc']].X else: node.X = 'stop' not in sym # Note that JUMP analysis is incomplete. To do it correctly, we # should follow the jump right to its destination, in order to know # if that branch leads to a RETURN or completely stops the event. # With our code structure, following the JUMP is unfeasible. # For that reason, we can't track whether a branch ends in RETURN # or in something more powerful like a script reset, in order to # propagate it through the function definition to the caller. # # In practice, this means that the caller can't distinguish this: # fn() { return; } # from this: # fn() { llResetScript(); } # and therefore, invocations of the function that are followed by # code can't know whether that code is dead or not. # # What does that have to do with jumps? Well, imagine this: # fn(integer x) { if (x) jump x1; else jump x2; # @x1; return; @x2; llResetScript(); } # What of the branches of if() is taken, depends on where the jumps # lead to. Assuming the last one always is wrong, because it would # mark in the caller code that may execute, as dead, e.g. here: # fn2() { fn(); x = 1; } return node.X if nt == 'DECL': sym = self.symtab[node.scope][node.name] if child is not None: sym['W'] = child[0] else: sym['W'] = nr(nt='CONST', t=node.t, value=self.DefaultValues[node.t]) node.X = True if child is not None: if hasattr(child[0], 'orig'): orig = child[0].orig self.MarkReferences(orig) child[0].X = orig.X if orig.nt == 'LIST': # Add fake writes to variables used in list elements in # 'orig', so they don't get deleted (Issue #3) for subnode in orig.ch: if subnode.nt == 'IDENT': # can only happen in globals assert subnode.scope == 0 sym = self.symtab[0][subnode.name] sym['W'] = False self.tree[sym['Loc']].X = True elif subnode.nt in ('VECTOR', 'ROTATION'): for sub2node in subnode.ch: if sub2node.nt == 'IDENT': # can only happen in globals assert sub2node.scope == 0 sym = self.symtab[0][sub2node.name] sym['W'] = False self.tree[sym['Loc']].X = True else: self.MarkReferences(child[0]) return True # ---- Starting here, all node types return through the bottom # (except '='). node.X = None # provisional if nt in self.assign_ops or nt in ('--V', '++V', 'V++', 'V--'): ident = node.ch[0] if ident.nt == 'FLD': ident = ident.ch[0] assert ident.nt == 'IDENT' sym = self.symtab[ident.scope][ident.name] if ident.scope == 0: # Mark the global first. self.MarkReferences(self.tree[sym['Loc']]) # In any case, this is at least the second write, so mark it as such # (SSA would be a plus for this to be optimal) sym['W'] = False if nt == '=': # Prevent the first node from being mistaken as a read, by # recursing only on the RHS node. self.MarkReferences(child[1]) node.X = True return True elif nt == 'FLD': # Mark this variable as referenced by a Field (recursing will mark # the ident as read later) self.symtab[child[0].scope][child[0].name]['Fld'] = True elif nt == 'IDENT': sym = self.symtab[node.scope][node.name] # Mark global if it's one. if 'W' not in sym and node.scope == 0: self.MarkReferences(self.tree[sym['Loc']]) # Increase read counter if 'R' in sym: sym['R'] += 1 else: sym['R'] = 1 node.X = True if child is not None: for subnode in child: self.MarkReferences(subnode) return True
def test_coverage_misc(self): """Miscellaneous tests that can't be computed or are too difficult to compute with scripts """ sys.stderr.write('\nRunning misc coverage tests: ') # Doesn't accept bytes self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.zstr, b"blah") # Can't typecast float to vector self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, lslfuncs.F32(1.2), lslcommon.Vector) # Can't typecast integer to vector self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, 1, lslcommon.Vector) # Can't typecast vector to key self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, lslcommon.Vector((1., 2., 3.)), lslcommon.Key) # Can't typecast quaternion to key self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, lslcommon.Quaternion((1., 2., 3., 4.)), lslcommon.Key) # Can't typecast list to vector self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, [ 1, 1., lslcommon.Key(u'blah'), lslcommon.Quaternion((1., 0., 0., 0.)) ], lslcommon.Vector) # Can't typecast key to integer self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast, lslcommon.Key(u"1"), int) # Can't negate string self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.neg, u"3") # Can't add two keys self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.add, lslcommon.Key(u"1"), lslcommon.Key(u"2")) # Can't subtract two strings self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.sub, u"1", u"2") # Can't multiply two strings self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul, u"1", u"2") # Can't multiply quaternion and float in any order self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul, lslcommon.Quaternion((1., 2., 3., 4.)), 1.) self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul, 1., lslcommon.Quaternion((1., 2., 3., 4.))) # Can't multiply quaternion by vector (but the opposite order is OK) self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul, lslcommon.Quaternion((1., 2., 3., 4.)), lslcommon.Vector((1., 2., 3.))) # Can't divide quaternion by vector either self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.div, lslcommon.Quaternion((1., 2., 3., 4.)), lslcommon.Vector((1., 2., 3.))) # Can't mod floats self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mod, 3., 3) # Can't compare string and integer self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.compare, u'3', 4) self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.less, u'3', 4) # Bytes is not a valid type to multiply by (in any order) self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.mul, b"a", 3) self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.mul, lslcommon.Vector((3., 4., 5.)), b"a") self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.typecast, b"", unicode) # v2f/q2f coverage (force conversion from ints to floats) self.assertEqual(repr(lslfuncs.v2f(lslcommon.Vector((1, 0, 0)))), 'Vector((1.0, 0.0, 0.0))') self.assertEqual( repr(lslfuncs.q2f(lslcommon.Quaternion((1, 0, 0, 0)))), 'Quaternion((1.0, 0.0, 0.0, 0.0))') # Key repr coverage self.assertEqual(repr(lslcommon.Key(u'')), "Key(u'')" if str != unicode else "Key('')") # string + key coverage self.assertEqual(lslfuncs.add(u'a', lslcommon.Key(u'b')), u'ab') self.assertEqual(type(lslfuncs.add(u'a', lslcommon.Key(u'b'))), unicode) # The SEF table prevents this assertion from being reachable via script. self.assertRaises(lslfuncs.ELSLCantCompute, lslfuncs.llXorBase64Strings, u"AABA", u"AABA") self.assertRaises(lslfuncs.ELSLCantCompute, lslfuncs.llModPow, 3, 5, 7) # Check invalid type in llGetListEntryType self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.llGetListEntryType, [b'a'], 0) # Check that Value2LSL raises an exception if the type is unknown. outmod = lsloutput.outscript() # Script with a single node of type Expression, containing a constant # of type Bytes. That's rejected by the output module. msg = None script = [ nr(nt='EXPR', t='string', ch=[nr(nt='CONST', t='string', value=b'ab')]) ] save_IsCalc = lslcommon.IsCalc lslcommon.IsCalc = True try: try: outmod.output((script, ())) except AssertionError as e: msg = str(e) finally: lslcommon.IsCalc = save_IsCalc self.assertEqual( msg, u"Value of unknown type in Value2LSL: 'ab'" if python2 else u"Value of unknown type in Value2LSL: b'ab'") del msg # Extended assignment in output script = [ nr(nt='EXPR', t='integer', ch=[ nr(nt='^=', t='integer', ch=[ nr(nt='IDENT', t='integer', name='a', scope=0), nr(nt='CONST', t='integer', value=3) ]) ]) ] save_IsCalc = lslcommon.IsCalc lslcommon.IsCalc = True try: out = outmod.output((script, [{ 'a': { 'Kind': 'v', 'Loc': 1, 'Scope': 0, 'Type': 'integer' } }])) finally: lslcommon.IsCalc = save_IsCalc self.assertEqual(out, 'a = a ^ (3)') del out, script, outmod, save_IsCalc