from . import exceptions, inspect
from .interpreter import conversions
from .utility import visitation
import collections.abc, contextlib, itertools, math
[docs]
class Stringifier(object):
'''
Recursively invokes ``show`` on the argument.
Two styles are supported. In 'str' mode, the implementation tries to match
the output to natural Curry syntax. Lists, for instance, are written using
brackets and parentheses are added where needed. In 'repr' mode, the
output more closely reflects the node structure of the graph. Each node
is written in label-successor form within angle brackets.
In both modes, cyclical expressions are displayed by replacing
back-references with ellipses.
Args:
style:
One of 'str' or 'repr'.
Returns:
A string.
'''
def __init__(self, style='str'):
self.memo = collections.Counter()
self.style = str(style)
if self.style not in ['str', 'repr']:
raise ValueError(
'invalid style %r, expected %r or %r' % (self.style, 'str', 'repr')
)
[docs]
@contextlib.contextmanager
def enter_subexpr(self, arg):
status = self.push_subexpr(arg)
try:
yield status
finally:
self.pop_subexpr(arg)
[docs]
def push_subexpr(self, arg):
addr = id(arg)
self.memo[addr] += 1
return self.memo[addr] == 1
[docs]
def pop_subexpr(self, arg):
addr = id(arg)
self.memo[addr] -= 1
[docs]
def stringify(self, arg, outer=False, partial=False):
'''
Converts the argument to a string according to the specified style.
Subclasses are expected to override the ``format`` method to apply
special formatting.
Unless ``outer`` is True, the result will be parenthesized if it contains
spaces and does not begin with a bracket-like character.
If ``partial`` is supplied, the argument treated as a subexpression of a
partial application.
'''
with self.enter_subexpr(arg) as status:
if not status:
return '...'
elif self.style == 'repr':
if hasattr(arg, 'info'):
return '<%s>' % ' '.join(self.flatten(arg))
else:
return self.format(arg, partial=partial)
else:
if inspect.isa_fwd(arg):
target = inspect.fwd_target(arg)
return self.stringify(target, outer)
else:
string = self.format(arg, partial=partial)
if not outer and self.__needparens(arg, string):
return '(%s)' % string
else:
return string
[docs]
def flatten(self, arg):
# Special cases. For certain types, including lists and tuples,
# subexpressions should be treated as outer expressions. For example, we
# write [f a] not [(f a)] and (f a, f b) rather than ((f a), (f b)). For
# partial applications, do not recurse (the to-string routines will fail
# for a partial application of ':').
# if inspect.isa_operator_name(arg.info.name) and self.style != 'repr':
if arg.info.is_operator and self.style != 'repr':
# This could be useful, but needs to be controlled.
#
# if arg.info.arity == 2:
# # Assume binary operators are infix. ICurry does not have this
# # information.
# yield self.stringify(arg.successor(0))
# yield arg.info.name
# yield self.stringify(arg.successor(1))
# return
# else:
yield '(' + arg.info.name + ')'
else:
yield arg.info.name
treat_subexpr_as_outer = inspect.isa_list(arg) or inspect.isa_tuple(arg)
for succ in arg.successors:
yield self.stringify(
succ, outer=treat_subexpr_as_outer, partial=arg.info.is_partial
)
@staticmethod
def __needparens(arg, string):
assert not inspect.isa_fwd(arg)
if inspect.isa_list(arg) or inspect.isa_tuple(arg):
return False
elif any(string.startswith(q) and string.endswith(q) for q in '"\''):
return False
else:
return ' ' in string
SYMBOLIC_ESCAPES = {
ord('\a'): '\\a'
, ord('\b'): '\\b'
, ord('\f'): '\\f'
, ord('\n'): '\\n'
, ord('\r'): '\\r'
, ord('\t'): '\\t'
, ord('\v'): '\\v'
, ord('"') : '\\"'
, ord('\\'): '\\\\'
, ord('\''): '\\\''
}
[docs]
def show_float(value):
'''
The text of a Float as PAKCS writes it, so that both backends print one
text (issue #34).
The digits are the shortest that read back to the same value, the digits
of ``repr``. The point stands in the digits when the value is at least
1.0e-4 and either below 1.0e15 or not integral; otherwise the text is
d.ddde<exp>, where the exponent carries its sign and no padding, and a
lone digit gets ".0": 0.0001, 1.0e-5, 10000000.0, 1.0e+15, 1.0e+22. An
integral value ends in ".0". The infinities and NaN, which PAKCS cannot
make, are written Infinity, -Infinity, and NaN, as KiCS2 writes them.
The C++ runtime has the same rule in graph/show.cpp.
'''
if math.isnan(value):
return 'NaN'
sign = '-' if math.copysign(1.0, value) < 0 else ''
value = abs(value)
if math.isinf(value):
return sign + 'Infinity'
digits, decpt = _float_digits(value)
n = len(digits)
if digits == '0':
text = '0.0'
elif decpt <= -4 or (decpt > 15 and decpt >= n):
text = '%s.%se%+d' % (digits[0], digits[1:] or '0', decpt - 1)
elif decpt <= 0:
text = '0.' + '0' * -decpt + digits
elif decpt < n:
text = digits[:decpt] + '.' + digits[decpt:]
else:
text = digits + '0' * (decpt - n) + '.0'
return sign + text
def _float_digits(value):
'''
The shortest digits of a non-negative finite float and the position of
its point: value = 0.<digits> * 10**decpt. Zero gives ('0', 0).
'''
mantissa, _, exponent = repr(value).partition('e')
whole, _, fraction = mantissa.partition('.')
digits = whole + fraction
stripped = digits.lstrip('0')
decpt = len(whole) - (len(digits) - len(stripped)) + int(exponent or 0)
digits = stripped.rstrip('0')
return (digits, decpt) if digits else ('0', 0)
[docs]
def escape_char(char, quote):
'''
Writes one character as it appears in a literal delimited by ``quote``.
Printable ASCII is written as is. Every other code point is written as a
decimal escape of at least two digits, as PAKCS writes it: show '\\228' is
"'\\\\228'".
'''
codepoint = ord(char)
if char == quote or char == '\\':
return '\\' + char
elif char in '"\'':
return char
elif codepoint in SYMBOLIC_ESCAPES:
return SYMBOLIC_ESCAPES[codepoint]
elif 0x20 <= codepoint < 0x7f:
return char
else:
return '\\%02d' % codepoint
[docs]
class ListStringifier(Stringifier):
'''
Formats lists using the best style. Depending on the list type and whether
it is ground, the style will use square brackets, cons symbols, or
double-quotes.
'''
[docs]
class LitNormalStringifier(Stringifier):
'''Represents literals in the usual, human-readable, way.'''
@visitation.dispatch.on('arg')
def format(self, arg, **kwds):
return super(LitNormalStringifier, self).format(arg, **kwds)
@format.when(int)
def format(self, lit, **kwds):
return ('(%r)' if lit<0 else '%r') % lit
@format.when(float)
def format(self, lit, **kwds):
return ('(%s)' if lit<0 else '%s') % show_float(lit)
@format.when(str)
def format(self, lit, **kwds):
assert len(lit) == 1
return "'%s'" % escape_char(lit, "'")
[docs]
class LitUnboxedStringifier(Stringifier):
'''Represents unboxed literals by appending a #.'''
@visitation.dispatch.on('arg')
def format(self, arg, **kwds):
return super(LitUnboxedStringifier, self).format(arg, **kwds)
@format.when(int)
def format(self, lit, **kwds):
return '%r#' % lit # e.g., 1# for unboxed integer 1.
@format.when(float)
def format(self, lit, **kwds):
return '%s#' % show_float(lit)
[docs]
class FreeVarStringifier(Stringifier):
'''Represents free variables as _a, _b, etc.'''
def __init__(self, **kwds):
self.i = itertools.count()
self.tr = {}
super(FreeVarStringifier, self).__init__(**kwds)
@staticmethod
def _toalpha(n):
# '_a', '_b', ... '_z', '_aa', '_ab', ...
assert 0 <= n
while True:
yield chr(97 + n % 26)
n = n // 26 - 1
if n < 0:
break
[docs]
class DefaultStringifier(
FreeVarStringifier, ListStringifier, LitNormalStringifier
):
'''
A stringifier for REPL output. This translates free variables into
identifiers _a, _b, etc. and represents literals in the usual,
human-readable, way.
'''
pass
[docs]
def show(arg, stringifier=None, **kwds):
'''
Converts an expression to a string.
Args:
stringifier:
Specifies a callable object used to stringify subexpressions. This can
be used to apply arbitrary translations, e.g., from free variables to
stylized names such as _a, _b, etc.
kwds:
As an alternative to supplying the stringifier, when ``stringifier`` is
None, additional keywords are passed to the constructor of the default
stringifier.
Returns:
The string representation of ``arg``.
'''
stringifier = stringifier or DefaultStringifier(**kwds)
return stringifier.stringify(arg, outer=True)