mirror of
https://github.com/Sei-Lisa/LSL-PyOptimizer
synced 2025-07-01 23:58:20 +00:00
Reformatting
This commit is contained in:
parent
e123866e6e
commit
141301d7ff
1 changed files with 101 additions and 66 deletions
|
@ -31,8 +31,8 @@ class foldconst(object):
|
||||||
def isLocalVar(self, node):
|
def isLocalVar(self, node):
|
||||||
name = node.name
|
name = node.name
|
||||||
scope = node.scope
|
scope = node.scope
|
||||||
return self.symtab[scope][name]['Kind'] == 'v' \
|
return (self.symtab[scope][name]['Kind'] == 'v'
|
||||||
and 'Loc' not in self.symtab[scope][name]
|
and 'Loc' not in self.symtab[scope][name])
|
||||||
|
|
||||||
def GetListNodeLength(self, node):
|
def GetListNodeLength(self, node):
|
||||||
"""Get the length of a list that is expressed as a CONST, LIST or CAST
|
"""Get the length of a list that is expressed as a CONST, LIST or CAST
|
||||||
|
@ -230,17 +230,20 @@ class foldconst(object):
|
||||||
that the result is boolean.
|
that the result is boolean.
|
||||||
"""
|
"""
|
||||||
nt = node.nt
|
nt = node.nt
|
||||||
if nt in ('<', '!', '>', '<=', '>=', '==', '||', '&&') \
|
if (nt in ('<', '!', '>', '<=', '>=', '==', '||', '&&')
|
||||||
or nt == '!=' and node.ch[0].t != 'list' \
|
or nt == '!=' and node.ch[0].t != 'list'
|
||||||
or nt == '&' and (self.IsBool(node.ch[0]) or self.IsBool(node.ch[1])) \
|
or nt == '&' and any(self.IsBool(node.ch[i]) for i in (0, 1))
|
||||||
or nt in ('|', '^', '*') and self.IsBool(node.ch[0]) and self.IsBool(node.ch[1]) \
|
or nt in ('|', '^', '*')
|
||||||
or nt == 'CONST' and node.t == 'integer' and node.value in (0, 1):
|
and all(self.IsBool(node.ch[i]) for i in (0, 1))
|
||||||
|
or nt == 'CONST' and node.t == 'integer' and node.value in (0, 1)
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if nt == 'FNCALL':
|
if nt == 'FNCALL':
|
||||||
sym = self.symtab[0][node.name]
|
sym = self.symtab[0][node.name]
|
||||||
if sym['Type'] == 'integer' and 'min' in sym and 'max' in sym \
|
if (sym['Type'] == 'integer' and 'min' in sym and 'max' in sym
|
||||||
and sym['min'] >= 0 and sym['max'] <= 1:
|
and sym['min'] >= 0 and sym['max'] <= 1
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -248,7 +251,7 @@ class foldconst(object):
|
||||||
def IsAndBool(self, node):
|
def IsAndBool(self, node):
|
||||||
"""For bitwise AND, in some cases we can relax the condition to this:
|
"""For bitwise AND, in some cases we can relax the condition to this:
|
||||||
when bit 0 is 0, all other bits are guaranteed to be 0 as well. That's
|
when bit 0 is 0, all other bits are guaranteed to be 0 as well. That's
|
||||||
the case of -bool which is the only case we deal with here, but an
|
the case of -bool, which is the only case we deal with here, but an
|
||||||
important one because we generate it as an intermediate result in some
|
important one because we generate it as an intermediate result in some
|
||||||
operations.
|
operations.
|
||||||
"""
|
"""
|
||||||
|
@ -296,8 +299,9 @@ class foldconst(object):
|
||||||
# We have !(int == int). Replace with int ^ int or with int - 1
|
# We have !(int == int). Replace with int ^ int or with int - 1
|
||||||
node = parent[index] = child[0] # remove the negation
|
node = parent[index] = child[0] # remove the negation
|
||||||
child = child[0].ch
|
child = child[0].ch
|
||||||
if child[0].nt == 'CONST' and child[0].value == 1 \
|
if (child[0].nt == 'CONST' and child[0].value == 1
|
||||||
or child[1].nt == 'CONST' and child[1].value == 1:
|
or child[1].nt == 'CONST' and child[1].value == 1
|
||||||
|
):
|
||||||
# a != 1 -> a - 1 (which FoldTree will transform to ~-a)
|
# a != 1 -> a - 1 (which FoldTree will transform to ~-a)
|
||||||
node.nt = '-'
|
node.nt = '-'
|
||||||
else:
|
else:
|
||||||
|
@ -329,9 +333,10 @@ class foldconst(object):
|
||||||
|
|
||||||
if nt in self.binary_ops and child[0].t == child[1].t == 'integer':
|
if nt in self.binary_ops and child[0].t == child[1].t == 'integer':
|
||||||
if nt == '==':
|
if nt == '==':
|
||||||
if child[0].nt == 'CONST' and -1 <= child[0].value <= 1 \
|
if (child[0].nt == 'CONST' and -1 <= child[0].value <= 1
|
||||||
or child[1].nt == 'CONST' and -1 <= child[1].value <= 1:
|
or child[1].nt == 'CONST' and -1 <= child[1].value <= 1
|
||||||
# Transform a==b into !(a-b) if either a or b are in [-1, 1]
|
):
|
||||||
|
# Transform a==b into !(a-b) if either a or b are in [-1,1]
|
||||||
parent[index] = nr(nt='!', t='integer', ch=[node])
|
parent[index] = nr(nt='!', t='integer', ch=[node])
|
||||||
node.nt = '-'
|
node.nt = '-'
|
||||||
self.FoldTree(parent, index)
|
self.FoldTree(parent, index)
|
||||||
|
@ -392,7 +397,7 @@ class foldconst(object):
|
||||||
# (!~(x|~r) && x&s) -> !~(x|(~r&~s))
|
# (!~(x|~r) && x&s) -> !~(x|(~r&~s))
|
||||||
# This is implemented as:
|
# This is implemented as:
|
||||||
# ~(x|~r) | !(x&s) -> ~(x|~(r|s))
|
# ~(x|~r) | !(x&s) -> ~(x|~(r|s))
|
||||||
# because that's the intermediate result after conversion of &&.
|
# since that's the intermediate result after conversion of &&.
|
||||||
# a and b are going to be the children of the main |
|
# a and b are going to be the children of the main |
|
||||||
# a is going to be child that has the ~
|
# a is going to be child that has the ~
|
||||||
# b is the other child (with the !)
|
# b is the other child (with the !)
|
||||||
|
@ -437,18 +442,23 @@ class foldconst(object):
|
||||||
Invertible = [False, False]
|
Invertible = [False, False]
|
||||||
for a in (0, 1):
|
for a in (0, 1):
|
||||||
Invertible[a] = child[a].nt == '!'
|
Invertible[a] = child[a].nt == '!'
|
||||||
if child[a].nt == '<' \
|
if (child[a].nt == '<'
|
||||||
and child[a].ch[0].t == child[a].ch[1].t == 'integer':
|
and child[a].ch[0].t == child[a].ch[1].t == 'integer'
|
||||||
if child[a].ch[0].nt == 'CONST' \
|
):
|
||||||
and child[a].ch[0].value != 2147483647 \
|
if (child[a].ch[0].nt == 'CONST'
|
||||||
or child[a].ch[1].nt == 'CONST' \
|
and child[a].ch[0].value != 2147483647
|
||||||
and child[a].ch[1].value != int(-2147483648):
|
or child[a].ch[1].nt == 'CONST'
|
||||||
|
and child[a].ch[1].value != int(-2147483648)
|
||||||
|
):
|
||||||
Invertible[a] = True
|
Invertible[a] = True
|
||||||
|
|
||||||
# Deal with our optimization of a<0 -> a&0x80000000 (see below)
|
# Deal with our optimization of a<0 -> a&0x80000000
|
||||||
|
# (see below)
|
||||||
if child[a].nt == '&' and (
|
if child[a].nt == '&' and (
|
||||||
child[a].ch[0].nt == 'CONST' and child[a].ch[0].value == int(-2147483648)
|
child[a].ch[0].nt == 'CONST'
|
||||||
or child[a].ch[1].nt == 'CONST' and child[a].ch[1].value == int(-2147483648)
|
and child[a].ch[0].value == int(-2147483648)
|
||||||
|
or child[a].ch[1].nt == 'CONST'
|
||||||
|
and child[a].ch[1].value == int(-2147483648)
|
||||||
):
|
):
|
||||||
Invertible[a] |= ParentIsNegation
|
Invertible[a] |= ParentIsNegation
|
||||||
|
|
||||||
|
@ -560,8 +570,10 @@ class foldconst(object):
|
||||||
# Put constant in child[b], if present
|
# Put constant in child[b], if present
|
||||||
if child[b].nt != 'CONST':
|
if child[b].nt != 'CONST':
|
||||||
a, b = 1, 0
|
a, b = 1, 0
|
||||||
if child[b].nt == 'CONST' and child[b].value == int(-2147483648) \
|
if (child[b].nt == 'CONST'
|
||||||
and child[a].nt == 'FNCALL':
|
and child[b].value == int(-2147483648)
|
||||||
|
and child[a].nt == 'FNCALL'
|
||||||
|
):
|
||||||
sym = self.symtab[0][child[a].name]
|
sym = self.symtab[0][child[a].name]
|
||||||
if 'min' in sym and sym['min'] == -1:
|
if 'min' in sym and sym['min'] == -1:
|
||||||
node = parent[index] = nr(nt='~', t='integer',
|
node = parent[index] = nr(nt='~', t='integer',
|
||||||
|
@ -618,8 +630,8 @@ class foldconst(object):
|
||||||
self.FoldTree(child, 0)
|
self.FoldTree(child, 0)
|
||||||
node.SEF = child[0].SEF
|
node.SEF = child[0].SEF
|
||||||
|
|
||||||
if child[0].nt == '+' and (child[0].ch[0].nt == 'NEG'
|
if child[0].nt == '+' and any(child[0].ch[i].nt == 'NEG'
|
||||||
or child[0].ch[1].nt == 'NEG'):
|
for i in (0, 1)):
|
||||||
node = parent[index] = child[0]
|
node = parent[index] = child[0]
|
||||||
child = node.ch
|
child = node.ch
|
||||||
for a in (0, 1):
|
for a in (0, 1):
|
||||||
|
@ -670,14 +682,16 @@ class foldconst(object):
|
||||||
if snt == '<':
|
if snt == '<':
|
||||||
lop = subexpr.ch[0]
|
lop = subexpr.ch[0]
|
||||||
rop = subexpr.ch[1]
|
rop = subexpr.ch[1]
|
||||||
if lop.nt == 'CONST' and lop.t == rop.t == 'integer' \
|
if (lop.nt == 'CONST' and lop.t == rop.t == 'integer'
|
||||||
and lop.value < 2147483647:
|
and lop.value < 2147483647
|
||||||
|
):
|
||||||
lop.value += 1
|
lop.value += 1
|
||||||
subexpr.ch[0], subexpr.ch[1] = subexpr.ch[1], subexpr.ch[0]
|
subexpr.ch[0], subexpr.ch[1] = subexpr.ch[1], subexpr.ch[0]
|
||||||
parent[index] = subexpr # remove the !
|
parent[index] = subexpr # remove the !
|
||||||
return
|
return
|
||||||
if rop.nt == 'CONST' and lop.t == rop.t == 'integer' \
|
if (rop.nt == 'CONST' and lop.t == rop.t == 'integer'
|
||||||
and rop.value > int(-2147483648):
|
and rop.value > int(-2147483648)
|
||||||
|
):
|
||||||
rop.value -= 1
|
rop.value -= 1
|
||||||
subexpr.ch[0], subexpr.ch[1] = subexpr.ch[1], subexpr.ch[0]
|
subexpr.ch[0], subexpr.ch[1] = subexpr.ch[1], subexpr.ch[0]
|
||||||
parent[index] = subexpr # remove the !
|
parent[index] = subexpr # remove the !
|
||||||
|
@ -686,7 +700,9 @@ class foldconst(object):
|
||||||
a, b = 0, 1
|
a, b = 0, 1
|
||||||
if subexpr.ch[b].nt != 'CONST':
|
if subexpr.ch[b].nt != 'CONST':
|
||||||
a, b = 1, 0
|
a, b = 1, 0
|
||||||
if subexpr.ch[b].nt == 'CONST' and subexpr.ch[b].value == int(-2147483648):
|
if (subexpr.ch[b].nt == 'CONST'
|
||||||
|
and subexpr.ch[b].value == int(-2147483648)
|
||||||
|
):
|
||||||
# !(i & 0x80000000) -> -1 < i (because one of our
|
# !(i & 0x80000000) -> -1 < i (because one of our
|
||||||
# optimizations can be counter-productive, see FoldCond)
|
# optimizations can be counter-productive, see FoldCond)
|
||||||
subexpr.nt = '<'
|
subexpr.nt = '<'
|
||||||
|
@ -1134,10 +1150,10 @@ class foldconst(object):
|
||||||
# -intvar could equal intvar if intvar = -2147483648,
|
# -intvar could equal intvar if intvar = -2147483648,
|
||||||
# so the sign is a no-op and pushing it to floatconst would
|
# so the sign is a no-op and pushing it to floatconst would
|
||||||
# make the result be different.
|
# make the result be different.
|
||||||
if child[a].nt == 'NEG' \
|
if (child[a].nt == 'NEG'
|
||||||
and (self.cornermath
|
and (self.cornermath
|
||||||
or child[a].t != 'integer'
|
or child[a].t != 'integer'
|
||||||
or child[b].t != 'float'
|
or child[b].t != 'float')
|
||||||
):
|
):
|
||||||
# Expression is of the form (-float)*const or (-float)/const or const/(-float)
|
# Expression is of the form (-float)*const or (-float)/const or const/(-float)
|
||||||
if val != int(-2147483648) or child[a].t == 'integer': # can't be optimized otherwise
|
if val != int(-2147483648) or child[a].t == 'integer': # can't be optimized otherwise
|
||||||
|
@ -1153,9 +1169,11 @@ class foldconst(object):
|
||||||
# ident * -2 -> -(ident + ident) (only if ident is local)
|
# ident * -2 -> -(ident + ident) (only if ident is local)
|
||||||
# expr/1 -> expr
|
# expr/1 -> expr
|
||||||
# expr/-1 -> -expr
|
# expr/-1 -> -expr
|
||||||
if nt == '*' and child[b].t in ('float', 'integer') \
|
if (nt == '*' and child[b].t in ('float', 'integer')
|
||||||
and val in (-2, -1, 0, 1, 2) \
|
and val in (-2, -1, 0, 1, 2)
|
||||||
or nt == '/' and b == 1 and val in (-1, 1):
|
or nt == '/'
|
||||||
|
and b == 1 and val in (-1, 1)
|
||||||
|
):
|
||||||
if val == 1:
|
if val == 1:
|
||||||
parent[index] = child[a]
|
parent[index] = child[a]
|
||||||
return
|
return
|
||||||
|
@ -1201,7 +1219,9 @@ class foldconst(object):
|
||||||
SEF=node.SEF)
|
SEF=node.SEF)
|
||||||
node = parent[index] = nr(nt='!', t='integer',
|
node = parent[index] = nr(nt='!', t='integer',
|
||||||
ch=[node], SEF=node.SEF)
|
ch=[node], SEF=node.SEF)
|
||||||
del child
|
# Can't delete
|
||||||
|
# See https://docs.python.org/2/reference/simple_stmts.html#del
|
||||||
|
child = None
|
||||||
self.FoldTree(parent, index)
|
self.FoldTree(parent, index)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1265,15 +1285,15 @@ class foldconst(object):
|
||||||
and not lslfuncs.less(child[0].value,
|
and not lslfuncs.less(child[0].value,
|
||||||
self.symtab[0][child[1].name]['max'])
|
self.symtab[0][child[1].name]['max'])
|
||||||
):
|
):
|
||||||
parent[index] = nr(nt='CONST', t='integer', value=0,
|
parent[index] = nr(nt='CONST', t='integer',
|
||||||
SEF=True)
|
value=0, SEF=True)
|
||||||
return
|
return
|
||||||
if ('min' in self.symtab[0][child[1].name]
|
if ('min' in self.symtab[0][child[1].name]
|
||||||
and lslfuncs.less(child[0].value,
|
and lslfuncs.less(child[0].value,
|
||||||
self.symtab[0][child[1].name]['min'])
|
self.symtab[0][child[1].name]['min'])
|
||||||
):
|
):
|
||||||
parent[index] = nr(nt='CONST', t='integer', value=1,
|
parent[index] = nr(nt='CONST', t='integer',
|
||||||
SEF=True)
|
value=1, SEF=True)
|
||||||
return
|
return
|
||||||
if (child[1].nt == 'CONST'
|
if (child[1].nt == 'CONST'
|
||||||
and child[0].nt == 'FNCALL'
|
and child[0].nt == 'FNCALL'
|
||||||
|
@ -1287,22 +1307,25 @@ class foldconst(object):
|
||||||
self.symtab[0][child[0].name]['max']
|
self.symtab[0][child[0].name]['max']
|
||||||
, child[1].value)
|
, child[1].value)
|
||||||
):
|
):
|
||||||
parent[index] = nr(nt='CONST', t='integer', value=1,
|
parent[index] = nr(nt='CONST', t='integer',
|
||||||
SEF=True)
|
value=1, SEF=True)
|
||||||
return
|
return
|
||||||
if ('min' in self.symtab[0][child[0].name]
|
if ('min' in self.symtab[0][child[0].name]
|
||||||
and not lslfuncs.less(
|
and not lslfuncs.less(
|
||||||
self.symtab[0][child[0].name]['min'],
|
self.symtab[0][child[0].name]['min'],
|
||||||
child[1].value)
|
child[1].value)
|
||||||
):
|
):
|
||||||
parent[index] = nr(nt='CONST', t='integer', value=0,
|
parent[index] = nr(nt='CONST', t='integer',
|
||||||
SEF=True)
|
value=0, SEF=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Convert 2147483647<i and i<-2147483648 to i&0
|
# Convert 2147483647<i and i<-2147483648 to i&0
|
||||||
if child[0].t == child[1].t == 'integer' \
|
if (child[0].t == child[1].t == 'integer'
|
||||||
and (child[0].nt == 'CONST' and child[0].value == 2147483647
|
and (child[0].nt == 'CONST'
|
||||||
or child[1].nt == 'CONST' and child[1].value == int(-2147483648)):
|
and child[0].value == 2147483647
|
||||||
|
or child[1].nt == 'CONST'
|
||||||
|
and child[1].value == int(-2147483648))
|
||||||
|
):
|
||||||
a, b = 0, 1
|
a, b = 0, 1
|
||||||
# Put the constant in child[b]
|
# Put the constant in child[b]
|
||||||
if child[a].nt == 'CONST':
|
if child[a].nt == 'CONST':
|
||||||
|
@ -1328,13 +1351,21 @@ class foldconst(object):
|
||||||
|
|
||||||
if child[b].nt == 'CONST':
|
if child[b].nt == 'CONST':
|
||||||
val = child[b].value
|
val = child[b].value
|
||||||
if nt == '|' and val == 0 or nt == '&' and (val == -1 or val == 1 and self.IsBool(child[a])):
|
if (nt == '|' and val == 0
|
||||||
|
or nt == '&'
|
||||||
|
and (val == -1
|
||||||
|
or val == 1 and self.IsBool(child[a]))
|
||||||
|
):
|
||||||
# a|0 -> a
|
# a|0 -> a
|
||||||
# a&-1 -> a
|
# a&-1 -> a
|
||||||
# a&1 -> a if a is boolean
|
# a&1 -> a if a is boolean
|
||||||
parent[index] = child[a]
|
parent[index] = child[a]
|
||||||
return
|
return
|
||||||
if nt == '|' and (val == -1 or val == 1 and self.IsBool(child[a])) or nt == '&' and val == 0:
|
if (nt == '|'
|
||||||
|
and (val == -1
|
||||||
|
or val == 1 and self.IsBool(child[a]))
|
||||||
|
or nt == '&' and val == 0
|
||||||
|
):
|
||||||
# a|-1 -> -1 if a is SEF
|
# a|-1 -> -1 if a is SEF
|
||||||
# a|1 -> 1 if a is bool and SEF
|
# a|1 -> 1 if a is bool and SEF
|
||||||
# a&0 -> 0 if a is SEF
|
# a&0 -> 0 if a is SEF
|
||||||
|
@ -1347,6 +1378,7 @@ class foldconst(object):
|
||||||
if child[0].nt == child[1].nt == opposite:
|
if child[0].nt == child[1].nt == opposite:
|
||||||
left = child[0].ch
|
left = child[0].ch
|
||||||
right = child[1].ch
|
right = child[1].ch
|
||||||
|
# Can't loop individually because we must break out of both
|
||||||
for c, d in ((0, 0), (0, 1), (1, 0), (1, 1)):
|
for c, d in ((0, 0), (0, 1), (1, 0), (1, 1)):
|
||||||
if self.CompareTrees(left[c], right[d]):
|
if self.CompareTrees(left[c], right[d]):
|
||||||
child[1].nt = nt
|
child[1].nt = nt
|
||||||
|
@ -1445,13 +1477,14 @@ class foldconst(object):
|
||||||
# We have a regular assignment either way now. Simplify the RHS.
|
# We have a regular assignment either way now. Simplify the RHS.
|
||||||
self.FoldTree(node.ch, 1)
|
self.FoldTree(node.ch, 1)
|
||||||
chkequal = child[1].ch[0] if child[1].nt == '=' else child[1]
|
chkequal = child[1].ch[0] if child[1].nt == '=' else child[1]
|
||||||
if child[0].nt == chkequal.nt == 'IDENT' \
|
if (child[0].nt == chkequal.nt == 'IDENT'
|
||||||
and chkequal.name == child[0].name \
|
and chkequal.name == child[0].name
|
||||||
and chkequal.scope == child[0].scope \
|
and chkequal.scope == child[0].scope
|
||||||
or child[0].nt == chkequal.nt == 'FLD' \
|
or child[0].nt == chkequal.nt == 'FLD'
|
||||||
and chkequal.ch[0].name == child[0].ch[0].name \
|
and chkequal.ch[0].name == child[0].ch[0].name
|
||||||
and chkequal.ch[0].scope == child[0].ch[0].scope \
|
and chkequal.ch[0].scope == child[0].ch[0].scope
|
||||||
and chkequal.fld == child[0].fld:
|
and chkequal.fld == child[0].fld
|
||||||
|
):
|
||||||
parent[index] = child[1]
|
parent[index] = child[1]
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1658,14 +1691,15 @@ class foldconst(object):
|
||||||
if child[idx].nt == 'JUMP':
|
if child[idx].nt == 'JUMP':
|
||||||
idx2 = idx + 1
|
idx2 = idx + 1
|
||||||
while idx2 < nchild:
|
while idx2 < nchild:
|
||||||
# Search for a label that is the destination of this
|
# Search for a label that is the destination of
|
||||||
# JUMP, skipping other labels
|
# this JUMP, skipping other labels
|
||||||
if child[idx2].nt != '@':
|
if child[idx2].nt != '@':
|
||||||
break
|
break
|
||||||
if (child[idx].scope == child[idx2].scope
|
if (child[idx].scope == child[idx2].scope
|
||||||
and child[idx].name == child[idx2].name
|
and child[idx].name == child[idx2].name
|
||||||
):
|
):
|
||||||
sym = self.symtab[child[idx].scope][child[idx].name]
|
sym = self.symtab[child[idx].scope]
|
||||||
|
sym = sym[child[idx].name]
|
||||||
# remove the JUMP
|
# remove the JUMP
|
||||||
del child[idx]
|
del child[idx]
|
||||||
advance = 0
|
advance = 0
|
||||||
|
@ -1882,8 +1916,9 @@ class foldconst(object):
|
||||||
else:
|
else:
|
||||||
self.FoldTree(child, 0)
|
self.FoldTree(child, 0)
|
||||||
# Remove assignment if integer zero.
|
# Remove assignment if integer zero.
|
||||||
if node.t == 'integer' and child[0].nt == 'CONST' \
|
if (node.t == 'integer' and child[0].nt == 'CONST'
|
||||||
and not child[0].value:
|
and not child[0].value
|
||||||
|
):
|
||||||
node.ch = None
|
node.ch = None
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue