LSL-PyOptimizer/lslopt/lsloutput.py
Sei Lisa fa547cd9e8 Add blank lines to make the output somewhat prettier
Add blank lines between functions, between functions and states, between variables and functions or states, between states, and between events.

Or more concisely: add blank lines between events and between all elements at the global level except between variables (that actually describes the algorithm).

Some test cases expected no newlines; fix them.
2018-12-23 18:12:10 +01:00

522 lines
21 KiB
Python

# (C) Copyright 2015-2018 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/>.
# Convert an abstract syntax tree + symbol table back to a script as text.
import lslfuncs
import lslcommon
from lslcommon import Key, Vector, Quaternion, warning
from math import copysign
class outscript(object):
binary_operands = frozenset(('||','&&','^','|','&','==','!=','<','<=','>',
'>=','<<','>>','+','-','*','/','%', '=', '+=', '-=', '*=', '/=','%=',
))
extended_assignments = frozenset(('&=', '|=', '^=', '<<=', '>>='))
unary_operands = frozenset(('NEG', '!', '~'))
op_priority = {'=':0, '+=':0, '-=':0, '*=':0, '/=':0, '%=':0, '&=':0,
'|=':0, '^=':0, '<<=':0, '>>=':0,
'||':1, '&&':1, '|':2, '^':3, '&':4, '==':5, '!=':5,
'<':6, '<=':6, '>':6, '>=':6, '<<':7, '>>':7, '+':8, '-':8,# 'NEG':8,
'*':9, '/':9, '%':9}#, '!':10, '~':10, '++':10, '--':10, }
assignment_ops = ('=', '+=', '-=', '*=', '/=','%=')
def Value2LSL(self, value):
tvalue = type(value)
if tvalue in (Key, unicode):
pfx = sfx = ''
if type(value) == Key:
# Constants of type key can not be represented
#raise lslfuncs.ELSLTypeMismatch
# Actually they can be the result of folding.
# On second thought, if we report the error, the location info
# is lost. So we emit a warning instead, letting the compiler
# report the error in the generated source.
if self.globalmode and self.listmode:
warning(u"Illegal combo: Key type inside a global list")
if self.listmode or not self.globalmode:
if self.globalmode:
pfx = '(key)'
else:
pfx = '((key)'
sfx = ')'
if u'\t' in value and self.warntabs:
warning(u"A string contains a tab. Tabs are expanded to four"
" spaces by the viewer when copy-pasting the code"
" (disable this warning by disabling the 'warntabs'"
" option).")
return pfx + '"' + value.encode('utf8').replace('\\','\\\\') \
.replace('"','\\"').replace('\n','\\n') + '"' + sfx
if tvalue == int:
if value < 0 and not self.globalmode and self.optsigns:
#return '0x%X' % (value + 4294967296)
return '((integer)' + str(value) + ')'
return str(value)
if tvalue == float:
if self.optfloats and value.is_integer() and -2147483648.0 <= value < 2147483648.0:
if self.globalmode and not self.listmode:
if value == 0 and copysign(1, value) == -1:
return '-0.'
return str(int(value))
elif not self.globalmode:
# Important inside lists!!
if value == 0 and copysign(1, value) == -1:
return '(-(float)0)'
return '((float)' + str(int(value)) + ')'
s = repr(value)
if s == 'nan':
return '(1e40*0)' if copysign(1, value) < 0 else '(-1e40*0)'
if s == 'inf':
return '1e40'
if s == '-inf':
return '-1e40' if self.globalmode else '((float)-1e40)'
# Try to remove as many decimals as possible but keeping the F32 value intact
exp = s.find('e')
if ~exp:
s, exp = s[:exp], s[exp:]
if exp[1] == '+':
exp = exp[:1] + exp[2:]
if '.' not in s:
# I couldn't produce one but it's assumed that if it happens,
# this code deals with it correctly
s += '.' # pragma: no cover
else:
if '.' not in s:
# This should never happen (Python should always return a point or exponent)
return s + '.' # pragma: no cover
exp = ''
# Shorten the float as much as possible.
while s[-1] != '.' and lslfuncs.F32(float(s[:-1]+exp)) == value:
s = s[:-1]
if s[-1] != '.':
news = s[:-1]
neg = ''
if s[0] == '-':
news = news[1:]
neg = '-'
# Try harder
point = news.index('.') + 1 - len(news)
if point:
news = str(int(news[:point-1] + news[point:]) + 1).zfill(len(news)-1) # Increment
else:
news = str(int(news[:-1])+1).zfill(len(news)-1)
news = news[:point + len(news)] + '.' + news[point + len(news):] # Reinsert point
# Repeat the operation with the incremented number
while news[-1] != '.' and lslfuncs.F32(float(neg+news[:-1]+exp)) == value:
news = news[:-1]
if len(neg+news) < len(s) and lslfuncs.F32(float(neg+news+exp)) == value:
# Success! But we try even harder. We may have converted
# 9.9999e3 into 10.e3; that needs to be turned into 1.e4.
if exp != '':
if news[2:3] == '.': # we converted 9.9... into 10.
newexp = 'e' + str(int(exp[1:])+1) # increase exponent
news2 = news[0] + '.' + news[1] + news[3:] # move dot to the left
while news2[-1] == '0': # remove trailing zeros
news2 = news2[:-1]
if len(neg+news2) < len(s) and lslfuncs.F32(float(neg+news2+newexp)) == value:
news = news2
exp = newexp
s = neg+news
if exp and s[-1] == '.':
s = s[:-1] # transfrom e.g. 1.e-30 into 1e-30
if value >= 0 or self.globalmode or not self.optsigns:
return s + exp
return '((float)' + s + exp + ')'
if tvalue == Vector:
return '<' + self.Value2LSL(value[0]) + ', ' + self.Value2LSL(value[1]) \
+ ', ' + self.Value2LSL(value[2]) + '>'
if tvalue == Quaternion:
return '<' + self.Value2LSL(value[0]) + ', ' + self.Value2LSL(value[1]) \
+ ', ' + self.Value2LSL(value[2]) + ', ' + self.Value2LSL(value[3]) + '>'
if tvalue == list:
if value == []:
return '[]'
if len(value) < 5:
save_listmode = self.listmode
self.listmode = True
ret = '[' + self.Value2LSL(value[0])
for elem in value[1:]:
ret += ', ' + self.Value2LSL(elem)
ret += ']'
self.listmode = save_listmode
return ret
ret = '' if lslcommon.IsCalc else '\n'
first = True
self.indentlevel += 0 if lslcommon.IsCalc else 1
for entry in value:
ret += self.dent() + ('[ ' if first else ', ')
save_listmode = self.listmode
self.listmode = True
ret += self.Value2LSL(entry) + '\n'
self.listmode = save_listmode
first = False
ret += self.dent()
self.indentlevel -= 0 if lslcommon.IsCalc else 1
return ret + ']'
assert False, u'Value of unknown type in Value2LSL: ' + repr(value)
def dent(self):
return self.indent * self.indentlevel
def FindName(self, node, scope = None):
if scope is None:
# node is a node
if (hasattr(node, 'scope')
and 'NewName' in self.symtab[node.scope][node.name]):
return self.symtab[node.scope][node.name]['NewName']
if node.nt == 'FNCALL' and 'NewName' in self.symtab[0][node.name]:
return self.symtab[0][node.name]['NewName']
return node.name
# node is a name
if 'NewName' in self.symtab[scope][node]:
return self.symtab[scope][node]['NewName']
return node
def OutIndented(self, node):
if node.nt != '{}':
self.indentlevel += 1
ret = self.OutCode(node)
if node.nt != '{}':
self.indentlevel -= 1
return ret
def OutExprList(self, L):
ret = ''
if L:
First = True
for item in L:
if not First:
ret += ', '
ret += self.OutExpr(item)
First = False
return ret
def OutExpr(self, expr):
# Handles expression nodes (as opposed to statement nodes)
nt = expr.nt
child = expr.ch
if nt in self.binary_operands:
lnt = child[0].nt
lparen = False
rnt = child[1].nt
rparen = False
if nt in self.assignment_ops and nt in self.op_priority:
# Assignment is right-associative, so it needs to be dealt with
# separately.
base_pri = self.op_priority[nt]
if rnt in self.op_priority:
if self.op_priority[rnt] < base_pri: # should never happen
rparen = True
elif nt in self.op_priority:
base_pri = self.op_priority[nt]
if lnt in self.op_priority:
if self.op_priority[lnt] < base_pri:
lparen = True
elif lnt == 'NEG' and base_pri > self.op_priority['-']:
lparen = True
# This situation has ugly cases due to the strange precedence
# of unary minus. Consider the following two statements:
# (~-a) * a
# a * (~-a) * a
# In one case, the (~-a) is a left child; in the other, it's
# part of a right child. In both, cases, the parentheses are
# mandatory, or they would be interpreted respectively as:
# ~-(a * a)
# a * ~-(a * a)
# Yet the tree structure makes it quite hard to detect these.
# So as a safeguard, for now we parenthesize all ~ and ! within
# binary operands, as they have a deceitful binding power when
# there's a unary minus downstream.
#
# TODO: See if the parenthesizing of ~ and ! can be improved.
elif lnt in ('~', '!'):
lparen = True
if rnt in self.op_priority:
if self.op_priority[rnt] <= base_pri:
rparen = True
# see above
elif rnt in ('~', '!'):
rparen = True
if lparen:
ret = '(' + self.OutExpr(child[0]) + ')'
else:
ret = self.OutExpr(child[0])
ret += ' ' + nt + ' '
if rparen:
ret += '(' + self.OutExpr(child[1]) + ')'
else:
ret += self.OutExpr(child[1])
return ret
if nt == 'IDENT':
return self.FindName(expr)
if nt == 'CONST':
if (self.foldconst and expr.t == 'list' and len(expr.value) == 1
and not self.globalmode):
return '(list)' + self.Value2LSL(expr.value[0])
return self.Value2LSL(expr.value)
if nt == 'CAST' or self.foldconst and nt in ('LIST', 'CONST') and len(child)==1 and not self.globalmode:
ret = '(' + expr.t + ')'
expr = child[0]
if expr.nt in ('CONST', 'IDENT', 'V++', 'V--', 'VECTOR',
'ROTATION', 'LIST', 'FIELD', 'PRINT', 'FNCALL'):
if expr.nt != 'LIST' or len(expr.ch) != 1:
return ret + self.OutExpr(expr)
return ret + '(' + self.OutExpr(expr) + ')'
if nt == 'LIST':
self.listmode = True
if len(child) < 5:
ret = '[' + self.OutExprList(child) + ']'
else:
self.indentlevel += 0 if lslcommon.IsCalc else 1
ret = '' if lslcommon.IsCalc else '\n'
first = True
for elem in child:
ret += self.dent() + ('[ ' if first else ', ')
ret += self.OutExpr(elem) + '\n'
first = False
ret += self.dent() + ']'
self.indentlevel -= 0 if lslcommon.IsCalc else 1
self.listmode = False
return ret
if nt in ('VECTOR', 'ROTATION'):
ret = ('<' + self.OutExpr(child[0]) + ','
+ self.OutExpr(child[1]) + ',')
if nt == 'ROTATION':
ret += self.OutExpr(child[2]) + ','
lnt = child[-1].nt
if lnt in self.op_priority \
and self.op_priority[lnt] <= self.op_priority['>']:
ret += '(' + self.OutExpr(child[-1]) + ')'
else:
ret += self.OutExpr(child[-1])
return ret + '>'
if nt == 'FNCALL':
return self.FindName(expr) + '(' + self.OutExprList(child) + ')'
if nt == 'PRINT':
return 'print(' + self.OutExpr(child[0]) + ')'
if nt in self.unary_operands:
ret = nt
lnt = child[0].nt
paren = False
if nt == 'NEG':
ret = '-'
if (lnt == 'CONST' and child[0].t == 'integer'
and child[0].value < 0
):
# shortcut
ret += str(child[0].value + 4294967296)
return ret
if lnt in self.op_priority:
paren = self.op_priority[lnt] <= self.op_priority['-']
elif (lnt == 'NEG' or lnt == '--V'
or lnt == 'CONST'
and child[0].t == 'float'
and child[0].value < 0
):
ret += ' ' # don't output "--" as that's a different token
else:
if lnt in self.op_priority:
paren = True
if paren:
ret += '(' + self.OutExpr(child[0]) + ')'
else:
ret += self.OutExpr(child[0])
return ret
if nt == 'FLD':
return self.OutExpr(child[0]) + '.' + expr.fld
if nt in ('V--', 'V++'):
return self.OutExpr(child[0]) + ('++' if nt == 'V++' else '--')
if nt in ('--V', '++V'):
return ('++' if nt == '++V' else '--') + self.OutExpr(child[0])
if nt in self.extended_assignments:
lvalue = self.OutExpr(child[0])
return lvalue + ' = ' + lvalue + ' ' + nt[:-1] + ' (' + self.OutExpr(child[1]) + ')'
if nt == 'EXPRLIST':
return self.OutExprList(child)
if nt == 'SUBIDX':
return '(MISSING TYPE)' + self.OutExpr(child[0]) + '[' + self.OutExprList(child[1:]) + ']'
assert False, 'Internal error: expression type "' + nt + '" not handled' # pragma: no cover
def OutCode(self, node):
nt = node.nt
child = node.ch
if nt == 'IF':
ret = self.dent()
while True:
ret += 'if (' + self.OutExpr(child[0]) + ')\n'
# Do we need to add braces around the THEN side?
needs_braces = False
if len(child) == 3:
testnode = child[1]
# Find last IF in an ELSE IF chain
while testnode.nt == 'IF' and len(testnode.ch) == 3:
testnode = testnode.ch[2]
if testnode.nt == 'IF':
# hit an IF without ELSE at the end of the chain
needs_braces = True
if needs_braces:
ret += self.dent() + '{\n'
ret += self.OutIndented(child[1])
ret += self.dent() + '}\n'
else:
ret += self.OutIndented(child[1])
if len(child) < 3:
return ret
if child[2].nt != 'IF':
ret += self.dent() + 'else\n' + self.OutIndented(child[2])
return ret
ret += self.dent() + 'else '
node = child[2]
child = node.ch
if nt == 'WHILE':
ret = self.dent() + 'while (' + self.OutExpr(child[0]) + ')\n'
ret += self.OutIndented(child[1])
return ret
if nt == 'DO':
ret = self.dent() + 'do\n'
ret += self.OutIndented(child[0])
return ret + self.dent() + 'while (' + self.OutExpr(child[1]) + ');\n'
if nt == 'FOR':
ret = self.dent() + 'for ('
ret += self.OutExpr(child[0])
ret += '; ' + self.OutExpr(child[1]) + '; '
ret += self.OutExpr(child[2])
ret += ')\n'
ret += self.OutIndented(child[3])
return ret
if nt == '@':
return self.dent() + '@' + self.FindName(node) + ';\n'
if nt == 'JUMP':
return self.dent() + 'jump ' + self.FindName(node) + ';\n'
if nt == 'STSW':
return self.dent() + 'state ' + self.FindName(node) + ';\n'
if nt == 'RETURN':
if child:
return self.dent() + 'return ' + self.OutExpr(child[0]) + ';\n'
return self.dent() + 'return;\n'
if nt == 'DECL':
ret = self.dent() + node.t + ' ' + self.FindName(node)
if child:
if hasattr(child[0], 'orig') and (child[0].orig.nt != 'IDENT'
or child[0].orig.name
in self.symtab[child[0].orig.scope]):
ret += ' = ' + self.OutExpr(child[0].orig)
else:
ret += ' = ' + self.OutExpr(child[0])
return ret + ';\n'
if nt == ';':
return self.dent() + ';\n'
if nt in ('STDEF', '{}'):
ret = ''
if nt == 'STDEF':
if node.name == 'default':
ret = self.dent() + 'default\n'
else:
ret = self.dent() + 'state ' + self.FindName(node) + '\n'
ret += self.dent() + '{\n'
self.indentlevel += 1
firstnode = True
for stmt in node.ch:
if stmt.nt == 'LAMBDA':
continue
if nt == 'STDEF' and not firstnode:
ret += '\n'
ret += self.OutCode(stmt)
firstnode = False
self.indentlevel -= 1
return ret + self.dent() + '}\n'
if nt == 'FNDEF':
ret = self.dent()
if node.t is not None:
ret += node.t + ' '
ret += self.FindName(node) + '('
scope = node.pscope
ret += ', '.join(typ + ' ' + self.FindName(name, scope)
for typ, name in zip(node.ptypes, node.pnames))
return ret + ')\n' + self.OutCode(child[0])
if nt == 'EXPR':
return self.dent() + self.OutExpr(child[0]) + (
';\n' if not lslcommon.IsCalc else '')
if nt == 'LAMBDA':
return ''
assert False, "Internal error: node type not handled: " + nt # pragma: no cover
def output(self, treesymtab, options = ('optimize',
'optsigns','optfloats','warntabs')):
# Build a sorted list of dict entries
self.tree, self.symtab = treesymtab
# Grab options
self.optimize = 'optimize' in options
# These are optimization options that depend on the above:
self.optsigns = self.optimize and 'optsigns' in options
self.optfloats = self.optimize and 'optfloats' in options
self.foldconst = self.optimize and 'constfold' in options
self.warntabs = 'warntabs' in options
ret = ''
self.indent = ' '
self.indentlevel = 0
self.globalmode = False
self.listmode = False
firstnode = True
prevnt = None
for node in self.tree:
if node.nt == 'LAMBDA':
# these don't produce output, skip
continue
if not firstnode and (node.nt != 'DECL' or prevnt != 'DECL'):
ret += '\n'
self.globalmode = node.nt == 'DECL'
ret += self.OutCode(node)
self.globalmode = False
firstnode = False
prevnt = node.nt
return ret