I'm toying with the idea of writing a "bigint" library in 6502 assembler that would handle variable-length integers up to 255 bytes long. Since this would be unit tested using pytest, and to help wrap my mind around the representation issues, I've come up with a function in Python that takes a decmial integer and deposits it into (emulated) memory as its binary representation: as a length byte followed by the bytes of the value from least to most significant. (The 6502 is little-endian.) This could be used, for example, to generate a couple of inputs to test an "add" or "multiply" function.
As background, the M or Machine object here is the emulated CPU
and RAM, with the RAM as an array of unsigned 0-255 integers. It
offers a deposit(addr, list) method to put values into memory,
byte(addr) to retrive a single byte, and bytes(addr, len) to
retrieve multiple bytes. (This is actually a wrapper around a
py65.devices.mpu6502 from py65; all the code is in my 8bitdev
repo if you're curious.)
from testmc.m6502 import Machine, Registers as R, Instructions as I
import pytest
@pytest.fixture
def M():
M = Machine()
M.load('.build/obj/objects')
return M
def depint(M, addr, value):
''' Deposit a bigint in locations starting at `addr`.
`addr` contains the length of the following bytes,
which hold the value from LSB to MSB.
'''
next = addr + 1 # Skip length byte; filled in at end
if value >= 0: # Positive number; fill byte by byte
while next == addr+1 or value > 0:
value, byte = divmod(value, 0x100)
M.deposit(next, [byte])
next += 1
if byte >= 0x80: # MSbit = 1; sign in additional byte
M.deposit(next, [0x00])
next += 1
else: # Negative: fill with two's complement values
value = abs(value+1) # two's complement = -(n+1)
while next == addr+1 or value > 0:
value, byte = divmod(value, 0x100)
byte = 0xFF - byte # two's complement
M.deposit(next, [byte])
next += 1
if byte < 0x80: # MSbit = 0; sign in additional byte
M.deposit(next, [0xFF])
next += 1
# Store the length
M.deposit(addr, [next - (addr + 1)])
@pytest.mark.parametrize('value, bytes', [
(0, [0x00]),
(1, [0x01]),
(127, [0x7F]),
(128, [0x80, 0x00]),
(255, [0xFF, 0x00]),
(256, [0x00, 0x01]),
(0x40123456, [0x56, 0x34, 0x12, 0x40]),
(0xC0123456, [0x56, 0x34, 0x12, 0xC0, 0x00]),
(-1, [0xFF]),
(-128, [0x80]),
(-129, [0x7F, 0xFF]),
(-255, [0x01, 0xFF]),
(-256, [0x00, 0xFF]),
(-257, [0xFF, 0xFE]),
(0-0x40123456, [0xFF-0x56+1, 0xFF-0x34, 0xFF-0x12, 0xFF-0x40]),
(0-0xC0123456, [0xFF-0x56+1, 0xFF-0x34, 0xFF-0x12, 0xFF-0xC0, 0xFF]),
])
def test_depint(M, value, bytes):
print('DEPOSIT', value, 'expecting', bytes)
addr = 30000 # arbitrary location for deposit
size = len(bytes) + 2 # length byte + value + guard byte
M.deposit(addr, [222] * size) # 222 ensures any 0s really were written
depint(M, addr, value)
bvalue = M.bytes(addr+1, len(bytes))
assert (len(bytes), bytes, 222) \
== (M.byte(addr), bvalue, M.byte(addr+size-1))
# Test against Python's conversion
assert list(value.to_bytes(len(bytes), 'little', signed=True)) \
== bvalue
Things you might consider when reviewing:
- Is it actually correct? Do the tests provide sufficient coverage? (FWIW, in this particular implementation I don't feel the need to test for overflow, since all input values would be fully under my control.)
- Is there a clearer way to describe the tests?
- The
next == addr+1condition in thewhileloops is a bit awkward; is there a better way of handling this? The obvious thing would be to use adoloop (the first time I can recall wanting one in a long time), but Python doesn't have them. - Perhaps it would make more sense just to use Python's
bit_length()andto_bytes()to handle the conversion. - Regarding how useful this is, it's occurred to me that I'll probably
want a reader routine (in 6502 assembler) to do this same ASCII
decmal → bigint conversion, which would mean I could just unit test
it using Python's
to_bytes()and use that routine in unit tests for add, multiply, etc. routines. The only issue might be that it could be a lot slower in 6502 emulation than using the native Python library would be.