Add 'inline' directive to forcibly inline function calls

This commit is contained in:
Sei Lisa 2019-01-01 22:29:12 +01:00
parent 7bb07ecf38
commit a4986f21df
3 changed files with 398 additions and 6 deletions

374
lslopt/lslinliner.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
# 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)

View file

@ -1011,7 +1011,8 @@ class parser(object):
params = (['list', 'integer', 'list'], params = (['list', 'integer', 'list'],
['L', 'i', 'v']) ['L', 'i', 'v'])
self.AddSymbol('f', 0, 'lazy_list_set', Loc=self.usedspots, 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, 'L', Type='list')
self.AddSymbol('v', paramscope, 'i', Type='integer') self.AddSymbol('v', paramscope, 'i', Type='integer')
self.AddSymbol('v', paramscope, 'v', Type='list') self.AddSymbol('v', paramscope, 'v', Type='list')
@ -2507,16 +2508,22 @@ list lazy_list_set(list L, integer i, list v)
self.NextToken() self.NextToken()
self.localevents = None self.localevents = None
self.locallabels = set() 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) body = self.Parse_code_block(typ)
del self.locallabels del self.locallabels
if typ and not getattr(body, 'LIR', False): # is LastIsReturn flag set? if typ and not getattr(body, 'LIR', False): # is LastIsReturn flag set?
raise EParseCodePathWithoutRet(self) raise EParseCodePathWithoutRet(self)
paramscope = self.scopeindex paramscope = self.scopeindex
self.AddSymbol('f', 0, name, Loc=len(self.tree), Type=typ, self.AddSymbol('f', 0, name, Loc=len(self.tree), Type=typ,
Inline=force_inline,
ParamTypes=params[0], ParamNames=params[1]) ParamTypes=params[0], ParamNames=params[1])
self.tree.append(nr(nt='FNDEF', t=typ, name=name, scope=0, self.tree.append(nr(nt='FNDEF', t=typ, name=name, scope=0,
pscope=paramscope, pscope=paramscope, ptypes=params[0], pnames=params[1],
ptypes=params[0], pnames=params[1], ch=[body])) ch=[body]))
self.PopScope() self.PopScope()
assert self.scopeindex == 0 assert self.scopeindex == 0
else: else:
@ -2667,6 +2674,8 @@ list lazy_list_set(list L, integer i, list v)
break break
self.NextToken() self.NextToken()
self.NextToken() self.NextToken()
if self.tok[0] == 'IDENT' and self.tok[1] == 'inline':
self.NextToken()
if self.tok[0] != '{': if self.tok[0] != '{':
return ret return ret
self.NextToken() # Enter the first brace 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. # coding pattern is normally easy to work around anyway.
self.optenabled = 'optimize' in options self.optenabled = 'optimize' in options
# Inline keyword
self.enable_inline = 'inline' in options
# Symbol table: # Symbol table:
# This is a list of all local and global symbol tables. # This is a list of all local and global symbol tables.
# The first element (0) is the global scope. Each symbol table is a # 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.globals
del self.scopestack del self.scopestack
if self.enable_inline:
import lslinliner
lslinliner.inliner().inline(self.tree, self.symtab)
treesymtab = self.tree, self.symtab treesymtab = self.tree, self.symtab
del self.tree del self.tree
del self.symtab del self.symtab

View file

@ -27,7 +27,7 @@ import lslopt.lslcommon
import lslopt.lslloadlib import lslopt.lslloadlib
VERSION = '0.2.2beta' VERSION = '0.3.0beta'
def ReportError(script, e): def ReportError(script, e):
@ -274,6 +274,8 @@ Case insensitive.
with that name). This flag works around that limitation with that name). This flag works around that limitation
by replacing the names of the labels in the output with by replacing the names of the labels in the output with
unique ones. unique ones.
Inline + Enable 'inline' keyword to force functions to be inlined
(EXPERIMENTAL)
Deprecated / compatibility syntax extensions options: Deprecated / compatibility syntax extensions options:
@ -356,7 +358,7 @@ validoptions = frozenset(('extendedglobalexpr','breakcont','extendedtypecast',
'lazylists','enableswitch','errmissingdefault','funcoverride','optimize', 'lazylists','enableswitch','errmissingdefault','funcoverride','optimize',
'optsigns','optfloats','constfold','dcr','shrinknames','addstrings', 'optsigns','optfloats','constfold','dcr','shrinknames','addstrings',
'foldtabs','warntabs','processpre','explicitcast','listlength','listadd', 'foldtabs','warntabs','processpre','explicitcast','listlength','listadd',
'help', 'inline', 'help',
# undocumented # undocumented
'lso','expr','rsrclimit', 'lso','expr','rsrclimit',
# 'clear' is handled as a special case # 'clear' is handled as a special case
@ -374,7 +376,7 @@ def main(argv):
options = set(('extendedglobalexpr','extendedtypecast','extendedassignment', options = set(('extendedglobalexpr','extendedtypecast','extendedassignment',
'allowkeyconcat','allowmultistrings','processpre','warntabs','optimize', 'allowkeyconcat','allowmultistrings','processpre','warntabs','optimize',
'optsigns','optfloats','constfold','dcr','errmissingdefault', 'optsigns','optfloats','constfold','dcr','errmissingdefault',
'listlength','listadd', 'listlength','listadd','inline',
)) ))
assert not (options - validoptions), (u"Default options not present in" assert not (options - validoptions), (u"Default options not present in"