mirror of
https://github.com/Sei-Lisa/LSL-PyOptimizer
synced 2025-07-01 15:48:21 +00:00
561 lines
23 KiB
Python
561 lines
23 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/>.
|
|
|
|
# Convert an abstract syntax tree + symbol table back to a script as text.
|
|
|
|
from lslopt import lslfuncs
|
|
from lslopt import lslcommon
|
|
from lslopt.lslcommon import Key, Vector, Quaternion, warning
|
|
from math import copysign
|
|
from strutil import *
|
|
|
|
debugScopes = False
|
|
|
|
class outscript(object):
|
|
|
|
binary_operators = frozenset({'||','&&','^','|','&','==','!=','<','<=','>',
|
|
'>=','<<','>>','+','-','*','/','%', '=', '+=', '-=', '*=', '/=','%=',
|
|
})
|
|
extended_assignments = frozenset({'&=', '|=', '^=', '<<=', '>>='})
|
|
unary_operators = 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 = frozenset({'=', '+=', '-=', '*=', '/=','%='})
|
|
|
|
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 + '"' + u2str(unicode(value), '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_operators:
|
|
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, paying
|
|
# attention to the binding power of the binary not ~ operator:
|
|
# (~-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.
|
|
# We have to descend down the left nodes as we keep finding
|
|
# binary operators or unary operators, to find whether the
|
|
# symbol is followed by unary minus. If that's the case, we
|
|
# need to act as if there was a negation right here.
|
|
elif lnt in ('~', '!'):
|
|
lnode = child[0]
|
|
while True:
|
|
lnode = lnode.ch[0]
|
|
if (lnode.nt not in self.op_priority
|
|
and lnode.nt not in ('~', '!')
|
|
):
|
|
break
|
|
if lnode.nt == 'NEG' and base_pri > self.op_priority['-']:
|
|
lparen = True
|
|
|
|
if rnt in self.op_priority:
|
|
if self.op_priority[rnt] <= base_pri:
|
|
rparen = True
|
|
elif rnt == 'NEG' and self.op_priority['-'] < base_pri:
|
|
rparen = True
|
|
# see above
|
|
elif rnt in ('~', '!'):
|
|
lnode = child[1]
|
|
while True:
|
|
# Descend down the left nodes of the right node, to
|
|
# find whether it's immediately followed by -
|
|
lnode = lnode.ch[0]
|
|
if lnode.nt not in self.op_priority and lnode.nt not in ('~', '!'):
|
|
break
|
|
if lnode.nt == 'NEG' and base_pri > self.op_priority['-']:
|
|
# TODO: Improve right operand parenthesis removal
|
|
# for minus signs.
|
|
# Shouldn't this look into the RHS node?
|
|
# a * -b * c
|
|
# is currently translated to
|
|
# a * (-b * c)
|
|
# which is correct but not optimal.
|
|
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':
|
|
if debugScopes:
|
|
return self.FindName(expr) + '\\' + str(expr.scope) + '/'
|
|
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_operators:
|
|
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 == '@':
|
|
if debugScopes:
|
|
return self.dent() + '@' + self.FindName(node) \
|
|
+ '\\' + str(node.scope) + '/;\n'
|
|
return self.dent() + '@' + self.FindName(node) + ';\n'
|
|
if nt == 'JUMP':
|
|
if debugScopes:
|
|
return self.dent() + 'jump ' + self.FindName(node) \
|
|
+ '\\' + str(node.scope) + '/;\n'
|
|
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 debugScopes:
|
|
ret += '\\' + str(node.scope) + '/'
|
|
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
|