diff --git a/lslopt/lslinliner.py b/lslopt/lslinliner.py new file mode 100644 index 0000000..e2413a1 --- /dev/null +++ b/lslopt/lslinliner.py @@ -0,0 +1,374 @@ +# (C) Copyright 2015-2019 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 . + +# Expand inlined functions. This could perhaps be made at parse time, but that +# would obfuscate the source too much. + +from lslcommon import nr + +# Statement-level nodes that have at most 1 child and is of type expression +SINGLE_OPT_EXPR_CHILD_NODES = frozenset({'DECL', 'EXPR', 'RETURN', + '@', 'STSW', 'JUMP', ';', 'LAMBDA'}) + +class ENameAlreadyExists(Exception): pass + +class EExpansionLoop(Exception): + def __init__(self): + super(EExpansionLoop, self).__init__(u"Loop found in expansion of" + u" inline functions") + +class inliner(object): + def FixJumps(self, node): + """Change name and scope of JUMPs to point to the correct symtab entry + """ + nt = node.nt + if nt == 'JUMP': + orig = self.symtab[node.scope][node.name] + if 'NewSymbolName' in orig: + node.name = orig['NewSymbolName'] + node.scope = orig['NewSymbolScope'] + return + + if nt in SINGLE_OPT_EXPR_CHILD_NODES: + return + + if nt == '{}': + for i in node.ch: + self.FixJumps(i) + return + + if nt == 'IF' or nt == 'WHILE': + self.FixJumps(node.ch[1]) + if len(node.ch) > 2: + self.FixJumps(node.ch[2]) + return + + if nt == 'DO': + self.FixJumps(node.ch[0]) + return + + if nt == 'FOR': + self.FixJumps(node.ch[3]) + return + + assert False, u"Unexpected node type: %s" % nt.decode('utf8') + + 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. + """ + 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 = len(self.symtab) + copy.ch = [] + self.symtab.append({}) + 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() + copy.ch[-1].scope = copy.scope + return copy + + if nt == '@': + copy = node.copy() + oldscope = node.scope + oldname = node.name + self.lblCount += 1 + copy.name = '___lbl__%05d' % self.lblCount + copy.scope = scope + if copy.name in self.symtab[scope]: + raise ENameAlreadyExists( + u"Label already exists: %s" % copy.name.decode('utf8')) + self.symtab[scope][copy.name] = {'Type':'l', '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) + 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=len(self.symtab), 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 + ]) + self.symtab.append({}) + self.retused = True + return newnode + + 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 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. + self.retCount += 1 + retvar = '___ret__%05d' % self.retCount + if retvar in self.symtab[scope]: + raise ENameAlreadyExists(u"Symbol %s already exists" + % retvar.decode('utf8')) + # Add the symbol to the symbol table + self.symtab[scope][retvar] = {'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() + + self.expanding.append(node.name) + self.retvar = retvar + self.retscope = scope + self.retlscope = scope + self.lblCount += 1 + retlabel = '___rtl__%05d' % self.lblCount + self.retlabel = retlabel + self.symtab[scope][retlabel] = {'Type':'l', 'Scope':scope} + + # Get a copy of the function + self.retused = False + blk = [self.GetFuncCopy(self.tree[fnsym['Loc']])] + retused = self.retused + 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 + + if fnsym['ParamNames']: + # Add a new block + symbols + assignments for parameter values + pscope = len(self.symtab) + self.symtab.append({}) + outer = nr(nt='{}', t=None, scope=pscope, ch=[]) + 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} + # BUG: We don't honour ExplicitCast here. + outer.ch.append(nr(nt='DECL', t=ptype, name=pname, scope=pscope, + ch=[value])) + outer.ch.extend(blk) + blk = [outer] + + 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 RecurseExpression(self, parent, index, scope): + node = parent[index] + nt = node.nt + fns = [] + + if nt == 'FNCALL' and self.symtab[0][node.name].get('Inline', False): + fns.extend(self.ConvertFunction(parent, index, scope)) + elif node.ch: + for i in range(len(node.ch)): + fns.extend(self.RecurseExpression(node.ch, i, scope)) + return fns + + def RecurseSingleStatement(self, parent, index, scope): + # Synthesize a block node whose child is the statement. + newscope = len(self.symtab) + self.symtab.append({}) + 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 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) + + # TODO: Handle loops properly + # 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. + # + # Do loops are different: + # + # do + # llOwnerSay("doing stuff"); + # while (f()); + # + # should be 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) + + elif nt == 'WHILE': + fns = self.RecurseExpression(child, 0, scope) + self.RecurseSingleStatement(child, 1, scope) + + elif nt == 'FOR': + assert child[0].nt == 'EXPRLIST' + assert child[2].nt == 'EXPRLIST' + fns = [] + for i in range(len(child[0].ch)): + fns.extend(self.RecurseExpression(child[0].ch, i, scope)) + fns.extend(self.RecurseExpression(child, 1, scope)) + for i in range(len(child[2].ch)): + fns.extend(self.RecurseExpression(child[2].ch, i, scope)) + self.RecurseSingleStatement(child, 3, scope) + + else: + assert False, u"Unexpected node type: %s" % nt.decode('utf8') + + if fns: + parent[index:index] = fns + + def inline(self, tree, symtab): + self.tree = tree + self.symtab = symtab + self.retCount = 0 + self.lblCount = 0 + 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) diff --git a/lslopt/lslparse.py b/lslopt/lslparse.py index 963ed46..1baf210 100644 --- a/lslopt/lslparse.py +++ b/lslopt/lslparse.py @@ -1011,7 +1011,8 @@ class parser(object): params = (['list', 'integer', 'list'], ['L', 'i', 'v']) self.AddSymbol('f', 0, 'lazy_list_set', Loc=self.usedspots, - Type='list', ParamTypes=params[0], ParamNames=params[1]) + Type='list', ParamTypes=params[0], ParamNames=params[1], + Inline=False) self.AddSymbol('v', paramscope, 'L', Type='list') self.AddSymbol('v', paramscope, 'i', Type='integer') self.AddSymbol('v', paramscope, 'v', Type='list') @@ -2507,16 +2508,22 @@ list lazy_list_set(list L, integer i, list v) self.NextToken() self.localevents = None self.locallabels = set() + force_inline = False + if (self.enable_inline and self.tok[0] == 'IDENT' + and self.tok[1] == 'inline'): + self.NextToken() + force_inline = True body = self.Parse_code_block(typ) del self.locallabels if typ and not getattr(body, 'LIR', False): # is LastIsReturn flag set? raise EParseCodePathWithoutRet(self) paramscope = self.scopeindex self.AddSymbol('f', 0, name, Loc=len(self.tree), Type=typ, + Inline=force_inline, ParamTypes=params[0], ParamNames=params[1]) self.tree.append(nr(nt='FNDEF', t=typ, name=name, scope=0, - pscope=paramscope, - ptypes=params[0], pnames=params[1], ch=[body])) + pscope=paramscope, ptypes=params[0], pnames=params[1], + ch=[body])) self.PopScope() assert self.scopeindex == 0 else: @@ -2667,6 +2674,8 @@ list lazy_list_set(list L, integer i, list v) break self.NextToken() self.NextToken() + if self.tok[0] == 'IDENT' and self.tok[1] == 'inline': + self.NextToken() if self.tok[0] != '{': return ret self.NextToken() # Enter the first brace @@ -2845,6 +2854,9 @@ list lazy_list_set(list L, integer i, list v) # coding pattern is normally easy to work around anyway. self.optenabled = 'optimize' in options + # Inline keyword + self.enable_inline = 'inline' in options + # Symbol table: # This is a list of all local and global symbol tables. # The first element (0) is the global scope. Each symbol table is a @@ -2948,6 +2960,10 @@ list lazy_list_set(list L, integer i, list v) del self.globals del self.scopestack + if self.enable_inline: + import lslinliner + lslinliner.inliner().inline(self.tree, self.symtab) + treesymtab = self.tree, self.symtab del self.tree del self.symtab diff --git a/main.py b/main.py index a0f44d5..10d7ec8 100755 --- a/main.py +++ b/main.py @@ -27,7 +27,7 @@ import lslopt.lslcommon import lslopt.lslloadlib -VERSION = '0.2.2beta' +VERSION = '0.3.0beta' def ReportError(script, e): @@ -274,6 +274,8 @@ Case insensitive. with that name). This flag works around that limitation by replacing the names of the labels in the output with unique ones. + Inline + Enable 'inline' keyword to force functions to be inlined + (EXPERIMENTAL) Deprecated / compatibility syntax extensions options: @@ -356,7 +358,7 @@ validoptions = frozenset(('extendedglobalexpr','breakcont','extendedtypecast', 'lazylists','enableswitch','errmissingdefault','funcoverride','optimize', 'optsigns','optfloats','constfold','dcr','shrinknames','addstrings', 'foldtabs','warntabs','processpre','explicitcast','listlength','listadd', - 'help', + 'inline', 'help', # undocumented 'lso','expr','rsrclimit', # 'clear' is handled as a special case @@ -374,7 +376,7 @@ def main(argv): options = set(('extendedglobalexpr','extendedtypecast','extendedassignment', 'allowkeyconcat','allowmultistrings','processpre','warntabs','optimize', 'optsigns','optfloats','constfold','dcr','errmissingdefault', - 'listlength','listadd', + 'listlength','listadd','inline', )) assert not (options - validoptions), (u"Default options not present in"