Skip to content

Commit 1f9c277

Browse files
committed
Wrap int operations in int-safe functions
1 parent 90ac9a6 commit 1f9c277

6 files changed

Lines changed: 128 additions & 20 deletions

File tree

src/BigDecimal.php

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Brick\Math\Exception\UnsupportedPlatformException;
1313
use Brick\Math\Internal\CalculatorRegistry;
1414
use Brick\Math\Internal\DecimalHelper;
15+
use Brick\Math\Internal\SafeInt;
1516
use LogicException;
1617
use Override;
1718

@@ -97,7 +98,7 @@ public static function ofUnscaledValue(BigNumber|int|string $value, int $scale =
9798

9899
if ($scale < 0) {
99100
if ($value !== '0') {
100-
$value .= str_repeat('0', -$scale);
101+
$value .= str_repeat('0', SafeInt::neg($scale));
101102
}
102103
$scale = 0;
103104
}
@@ -385,12 +386,14 @@ public function multipliedBy(BigNumber|int|string $that): BigDecimal
385386
return $that;
386387
}
387388

389+
/** @var non-negative-int $scale */
390+
$scale = SafeInt::add($this->scale, $that->scale);
391+
388392
if ($this->isZero() || $that->isZero()) {
389-
return new BigDecimal('0', $this->scale + $that->scale);
393+
return new BigDecimal('0', $scale);
390394
}
391395

392396
$value = CalculatorRegistry::get()->mul($this->value, $that->value);
393-
$scale = $this->scale + $that->scale;
394397

395398
return new BigDecimal($value, $scale);
396399
}
@@ -426,8 +429,8 @@ public function dividedBy(BigNumber|int|string $that, int $scale, RoundingMode $
426429
return $this;
427430
}
428431

429-
$p = $this->valueWithMinScale($that->scale + $scale);
430-
$q = $that->valueWithMinScale($this->scale - $scale);
432+
$p = $this->valueWithMinScale(SafeInt::add($that->scale, $scale));
433+
$q = $that->valueWithMinScale(SafeInt::sub($this->scale, $scale));
431434

432435
$calculator = CalculatorRegistry::get();
433436
$result = $calculator->divRound($p, $q, $roundingMode);
@@ -508,7 +511,10 @@ public function power(int $exponent): BigDecimal
508511
throw InvalidArgumentException::negativeExponent();
509512
}
510513

511-
return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
514+
/** @var non-negative-int $scale */
515+
$scale = SafeInt::mul($this->scale, $exponent);
516+
517+
return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $scale);
512518
}
513519

514520
/**
@@ -658,14 +664,14 @@ public function sqrt(int $scale, RoundingMode $roundingMode = RoundingMode::Unne
658664

659665
if ($inputScale % 2 !== 0) {
660666
$value .= '0';
661-
$inputScale++;
667+
$inputScale = SafeInt::add($inputScale, 1);
662668
}
663669

664670
$calculator = CalculatorRegistry::get();
665671

666672
// Keep one extra digit for rounding.
667-
$intermediateScale = max($scale, intdiv($inputScale, 2)) + 1;
668-
$value .= str_repeat('0', 2 * $intermediateScale - $inputScale);
673+
$intermediateScale = SafeInt::add(max($scale, intdiv($inputScale, 2)), 1);
674+
$value .= str_repeat('0', SafeInt::sub(SafeInt::mul(2, $intermediateScale), $inputScale));
669675

670676
$sqrt = $calculator->sqrt($value);
671677
$isExact = $calculator->mul($sqrt, $sqrt) === $value;
@@ -710,10 +716,13 @@ public function withPointMovedLeft(int $places): BigDecimal
710716
}
711717

712718
if ($places < 0) {
713-
return $this->withPointMovedRight(-$places);
719+
return $this->withPointMovedRight(SafeInt::neg($places));
714720
}
715721

716-
return new BigDecimal($this->value, $this->scale + $places);
722+
/** @var non-negative-int $scale */
723+
$scale = SafeInt::add($this->scale, $places);
724+
725+
return new BigDecimal($this->value, $scale);
717726
}
718727

719728
/**
@@ -730,15 +739,15 @@ public function withPointMovedRight(int $places): BigDecimal
730739
}
731740

732741
if ($places < 0) {
733-
return $this->withPointMovedLeft(-$places);
742+
return $this->withPointMovedLeft(SafeInt::neg($places));
734743
}
735744

736745
$value = $this->value;
737-
$scale = $this->scale - $places;
746+
$scale = SafeInt::sub($this->scale, $places);
738747

739748
if ($scale < 0) {
740749
if ($value !== '0') {
741-
$value .= str_repeat('0', -$scale);
750+
$value .= str_repeat('0', SafeInt::neg($scale));
742751
}
743752
$scale = 0;
744753
}

src/BigInteger.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Brick\Math\Exception\RoundingNecessaryException;
1616
use Brick\Math\Internal\Calculator;
1717
use Brick\Math\Internal\CalculatorRegistry;
18+
use Brick\Math\Internal\SafeInt;
1819
use LogicException;
1920
use Override;
2021
use Throwable;
@@ -985,7 +986,7 @@ public function shiftedLeft(int $bits): BigInteger
985986
}
986987

987988
if ($bits < 0) {
988-
return $this->shiftedRight(-$bits);
989+
return $this->shiftedRight(SafeInt::neg($bits));
989990
}
990991

991992
return $this->multipliedBy(BigInteger::of(2)->power($bits));
@@ -1005,7 +1006,7 @@ public function shiftedRight(int $bits): BigInteger
10051006
}
10061007

10071008
if ($bits < 0) {
1008-
return $this->shiftedLeft(-$bits);
1009+
return $this->shiftedLeft(SafeInt::neg($bits));
10091010
}
10101011

10111012
$operand = BigInteger::of(2)->power($bits);

src/BigNumber.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Brick\Math\Exception\MathException;
1111
use Brick\Math\Exception\NumberFormatException;
1212
use Brick\Math\Exception\RoundingNecessaryException;
13+
use Brick\Math\Internal\SafeInt;
1314
use JsonSerializable;
1415
use Override;
1516
use Stringable;
@@ -632,11 +633,11 @@ private static function _of(BigNumber|int|string $value): BigNumber
632633

633634
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
634635

635-
$scale = strlen($fractional) - $exponent;
636+
$scale = SafeInt::sub(strlen($fractional), $exponent);
636637

637638
if ($scale < 0) {
638639
if ($unscaledValue !== '0') {
639-
$unscaledValue .= str_repeat('0', -$scale);
640+
$unscaledValue .= str_repeat('0', SafeInt::neg($scale));
640641
}
641642
$scale = 0;
642643
}

src/BigRational.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Brick\Math\Exception\MathException;
1010
use Brick\Math\Exception\RoundingNecessaryException;
1111
use Brick\Math\Internal\DecimalHelper;
12+
use Brick\Math\Internal\SafeInt;
1213
use LogicException;
1314
use Override;
1415

@@ -334,7 +335,7 @@ public function power(int $exponent): BigRational
334335
throw DivisionByZeroException::zeroToNegativePower();
335336
}
336337

337-
return $this->reciprocal()->power(-$exponent);
338+
return $this->reciprocal()->power(SafeInt::neg($exponent));
338339
}
339340

340341
return new BigRational(

src/Exception/IntegerOverflowException.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use const PHP_INT_MIN;
1414

1515
/**
16-
* Exception thrown when an integer overflow occurs.
16+
* Exception thrown when a native integer overflow occurs.
1717
*/
1818
final class IntegerOverflowException extends RuntimeException implements MathException
1919
{
@@ -38,4 +38,19 @@ public static function integerOutOfRange(BigInteger $value): self
3838

3939
return new self(sprintf($message, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
4040
}
41+
42+
/**
43+
* @internal
44+
*
45+
* @pure
46+
*/
47+
public static function nativeIntegerOverflow(string $expression): self
48+
{
49+
return new self(sprintf(
50+
'Cannot compute %s because the result is outside the native integer range [%d, %d].',
51+
$expression,
52+
PHP_INT_MIN,
53+
PHP_INT_MAX,
54+
));
55+
}
4156
}

src/Internal/SafeInt.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Math\Internal;
6+
7+
use Brick\Math\Exception\IntegerOverflowException;
8+
9+
use function is_int;
10+
use function sprintf;
11+
12+
use const PHP_INT_MIN;
13+
14+
/**
15+
* Checked helpers for native integer arithmetic.
16+
*
17+
* @internal
18+
*/
19+
final class SafeInt
20+
{
21+
private function __construct()
22+
{
23+
}
24+
25+
/**
26+
* @pure
27+
*/
28+
public static function add(int $a, int $b): int
29+
{
30+
$result = $a + $b;
31+
32+
/** @phpstan-ignore function.alreadyNarrowedType (PHP int overflow promotes to float at runtime.) */
33+
if (! is_int($result)) {
34+
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d + %d', $a, $b));
35+
}
36+
37+
return $result;
38+
}
39+
40+
/**
41+
* @pure
42+
*/
43+
public static function sub(int $a, int $b): int
44+
{
45+
$result = $a - $b;
46+
47+
/** @phpstan-ignore function.alreadyNarrowedType (PHP int overflow promotes to float at runtime.) */
48+
if (! is_int($result)) {
49+
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d - %d', $a, $b));
50+
}
51+
52+
return $result;
53+
}
54+
55+
/**
56+
* @pure
57+
*/
58+
public static function mul(int $a, int $b): int
59+
{
60+
$result = $a * $b;
61+
62+
/** @phpstan-ignore function.alreadyNarrowedType (PHP int overflow promotes to float at runtime.) */
63+
if (! is_int($result)) {
64+
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d * %d', $a, $b));
65+
}
66+
67+
return $result;
68+
}
69+
70+
/**
71+
* @pure
72+
*/
73+
public static function neg(int $value): int
74+
{
75+
if ($value === PHP_INT_MIN) {
76+
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('-(%d)', $value));
77+
}
78+
79+
return -$value;
80+
}
81+
}

0 commit comments

Comments
 (0)