mirror of
https://github.com/Sei-Lisa/LSL-PyOptimizer
synced 2025-07-01 23:58:20 +00:00
Fix type conversion on calling LSL functions.
We had a big chaos with type conversion. That caused a bug where passing a key to a function that required a string, or vice versa, crashed the script. Diminish the chaos by modifying the parameters just prior to invocation (in lslfoldconst). We also remove the, now unnecessary, calls to force floats, either alone or within vectors or quaternions.
This commit is contained in:
parent
27eeec06cf
commit
6879037735
4 changed files with 80 additions and 47 deletions
|
@ -259,8 +259,20 @@ def zstr(s):
|
||||||
def ff(x):
|
def ff(x):
|
||||||
"""Force x to be a float"""
|
"""Force x to be a float"""
|
||||||
if type(x) != float:
|
if type(x) != float:
|
||||||
x = F32(float(x))
|
x = float(x)
|
||||||
return x
|
return F32(x)
|
||||||
|
|
||||||
|
def fk(k):
|
||||||
|
"""Force k to be a key"""
|
||||||
|
if type(k) != Key:
|
||||||
|
k = Key(k)
|
||||||
|
return k
|
||||||
|
|
||||||
|
def fs(s):
|
||||||
|
"""Force s to be a string"""
|
||||||
|
if type(s) != unicode:
|
||||||
|
s = unicode(s)
|
||||||
|
return s
|
||||||
|
|
||||||
def q2f(q):
|
def q2f(q):
|
||||||
if type(q[0]) == type(q[1]) == type(q[2]) == type(q[3]) == float:
|
if type(q[0]) == type(q[1]) == type(q[2]) == type(q[3]) == float:
|
||||||
|
@ -770,7 +782,7 @@ def isinteger(x):
|
||||||
return type(x) == int
|
return type(x) == int
|
||||||
|
|
||||||
def isfloat(x):
|
def isfloat(x):
|
||||||
return type(x) in (float, int)
|
return type(x) == float
|
||||||
|
|
||||||
def isvector(x):
|
def isvector(x):
|
||||||
return type(x) == Vector and len(x) == 3 and type(x[0]) == type(x[1]) == type(x[2]) == float
|
return type(x) == Vector and len(x) == 3 and type(x[0]) == type(x[1]) == type(x[2]) == float
|
||||||
|
@ -804,38 +816,32 @@ def llAbs(i):
|
||||||
def llAcos(f):
|
def llAcos(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
try:
|
try:
|
||||||
return F32(math.acos(ff(f)))
|
return F32(math.acos(f))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return NaN
|
return NaN
|
||||||
|
|
||||||
def llAngleBetween(r1, r2):
|
def llAngleBetween(r1, r2):
|
||||||
assert isrotation(r1)
|
assert isrotation(r1)
|
||||||
assert isrotation(r2)
|
assert isrotation(r2)
|
||||||
r1 = q2f(r1)
|
|
||||||
r2 = q2f(r2)
|
|
||||||
return llRot2Angle(div(r1, r2, f32=False))
|
return llRot2Angle(div(r1, r2, f32=False))
|
||||||
|
|
||||||
def llAsin(f):
|
def llAsin(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
try:
|
try:
|
||||||
return F32(math.asin(ff(f)))
|
return F32(math.asin(f))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return NaN
|
return NaN
|
||||||
|
|
||||||
def llAtan2(y, x):
|
def llAtan2(y, x):
|
||||||
assert isfloat(y)
|
assert isfloat(y)
|
||||||
assert isfloat(x)
|
assert isfloat(x)
|
||||||
return F32(math.atan2(ff(y), ff(x)))
|
return F32(math.atan2(y, x))
|
||||||
|
|
||||||
def llAxes2Rot(fwd, left, up):
|
def llAxes2Rot(fwd, left, up):
|
||||||
assert isvector(fwd)
|
assert isvector(fwd)
|
||||||
assert isvector(left)
|
assert isvector(left)
|
||||||
assert isvector(up)
|
assert isvector(up)
|
||||||
|
|
||||||
fwd = v2f(fwd)
|
|
||||||
left = v2f(left)
|
|
||||||
up = v2f(up)
|
|
||||||
|
|
||||||
# One of the hardest.
|
# One of the hardest.
|
||||||
|
|
||||||
t = math.fsum((fwd[0], left[1], up[2]))
|
t = math.fsum((fwd[0], left[1], up[2]))
|
||||||
|
@ -877,8 +883,8 @@ def llAxisAngle2Rot(axis, angle):
|
||||||
axis = llVecNorm(axis, f32=False)
|
axis = llVecNorm(axis, f32=False)
|
||||||
if axis == ZERO_VECTOR:
|
if axis == ZERO_VECTOR:
|
||||||
angle = 0.
|
angle = 0.
|
||||||
c = math.cos(ff(angle)*0.5)
|
c = math.cos(angle*0.5)
|
||||||
s = math.sin(ff(angle)*0.5)
|
s = math.sin(angle*0.5)
|
||||||
return Quaternion(F32((axis[0]*s, axis[1]*s, axis[2]*s, c)))
|
return Quaternion(F32((axis[0]*s, axis[1]*s, axis[2]*s, c)))
|
||||||
|
|
||||||
# NOTE: This one does not always return the same value in LSL. When it isn't
|
# NOTE: This one does not always return the same value in LSL. When it isn't
|
||||||
|
@ -985,14 +991,12 @@ def llCSV2List(s):
|
||||||
|
|
||||||
def llCeil(f):
|
def llCeil(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isnan(f) or math.isinf(f) or f >= 2147483648.0 or f < -2147483648.0:
|
if math.isnan(f) or math.isinf(f) or f >= 2147483648.0 or f < -2147483648.0:
|
||||||
return -2147483648
|
return -2147483648
|
||||||
return int(math.ceil(f))
|
return int(math.ceil(f))
|
||||||
|
|
||||||
def llCos(f):
|
def llCos(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isinf(f):
|
if math.isinf(f):
|
||||||
return Indet
|
return Indet
|
||||||
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
||||||
|
@ -1027,7 +1031,6 @@ def llEscapeURL(s):
|
||||||
|
|
||||||
def llEuler2Rot(v):
|
def llEuler2Rot(v):
|
||||||
assert isvector(v)
|
assert isvector(v)
|
||||||
v = v2f(v)
|
|
||||||
c0 = math.cos(v[0]*0.5)
|
c0 = math.cos(v[0]*0.5)
|
||||||
s0 = math.sin(v[0]*0.5)
|
s0 = math.sin(v[0]*0.5)
|
||||||
c1 = math.cos(v[1]*0.5)
|
c1 = math.cos(v[1]*0.5)
|
||||||
|
@ -1042,11 +1045,10 @@ def llEuler2Rot(v):
|
||||||
|
|
||||||
def llFabs(f):
|
def llFabs(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
return math.fabs(ff(f))
|
return math.fabs(f)
|
||||||
|
|
||||||
def llFloor(f):
|
def llFloor(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isnan(f) or math.isinf(f) or f >= 2147483648.0 or f < -2147483648.0:
|
if math.isnan(f) or math.isinf(f) or f >= 2147483648.0 or f < -2147483648.0:
|
||||||
return -2147483648
|
return -2147483648
|
||||||
return int(math.floor(f))
|
return int(math.floor(f))
|
||||||
|
@ -1069,7 +1071,7 @@ if lslcommon.IsCalc:
|
||||||
def llGenerateKey():
|
def llGenerateKey():
|
||||||
s = hashlib.md5((u'%.17g %f %f' % (time.time(), random.random(),
|
s = hashlib.md5((u'%.17g %f %f' % (time.time(), random.random(),
|
||||||
random.random())).encode('utf8')
|
random.random())).encode('utf8')
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
return Key(s[:8] + '-' + s[8:12] + '-' + s[12:16] + '-' + s[16:20]
|
return Key(s[:8] + '-' + s[8:12] + '-' + s[12:16] + '-' + s[16:20]
|
||||||
+ '-' + s[20:32])
|
+ '-' + s[20:32])
|
||||||
|
|
||||||
|
@ -1416,14 +1418,12 @@ def llListStatistics(op, lst):
|
||||||
|
|
||||||
def llLog(f):
|
def llLog(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isinf(f) and f < 0 or math.isnan(f) or f <= 0.0:
|
if math.isinf(f) and f < 0 or math.isnan(f) or f <= 0.0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return F32(math.log(f))
|
return F32(math.log(f))
|
||||||
|
|
||||||
def llLog10(f):
|
def llLog10(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isinf(f) and f < 0 or math.isnan(f) or f <= 0.0:
|
if math.isinf(f) and f < 0 or math.isnan(f) or f <= 0.0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return F32(math.log10(f))
|
return F32(math.log10(f))
|
||||||
|
@ -1489,8 +1489,6 @@ def llParseStringKeepNulls(s, exc, inc):
|
||||||
def llPow(base, exp):
|
def llPow(base, exp):
|
||||||
assert isfloat(base)
|
assert isfloat(base)
|
||||||
assert isfloat(exp)
|
assert isfloat(exp)
|
||||||
base = ff(base)
|
|
||||||
exp = ff(exp)
|
|
||||||
try:
|
try:
|
||||||
# Python corner cases and LSL corner cases differ
|
# Python corner cases and LSL corner cases differ
|
||||||
|
|
||||||
|
@ -1521,20 +1519,17 @@ def llPow(base, exp):
|
||||||
def llRot2Angle(r):
|
def llRot2Angle(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
# Used by llAngleBetween.
|
# Used by llAngleBetween.
|
||||||
r = q2f(r)
|
|
||||||
# Version based on research by Moon Metty, Miranda Umino and Strife Onizuka
|
# Version based on research by Moon Metty, Miranda Umino and Strife Onizuka
|
||||||
return F32(2.*math.atan2(math.sqrt(math.fsum((r[0]*r[0], r[1]*r[1], r[2]*r[2]))), abs(r[3])));
|
return F32(2.*math.atan2(math.sqrt(math.fsum((r[0]*r[0], r[1]*r[1], r[2]*r[2]))), abs(r[3])));
|
||||||
|
|
||||||
def llRot2Axis(r):
|
def llRot2Axis(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
r = q2f(r)
|
|
||||||
if r[3] < 0:
|
if r[3] < 0:
|
||||||
return llVecNorm(Vector((-r[0], -r[1], -r[2])))
|
return llVecNorm(Vector((-r[0], -r[1], -r[2])))
|
||||||
return llVecNorm(Vector((r[0], r[1], r[2])))
|
return llVecNorm(Vector((r[0], r[1], r[2])))
|
||||||
|
|
||||||
def llRot2Euler(r):
|
def llRot2Euler(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
r = q2f(r)
|
|
||||||
|
|
||||||
# Another one of the hardest. The formula for Z angle in the
|
# Another one of the hardest. The formula for Z angle in the
|
||||||
# singularity case was inspired by the viewer code.
|
# singularity case was inspired by the viewer code.
|
||||||
|
@ -1554,7 +1549,6 @@ def llRot2Euler(r):
|
||||||
|
|
||||||
def llRot2Fwd(r):
|
def llRot2Fwd(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
r = q2f(r)
|
|
||||||
v = (1., 0., 0.)
|
v = (1., 0., 0.)
|
||||||
if r == (0., 0., 0., 0.):
|
if r == (0., 0., 0., 0.):
|
||||||
return v
|
return v
|
||||||
|
@ -1562,7 +1556,6 @@ def llRot2Fwd(r):
|
||||||
|
|
||||||
def llRot2Left(r):
|
def llRot2Left(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
r = q2f(r)
|
|
||||||
v = (0., 1., 0.)
|
v = (0., 1., 0.)
|
||||||
if r == (0., 0., 0., 0.):
|
if r == (0., 0., 0., 0.):
|
||||||
return v
|
return v
|
||||||
|
@ -1570,7 +1563,6 @@ def llRot2Left(r):
|
||||||
|
|
||||||
def llRot2Up(r):
|
def llRot2Up(r):
|
||||||
assert isrotation(r)
|
assert isrotation(r)
|
||||||
r = q2f(r)
|
|
||||||
v = (0., 0., 1.)
|
v = (0., 0., 1.)
|
||||||
if r == (0., 0., 0., 0.):
|
if r == (0., 0., 0., 0.):
|
||||||
return v
|
return v
|
||||||
|
@ -1579,8 +1571,6 @@ def llRot2Up(r):
|
||||||
def llRotBetween(v1, v2):
|
def llRotBetween(v1, v2):
|
||||||
assert isvector(v1)
|
assert isvector(v1)
|
||||||
assert isvector(v2)
|
assert isvector(v2)
|
||||||
v1 = v2f(v1)
|
|
||||||
v2 = v2f(v2)
|
|
||||||
|
|
||||||
aabb = math.sqrt(mul(v1, v1, f32=False) * mul(v2, v2, f32=False)) # product of the squared lengths of the arguments
|
aabb = math.sqrt(mul(v1, v1, f32=False) * mul(v2, v2, f32=False)) # product of the squared lengths of the arguments
|
||||||
if aabb == 0.:
|
if aabb == 0.:
|
||||||
|
@ -1628,7 +1618,6 @@ def llRotBetween(v1, v2):
|
||||||
|
|
||||||
def llRound(f):
|
def llRound(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isnan(f) or math.isinf(f) or f >= 2147483647.5 or f < -2147483648.0:
|
if math.isnan(f) or math.isinf(f) or f >= 2147483647.5 or f < -2147483648.0:
|
||||||
return -2147483648
|
return -2147483648
|
||||||
return int(math.floor(f+0.5))
|
return int(math.floor(f+0.5))
|
||||||
|
@ -1639,7 +1628,6 @@ def llSHA1String(s):
|
||||||
|
|
||||||
def llSin(f):
|
def llSin(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isinf(f):
|
if math.isinf(f):
|
||||||
return Indet
|
return Indet
|
||||||
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
||||||
|
@ -1648,7 +1636,6 @@ def llSin(f):
|
||||||
|
|
||||||
def llSqrt(f):
|
def llSqrt(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if f < 0.0:
|
if f < 0.0:
|
||||||
return Indet
|
return Indet
|
||||||
# LSL and Python both produce -0.0 when the input is -0.0.
|
# LSL and Python both produce -0.0 when the input is -0.0.
|
||||||
|
@ -1683,7 +1670,6 @@ def llSubStringIndex(s, pattern):
|
||||||
|
|
||||||
def llTan(f):
|
def llTan(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
f = ff(f)
|
|
||||||
if math.isinf(f):
|
if math.isinf(f):
|
||||||
return Indet
|
return Indet
|
||||||
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
if -9223372036854775808.0 < f < 9223372036854775808.0:
|
||||||
|
@ -1735,8 +1721,6 @@ def llUnescapeURL(s):
|
||||||
def llVecDist(v1, v2):
|
def llVecDist(v1, v2):
|
||||||
assert isvector(v1)
|
assert isvector(v1)
|
||||||
assert isvector(v2)
|
assert isvector(v2)
|
||||||
v1 = v2f(v1)
|
|
||||||
v2 = v2f(v2)
|
|
||||||
# For improved accuracy, do the intermediate calcs as doubles
|
# For improved accuracy, do the intermediate calcs as doubles
|
||||||
vx = v1[0]-v2[0]
|
vx = v1[0]-v2[0]
|
||||||
vy = v1[1]-v2[1]
|
vy = v1[1]-v2[1]
|
||||||
|
@ -1745,12 +1729,10 @@ def llVecDist(v1, v2):
|
||||||
|
|
||||||
def llVecMag(v):
|
def llVecMag(v):
|
||||||
assert isvector(v)
|
assert isvector(v)
|
||||||
v = v2f(v)
|
|
||||||
return F32(math.sqrt(math.fsum((v[0]*v[0], v[1]*v[1], v[2]*v[2]))))
|
return F32(math.sqrt(math.fsum((v[0]*v[0], v[1]*v[1], v[2]*v[2]))))
|
||||||
|
|
||||||
def llVecNorm(v, f32 = True):
|
def llVecNorm(v, f32 = True):
|
||||||
assert isvector(v)
|
assert isvector(v)
|
||||||
v = v2f(v)
|
|
||||||
if v == ZERO_VECTOR:
|
if v == ZERO_VECTOR:
|
||||||
return v
|
return v
|
||||||
f = math.sqrt(math.fsum((v[0]*v[0], v[1]*v[1], v[2]*v[2])))
|
f = math.sqrt(math.fsum((v[0]*v[0], v[1]*v[1], v[2]*v[2])))
|
||||||
|
|
|
@ -150,7 +150,7 @@ def llEdgeOfWorld(v1, v2):
|
||||||
if not lslcommon.IsCalc:
|
if not lslcommon.IsCalc:
|
||||||
def llFrand(f):
|
def llFrand(f):
|
||||||
assert isfloat(f)
|
assert isfloat(f)
|
||||||
if f == 0.:
|
if f == 0:
|
||||||
return 0.
|
return 0.
|
||||||
raise ELSLCantCompute
|
raise ELSLCantCompute
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
# Constant folding and simplification of expressions and statements.
|
# Constant folding and simplification of expressions and statements.
|
||||||
|
|
||||||
|
import lslcommon
|
||||||
import lslfuncs
|
import lslfuncs
|
||||||
import math
|
import math
|
||||||
from lslparse import warning
|
from lslparse import warning
|
||||||
|
@ -930,20 +931,48 @@ class foldconst(object):
|
||||||
if child[idx]['nt'] != 'CONST':
|
if child[idx]['nt'] != 'CONST':
|
||||||
CONSTargs = False
|
CONSTargs = False
|
||||||
|
|
||||||
OptimizeParams(node, self.symtab[0][node['name']])
|
sym = self.symtab[0][node['name']]
|
||||||
if 'Fn' in self.symtab[0][node['name']]:
|
OptimizeParams(node, sym)
|
||||||
|
if 'Fn' in sym:
|
||||||
# Guaranteed to be side-effect free if the children are.
|
# Guaranteed to be side-effect free if the children are.
|
||||||
if SEFargs:
|
if SEFargs:
|
||||||
node['SEF'] = True
|
node['SEF'] = True
|
||||||
if CONSTargs:
|
if CONSTargs:
|
||||||
# Call it
|
# Call it
|
||||||
fn = self.symtab[0][node['name']]['Fn']
|
fn = sym['Fn']
|
||||||
try:
|
try:
|
||||||
|
args = [arg['value'] for arg in child]
|
||||||
|
argtypes = sym['ParamTypes']
|
||||||
|
assert(len(args) == len(argtypes))
|
||||||
|
for argnum in range(len(args)):
|
||||||
|
# Adapt types of params
|
||||||
|
if argtypes[argnum] == 'string':
|
||||||
|
args[argnum] = lslfuncs.fs(args[argnum])
|
||||||
|
elif argtypes[argnum] == 'key':
|
||||||
|
args[argnum] = lslfuncs.fk(args[argnum])
|
||||||
|
elif argtypes[argnum] == 'float':
|
||||||
|
args[argnum] = lslfuncs.ff(args[argnum])
|
||||||
|
elif argtypes[argnum] == 'vector':
|
||||||
|
args[argnum] = lslfuncs.v2f(args[argnum])
|
||||||
|
elif argtypes[argnum] == 'quaternion':
|
||||||
|
args[argnum] = lslfuncs.q2f(args[argnum])
|
||||||
|
elif argtypes[argnum] == 'list':
|
||||||
|
# ensure vectors and quaternions passed to
|
||||||
|
# functions have only float components
|
||||||
|
assert type(args[argnum]) == list
|
||||||
|
# make a shallow copy
|
||||||
|
args[argnum] = args[argnum][:]
|
||||||
|
for i in range(len(args[argnum])):
|
||||||
|
if type(args[argnum][i]) == lslcommon.Quaternion:
|
||||||
|
args[argnum][i] = lslfuncs.q2f(args[argnum][i])
|
||||||
|
elif type(args[argnum][i]) == lslcommon.Vector:
|
||||||
|
args[argnum][i] = lslfuncs.v2f(args[argnum][i])
|
||||||
|
del argtypes
|
||||||
if node['name'][:10] == 'llDetected':
|
if node['name'][:10] == 'llDetected':
|
||||||
value = fn(*tuple(arg['value'] for arg in child),
|
value = fn(*args, event=self.CurEvent)
|
||||||
event=self.CurEvent)
|
|
||||||
else:
|
else:
|
||||||
value = fn(*tuple(arg['value'] for arg in child))
|
value = fn(*args)
|
||||||
|
del args
|
||||||
if not self.foldtabs:
|
if not self.foldtabs:
|
||||||
generatesTabs = (
|
generatesTabs = (
|
||||||
isinstance(value, unicode) and '\t' in value
|
isinstance(value, unicode) and '\t' in value
|
||||||
|
|
|
@ -482,6 +482,28 @@ class Test03_Optimizer(UnitTestCase):
|
||||||
self.assertRaises(EParseAlreadyDefined, self.parser.parse,
|
self.assertRaises(EParseAlreadyDefined, self.parser.parse,
|
||||||
'default { timer() {} timer() {} }')
|
'default { timer() {} timer() {} }')
|
||||||
|
|
||||||
|
p = self.parser.parse('default{timer(){\n'
|
||||||
|
'llLog(3);llLog(3.0);\n'
|
||||||
|
'llStringToBase64((key)"");llGetAgentInfo("");\n'
|
||||||
|
'llStringToBase64("");llGetAgentInfo((key)"");\n'
|
||||||
|
'}}\n'
|
||||||
|
)
|
||||||
|
self.opt.optimize(p, ('optimize','constfold'))
|
||||||
|
out = self.outscript.output(p)
|
||||||
|
self.assertEqual(out, 'default\n'
|
||||||
|
'{\n'
|
||||||
|
' timer()\n'
|
||||||
|
' {\n'
|
||||||
|
' 1.0986123;\n'
|
||||||
|
' 1.0986123;\n'
|
||||||
|
' "";\n'
|
||||||
|
' 0;\n'
|
||||||
|
' "";\n'
|
||||||
|
' 0;\n'
|
||||||
|
' }\n'
|
||||||
|
'}\n'
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.parser.parse('default { timer() { return } }')
|
self.parser.parse('default { timer() { return } }')
|
||||||
# should raise EParseSyntax, so it should never get here
|
# should raise EParseSyntax, so it should never get here
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue