intops: core integer primitives

This, together with bitops2 and endians2, forms the core primitive
offering for working with integers as the computer sees them.

The focus of intops is to expose a number of common integer operations
typically used to build more complex abstractions such as bigints,
mp-ints etc while having access to the best performance the compiler and
cpu can offer.

There is more to do here, but this provides an outline of what this
module could look like.

Obviously, there are no exceptions or defects around - the point of
these utilities is to stay as close as possible to bare metal. They
could be used to implement such features however (similar to how
`system/integerops` works).
This commit is contained in:
Jacek Sieka 2023-05-09 21:28:35 +02:00
parent 407a598836
commit 391c2e24b1
No known key found for this signature in database
GPG Key ID: A1B09461ABB656B8
3 changed files with 191 additions and 0 deletions

122
stew/intops.nim Normal file
View File

@ -0,0 +1,122 @@
## Core integer primitives suitable as building blocks for higher-level
## functionality such as bigints, saturating integer types etc - where
## applicable, these use compiler builtins - otherwise, they fall back on native
## Nim code that may be less efficient.
##
## In using these functions, it is recommended that you always call the function
## that returns the least information needed - for example, `mulOverflow` may
## be implemented more efficiently than `mulWiden`, meaning that if overflow
## detection is all that is needed, use the former.
# Implementation notes:
#
# * `uintN` is assumed to be wrapping
# * "*Overflow" perform wrapping arithmetic while returning a bool for overflow
# * "*Widen" return full result in multiple words
# * overloads with carry/borrow exposed for chaining limbs
#
# TODO
# * use compiler intrinsics
# * signed ops
# * saturating ops
# * more primitives commonly available on CPU:s / intrinsics (pow / divmod / etc)
func addOverflow*(x, y: SomeUnsignedInt):
tuple[result: SomeUnsignedInt, overflow: bool] =
## Add the two integers using wrapping arithmetic, returning the result and a
## boolean indicating that overflow happened.
##
## When used to construct bigint arithmetic, the overflow flag can be passed
## as carry to the next more significant word.
let r = x + y
(r, r < x)
func addOverflow*(x, y: SomeUnsignedInt, carry: bool):
tuple[result: SomeUnsignedInt, overflow: bool] =
## Add two integers and carry using wrapping arithmetic, returning the
## result and a boolean indicating that overflow happened.
##
## When used to construct bigint arithmetic, the overflow flag can be passed
## as carry to the next more significant word.
let
(a, b) = addOverflow(x, y)
(c, d) = addOverflow(a, typeof(a)(carry))
(c, b or d)
func subOverflow*(x, y: SomeUnsignedInt):
tuple[result: SomeUnsignedInt, overflow: bool] =
## Subtract y and borrow from x using wrapping arithmetic, returning the
## result and a boolean indicating whether overflow happened.
let r = x - y
(r, y > x)
func subOverflow*(x, y: SomeUnsignedInt, borrow: bool):
tuple[result: SomeUnsignedInt, overflow: bool] =
## Subtract y and borrow from x using wrapping arithmetic, returning the
## result and a boolean indicating whether overflow happened.
##
## When used to construct bigint arithmetic, the overflow flag can be passed
## as carry to the next more significant word.
let
(a, b) = subOverflow(x, y)
(c, d) = subOverflow(a, typeof(a)(borrow))
(c, b or d)
func mulWiden*(x, y: uint64): tuple[lo, hi: uint64] =
let
x0 = x and uint32.high
x1 = x shr 32
y0 = y and uint32.high
y1 = y shr 32
p11 = x1 * y1
p01 = x0 * y1
p10 = x1 * y0
p00 = x0 * y0
middle = p10 + (p00 shr 32) + (p01 and uint32.high)
rhi = p11 + (middle shr 32) + (p01 shr 32)
rlo = (middle shl 32) or (p00 and uint32.high)
(rlo, rhi)
func mulWiden*(x, y: uint32): tuple[lo, hi: uint32] =
let r = x.uint64 * y.uint64
(cast[uint32](r and uint32.high), cast[uint32](r shr 32))
func mulWiden*(x, y: uint16): tuple[lo, hi: uint16] =
let r = x.uint32 * y.uint32
(cast[uint16](r and uint16.high), cast[uint16](r shr 16))
func mulWiden*(x, y: uint8): tuple[lo, hi: uint8] =
let r = x.uint16 * y.uint16
(cast[uint8](r and uint8.high), cast[uint8](r shr 8))
func mulWiden*(x, y: uint): tuple[lo, hi: uint] =
## Perform `(x * y)` as if the computiation had been carried out in twice as
## wide a type returning the low and high words.
when sizeof(uint) == sizeof(uint64):
let (a, b) = mulWiden(uint64(x), uint64(y))
else:
let (a, b) = mulWiden(uint32(x), uint64(y))
(uint(a), uint(b))
func mulWiden*(x, y, carry: SomeUnsignedInt): tuple[lo, hi: SomeUnsignedInt] =
## Perform `((x * y) + carry)` as if the computiation had been carried out in
## twice as wide a type returning the low and high words
let
(lo, hi) = mulWiden(x, y)
(a, b) = addOverflow(lo, carry)
# The carry from this overflowing add can be ignored since the result of
# a multiplication always leaves room for adding one more `high`
(c, _) = addOverflow(hi, typeof(hi)(0), b)
(a, c)
func mulOverflow*(x, y: SomeUnsignedInt):
tuple[result: SomeUnsignedInt, overflow: bool] =
## Perform `(x * y)` using wrapping arithmetic, returning the result and a
## boolean indicating that overflow happened.
let
(a, b) = mulWiden(x, y)
(a, b > 0)

View File

@ -25,6 +25,7 @@ import
test_keyed_queue,
test_sorted_set,
test_interval_set,
test_intops,
test_macros,
test_objects,
test_ptrops,

68
tests/test_intops.nim Normal file
View File

@ -0,0 +1,68 @@
import unittest2
import ../stew/intops
template testAddOverflow[T: SomeUnsignedInt]() =
doAssert addOverflow(T.low, T.low) == (T.low, false)
doAssert addOverflow(T.high, T.low) == (T.high, false)
doAssert addOverflow(T.low, T.high) == (T.high, false)
doAssert addOverflow(T.high, T.high) == (T.high - 1, true)
doAssert addOverflow(T.high, T(0), false) == (T.high, false)
doAssert addOverflow(T.high, T(0), true) == (T(0), true)
doAssert addOverflow(T.high, T.high, true) == (T.high, true)
template testSubOverflow[T: SomeUnsignedInt]() =
doAssert subOverflow(T.low, T.low) == (T.low, false)
doAssert subOverflow(T.high, T.low) == (T.high, false)
doAssert subOverflow(T.high, T.high) == (T.low, false)
doAssert subOverflow(T.low, T.high) == (T(1), true)
doAssert subOverflow(T.high, T.high, false) == (T(0), false)
doAssert subOverflow(T.high, T.high, true) == (T.high, true)
template testAddOverflow() =
testAddOverflow[uint8]()
testAddOverflow[uint16]()
testAddOverflow[uint32]()
testAddOverflow[uint64]()
testAddOverflow[uint]()
template testSubOverflow() =
testSubOverflow[uint8]()
testSubOverflow[uint16]()
testSubOverflow[uint32]()
testSubOverflow[uint64]()
testSubOverflow[uint]()
template testMulWiden[T: SomeUnsignedInt]() =
doAssert mulWiden(T.low, T.low) == (T.low, T.low)
doAssert mulWiden(T(2), T(2)) == (T(4), T(0))
doAssert mulWiden(T.high, T(1)) == (T.high, T(0))
doAssert mulWiden(T(1), T.high) == (T.high, T(0))
doAssert mulWiden(T.high, T.high) == (T(1), T.high - 1)
doAssert mulWiden(T.high, T.high, T(0)) == (T(1), T.high - 1)
doAssert mulWiden(T.high, T.high, T.high) == (T(0), T.high)
# TODO testMulOverflow
template testMulWiden() =
testMulWiden[uint8]()
testMulWiden[uint16]()
testMulWiden[uint32]()
testMulWiden[uint64]()
testMulWiden[uint]()
template test() =
testAddOverflow()
testSubOverflow()
testMulWiden()
static: test()
suite "intops":
test "test":
test()