Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
--file results.json \
--branch '${{ github.head_ref || github.ref_name }}' \
--testbed "${MATRIX_OS}-${PLATFORM}-${CPU}-${NIM_VERSION}" \
--err \
--threshold-measure latency \
--threshold-test t_test \
--threshold-lower-boundary _ \
Expand Down
8 changes: 8 additions & 0 deletions benchmarks/latency/add.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import ../utils
proc runLatencyOverflowing() {.noinline.} =
benchTypesAndImpls(benchLatencyOverflowing, overflowingAdd)

proc runLatencyRaising() {.noinline.} =
benchTypesAndImpls(benchLatencyRaising, raisingAdd)

proc runLatencyWrapping() {.noinline.} =
benchTypesAndImpls(benchLatencyWrapping, wrappingAdd)

proc runLatencySaturating() {.noinline.} =
benchTypesAndImpls(benchLatencySaturating, saturatingAdd)

Expand All @@ -15,5 +21,7 @@ when isMainModule:
echo "\n# Latency, Addition"

runLatencyOverflowing()
runLatencyRaising()
runLatencyWrapping()
runLatencySaturating()
runLatencyCarrying()
8 changes: 8 additions & 0 deletions benchmarks/latency/sub.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import ../utils
proc runLatencyOverflowing() {.noinline.} =
benchTypesAndImpls(benchLatencyOverflowing, overflowingSub)

proc runLatencyRaising() {.noinline.} =
benchTypesAndImpls(benchLatencyRaising, raisingSub)

proc runLatencyWrapping() {.noinline.} =
benchTypesAndImpls(benchLatencyWrapping, wrappingSub)

proc runLatencySaturating() {.noinline.} =
benchTypesAndImpls(benchLatencySaturating, saturatingSub)

Expand All @@ -15,5 +21,7 @@ when isMainModule:
echo "\n# Latency, Subtraction"

runLatencyOverflowing()
runLatencyRaising()
runLatencyWrapping()
runLatencySaturating()
runLatencyCarrying()
8 changes: 8 additions & 0 deletions benchmarks/throughput/add.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import ../utils
proc runThroughputOverflowing() {.noinline.} =
benchTypesAndImpls(benchThroughputOverflowing, overflowingAdd)

proc runThroughputRaising() {.noinline.} =
benchTypesAndImpls(benchThroughputRaising, raisingAdd)

proc runThroughputWrapping() {.noinline.} =
benchTypesAndImpls(benchThroughputWrapping, wrappingAdd)

proc runThroughputSaturating() {.noinline.} =
benchTypesAndImpls(benchThroughputSaturating, saturatingAdd)

Expand All @@ -15,5 +21,7 @@ when isMainModule:
echo "\n# Throughput, Addition"

runThroughputOverflowing()
runThroughputRaising()
runThroughputWrapping()
runThroughputSaturating()
runThroughputCarrying()
8 changes: 8 additions & 0 deletions benchmarks/throughput/sub.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import ../utils
proc runThroughputOverflowing() {.noinline.} =
benchTypesAndImpls(benchThroughputOverflowing, overflowingSub)

proc runThroughputRaising() {.noinline.} =
benchTypesAndImpls(benchThroughputRaising, raisingSub)

proc runThroughputWrapping() {.noinline.} =
benchTypesAndImpls(benchThroughputWrapping, wrappingSub)

proc runThroughputSaturating() {.noinline.} =
benchTypesAndImpls(benchThroughputSaturating, saturatingSub)

Expand All @@ -15,5 +21,7 @@ when isMainModule:
echo "\n# Throughput, Subtraction"

runThroughputOverflowing()
runThroughputRaising()
runThroughputWrapping()
runThroughputSaturating()
runThroughputCarrying()
83 changes: 83 additions & 0 deletions benchmarks/utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,51 @@ template benchLatencyOverflowing*(typ: typedesc, op: untyped) =
doNotOptimize(resFlush)
doNotOptimize(ovfFlush)

template benchLatencyRaising*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

when not compiles op(default(typ), default(typ)):
echo alignLeft(opName, 35), " -"
elif typeof(op(default(typ), default(typ))) isnot typ:
echo alignLeft(opName, 35), " -"
else:
measureLatency(typ, opName):
# Normalize inputs so that the operation never overflows:
# cap `inputsB` to 1/4 of the type range,
# constain `inputsA` between 1/4 and 1/2.
const quarter = (high(typ) shr 2)

for i in 0 ..< bufSize:
inputsB[i] = inputsB[i] and (quarter - 1)

var
flush {.inject.}: typ
currentA {.inject.} = (inputsA[0] and (quarter - 1)) + quarter
do:
let res = op(currentA, inputsB[idx])
currentA = (res and (quarter - 1)) + quarter
flush = currentA
do:
doNotOptimize(flush)

template benchLatencyWrapping*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

when not compiles op(default(typ), default(typ)):
echo alignLeft(opName, 35), " -"
elif typeof(op(default(typ), default(typ))) isnot typ:
echo alignLeft(opName, 35), " -"
else:
measureLatency(typ, opName):
var
currentA {.inject.} = inputsA[0]
flush {.inject.}: typ
do:
currentA = op(currentA, inputsB[idx])
flush = currentA
do:
doNotOptimize(flush)

template benchLatencySaturating*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

Expand Down Expand Up @@ -227,6 +272,44 @@ template benchThroughputOverflowing*(typ: typedesc, op: untyped) =
do:
doNotOptimize(flush)

template benchThroughputRaising*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

when not compiles op(default(typ), default(typ)):
echo alignLeft(opName, 35), " -"
elif typeof(op(default(typ), default(typ))) isnot typ:
echo alignLeft(opName, 35), " -"
else:
measureThroughput(typ, opName):
const quarter = (high(typ) shr 2)

var flush {.inject.}: typ

for i in 0 ..< bufSize:
inputsA[i] = (inputsA[i] and (quarter - 1)) + quarter
inputsB[i] = inputsB[i] and (quarter - 1)
do:
let res = op(inputsA[idx], inputsB[idx])
flush = flush xor res
do:
doNotOptimize(flush)

template benchThroughputWrapping*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

when not compiles op(default(typ), default(typ)):
echo alignLeft(opName, 35), " -"
elif typeof(op(default(typ), default(typ))) isnot typ:
echo alignLeft(opName, 35), " -"
else:
measureThroughput(typ, opName):
var flush {.inject.}: typ
do:
let res = op(inputsA[idx], inputsB[idx])
flush = flush xor res
do:
doNotOptimize(flush)

template benchThroughputSaturating*(typ: typedesc, op: untyped) =
let opName = astToStr(op)

Expand Down
8 changes: 7 additions & 1 deletion book/src/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ intops implements the following operations for signed and unsigned 32- and 64-bi
| | addition | subtraction | multiplication | division | muladd |
| ----------- | -------------------- | -------------------- | ------------------ | -------------------- | ------------------ |
| overflowing | Nim, intrinsics | Nim, intrinsics | | | |
| raising | Nim, intrinsics | Nim, intrinsics | | | |
| wrapping | Nim | Nim | | | |
| saturating | Nim, intrinsics, ASM | Nim, intrinsics, ASM | | | |
| carrying | Nim, intrinsics, C | | | | |
| borrowing | | Nim, intrinsics, C | | | |
| widening | | | Nim, intrinsics, C | | Nim, intrinsics, C |
| narrowing | | | | Nim, intrinsics, ASM | |

_Overflowing_ operations return an explicit `didOverflow` bool flag that tells you if an overflow happened during the operation. These operations wrap for both signed and unsigned integers.
_Overflowing_ operations return an explicit `didOverflow` bool flag that tells you if an overflow happened during the operation. These operations wrap for both signed and unsigned integers. Never raises overflow exceptions.

_Raising_ operations are guaranteed to raise an exception if the operation overflows. This is unlike Nim's builtin `+` and `-` for signed ints, which won't raise if overflow checks are disabled during compilation (e.g. when compiled with `-d:danger`), and unlike `+` and `-` for unsigned ints, which silently wrap without raising.

_Wrapping_ operations are guaranteed to wrap if the operation overflows. This is unlike Nim's builtin `+` and `-` for signed ints, which do not wrap.

_Saturating_ operations do not return an overflow flag but silently return the highest or the lowest available value for a given type if an overflow is to occur.

Expand Down
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- [t]—test suite improvement
- [d]—docs improvement

## 1.0.8 (WIP)
- [+] Ops: Added raising and wrapping flavors to addition and subtraction (#15).

## 1.0.7 (February 2, 2026)

- [f] Ops: composite: Added missing imports that prevented individual import of `intops/ops/composite` (#23).
Expand Down
1 change: 1 addition & 0 deletions intops.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ task test, "Run tests":
echo fmt"# Flags: {flags}"

selfExec fmt"r {flags} tests/tintops.nim"
selfExec fmt"""r {flags} -d:danger tests/tintops.nim "Raising operations::""""

task bench, "Run benchmarks":
var
Expand Down
20 changes: 20 additions & 0 deletions src/intops/impl/intrinsics/gcc.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ when compilerGccCompatible and canUseIntrinsics:

(res, didOverflow)

func raisingAdd*[T: SomeInteger](a, b: T): T {.raises: [OverflowDefect].} =
var res {.noinit.}: T

let didOverflow = builtinOverflowingAdd(a, b, res)

if unlikely(didOverflow):
raise newException(OverflowDefect, "overflow")

res

func saturatingAdd*[T: SomeUnsignedInt](a, b: T): T =
var res {.noinit.}: T

Expand Down Expand Up @@ -70,6 +80,16 @@ when compilerGccCompatible and canUseIntrinsics:

(res, didBorrow)

func raisingSub*[T: SomeInteger](a, b: T): T {.raises: [OverflowDefect].} =
var res {.noinit.}: T

let didOverflow = builtinOverflowingSub(a, b, res)

if unlikely(didOverflow):
raise newException(OverflowDefect, "underflow")

res

func saturatingSub*[T: SomeUnsignedInt](a, b: T): T =
var res {.noinit.}: T

Expand Down
78 changes: 53 additions & 25 deletions src/intops/impl/pure.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,19 @@ func overflowingAdd*[T: SomeSignedInt](a, b: T): (T, bool) =

(res, didOverflow)

func carryingAdd*[T: SomeUnsignedInt](a, b: T, carryIn: bool): (T, bool) =
let
sum = a + b
c1 = sum < a
res = sum + T(carryIn)
c2 = res < sum
func raisingAdd*[T: SomeInteger](a, b: T): T {.raises: [OverflowDefect].} =
let (res, didOverflow) = pure.overflowingAdd(a, b)

(res, c1 or c2)
if unlikely(didOverflow):
raise newException(OverflowDefect, "overflow")

func carryingAdd*[T: SomeSignedInt](a, b: T, carryIn: bool): (T, bool) =
let
(sum1, o1) = pure.overflowingAdd(a, b)
(final, o2) = pure.overflowingAdd(sum1, T(carryIn))
res

(final, o1 or o2)
func wrappingAdd*[T: SomeUnsignedInt](a, b: T): T =
a + b

func wrappingAdd*[T: SomeSignedInt](a, b: T): T =
a +% b

func saturatingAdd*[T: SomeUnsignedInt](a, b: T): T =
let (res, didOverflow) = pure.overflowingAdd(a, b)
Expand All @@ -62,6 +60,22 @@ func saturatingAdd*[T: SomeSignedInt](a, b: T): T =

res

func carryingAdd*[T: SomeUnsignedInt](a, b: T, carryIn: bool): (T, bool) =
let
sum = a + b
c1 = sum < a
res = sum + T(carryIn)
c2 = res < sum

(res, c1 or c2)

func carryingAdd*[T: SomeSignedInt](a, b: T, carryIn: bool): (T, bool) =
let
(sum1, o1) = pure.overflowingAdd(a, b)
(final, o2) = pure.overflowingAdd(sum1, T(carryIn))

(final, o1 or o2)

func overflowingSub*[T: SomeUnsignedInt](a, b: T): (T, bool) =
let
res = a - b
Expand All @@ -71,26 +85,24 @@ func overflowingSub*[T: SomeUnsignedInt](a, b: T): (T, bool) =

func overflowingSub*[T: SomeSignedInt](a, b: T): (T, bool) =
let
res = T(a -% b)
res = a -% b
didOverflow = ((a xor b) < 0) and ((a xor res) < 0)

(res, didOverflow)

func borrowingSub*[T: SomeUnsignedInt](a, b: T, borrowIn: bool): (T, bool) =
let
diff = a - b
b1 = a < b
res = diff - T(borrowIn)
b2 = diff < T(borrowIn)
func raisingSub*[T: SomeInteger](a, b: T): T {.raises: [OverflowDefect].} =
let (res, didOverflow) = pure.overflowingSub(a, b)

(res, b1 or b2)
if didOverflow:
raise newException(OverflowDefect, "underflow")

func borrowingSub*[T: SomeSignedInt](a, b: T, borrowIn: bool): (T, bool) =
let
(diff1, o1) = pure.overflowingSub(a, b)
(final, o2) = pure.overflowingSub(diff1, T(borrowIn))
res

(final, o1 or o2)
func wrappingSub*[T: SomeUnsignedInt](a, b: T): T =
a - b

func wrappingSub*[T: SomeSignedInt](a, b: T): T =
a -% b

func saturatingSub*[T: SomeUnsignedInt](a, b: T): T =
let (res, didBorrow) = pure.overflowingSub(a, b)
Expand All @@ -111,6 +123,22 @@ func saturatingSub*[T: SomeSignedInt](a, b: T): T =

res

func borrowingSub*[T: SomeUnsignedInt](a, b: T, borrowIn: bool): (T, bool) =
let
diff = a - b
b1 = a < b
res = diff - T(borrowIn)
b2 = diff < T(borrowIn)

(res, b1 or b2)

func borrowingSub*[T: SomeSignedInt](a, b: T, borrowIn: bool): (T, bool) =
let
(diff1, o1) = pure.overflowingSub(a, b)
(final, o2) = pure.overflowingSub(diff1, T(borrowIn))

(final, o1 or o2)

func wideningMul*(a, b: uint64): (uint64, uint64) =
const halfMask = 0xFFFFFFFF'u64

Expand Down
Loading