Non-restoring division in Python
Binary division that skips the restore step by allowing negative partial remainders.
- File
non_restoring_division.py, 122 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/non-restoring-division/. The authors are listed in prov/HPV12R.jsonl.
"""Non-restoring binary division on limb arrays.
Numbers are lists of limbs in radix 2**w (w >= 2), least significant limb
first. The partial remainder R lies in [-V, V) and is held in two's complement
in n + 1 limbs, where n is the number of limbs of the divisor V; its sign is
the top binary digit of the top limb.
"""
def _trim(a: list[int]) -> list[int]:
while a and a[-1] == 0:
a.pop()
return a
def _shift_in(r: list[int], bit: int, w: int) -> None:
"""r <- 2 r + bit modulo 2**(w len(r)), in place."""
mask = (1 << w) - 1
carry = bit
for i in range(len(r)):
t = (r[i] << 1) | carry
carry = t >> w
r[i] = t & mask
def _add_in_place(r: list[int], v: list[int], w: int) -> None:
"""r <- r + v modulo 2**(w len(r))."""
mask = (1 << w) - 1
carry = 0
for i in range(len(r)):
t = r[i] + (v[i] if i < len(v) else 0) + carry
carry = t >> w
r[i] = t & mask
def _sub_in_place(r: list[int], v: list[int], w: int) -> None:
"""r <- r - v modulo 2**(w len(r))."""
mask = (1 << w) - 1
borrow = 0
for i in range(len(r)):
t = r[i] - (v[i] if i < len(v) else 0) - borrow
borrow = 1 if t < 0 else 0
r[i] = t & mask
def _negative(r: list[int], w: int) -> bool:
return (r[-1] >> (w - 1)) & 1 == 1
def bit_length(u: list[int], w: int) -> int:
u = _trim(list(u))
if not u:
return 0
return (len(u) - 1) * w + u[-1].bit_length()
def non_restoring_divide(u: list[int], v: list[int], w: int = 8, stats: dict | None = None) -> tuple[list[int], list[int]]:
"""Quotient and remainder of the limb arrays u and v (v nonzero) in radix 2**w.
Each step shifts in the next binary digit of u and then subtracts v if the
partial remainder is nonnegative, or adds v if it is negative. The quotient
digit is 1 when the new remainder is nonnegative. One final addition makes
the remainder nonnegative. If stats is given, it counts the steps and the
final corrections.
"""
if w < 2:
raise ValueError("w must be at least 2, so that 2V fits in n + 1 limbs with a sign")
v = _trim(list(v))
if not v:
raise ZeroDivisionError("division by zero")
m = bit_length(u, w)
q = [0] * ((m + w - 1) // w)
r = [0] * (len(v) + 1) # @line L1: R <- 0
for i in range(m - 1, -1, -1): # @line L2
was_negative = _negative(r, w)
_shift_in(r, (u[i // w] >> (i % w)) & 1, w)
if not was_negative: # @line L3: R >= 0, so R <- 2R + u_i - V
_sub_in_place(r, v, w)
else: # @line L4: R < 0, so R <- 2R + u_i + V
_add_in_place(r, v, w)
if not _negative(r, w): # @line L5: q_i <- [R >= 0]
q[i // w] |= 1 << (i % w)
corrected = 0
if _negative(r, w): # @line L6: final correction
_add_in_place(r, v, w)
corrected = 1
if stats is not None:
stats["steps"] = stats.get("steps", 0) + m
stats["corrections"] = stats.get("corrections", 0) + corrected
return _trim(q), _trim(r) # @line L7
def signed_digit_quotient(u: int, v: int) -> tuple[list[int], int]:
"""The quotient digits d_i in {-1, +1} of the textbook form, from the top,
and the uncorrected remainder R_0, with u = (sum d_i 2**i) v + R_0."""
m = u.bit_length()
r = 0
digits = []
for i in range(m - 1, -1, -1):
d = 1 if r >= 0 else -1
r = 2 * r + ((u >> i) & 1) - d * v
digits.append(d)
return digits, r
def to_limbs(x: int, w: int) -> list[int]:
out = []
mask = (1 << w) - 1
while x:
out.append(x & mask)
x >>= w
return out
def from_limbs(a: list[int], w: int) -> int:
x = 0
for t in reversed(a):
x = (x << w) | t
return x
test_non_restoring_division.py 75 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/non-restoring-division/. The authors are listed in prov/HPV12R.jsonl.
import random
import pytest
from non_restoring_division import (
from_limbs,
non_restoring_divide,
signed_digit_quotient,
to_limbs,
)
def check(a: int, b: int, w: int) -> None:
q, r = non_restoring_divide(to_limbs(a, w), to_limbs(b, w), w)
assert (from_limbs(q, w), from_limbs(r, w)) == divmod(a, b)
assert not q or q[-1] != 0
assert not r or r[-1] != 0
def test_example() -> None:
# 100 = 14 * 7 + 2: 7 steps, and the last remainder -5 needs the correction.
stats: dict = {}
assert non_restoring_divide([100], [7], 8, stats) == ([14], [2])
assert stats == {"steps": 7, "corrections": 1}
digits, r0 = signed_digit_quotient(100, 7)
assert digits == [1, -1, -1, -1, 1, 1, 1]
assert r0 == -5
assert sum(d << (len(digits) - 1 - k) for k, d in enumerate(digits)) == 15
def test_edges() -> None:
for w in (2, 4, 8):
B = 1 << w
for a in [0, 1, 2, B - 1, B, B + 1, B * B - 1, B**3, B**4 - 1]:
for b in [1, 2, 3, B - 1, B, B + 1, B * B - 1, a if a else 1, a + 1]:
check(a, b, w)
def test_errors() -> None:
with pytest.raises(ZeroDivisionError):
non_restoring_divide([5], [0], 8)
with pytest.raises(ValueError):
non_restoring_divide([1], [1], 1)
def test_exhaustive_small() -> None:
for a in range(256):
for b in range(1, 40):
check(a, b, 4)
def test_random() -> None:
rng = random.Random(1958)
for w in (4, 8):
for _ in range(600):
a = rng.getrandbits(rng.randint(0, 160))
b = rng.getrandbits(rng.randint(1, 100)) or 1
check(a, b, w)
check(a, a // 5 + 1, w)
def test_signed_digit_form() -> None:
rng = random.Random(3)
for _ in range(2000):
a = rng.getrandbits(rng.randint(1, 64))
b = rng.randint(1, 1 << rng.randint(1, 40))
digits, r0 = signed_digit_quotient(a, b)
d = sum(x << (len(digits) - 1 - k) for k, x in enumerate(digits))
assert a == d * b + r0
assert -b <= r0 < b
q, r = divmod(a, b)
assert (q, r) == ((d, r0) if r0 >= 0 else (d - 1, r0 + b))
What the next level needs
- the tests did not run in this build