Description
(I apologize in advance for not finding a simpler example/use case for this.)
While running tests on our own fork of micropython, we encountered a bug that crashed the python interpreter (on Windows 32-bit builds only). This bug is also present in the 'stock' 1.10 release of Micropython. Rather than upload our entire test setup, the following (somewhat simpler) example should serve to illustrate the problem. Using the python module listed at the end of this comment, try the following (may need to repeat the last instruction a few times to see the issue):
MicroPython v1.10 on 2019-03-28; win32 version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> from sys import path
>>> path.append(r'<path where flt_hex.py is stored>')
>>> from flt_hex import flt_hex
>>> for idx in range(20000): exec('flt_hex(float({:d}))'.format(idx), locals(), globals())
After one or more iterations of the final step, the following assertion is triggered (followed by shutdown of the micropython.exe process):
>>> for idx in range(20000): exec('flt_hex(float({:d}))'.format(idx), locals(), globals())
Assertion failed: ATB_GET_KIND(block) == AT_HEAD, file <redacted>\micropython\py\gc.c, line 587
Listing for file 'flt_hex.py':
from array import array
from math import isinf, isnan
from sys import byteorder
_IS_DOUBLE = (1e-100 > 0)
def flt_hex(flt):
"""
Mimics the behavior of the 'hex()' float instance method, for platforms where this method
is not implemented.
:param flt: floating-point value to be converted.
:return: hexadecimal string representation of flt.
"""
if not isinstance(flt, float):
raise TypeError('first argument must be of type "float"')
if isnan(flt) or isinf(flt):
result = str(flt)
else:
# Form the string
# s0xc.mm...pe
# where
# s(ign) = '-' if flt is negative else '',
# c(haracteristic) = 1 if flt is normalized else 0,
# each m represent one digit of the fractional part of the significand (the 'mantissa')
# e(xponent) is the power of 2
# Convert to a list of integers (bytes objects are not trivially reversible in
# MicroPython)
bv = list(bytes(array('d' if _IS_DOUBLE else 'f', [flt])))
if byteorder == 'little':
bv = bv[::-1]
bv_len = len(bv) # 8 for double; 4 for single
# From IEEE-754 (1985), float layouts (big endian) are
# 0bseee eeee efff ffff ffff ffff ffff ffff for single precision
# 0bseee eeee eeee ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff for
# double precision
s = '-' if (bv[0] & 0x80) else ''
ee = ((((bv[0] & 0x7F) << 4) + ((bv[1] & 0xF0) >> 4)) if _IS_DOUBLE
else (((bv[0] & 0x7F) << 1) + ((bv[1] & 0x80) >> 7)))
ff = (bv[1] & (0x0F if _IS_DOUBLE else 0x7F)) << (8*(bv_len - 2))
ff += sum((val << (8*(bv_len - 3 - idx))) for idx, val in enumerate(bv[2:]))
if ee == 0:
# Zero or denormalized
characteristic = '0'
if ff:
exponent = '-1022' if _IS_DOUBLE else '-126'
# Since there are 23 bits after the decimal point for single precision, we
# need to shift left by one bit to fit in hex format (the last bit in the
# output should be ignored)
m = '{:=013x}'.format(ff) if _IS_DOUBLE else '{:=06x}'.format(ff << 1)
else:
exponent = '+0'
m = '0'
else:
# Normalized floats
characteristic = '1'
exponent = '{:+d}'.format(ee - (1023 if _IS_DOUBLE else 127))
m = '{:=013x}'.format(ff) if _IS_DOUBLE else '{:=06x}'.format(ff << 1)
result = '{s}0x{characteristic}.{m}p{exponent}'.format(s=s,
characteristic=characteristic,
m=m,
exponent=exponent)
return result