Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- // Notable future direction for the math stuff:
- // Int64 turned into IntNN and began supporting both 32-bit and 64-bit operations.
- // Int128 doesn't really have an analogous transform, but it should be possible
- // to create operations for that too...
- // But the thing that REALLY needs to happen is the creation of 3 bins:
- // functions that operate on (algebraically) atomic values, ex: cmp($a,$b)
- // functions that operate on values represented in two parts, ex: cmp($ah,$al, $bh,$bl)
- // functions that operate on values represented in _four_ parts, ex: cmp($a3,$a2,$a1,$a0, $b3,$b2,$b1,$b0)
- //
- // All of those will be "low level", in a sense:
- // It's hard to know dynamically which one to call, because the argument counts change.
- //
- // So there will need to be abstractions that represent numbers with the correct
- // magnitude of parts, and then select the appropriate functions to operate
- // on those composites. Such an abstraction might be the FixedPoint64 class.
- // (I'm starting to be tempted to call it Rational64, because that's more like
- // what it _really_ is, though FixedPoint64 still might reflect its _usage_ better,
- // hmmmmmmm.)
- // The FixedPoint64 class could represent its internals with as many or as few
- // integers as it wants, and could switch between operations based on the
- // host system's capabilities.
- //
- // I think that ideally, the file would be templated and preprocessed by
- // PHP code into 2 different class files, with each class implementing
- // a different magnitude of integer representation (atomic vs 2-part)
- // and calling the appropriate functions as needed. (We'll still need the
- // 4-part low-level functions, because doing things like multiplying
- // two 2-part numbers will require a 4-part multiplication function!)
- //
- // If that proves too difficult, then we could just always store FixedPoint64's
- // internal representation a 2-parts for each integer. In the 64-bit case,
- // one of them will always be unused. (Though, oddly enough, this would
- // open the possibility of _128-bit_ fixed-point numbers! Albeit, such things
- // would then only work on 64-bit systems and not 32-bit systems, and away
- // we go again... until we implement 8-part functions to support 128-bit
- // numbers on 32-bit systems. We'll be tempted to use the 256-bit math
- // that's now available on 64-bit systems, and then need to implement
- // 16-part functions as a way to make 256-bit math compatible with 32-bit
- // systems. OH NO OWN GOAL (etc)
- // Really, we'll probably just say 64-bit FixedPoint/Rational is good enough
- // for now, and we'll expand to 128-bit ONLY if we have a reason to.
- // But it'll at least be totally doable, with established patterns to guide
- // how it'd be done!)
- //
- /*
- declare(strict_types=1);
- namespace Kickback\Math;
- */
- final class Testing
- {
- /*
- * Apparently PHP devs made it _really_ hard for the `assert` statement to
- * actually halt or report errors when triggered:
- * https://stackoverflow.com/questions/40129963/assert-is-not-working-in-php-so-simple-what-am-i-doing-wrong
- *
- * So here's a version that Just Works.
- *
- * We should probably make sure it turns off in production mode.
- * Right now it just always goes.
- */
- public static function _assert(bool $expr, string $msg = "An assertion was triggered.") : void
- {
- if ( !$expr ) {
- throw new \AssertionError($msg);
- }
- }
- // Prevent instantiation/construction of the (static/constant) class.
- /** @return never */
- private function __construct() {
- throw new \Exception("Instantiation of static class " . get_class($this) . "is not allowed.");
- }
- }
- ?>
- <?php
- // //*
- // /declare(strict_types=1);
- // /
- // /namespace Kickback\Math\LowLevel;
- // /*/
- // /
- // /We probably don't need this anymore, because I (probably) figured out exactly
- // /when PHP converts int64 variables into floats. (It's on signed overflow.)
- // /With that knowledge, we can actually use int64 variables normally.
- // /Running on a 32-bit system might still present some challenges to make
- // /things work correctly, but that's coming along unexpectedly well, to the point
- // /where I might just *make it work* instead of trying to error out if we
- // /don't have the ints that we want.
- // /
- // /final class NumberModel
- // /{
- // / public const INVALID = 0;
- // / public const INT32 = 1;
- // / public const FLOAT64 = 2;
- // / public const INT64_STORAGE_ONLY = 3;
- // / public const INT64_ALL_OPS_WORK = 4;
- // /
- // / public const FIRST_OF_ALL = self::INT32;
- // / public const LAST_OF_ALL = self::INT64_ALL_OPS_WORK;
- // /
- // / public const COUNT_OF_ALL = 1+self::LAST_OF_ALL - self::FIRST_OF_ALL;
- // /
- // / private function is_available(int $model) : bool
- // / {
- // / switch($model)
- // / {
- // / case self::INVALID: return false;
- // / case self::INT32: return true;
- // / case self::FLOAT64: return true; TODO: FIGURE OUT WHEN THIS IS TRUE?!
- // / case self::INT64_STORAGE_ONLY:
- // / $num_int_bits = PHP_INT_SIZE*8;
- // / if ($num_int_bits >= 64) {
- // / return true;
- // / } else {
- // / return false;
- // / }
- // / case self::INT64_ALL_OPS_WORK: return false; // TODO: How to know?
- // / }
- // / // Anything else isn't even a math model, much less available.
- // / return false;
- // / }
- // /
- // / private static function compute_count_available() : int
- // / {
- // / $count = (int)0;
- // / for ($i = self::FIRST_OF_ALL; $i < self::COUNT_OF_ALL; $i++) {
- // / if (self::is_available($i)) {
- // / $count++;
- // / }
- // / }
- // / return $count;
- // / }
- // /
- // / private static int $count_available_ = 0;
- // / public static function count_of_available() : int
- // / {
- // / if ( 0 < self::$count_available_ ) {
- // / return self::$count_available_;
- // / } else {
- // / self::$count_available_ = self::compute_count_available();
- // / return self::$count_available_;
- // / }
- // / }
- // /
- // / public static function to_string(int $model) : string
- // / {
- // / switch($model)
- // / {
- // / self::INT32 : return __CLASS__."INT32";
- // / self::FLOAT64 : return __CLASS__."FLOAT64";
- // / self::INT64_STORAGE_ONLY: return __CLASS__."INT64_STORAGE_ONLY";
- // / self::INT64_ALL_OPS_WORK: return __CLASS__."INT64_ALL_OPS_WORK";
- // / }
- // / }
- // /
- // / // Prevent instantiation/construction of the (static/constant) class.
- // / /** @return never */
- // / private function __construct() {
- // / throw new \Exception("Instantiation of static class " . get_class($this) . "is not allowed.");
- // / }
- // /}
- ?>
- <?php
- /*
- declare(strict_types=1);
- namespace Kickback\Math\LowLevel;
- */
- final class IntNN
- {
- // We want to detect overflow during addition or subtraction in some cases.
- //
- // This function also gives us a chance to ensure that (int+int) addition
- // will always return an `int`, because the `+` operator doesn't actually
- // do that by default in PHP! (It usually will, but not if the result overflows.)
- //
- // In cases where there is a signed integer overflow, PHP will convert the
- // result of the addition into a floating point number. This is not what
- // we want when we expect ordinary twos-complement addition. Though we CAN
- // use this fact, along with the `is_int()` function, to detect all
- // signed overflow.
- //
- // In the case of unsigned integer overflow, we will need to detect it
- // separately using bitwise logic exclusively.
- //
- // The bitwise logic for unsigned overflow detection is as follows:
- //
- // In the below table, `a` and `b` are the inputs. `c` is the result of
- // the addition (after correcting any of PHP's signed-overflow-float-conversion).
- // And `r` is the unsigned carry.
- //
- // To derive our truth table, we observe what happens when we add two
- // 2-bit numbers together to yield a 3-bit number. The most-significant bit (msb)
- // of the 3-bit number will tell us if an overflow occurs.
- //
- // To help us analyse a bit further, we also write a 2nd table to the
- // right of the 1st one, and write down the same expression with only
- // the 2nd bit shown for each variable.
- //
- // a + b = c : a+b = c
- // 00+00 = 000 : 0+0 = 0 -> no carry
- // 00+01 = 001 : 0+0 = 0 -> no carry
- // 00+10 = 010 : 0+1 = 1 -> no carry
- // 00+11 = 011 : 0+1 = 1 -> no carry
- // 01+00 = 001 : 0+0 = 0 -> no carry
- // 01+01 = 010 : 0+0 = 1 -> no carry
- // 01+10 = 011 : 0+1 = 1 -> no carry
- // 01+11 = 100 : 0+1 = 0 -> overflow|carry (unsigned)
- // 10+00 = 010 : 1+0 = 1 -> no carry
- // 10+01 = 011 : 1+0 = 1 -> no carry
- // 10+10 = 100 : 1+1 = 0 -> overflow|carry (unsigned)
- // 10+11 = 101 : 1+1 = 0 -> overflow|carry (unsigned)
- // 11+00 = 011 : 1+0 = 1 -> no carry
- // 11+01 = 100 : 1+0 = 0 -> overflow|carry (unsigned)
- // 11+10 = 101 : 1+1 = 0 -> overflow|carry (unsigned)
- // 11+11 = 110 : 1+1 = 1 -> overflow|carry (unsigned)
- //
- // Notably, the less significant bit of these numbers is inconsequental
- // for unsigned carry detection (as long as we have `c`).
- // We only need to concern ourselves with the most significant bits!
- //
- // If we then eliminate the duplicate rows in the msb-only table,
- // we end up with this:
- //
- // a+b = c
- // 0+0 = 0 -> no carry
- // 0+0 = 1 -> no carry
- // 0+1 = 0 -> overflow|carry (unsigned)
- // 0+1 = 1 -> no carry
- // 1+0 = 0 -> overflow|carry (unsigned)
- // 1+0 = 1 -> no carry
- // 1+1 = 0 -> overflow|carry (unsigned)
- // 1+1 = 1 -> overflow|carry (unsigned)
- //
- // The above generalizes to all integers with larger bit-widths, because
- // we can always use right-bit-shift to discard everything besides the
- // most significant bit.
- //
- // Now we know the relationship between the `a-b-c` msbs, and the
- // carry-or-not status, which we'll call `r` in our truth table.
- //
- // Truth table that we want:
- //
- // : a b c : r :
- // -------------
- // : 0 0 0 : 0 :
- // : 0 0 1 : 0 :
- // : 0 1 0 : 1 :
- // : 0 1 1 : 0 :
- // : 1 0 0 : 1 :
- // : 1 0 1 : 0 :
- // : 1 1 0 : 1 :
- // : 1 1 1 : 1 :
- //
- // Now we'll test expressions in our truth table until we find something
- // that matches `r` by applying bitwise operations to `a`, `b`, and `c`:
- //
- // : a b c : r : ~c : (a|b) : (a&b) : (a|b)&~c : (a&b)|((a|b)&~c) :
- // ----------------------------------------------------------------
- // : 0 0 0 : 0 : 1 : 0 : 0 : 0 : 0 :
- // : 0 0 1 : 0 : 0 : 0 : 0 : 0 : 0 :
- // : 0 1 0 : 1 : 1 : 1 : 0 : 1 : 1 :
- // : 0 1 1 : 0 : 0 : 1 : 0 : 0 : 0 :
- // : 1 0 0 : 1 : 1 : 1 : 0 : 1 : 1 :
- // : 1 0 1 : 0 : 0 : 1 : 0 : 0 : 0 :
- // : 1 1 0 : 1 : 1 : 1 : 1 : 1 : 1 :
- // : 1 1 1 : 1 : 0 : 1 : 1 : 0 : 0 :
- //
- // We have now learned that the expression `(a&b)|((a|b)&~c)` will always
- // predict the correct value for `r`, which is unsigned carry (overflow).
- //
- // This is probably not the most optimal expression, but it may very well
- // be pretty close. We'd need to simplify aggressively to get any better,
- // and it probably won't matter anyways. Most importantly, we can correctly
- // guess the unsigned carry flag after computing an unsigned addition.
- //
- // In the below add and subtract functions, `a`, `b`, and `c` all have
- // the same name, and `r` is represented by the variable `$u_carry`.
- //
- // We can apply the above expression to entire {32|64}-bit integers, and the
- // sign bits in the integers will have the exact same outcomes as in
- // the above truth table.
- //
- // Then we'll need to bit-shift-right by all bits but one
- // (63 bits in the 64-bit case, or 31 bits in the 32-bit case)
- // to get a {0,1} set indicating no carry (0) or carry (1):
- // `$u_carry = (($a&$b)|(($a|$b)&~$c)) >> {31|63};`
- //
- // However, because PHP does not provide an _unsigned_ shift-right,
- // any shift-right on a value with a set sign-bit (sign=1) will result
- // in the variable being filled with 1s.
- //
- // So after the bit-shift, `$u_carry` will either be 0 or it will be 0xFFFF...FFFF.
- // But we want it to be 0 or 1.
- //
- // So we can clear the sign extension bits by ANDing with 1:
- // `$u_carry = (1 & ((($a&$b)|(($a|$b)&~$c)) >> {31|63}));`
- private const NUM_BITS_IN_INT = PHP_INT_SIZE*8;
- private const SIGN_SHIFT = (self::NUM_BITS_IN_INT - 1);
- private const HALF_SHIFT = (self::NUM_BITS_IN_INT >> 1);
- private const HALF_MASK = ((1 << self::HALF_SHIFT)-1);
- public static function add(int $a, int $b) : int
- {
- $s_carry = (int)0;
- $u_carry = (int)0;
- return self::subtract_with_borrow($a, $b, $s_carry, $u_carry);
- }
- public static function add_with_carry(int $a, int $b, int &$s_carry, &$u_carry) : int
- {
- $c = $a + $b;
- if ( is_int($c) ) {
- // Most common branch.
- $u_carry = (1 & ((($a&$b)|(($a|$b)&~$c)) >> self::SIGN_SHIFT));
- $s_carry = 0;
- //echo(sprintf("add_with_carry: a=%016X, b=%016X, c=%016X, (a&b)=%016X, ((a|b)&~c)=%016X, carry=%016X\n", $a, $b, $c, ($a&$b), (($a|$b)&~$c), $u_carry));
- return $c;
- } else {
- // !is_int($c)
- // Uncommon branch: signed overflow.
- $s_carry = 1;
- return self::add_by_parts($a,$b,$u_carry);
- }
- }
- // An earlier (and non-working) version of the above function used
- // the below bit-twiddle expressions, from
- // https://grack.com/blog/2022/12/20/deriving-a-bit-twiddling-hack/
- // to check for overflow. I didn't notice at the time, but these turned
- // out to be for SIGNED overflow detection. Since PHP does that for us
- // (and _to_ us, even), we won't need bit twiddles for the signed case.
- // So we won't be needing these:
- //$carry = ((($c ^ $a) & ($c ^ $b)) >> 63);
- //$carry = ((~($a ^ $b) & ($c ^ $a)) >> 63);
- // `add_by_parts(int a, int b, int u_carry):int`
- //
- // Slow-but-accurate addition function that breaks a 64-bit integer
- // into two 32-bit parts (lo and hi), then adds them together.
- // The result of each addition is 33-bits wide, but that fits just
- // fine into a 64-bit integer. After carry propagation, the (lo and hi)
- // parts are reassembled into the final 64-bit integer.
- //
- // This is potentially more complicated and slow than the "just use `+`"
- // function, but it has the tremendous advantage of never converting
- // the final result into a floating point number with incorrect contents.
- // (PHP will automatically convert the result of integer expressions
- // into a floating point value if the integer expression results in
- // a signed overflow.)
- //
- // Since this function already needs to do carry propagation, it can
- // return the unsigned carry status in the `&$u_carry` parameter
- // as a free action.
- //
- // Signed carry is not as trivial, so it is not performed here.
- // (It's still potentially very "easy", but it should probably be
- // computed by the caller only if it's needed.)
- //
- private static function add_by_parts(int $a, int $b, &$u_carry) : int
- {
- // Shorthand:
- $half_mask = self::HALF_MASK; // =0xFFFF on 32-bit host, and =0xFFFF_FFFF on 64-bit host.
- $half_shift = self::HALF_SHIFT; // =16 when on 32-bit host, and =32 when on 64-bit host.
- // Unpack $a:
- $al = $a & $half_mask; // $al : Low {16|32} bits of $a
- $ah = $a >> $half_shift; // $ah : High {16|32} bits of $a
- $ah &= $half_mask; // Leave space in $ah for carry/overflow detection.
- // Unpack $b:
- $bl = $b & $half_mask; // $bl : Low {16|32} bits of $b
- $bh = $b >> $half_shift; // $bh : High {16|32} bits of $b
- $bh &= $half_mask; // Leave space in $bh for carry/overflow detection.
- // Part-wise addition:
- $cl = $al + $bl; // $cl : Low {16|32} bits of result from $a+$b
- $ch = $ah + $bh; // $ch : High {16|32} bits of result from $a+$b, so far unadjusted for carry propagation.
- // Propagate low-carry into $ch:
- $carry_lo = ($cl >> $half_shift);
- $ch += $carry_lo;
- // Detect unsigned carry from the entire result:
- $carry_hi = ($ch >> $half_shift);
- //echo(sprintf("add_by_parts: ah=%016X, al=%016X, bh=%016X, bl=%016X, ch=%016X, cl=%016X, carry_hi=%016X, carry_lo=%016X\n", $ah,$al, $bh,$bl, $ch,$cl, $carry_hi,$carry_lo));
- // Pack the result back into a single integer:
- $c = ($ch << $half_shift) | ($cl & $half_mask);
- //echo(sprintf("add_by_parts: a=%016X, b=%016X, c=%016X, carry=%016X\n", $a, $b, $c, $carry_hi));
- // Return results:
- $u_carry = $carry_hi;
- return $c;
- }
- public static function unittest_add_with_carry() : void
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- // Shorthand to avoid having to write out long 64-bit hex values.
- $x0000 = (int)0;
- $x0001 = (int)1;
- $x0002 = (int)2;
- $x8000 = PHP_INT_MIN;
- $x8001 = PHP_INT_MIN+1;
- $xFFFF = (int)-1;
- $xFFFE = (int)-2;
- $x7FFF = PHP_INT_MAX;
- $x7FFE = PHP_INT_MAX-1;
- $s_carry = (int)0;
- $u_carry = (int)0;
- Testing::_assert($x0000 === self::add_with_carry($x0000,$x0000,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($x0001 === self::add_with_carry($x0000,$x0001,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($x0001 === self::add_with_carry($x0001,$x0000,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($x0002 === self::add_with_carry($x0001,$x0001,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($x0000 === self::add_with_carry($x0000,$x0000,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($xFFFF === self::add_with_carry($x0000,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($xFFFF === self::add_with_carry($xFFFF,$x0000,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(0 === $u_carry);
- Testing::_assert($xFFFE === self::add_with_carry($xFFFF,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(1 === $u_carry);
- Testing::_assert($x0000 === self::add_with_carry($x0001,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(1 === $u_carry);
- Testing::_assert($x7FFF === self::add_with_carry($xFFFF,$x8000,$s_carry,$u_carry));
- Testing::_assert(1 === $s_carry);
- Testing::_assert(1 === $u_carry); // PHP Math is b0rked?! It says 0xFFFF...FFFF + 0x8000...0000 === 0x8000...0000, which is WRONG. It should return 0x7FFF...FFFF.
- Testing::_assert($x0001 === self::add_with_carry($x0002,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(1 === $u_carry);
- Testing::_assert($x7FFE === self::add_with_carry($xFFFE,$x8000,$s_carry,$u_carry)); Testing::_assert(1 === $s_carry); Testing::_assert(1 === $u_carry); // PHP Math is b0rked?!
- Testing::_assert($x8000 === self::add_with_carry($x8001,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(1 === $u_carry);
- Testing::_assert($x7FFE === self::add_with_carry($x7FFF,$xFFFF,$s_carry,$u_carry)); Testing::_assert(0 === $s_carry); Testing::_assert(1 === $u_carry);
- echo(" done.\n");
- }
- public static function subtract(int $a, int $b) : int
- {
- $s_borrow = (int)0;
- $u_borrow = (int)0;
- return self::subtract_with_borrow($a, $b, $s_borrow, $u_borrow);
- }
- public static function subtract_with_borrow(int $a, int $b, int &$s_borrow, &$u_borrow) : int
- {
- $c = $a - $b;
- //echo(sprintf("a=%016X, b=%016X, c=%016X, (a&b)=%016X, ((a|b)&~c)=%016X, carry=%016X\n", $a, $b, $c, ($a&$b), (($a|$b)&~$c), $carry));
- if ( is_int($c) ) {
- // Most common branch.
- $u_borrow = (1 & ((($a&$b)|(($a|$b)&~$c)) >> self::SIGN_SHIFT)); // TODO: CHECK THIS LOGIC. It might be different for subtraction?
- $s_borrow = 0;
- return $c;
- } else {
- // !is_int($c)
- // Uncommon branch: signed overflow.
- $s_borrow = 1;
- return self::add_by_parts($a,$b,$u_borrow);
- }
- }
- // Pretty much the same thing as `add_by_parts`, except for subtraction.
- private static function subtract_by_parts(int $a, int $b, &$u_borrow) : int
- {
- // Shorthand:
- $half_mask = self::HALF_MASK; // =0xFFFF on 32-bit host, and =0xFFFF_FFFF on 64-bit host.
- $half_shift = self::HALF_SHIFT; // =16 when on 32-bit host, and =32 when on 64-bit host.
- // Unpack $a:
- $al = $a & $half_mask; // $al : Low {16|32} bits of $a
- $ah = $a >> $half_shift; // $ah : High {16|32} bits of $a
- $ah &= $half_mask; // Leave space in $ah for borrow/overflow detection.
- // Unpack $b:
- $bl = $b & $half_mask; // $bl : Low {16|32} bits of $b
- $bh = $b >> $half_shift; // $bh : High {16|32} bits of $b
- $bh &= $half_mask; // Leave space in $bh for borrow/overflow detection.
- // Part-wise subtraction:
- $cl = $al - $bl; // $cl : Low {16|32} bits of result from $a+$b
- $ch = $ah - $bh; // $ch : High {16|32} bits of result from $a+$b, so far unadjusted for borrow propagation.
- // Propagate low-borrow into $ch:
- $borrow_lo = (($cl >> $half_shift) & 1);
- $ch -= $borrow_lo;
- // Detect unsigned borrow from the entire result:
- $borrow_hi = (($ch >> $half_shift) & 1);
- // Pack the result back into a single integer:
- $c = ($ch << $half_shift) | ($cl & $half_mask);
- // Return results:
- $u_borrow = $borrow_hi;
- return $c;
- }
- public static function unittest() : void
- {
- echo("Unittests for ".__CLASS__."\n");
- self::unittest_add_with_carry();
- echo("\n");
- }
- }
- ?>
- <?php
- /*
- declare(strict_types=1);
- namespace Kickback\Math\LowLevel;
- */
- final class NumberModelsIterator implements \Iterator, \Countable
- {
- private int $num_passed = 0;
- private int $which = NumberModel::INVALID;
- private bool $include_unavailable = false;
- public function count() : int {
- if ( $this->include_unavailable ) {
- return NumberModel::COUNT_OF_ALL - $this->num_passed;
- } else {
- return NumberModel::count_of_available() - $this->num_passed;
- }
- }
- public function current() : int {
- return $this->which;
- }
- public function key() : int {
- return $this->num_passed;
- }
- private function scan_until_current_elem_is_available() : void
- {
- if ( $this->include_unavailable ) {
- return;
- }
- while (
- ($this->which < NumberModel::count_of_available()) &&
- (!NumberModel::is_available($this->which)) )
- {
- $this->which++;
- }
- return;
- }
- public function next() : void
- {
- if ($this->which < $this->count())
- {
- $this->num_passed++;
- $this->which++;
- $this->scan_until_current_elem_is_available();
- }
- }
- public function rewind() : void
- {
- $this->num_passed = 0;
- $this->which = NumberModel::FIRST_OF_ALL;
- $this->scan_until_current_elem_is_available();
- }
- public function valid() : bool {
- return (0 < $this->count());
- }
- }
- ?>
- <?php
- // /
- // / This class is almost certainly obsolete now.
- // / Will probably be removed in later revisions.
- // / But hey, it was very useful: its failing unittests taught us about
- // / how PHP will convert integer expression into float values _sometimes_
- // / instead of just using ordinary twos-complement arithmetic.
- // / Yeah... that, uh, matters!
- // /
- // /declare(strict_types=1);
- // /
- // /namespace Kickback\Math\LowLevel;
- // /
- // /final class Logic64
- // /{
- // / // IDEA: The int+int->float problem is probably PHP detecting overflow and
- // / // converting the value to floating point when an overflow occurs.
- // / // If true, then we can do 63-bit math safely. We'll even have efficient
- // / // carry extraction for the 63-bit ints, because PHP may very well
- // / // respect the int+int->int rule when doing such things.
- // / // However, the extra bit will be costly!
- // /
- // / private static \SplFixedArray $math_models_ = new \SplFixedArray();
- // / public static function math_models() : \SplFixedArray
- // / {
- // / if ( 0 < $math_models->count() ) {
- // / return self::$math_models_;
- // / }
- // /
- // / self::$math_models_->append(MATH_MODEL_FLOAT64);
- // / self::$math_models_->append(MATH_MODEL_INT32);
- // /
- // / $num_int_bits = PHP_INT_SIZE*8;
- // / if ($num_int_bits >= 64) {
- // / self::$math_models_->append(MATH_MODEL_INT64_STORAGE_ONLY);
- // / }
- // / return self::$math_models_;
- // / }
- // /
- // / private const MASK_SIGN = (1 << 63);
- // /
- // / public static function add(int $number_model, int $ah, int $al, int $bh, int $bl) : int
- // / {
- // / $carry = (int)0;
- // / return self::add_with_carry($number_model, $ah,$al, $bh,$bl, $carry);
- // / }
- // /
- // / public static function add_with_carry(int $a, int $b, int &$carry) : int
- // / {
- // / $al = $a & 0xFFFF_FFFF;
- // / $ah = $a >> 32;
- // / $ah &= 0xFFFF_FFFF;
- // /
- // / $bl = $b & 0xFFFF_FFFF;
- // / $bh = $b >> 32;
- // / $bh &= 0xFFFF_FFFF;
- // /
- // / $cl = $al + $bl;
- // / $ch = $ah + $bh;
- // /
- // / $carry_lo = (($cl >> 32) & 1);
- // / $ch += $carry_lo;
- // /
- // / $carry_hi = (($ch >> 32) & 1);
- // / $c = ($ch << 32) | ($cl & 0xFFFF_FFFF);
- // /
- // / $carry = $carry_hi;
- // / return $c;
- // / }
- // /
- // / public static function subtract(int $a, int $b) : int
- // / {
- // / $carry = (int)0;
- // / return self::subtract_with_carry($a, $b, $carry);
- // / }
- // /
- // / public static function subtract_with_borrow(int $a, int $b, int &$borrow) : int
- // / {
- // / $al = $a & 0xFFFF_FFFF;
- // / $ah = $a >> 32;
- // / $ah &= 0xFFFF_FFFF;
- // /
- // / $bl = $b & 0xFFFF_FFFF;
- // / $bh = $b >> 32;
- // / $bh &= 0xFFFF_FFFF;
- // /
- // / $cl = $al - $bl;
- // / $ch = $ah - $bh;
- // /
- // / $borrow_lo = (($cl >> 32) & 1);
- // / $ch -= $borrow_lo;
- // /
- // / $borrow_hi = (($ch >> 32) & 1);
- // / $c = ($ch << 32) | ($cl & 0xFFFF_FFFF);
- // /
- // / $borrow = $borrow_hi;
- // / return $c;
- // / }
- // /
- // / public static function unittest_add_with_carry() : void
- // / {
- // / echo(__CLASS__."::".__FUNCTION__."...");
- // /
- // / // Shorthand to avoid having to write out long 64-bit hex values.
- // / $x0000 = (int)0;
- // / $x0001 = (int)1;
- // / $x0002 = (int)2;
- // / $x8000 = PHP_INT_MIN;
- // / $x8001 = PHP_INT_MIN+1;
- // / $xFFFF = (int)-1;
- // / $xFFFE = (int)-2;
- // / $x7FFF = PHP_INT_MAX;
- // / $x7FFE = PHP_INT_MAX-1;
- // /
- // / $carry = (int)0;
- // /
- // / Testing::_assert($x0000 === self::add_with_carry($x0000,$x0000,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($x0001 === self::add_with_carry($x0000,$x0001,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($x0001 === self::add_with_carry($x0001,$x0000,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($x0002 === self::add_with_carry($x0001,$x0001,$carry)); Testing::_assert(0 === $carry);
- // /
- // / Testing::_assert($x0000 === self::add_with_carry($x0000,$x0000,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($xFFFF === self::add_with_carry($x0000,$xFFFF,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($xFFFF === self::add_with_carry($xFFFF,$x0000,$carry)); Testing::_assert(0 === $carry);
- // / Testing::_assert($xFFFE === self::add_with_carry($xFFFF,$xFFFF,$carry)); Testing::_assert(1 === $carry);
- // /
- // / Testing::_assert($x0000 === self::add_with_carry($x0001,$xFFFF,$carry)); Testing::_assert(1 === $carry);
- // / // Testing::_assert($x7FFF === self::add_with_carry($xFFFF,$x8000,$carry)); Testing::_assert(1 === $carry); PHP Math is b0rked?! It says 0xFFFF...FFFF + 0x8000...0000 === 0x8000...0000, which is WRONG. It should return 0x7FFF...FFFF.
- // /
- // / Testing::_assert($x0001 === self::add_with_carry($x0002,$xFFFF,$carry)); Testing::_assert(1 === $carry);
- // / // Testing::_assert($x7FFE === self::add_with_carry($xFFFE,$x8000,$carry)); Testing::_assert(1 === $carry); PHP Math is b0rked?!
- // /
- // / Testing::_assert($x8000 === self::add_with_carry($x8001,$xFFFF,$carry)); Testing::_assert(1 === $carry);
- // / Testing::_assert($x7FFE === self::add_with_carry($x7FFF,$xFFFF,$carry)); Testing::_assert(1 === $carry);
- // /
- // / echo(" done.\n");
- // / }
- // /
- // / public static function ensure_ints_are_64bit() : void
- // / {
- // / $num_int_bits = PHP_INT_SIZE*8;
- // / if ($num_int_bits < 64)
- // / {
- // / throw new SomethingSomethingException(
- // / "This system features ${num_int_bits}-bit integers, " .
- // / "but at least 64-bit wide integers are required for financial calculations.");
- // / }
- // / }
- // /
- // / public static function unittest() : void
- // / {
- // / echo("Unittests for ".__CLASS__."\n");
- // / self::ensure_ints_are_64bit();
- // / self::unittest_add_with_carry();
- // / echo("\n");
- // / }
- // /
- // / // Prevent instantiation/construction of the (static/constant) class.
- // / /// @return never
- // / private function __construct() {
- // / throw new \Exception("Instantiation of static class " . get_class($this) . "is not allowed.");
- // / }
- // /}
- // /*/
- /*
- ?>
- <?php
- //declare(strict_types=1);
- namespace Kickback\Math;
- use \Kickback\Math\Int64;
- */
- final class Int128
- {
- private const MASK_LO = ((1 << 32) - 1);
- private const MASK_SIGN = (1 << 63);
- /*
- // Internal version of `cmp`.
- // This version does not clamp it's outputs to the set {-1, 0, 1}.
- private static function cmp_unsaturated(int $a_hi, int $a_lo, int $b_hi, int $b_lo) : int
- {
- $res = $a_hi - $b_hi;
- if ( $res ) {
- return $res;
- }
- return ($a_lo - $b_lo);
- }
- */
- // Below is a branchless implementation of a comparison algorithm for
- // a comparison operation on 128-bit integers that area each specified
- // as `hi` and `lo` 64-bit integers.
- //
- // This probably doesn't need to be branchless, and I doubt that PHP code
- // will benefit from that very much.
- //
- // I'm mostly just practicing my skill at writing branchless code, because
- // it is a useful skill to have in other places.
- //
- // I'm still going to use the branchless version because (1) it works
- // and (2) it _might_ help somewhere. It's probably the best way to do things.
- // The only disadvantage would be that it just took longer to write.
- //
- // -- Lily Joan 2024-07-11
- //
- //
- // With the personal statement out of the way, here's out it actually works:
- //
- // The code looks like this:
- //
- // $d1 = $a_hi - $b_hi;
- // $d0 = $a_lo - $b_lo;
- //
- // // Set all bits of `$mask` to 1 if p is nonzero.
- // // Clear all bits if p is zero.
- // $mask = ($d1 | -$d1) >> 63;
- //
- // $r = $d1 | ($d0 & ~$mask);
- //
- // $p = $r >> 63;
- // $q = ~($r-1) >> 63
- //
- // return ($p-$q)|$p;
- //
- // =========================================================================
- // ================== $mask = ($d1 | -$d1) >> 63; ==========================
- // The statement `$mask = ($d1 | -$d1) >> 63;` calculates a mask that is all 1s
- // whenever `$d1` is non-zero, and all 0s whenever `$d1` is zero.
- //
- // The first step to understanding this is to realize that the subexpression
- // `($d1 | -$d1)` will, for non-zero inputs, ALWAYS have its sign-bit set.
- // For the input of zero, it will have its sign-bit clear.
- //
- // It does this by exploiting the fact that the number 0 is neither positive nor
- // negative. Here are the possibilities:
- // : $d1 : sign($d1) : -$d1 : sign(-$d1) : sign($d1) | sign(-$d1) :
- // ----------------------------------------------------------------------------
- // : $d1 > 0 : 0 : -$d1 < 0 : 1 : 1 :
- // : $d1 === 0 : 0 : -$d1 === 0 : 0 : 0 :
- // : $d1 < 0 : 1 : -$d1 > 0 : 0 : 1 :
- //
- // Once we have a sign bit that is 1 for all (and only) non-zero inputs,
- // we can shift the value to the right by at least 63 bits to sign-extend the
- // the sign bit to fill the entire variable.
- //
- // Here's another truth table showing what happens for 2-bit integers:
- //
- // : $d1 : 00 : 01 : 10 : 11 :
- // ---------------------------------------------------
- // : -$d1 : 00 : 10 : 10 : 01 :
- // : $d1 | -$d1 : 00 : 11 : 10 : 11 :
- // : ($d1 | -$d1) >> (#bits - 1) : 00 : 11 : 11 : 11 :
- //
- // =========================================================================
- // ================== $r = $d1 | ($d0 & ~$mask); ===========================
- // The statement `$r = $d1 | ($d0 & ~$mask);` selects which "digit" of
- // the 128-bit operands is being used for comparison. It uses information
- // in `$mask` to ensure it picks the high parts if they differ, and
- // picks the low parts if the high parts are equal.
- //
- // The full expression would look more like this:
- // `($d1 & $mask) | ($d0 & ~$mask);`
- //
- // We can simplify it to
- // `$d1 | ($d0 & ~$mask)`
- // by noting that $mask is derived from $d1 and will always be 0 when
- // $d1 is 0. As such, there is no reason to AND the two together,
- // because it will never alter the resulting value.
- //
- // (thought discontinued)
- // Other notes, most about how to derive p and q logic:
- // : p : 00 : 01 : 10 : 11 : 00 : 01 : 10 : 11 : 00 : 01 : 10 : 11 : 00 : 01 : 10 : 11 :
- // : q : 00 : 00 : 00 : 00 : 01 : 01 : 01 : 01 : 10 : 10 : 10 : 10 : 11 : 11 : 11 : 11 :
- // : m : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 :
- // -------------------------------------------------------------------------------------------
- // : p ^ q : 00 : 01 : 10 : 11 : 01 : 00 : 11 : 10 : 10 : 11 : 00 : 01 : 11 : 10 : 01 : 00 :
- //
- //
- // : p | -p : 00 : 11 : 10 : 11 :
- // : >> bits : 00 : 11 : 11 : 11 :
- //
- // $p = $r >> 63
- //
- // >0 =0 <0 min
- // 00 00 11 11 r >> 63 == p
- // //00 11 11 00 r-1 >> 63
- // 11 00 00 11 ~(r-1) >> 63 == q
- // //11 00 11 00 p^q
- // //00 00 00 11 p&q
- // //11 00 11 11 p|q
- // 01 00 11 00 p-q
- // 01 00 11 11 (p-q)|p
- /*
- * Returns -1, 0, or 1 based on a comparison of the given 128-bit integers.
- *
- * Defined such that:
- * * a < b implies cmp(a,b) < 0
- * * a > b implies cmp(a,b) > 0
- * * a == b implies cmp(a,b) == 0.
- */
- public static function cmp(int $a_hi, int $a_lo, int $b_hi, int $b_lo) : int
- {
- // TODO: BUG: Needs work to support integers with parts >PHP_INT_MAX
- // (Thus it currently does not pass unittests.)
- // (We'll probably end up looking at u_carry flags. This is a common
- // technique for assembly/CPUs to detect if one number is larger
- // than another: just subtract them and see if a borrow flag is set!)
- // TODO: Also, we should have signed+unsigned versions of this!
- $d1 = IntNN::subtract($a_hi, $b_hi);
- $d0 = IntNN::subtract($a_lo, $b_lo);
- // Set all bits of `$mask` to 1 if p is nonzero.
- // Clear all bits if p is zero.
- $mask = ($d1 | -$d1) >> 63;
- $r = $d1 | ($d0 & ~$mask);
- $p = $r >> 63;
- $q = ~($r-1) >> 63;
- $r2 = (($p-$q)|$p);
- echo(sprintf("\nd1=%016X, d0=%016X, mask=%016X, r=%016X, p=%016X, q=%016X, r2=%016X", $d1,$d0, $mask, $r, $p, $q, $r2));
- return $r2;
- }
- /*
- public static function cmp(int $a_hi, int $a_lo, int $b_hi, int $b_lo) : int
- {
- // First, we do the comparison with clamping the results to {-1, 0, 1}
- $r = self::cmp_unsaturated($a_hi,$a_lo, $b_hi,$b_lo);
- // Then we clamp the results to {-1, 0, 1} so that we fulfill the function's contract.
- // We'll do this by noting that if we shift the result _right_ by 62 bits
- // then we have 2 bits left with 4 possible states, and 3 of those states
- // are the results we are trying to acquire.
- //
- // The possible values of $r at the start look like this:
- //
- // case # : target values : $r, as a bit pattern : $r >> 62 : $r >> 62, low 2 bits.
- // ----------------------------------------------------------------------------------------------------
- // 1 : all positives : 0b0???_????_..._????_???? : 0b0000_..._000? : 0b0?
- // 2 : zero, 0 : 0b0000_0000_..._0000_0000 : 0b0000_..._0000 : 0b00
- // 3 : all negatives : 0b1???_????_..._????_???? : 0b1111_..._111? : 0b1?
- // 4 : corner case : 0b1000_0000_..._0000_0000 : 0b1111_..._1111 : 0b11
- //
- // But we already have a problem, because cases (1) and (2) aren't always
- // distinct (currently, 0 is _actually_ in case (1)).
- //
- // So we'll subtract by 1 before doing our bitshift, and then this happens:
- //
- // case # : target values : $r-1, as a bit pattern : $r-1 >> 62 : $r-1 >> 62, low 2 bits.
- // ----------------------------------------------------------------------------------
- // 1 : all positives : 0b0???_????_..._????_???? : 0b0000...000? : 0b0?
- // 2 : zero, 0 : 0b1111_1111_..._1111_1111 : 0b1111...1111 : 0b11
- // 3 : all negatives : 0b1???_????_..._????_???? : 0b1111...111? : 0b1?
- // 4 : corner case : 0b0111_1111_..._1111_1111 : 0b0000...0000 : 0b00
- //
- // OLD--------------------
- //
- // case # : target values : $r, as a bit pattern : $r >> 62 : $r >> 62, low 2 bits.
- // ----------------------------------------------------------------------------------------------------
- // 1a : all pos > 1 : 0b0???_????_..._????_???0 : 0b0000_..._0000 : 0b00
- // 1b : all pos > 1 : 0b0???_????_..._????_???1 : 0b0000_..._0001 : 0b01
- // 1.5 : one, 1 : 0b0000_0000_..._0000_0001 : 0b0000_..._0001 : 0b01
- // 2 : zero, 0 : 0b0000_0000_..._0000_0000 : 0b0000_..._0000 : 0b00
- // 3a : all neg > MIN : 0b1???_????_..._????_???0 : 0b1111_..._1110 : 0b10
- // 3b : all neg > MIN : 0b1???_????_..._????_???1 : 0b1111_..._1111 : 0b11
- // 4 : corner case : 0b1000_0000_..._0000_0000 : 0b1111_..._1111 : 0b11
- //
- // But we already have a problem, because cases (1) and (2) aren't always
- // distinct (currently, 0 is _actually_ in case (1)). In the same way,
- // cases (3) and (4) also aren't distinct.
- //
- // So we'll subtract by 1 before doing our bitshift, and then this happens:
- //
- // case # : target values : $r-1, as a bit pattern : $r-1 >> 62 : $r-1 >> 62, low 2 bits.
- // ----------------------------------------------------------------------------------
- // 1a : all pos > 1 : 0b0???_????_..._????_???1 : 0b0000_..._0001 : 0b01
- // 1b : all pos > 1 : 0b0???_????_..._????_???0 : 0b0000_..._0000 : 0b00
- // 1.5 : one, 1 : 0b0000_0000_..._0000_0000 : 0b0000_..._0000 : 0b00
- // 2 : zero, 0 : 0b1111_1111_..._1111_1111 : 0b1111_..._1111 : 0b11
- // 3a : all neg > MIN : 0b1???_????_..._????_???1 : 0b1111_..._1111 : 0b11
- // 3b : all neg > MIN : 0b1???_????_..._????_???0 : 0b1111_..._1110 : 0b10
- // 4 : corner case : 0b1111_1111_..._1111_1111 : 0b1111_..._1111 : 0b11
- // TODO: Did my earlier self fnck up?
- //
- if ( $r === 0x8000_0000_0000_0000 ) {
- return -1;
- }
- $r--; // >0 =0 <0
- $r >>= 62; // {0b000,0b111,0b110} = {0,-1,-2}
- $r++; // {0b001,0b000,0b111} = {1, 0,-1}
- return $r;
- }
- */
- public static function unittest_cmp()
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- Testing::_assert(self::cmp( 0, 0, 0, 0) === 0);
- Testing::_assert(self::cmp( 0, 0, 0, 1) < 0);
- Testing::_assert(self::cmp( 0, 1, 0, 0) > 0);
- Testing::_assert(self::cmp( 0, 1, 0, 1) === 0);
- Testing::_assert(self::cmp(-1, 0, -1, 0) === 0);
- Testing::_assert(self::cmp(-1, 0, 0, 0) < 0);
- Testing::_assert(self::cmp(-1, 0, 1, 0) < 0);
- Testing::_assert(self::cmp( 0, 0, -1, 0) > 0);
- Testing::_assert(self::cmp( 0, 0, 0, 0) === 0);
- Testing::_assert(self::cmp( 0, 0, 1, 0) < 0);
- Testing::_assert(self::cmp( 1, 0, -1, 0) > 0);
- Testing::_assert(self::cmp( 1, 0, 0, 0) > 0);
- Testing::_assert(self::cmp( 1, 0, 1, 0) === 0);
- // Ensure that it's returning values in the set {-1,0,1}, and not in the set {-N,0,N} or somesuch.
- Testing::_assert(self::cmp( 0, 0, 16, 0) === -1);
- Testing::_assert(self::cmp( 16, 0, 0, 0) === 1);
- Testing::_assert(self::cmp(-16, 0, 0, 0) === -1);
- Testing::_assert(self::cmp( 0, 0, -16, 0) === 1);
- // Also test another even value (looking for edge cases).
- Testing::_assert(self::cmp( 0, 0, 0, 0) === 0);
- Testing::_assert(self::cmp( 0, 0, 0, 2) < 0);
- Testing::_assert(self::cmp( 0, 2, 0, 0) > 0);
- Testing::_assert(self::cmp( 0, 2, 0, 2) === 0);
- Testing::_assert(self::cmp(-2, 0, -2, 0) === 0);
- Testing::_assert(self::cmp(-2, 0, 0, 0) < 0);
- Testing::_assert(self::cmp(-2, 0, 2, 0) < 0);
- Testing::_assert(self::cmp( 0, 0, -2, 0) > 0);
- Testing::_assert(self::cmp( 0, 0, 0, 0) === 0);
- Testing::_assert(self::cmp( 0, 0, 2, 0) < 0);
- Testing::_assert(self::cmp( 2, 0, -2, 0) > 0);
- Testing::_assert(self::cmp( 2, 0, 0, 0) > 0);
- Testing::_assert(self::cmp( 2, 0, 2, 0) === 0);
- // Notably, we've carefully avoided negative values in the less-significant parts, so far.
- // That's because the less-significant integers shall be treated as
- // unsigned integers, but PHP only has _signed_ comparison.
- // So we better check for mistakes of that kind!
- $x0000 = (int)0;
- $xFFFF = (int)-1;
- /*
- Testing::_assert(self::cmp($x0000,$x0000, $x0000,$x0000) === 0); // 0 === 0
- Testing::_assert(self::cmp($x0000,$x0000, $x0000,$xFFFF) < 0); // 0 < (2^32 - 1)
- Testing::_assert(self::cmp($x0000,$x0000, $xFFFF,$x0000) > 0); // 0 > -(2^32)
- Testing::_assert(self::cmp($x0000,$x0000, $xFFFF,$xFFFF) > 0); // 0 > -1
- Testing::_assert(self::cmp($x0000,$xFFFF, $x0000,$x0000) > 0); // (2^32 - 1) > 0
- Testing::_assert(self::cmp($x0000,$xFFFF, $x0000,$xFFFF) === 0); // (2^32 - 1) === (2^32 - 1)
- Testing::_assert(self::cmp($x0000,$xFFFF, $xFFFF,$x0000) > 0); // (2^32 - 1) > -(2^32)
- Testing::_assert(self::cmp($x0000,$xFFFF, $xFFFF,$xFFFF) > 0); // (2^32 - 1) > -1
- Testing::_assert(self::cmp($xFFFF,$x0000, $x0000,$x0000) < 0); // -(2^32) < 0
- Testing::_assert(self::cmp($xFFFF,$x0000, $x0000,$xFFFF) < 0); // -(2^32) < (2^32 - 1)
- Testing::_assert(self::cmp($xFFFF,$x0000, $xFFFF,$x0000) === 0); // -(2^32) === -(2^32)
- Testing::_assert(self::cmp($xFFFF,$x0000, $xFFFF,$xFFFF) < 0); // -(2^32) < -1
- Testing::_assert(self::cmp($xFFFF,$xFFFF, $x0000,$x0000) < 0); // -1 < 0
- Testing::_assert(self::cmp($xFFFF,$xFFFF, $x0000,$xFFFF) < 0); // -1 < (2^32 - 1)
- Testing::_assert(self::cmp($xFFFF,$xFFFF, $xFFFF,$x0000) > 0); // -1 > -(2^32)
- Testing::_assert(self::cmp($xFFFF,$xFFFF, $xFFFF,$xFFFF) === 0); // -1 === -1
- */
- echo(" done.\n");
- }
- /*
- * Multiplies two 64-bit integers, resulting in a single 128-bit integer.
- *
- * Because PHP does not have a native 128-bit integer type (or the ability
- * to define structs that can be returned from functions), the returned
- * value is placed into two 64-bit integer reference-type parameters.
- */
- public static function multiply_64x64(int $a, int $b, int &$out_hi, int &$out_lo) : void
- {
- self::multiply_NxN(32, $a, $b, $out_hi,$out_lo);
- }
- /*
- * It is probably better to use `multiply_64x64` in most cases.
- *
- * This is the underlying implementation of `multiply_64x64`, and having
- * it separated out as a different function is useful for testing purposes.
- */
- public static function multiply_NxN(int $digit_bit_width, int $a, int $b, int &$out_hi, int &$out_lo) : void
- {
- Testing::_assert($digit_bit_width <= 32);
- $mask_lo = ((1 << $digit_bit_width) - 1);
- $max_digit = $mask_lo;
- // Reorganize the two 64-bit integers into four 32-bit integers.
- // This helps because now the 32-bit integers will have at least
- // 32 bits of "headroom" for doing multiplications.
- $a_lo = $a & $mask_lo;
- $a_hi = $a >> $digit_bit_width;
- $b_lo = $b & $mask_lo;
- $b_hi = $b >> $digit_bit_width;
- // Multiplies. We need 4 of them for a school-style long multiply.
- // There are faster methods, but this will do for now.
- //
- // Graphically:
- // a_hi a_lo
- // x b_hi b_lo
- // -------------
- // x_hi x_lo
- // y_hi y_lo
- // z_hi z_lo
- // + w_hi w_lo
- // -----------------------
- // ???? ???? ???? ????
- //
- // Note that these multiplications, or at least the ones involving
- // the $*_lo operands, must be _unsigned_ multiplications.
- // However, because these are 32-bit values stored in 64-bit variables,
- // the sign bit will _never_ be set, so it won't matter if we
- // use a 64-bit signed multiplication on them.
- //
- $x = $a_lo * $b_lo;
- $y = $a_lo * $b_hi;
- $z = $a_hi * $b_lo;
- $w = $a_hi * $b_hi;
- // TODO: BUG? If the above multiplies cause a signed overflow, PHP will probably convert the result to a `float`!
- // To make the logic more clear, we will also define the
- // variables corresponding to the {x,y,z,w}_{hi,lo} placeholders.
- $x_lo = $x & $mask_lo;
- $y_lo = $y & $mask_lo;
- $z_lo = $z & $mask_lo;
- $w_lo = $w & $mask_lo;
- $x_hi = $x >> $digit_bit_width;
- $y_hi = $y >> $digit_bit_width;
- $z_hi = $z >> $digit_bit_width;
- $w_hi = $w >> $digit_bit_width;
- // PHP doesn't provide an unsigned right-shift, so we must clear
- // any sign-extended bits in things that were right-shifted.
- $x_hi = $x & $mask_lo;
- $y_hi = $y & $mask_lo;
- $z_hi = $z & $mask_lo;
- $w_hi = $w & $mask_lo;
- // Calculate the bottom row of 32-bit integers.
- //
- // Note that some of these additions might "overflow", but
- // because we are storing our 32-bit integers in 64-bit variables,
- // the carry values will be captured.
- //
- // These will get shuffled into the output integers
- // once the accumulation is done.
- //
- // Graphically:
- // a_hi a_lo
- // x b_hi b_lo
- // -------------
- // x_hi x_lo
- // y_hi y_lo
- // z_hi z_lo
- // + w_hi w_lo
- // -----------------------
- // out3 out2 out1 out0
- //
- $out0 = $x_lo;
- $out1 = $x_hi + $y_lo + $z_lo;
- $out2 = $y_hi + $z_hi + $w_lo;
- $out3 = $w_hi;
- // Perform carry operations.
- // (Beware: order-of-operations is important.)
- if ( $out1 > $max_digit )
- {
- $out2 += ($out1 >> $digit_bit_width);
- $out1 &= $mask_lo;
- }
- if ( $out2 > $max_digit )
- {
- $out3 += ($out2 >> $digit_bit_width);
- $out2 &= $mask_lo;
- }
- // Note that $out3 is involved in an addition, but won't generate
- // a carry-out. It won't overflow, for math reasons.
- // Now we have four proper 32-bit integers (two pairs) with no carry bits,
- // so we can concatenate the pairs to get two 64-bit integers and have our 128-bit result.
- $lo = $out0 | ($out1 << $digit_bit_width);
- $hi = $out2 | ($out3 << $digit_bit_width);
- // Return our results from the function.
- $out_lo = $lo;
- $out_hi = $hi;
- }
- // This is not part of the public API because there is no point.
- // It _should_ do exactly what the `*` operator does.
- // It's just designed to use the `multiply_64x64` function, so that
- // we can see if it gives results identical to `*`, at least whenever
- // the two are given operands that don't overflow.
- private static function multiply_NxN_lo(int $bit_width, int $a, int $b) : int
- {
- $out_lo = (int)0;
- $out_hi = (int)0;
- self::multiply_NxN($bit_width, $a, $b, $out_hi,$out_lo);
- return $out_lo;
- }
- /* TODO: cut?
- // Uses the `multiply_32H32Lx32H32L` function to emulate what
- // `multiply_64x64` would do if it had lower bit-width integers available
- // (ex: `multiply_32x32`).
- //
- // Because this is used for testing purposes, and because we've asserted
- // that we'll always use PHP on 64-bit systems and `int` will always have
- // a width of at least 64 bits, then the result of `multiply_32x32` and
- // smaller will always fit in a 64-bit return value. So we don't need
- // to return the output in reference parameters for this one, which is
- // very helpful for testing.
- private static function multiply_NxN_emul(int $bit_width, int $a, int $b) : int
- {
- Testing::_assert($bit_width <= 32);
- $mask_lo = ((1 << $bit_width) - 1);
- $a_lo = $a & $mask_lo;
- $a_hi = $a >> $bit_width;
- $b_lo = $b & $mask_lo;
- $b_hi = $b >> $bit_width;
- $out_lo = (int)0;
- $out_hi = (int)0;
- self::multiply_32H32Lx32H32L($a_hi,$a_lo, $b_hi,$b_lo, $out_hi,$out_lo);
- $int_max = $mask_lo;
- Testing::_assert($out_hi <= $int_max);
- Testing::_assert($out_lo <= $int_max);
- }
- */
- public static function unittest_multiply_64x64() : void
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- Testing::_assert(0x0000 === self::multiply_NxN_lo(8, 0x00, 0x00));
- Testing::_assert(0x0000 === self::multiply_NxN_lo(8, 0x00, 0x01));
- Testing::_assert(0x0000 === self::multiply_NxN_lo(8, 0x01, 0x00));
- Testing::_assert(0x0000 === self::multiply_NxN_lo(8, 0x00, 0xFF));
- Testing::_assert(0x0000 === self::multiply_NxN_lo(8, 0xFF, 0x00));
- Testing::_assert(0x0001 === self::multiply_NxN_lo(8, 0x01, 0x01));
- Testing::_assert(0x000F === self::multiply_NxN_lo(8, 0x01, 0x0F));
- Testing::_assert(0x000F === self::multiply_NxN_lo(8, 0x0F, 0x01));
- Testing::_assert(0x00E1 === self::multiply_NxN_lo(8, 0x0F, 0x0F));
- Testing::_assert(0x0010 === self::multiply_NxN_lo(8, 0x01, 0x10));
- Testing::_assert(0x0010 === self::multiply_NxN_lo(8, 0x10, 0x01));
- Testing::_assert(0x0100 === self::multiply_NxN_lo(8, 0x10, 0x10));
- Testing::_assert(0x4000 === self::multiply_NxN_lo(8, 0x80, 0x80));
- Testing::_assert(0x3F01 === self::multiply_NxN_lo(8, 0x7F, 0x7F));
- Testing::_assert(0xFC04 === self::multiply_NxN_lo(8, 0xFE, 0xFE));
- Testing::_assert(0xFD02 === self::multiply_NxN_lo(8, 0xFE, 0xFF));
- Testing::_assert(0xFD02 === self::multiply_NxN_lo(8, 0xFF, 0xFE));
- Testing::_assert(0xFE01 === self::multiply_NxN_lo(8, 0xFF, 0xFF));
- Testing::_assert(0x7E81 === self::multiply_NxN_lo(8, 0x7F, 0xFF));
- Testing::_assert(0x7F80 === self::multiply_NxN_lo(8, 0x80, 0xFF));
- Testing::_assert(0x3F80 === self::multiply_NxN_lo(8, 0x80, 0x7F));
- for ( $i = (int)0; $i < 256; $i++ )
- {
- for ( $j = (int)0; $j < 256; $j++ )
- {
- $expected = (int)($i*$j);
- $got = self::multiply_NxN_lo(8, $i, $j);
- Testing::_assert($expected === $got, sprintf("Operands: i=%02x; j=%02x; Expected: %04x; Got: %04X", $i, $j, $expected, $got));
- }
- }
- Testing::_assert(0x0000_0000 === self::multiply_NxN_lo(16, 0x0000, 0x0000));
- Testing::_assert(0x0000_0000 === self::multiply_NxN_lo(16, 0x0000, 0x0001));
- Testing::_assert(0x0000_0000 === self::multiply_NxN_lo(16, 0x0001, 0x0000));
- Testing::_assert(0x0000_0000 === self::multiply_NxN_lo(16, 0x0000, 0xFFFF));
- Testing::_assert(0x0000_0000 === self::multiply_NxN_lo(16, 0xFFFF, 0x0000));
- Testing::_assert(0x0000_0001 === self::multiply_NxN_lo(16, 0x0001, 0x0001));
- Testing::_assert(0x0000_FFFF === self::multiply_NxN_lo(16, 0x0001, 0xFFFF));
- Testing::_assert(0x0000_FFFF === self::multiply_NxN_lo(16, 0xFFFF, 0x0001));
- Testing::_assert(0xFFFE_0001 === self::multiply_NxN_lo(16, 0xFFFF, 0xFFFF));
- Testing::_assert(0XA518_60E1 === self::multiply_NxN_lo(16, 0xF00F, 0xB00F));
- Testing::_assert(0x0000_0000_0000_0000 === self::multiply_NxN_lo(32, 0x0000_0000, 0x0000_0000));
- Testing::_assert(0x0000_0000_0000_0000 === self::multiply_NxN_lo(32, 0x0000_0000, 0x0000_0001));
- Testing::_assert(0x0000_0000_0000_0000 === self::multiply_NxN_lo(32, 0x0000_0001, 0x0000_0000));
- Testing::_assert(0x0000_0000_0000_0000 === self::multiply_NxN_lo(32, 0x0000_0000, 0xFFFF_FFFF));
- Testing::_assert(0x0000_0000_0000_0000 === self::multiply_NxN_lo(32, 0xFFFF_FFFF, 0x0000_0000));
- Testing::_assert(0x0000_0000_0000_0001 === self::multiply_NxN_lo(32, 0x0000_0001, 0x0000_0001));
- Testing::_assert(0x0000_0000_FFFF_FFFF === self::multiply_NxN_lo(32, 0x0000_0001, 0xFFFF_FFFF));
- Testing::_assert(0x0000_0000_FFFF_FFFF === self::multiply_NxN_lo(32, 0xFFFF_FFFF, 0x0000_0001));
- Testing::_assert(0xFFFF_FFFE_0000_0001 === self::multiply_NxN_lo(32, 0xFFFF_FFFF, 0xFFFF_FFFF));
- // Explicit test of 128-bit returning version, and of 64-bit inputs.
- $a64 = (int)0xAceBe11e_CafeBabe;
- $b64 = (int)0xF100fCa7_F1edFa57;
- $out_hi = (int)0;
- $out_lo = (int)0;
- self::multiply_64x64($a64, $b64, $out_hi,$out_lo);
- Testing::_assert(0x8E9C_2945_7ED5_0292 === $out_lo);
- Testing::_assert(0xA2CA_B997_9FFE_C71C === $out_hi);
- echo(" done.\n");
- }
- /**
- * @param int $numerator_hi
- * @param int $numerator_lo
- * @param int $denominator
- * @param-out int $quotient_hi
- * @param-out int $quotient_lo
- * @returns int The remainder.
- */
- public static function divide_128x64(int $numerator_hi, int $numerator_lo, int $denominator, int &$quotient_hi, int &$quotient_lo) : int
- {
- // TODO: Test what PHP does with integer divide-by-zero.
- // In principle, PHP should throw a DivisionByZeroError whenever this
- // happens. In that case, we can just proceed as normal, because
- // the first time division is used in this function, it'll cause
- // that error to be thrown.
- // See: https://www.php.net/manual/en/class.divisionbyzeroerror.php
- // (But I haven't tested to make _sure_ PHP does this. If it doesn't
- // throw an exception, then we should. Propagating weird values
- // through the calculation could result in hard-to-find bugs.)
- // Special-case the value denominator value of `1`, both to make this
- // identity operation be fast, but also to ensure that we don't have
- // to worry about any weird corner cases for denominators that are
- // less than 2. (It can matter, because binary arithmatic.)
- if ( $denominator === 1 ) {
- $quotient_hi = $numerator_hi;
- $quotient_lo = $numerator_lo;
- return 0;
- }
- // Give these predictable values.
- // This will be helpful in debugging if an error happens:
- // you'll always know what these started as!
- $quotient_hi = (int)0;
- $quotient_lo = (int)0;
- // Store the sign bit that the result will have.
- $sign = ($numerator_hi ^ $denominator) & self::MASK_SIGN;
- // Sign extraction, so it doesn't mess with our division.
- $sign = $hi & self::MASK_SIGN;
- $hi &= ~self::MASK_SIGN;
- // Extract remainders.
- //
- // Right now we are mostly interested in $hi_remainder.
- // This will be necessary later for estimating the low portion of the result.
- //
- // The $lo_remainder will simply be returned by the function as-is,
- // since this may help the caller with rounding logic.
- $lo_remainder = $lo % $denominator;
- $hi_remainder = $hi % $denominator;
- // Component-wise division.
- //
- // (We use `intdiv` because `/` might do a check to see if one or both
- // of its operands are floating point numbers, because it is a floating
- // point operation by default. `intdiv`, on the other hand, is explicitly
- // intended for integer division, so it may perform/behave better for this use.)
- $lo = intdiv($lo, $denominator);
- $hi = intdiv($hi, $denominator);
- // Calculate the carry.
- $min_carry = PHP_INT_MAX; // Basically ((2^64 / 2) - 1). We'd prefer (2^64), but can't represent it.
- $min_carry = intdiv($min_carry, $denominator);
- $min_carry <<= 1; // Undo the earlier divide-by-2 in the ((2^64 / 2) - 1) = PHP_INT_MAX.
- // Perform the carry.
- $lo += ($hi_remainder * $min_carry);
- // For safety reasons, we'll also calculate a "max" carry.
- $max_carry_init = (1 << 62);
- $max_carry = intdiv($max_carry_init, $denominator);
- if ( ($denominator * $max_carry) !== $max_carry_init ) {
- // Always round up.
- $max_carry++;
- }
- $max_carry <<= 2;
- $max_carry += 3; // Always round up.
- // Perform the carry.
- $max_lo += ($hi_remainder * $max_carry);
- // This loop takes our approximation and improves it until we have
- // the correct quotient.
- $test_lo = (int)0;
- $test_hi = (int)0;
- $test_carry = (int)0;
- // TODO: BUG? Inspect the below `+=` operators and the IntNN:add_with_carry
- // to ensure that this function will be handling overflow conditions
- // correctly. Right now, it probably _isn't_ (though we could get lucky?).
- self::multiply_64x64($denominator, $lo, $test_hi,$test_lo);
- $test_hi += ($hi * $denominator);
- while ( true )
- {
- $test_lo = IntNN::add_with_carry($test_lo, $denominator, $test_carry);
- $test_hi += $test_carry;
- // if ( test > numerator )
- if ( self::cmp($test_hi, $test_lo, $numerator_hi, $numerator_lo) > 0 ) {
- break;
- }
- // The additional increment of $denominator in the $test variable
- // corresponds to incrementing the $lo value by 1.
- $lo++;
- // This handles the aforementioned "safety reasons".
- // It prevents us from infinite-looping, or from trying to
- // iterate most of a 64-bit integer's possible values
- // when it is already futile to get the correct answer.
- // Ideally, this will never be executed.
- // If the surrounding function works as intended, then the loop
- // will ALWAYS converge before this condition becomes true.
- if ($lo > $max_lo) {
- $class_name = __CLASS__;
- $func_name = __FUNCTION__;
- throw new \Exception(sprintf(
- "Internal error: `$class_name::$func_name` did not converge when it should have. ".
- "Aborting to prevent excessive looping and CPU drain. ".
- "This means that the `$func_name` function is broken and may return incorrect results, ".
- "even when this error isn't thrown. Please report this error. ".
- "Parameters: {numerator_hi:%016X, numerator_lo:%016X, denominator:%016X}",
- $numerator_hi, $numerator_lo, $denominator
- ));
- }
- }
- // Pass results to the caller.
- $quotient_hi = $hi;
- $quotient_lo = $lo;
- return $lo_remainder;
- /*
- TODO: Comment is kinda sorta relevant but also outdated already.
- // This will be multiplied by the $hi_remainder to get the portion
- // of the number that is being carried into the $lo component of the result.
- //
- // Note that this should be `(2^64/$denominator)` EXACTLY.
- // Unfortunately, we can't be exact for two reasons:
- // * Rational numbers (the exact result) can't be represented by finite integers.
- // * The number 2^64 is out of the range of 64-bit integers (one more than the max!)
- //
- // Now we'll get clever, and our function will get slower.
- //
- // We notice that the multiplier doesn't _need_ to be exact.
- // We can make a guess. And the guess can be wrong. As long as it's
- // always wrong in a way that's a little bit low.
- //
- // If it's low, it will result in our end-result being too low.
- // Then we can multiply our guess by the denominator.
- // The result will be less than the numerator.
- // (If we had exact numbers instead of integers, multiplying the quotient
- // by the denominator would result in _exactly_ the numerator.)
- //
- // Then we can just add the $denominator over and over, checking
- // it with a multiplication, until it is too high.
- // Once it is too high, we return the last number that multiplied
- // to a value
- //
- */
- }
- public static function unittest_divide_128x64()
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- // TODO: Oh god this needs testing BADLY.
- echo(" UNIMPLEMENTED! (Please fix!)\n");
- // echo(" done.\n");
- }
- public static function unittest()
- {
- echo("Unittests for ".__CLASS__."\n");
- self::unittest_cmp();
- self::unittest_multiply_64x64();
- self::unittest_divide_128x64();
- echo("\n");
- }
- // Prevent instantiation/construction of the (static/constant) class.
- /** @return never */
- private function __construct() {
- throw new \Exception("Instantiation of static class " . get_class($this) . "is not allowed.");
- }
- }
- /*
- ?>
- <?php
- declare(strict_types=1);
- namespace Kickback\Math;
- */
- // TODO: I forget if it's better to do this, or to use an `enum` type. Whatevs.
- final class RoundingMode
- {
- // Rounding modes.
- // (I think this is all of the possibilities? TODO: Verify.)
- public const ALL_TOWARDS_NEG_INF = 0; // Default; mimics x86 integer truncation. (I think? TODO: Double-check this.)
- public const ALL_TOWARDS_POS_INF = 1;
- public const ALL_TOWARDS_ZERO = 3;
- public const ALL_AWAY_FROM_ZERO = 4;
- public const ALL_TOWARDS_EVEN = 5;
- public const ALL_TOWARDS_ODD = 6;
- public const HALF_TOWARDS_NEG_INF = 7;
- public const HALF_TOWARDS_POS_INF = 8;
- public const HALF_TOWARDS_ZERO = 9; // Similar to PHP_ROUND_HALF_DOWN
- public const HALF_AWAY_FROM_ZERO = 10; // Similar to PHP_ROUND_HALF_UP
- public const HALF_TOWARDS_EVEN = 11; // Similar to PHP_ROUND_HALF_EVEN; bankers' rounding.
- public const HALF_TOWARDS_ODD = 12; // Similar to PHP_ROUND_HALF_ODD
- // This shall mimic x86 integer truncation.
- public const DEFAULT = self::ALL_TOWARDS_NEG_INF;
- public static function rounding_mode_to_string(int $rounding_mode) : string
- {
- switch($rounding_mode)
- {
- case self::ALL_TOWARDS_NEG_INF : return "ALL_TOWARDS_NEG_INF";
- case self::ALL_TOWARDS_POS_INF : return "ALL_TOWARDS_POS_INF";
- case self::ALL_TOWARDS_ZERO : return "ALL_TOWARDS_ZERO";
- case self::ALL_AWAY_FROM_ZERO : return "ALL_AWAY_FROM_ZERO";
- case self::ALL_TOWARDS_EVEN : return "ALL_TOWARDS_EVEN";
- case self::ALL_TOWARDS_ODD : return "ALL_TOWARDS_ODD";
- case self::HALF_TOWARDS_NEG_INF: return "HALF_TOWARDS_NEG_INF";
- case self::HALF_TOWARDS_POS_INF: return "HALF_TOWARDS_POS_INF";
- case self::HALF_TOWARDS_ZERO : return "HALF_TOWARDS_ZERO";
- case self::HALF_AWAY_FROM_ZERO : return "HALF_AWAY_FROM_ZERO";
- case self::HALF_TOWARDS_EVEN : return "HALF_TOWARDS_EVEN";
- case self::HALF_TOWARDS_ODD : return "HALF_TOWARDS_ODD";
- }
- return "Unknown rounding mode ($rounding_mode)";
- }
- // Prevent instantiation/construction of the (static/constant) class.
- /** @return never */
- private function __construct() {
- throw new \Exception("Instantiation of static class " . get_class($this) . "is not allowed.");
- }
- }/*
- ?>
- <?php
- declare(strict_types=1);
- namespace Kickback\Math\Types;
- use \Kickback\Math\Int128;
- use \Kickback\Math\RoundingMode;
- */
- final class FixedPoint64
- {
- private int $numerator;
- private int $divisor;
- private static function banker_round(int $numerator, int $increment) : int
- {
- Testing::_assert($increment > 1);
- // Even if we don't use $even_increment in most cases,
- // we should still assert that it fits in _all_ cases, just to give
- // this function consistent behavior (e.g. whether it asserts or not
- // should depend on `$increment` and not on `$numerator`.)
- //$even_increment = ($increment<<1);
- Testing::_assert(($increment<<1) <= PHP_INT_MAX);
- if ( $numerator >= 0 ) {
- return self::banker_round_unsigned_($numerator, $increment);
- }
- else {
- return self::banker_round_signed_($numerator, $increment);
- }
- }
- private static function banker_round_signed_(int $numerator, int $increment) : never
- {
- // TODO!
- Testing::_assert(false, "Signed rounding is not yet implemented.");
- }
- private static function banker_round_unsigned_(int $numerator, int $increment) : int
- {
- /* TODO: cut?
- // We calculate the $round_down_amount (conventional rounding)
- // from the $round_down_even_amount (bankers' rounding)
- // so that we avoid having to modulus more than once.
- $round_down_even_amount = $numerator % $even_increment;
- $round_down_amount = $round_down_even_amount;
- if ( $round_down_amount > $increment ) {
- $round_down_amount -= $increment;
- }
- */
- // First, attempt conventional rounding (e.g. "round-to-nearest-increment").
- $rounding_amount = (int)0;
- if ( self::get_rounding_amount_($numerator, $increment, $rounding_amount) ) {
- return $rounding_amount;
- } else {
- // If that didn't work, then fall back on the tie breaker.
- return self::banker_round_tie_breaker_($numerator, $increment);
- }
- }
- private static function get_rounding_amount_(int $numerator, int $increment, &$rounding_amount) : bool
- {
- $round_down_amount = $numerator % $increment;
- $round_up_amount = $increment - $round_down_amount;
- if ( $round_down_amount < $round_up_amount ) {
- $rounding_amount = -$round_down_amount;
- return true;
- } else
- if ( $round_down_amount > $round_up_amount ) {
- $rounding_amount = $round_up_amount;
- return true;
- } else {
- // If that didn't work, then there's a problem, and it's probably this tie:
- Testing::_assert($round_down_amount === $round_up_amount);
- $rounding_amount = 0; // TODO: Is this right?
- return false;
- }
- }
- // This is set out into its own function because the tie breaker
- // is unlikely to be executed very frequently. As such, it might
- // be faster (code-execution-wise) to break this out into it's own function.
- //
- // Reasons why this might be the case:
- // * Interpreter is less likely to need to parse/analyse this code when `banker_round` is called.
- // * This is more cache-friendly: the CPU won't need to load the instructions for the tie breaker every time `banker_round` is called.
- // * This makes `banker_round` smaller and therefore more inline-able.
- //
- // I am not sure how much any of those _actually_ matter, and it's not
- // really worth testing at the moment, but this is _usually_ a good way
- // to optimize code, and it seems to have few or no disadvantages.
- private static function banker_round_tie_breaker_(int $numerator, int $increment) : int
- {
- // Now the bankers' rounding comes into play.
- // We break the tie by rounding to the nearest _even_ increment.
- $even_increment = $increment << 1;
- // This involves just doing the rounding again, but with $increment*2.
- $rounding_amount = (int)0;
- Testing::_assert(self::get_rounding_amount_($numerator, $even_increment, $rounding_amount));
- return $rounding_amount;
- }
- // TODO: The above bankers' rounding code is pretty sus. I was very sleepy while writing it. o.O
- private static function unittest_banker_round() : void
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- // TODO: This needs testing if you want to trust any financials done with it.
- echo(" UNIMPLEMENTED! (Please fix!)\n");
- // echo(" done.\n");
- }
- /*
- * Beware: This operation is NOT commutative like normal multiplication.
- * That's because the choice of destination decides which divisor to use
- * when scaling back the oversized numerator that results from the
- * initial multiplication.
- *
- * The `multiply` and `multiply_into` functions have the commutative property.
- *
- * This method will not perform any explicit memory allocations
- * unless an error has occurred.
- */
- public function multiply_by(FixedPoint64 $other, int $rounding_mode = RoundingMode::DEFAULT) : void
- {
- // ASSUMPTION: We wish the output to have the same divisor as $this, not $other (or anything else).
- $this->numerator = self::multiply_raw($this, $other, $this->denominator, $rounding_mode);
- }
- /*
- * Allocates a new FixedPoint64 object, then uses the `multiply_into`
- * function to multiply parameters `$a` and `$b` and store the result
- * into the new FixedPoint64 object, which is then returned.
- *
- * The new FixedPoint64 object will have a $denominator field equal to
- * the `$divisor` parameter.
- *
- * This function could be handy as a quick-and-dirty way to draft code
- * that uses FixedPoint64 arithmetic. It has the disadvantage of doing
- * memory allocation for an operation where it isn't always necessary.
- * For production code, it is recommended to use `multiply_into` or
- * `multiply_by` instead.
- */
- public static function multiply(FixedPoint64 $a, FixedPoint64 $b, int $divisor, int $rounding_mode = RoundingMode::DEFAULT) : FixedPoint64
- {
- $result = new FixedPoint64(0, $divisor);
- return self::multiply_into($a, $b, $result, $rounding_mode);
- }
- /*
- * Multiplies `$fp64a` and `$fp64b` together, then stores the result
- * into the `$destination` object.
- *
- * As a convenience, the `$destination` object is returned.
- *
- * When rounding or truncating the fractional multiplication results,
- * the `$destination` parameter's `$denominator` field is used
- * as a divisor.
- *
- * This operation has commutative property over `$fp64a` and `$fp64b`.
- *
- * This function will not perform any explicit memory allocations
- * unless an error has occurred. (In some cases it is possible for
- * the caller to reuse existing FixedPoint64 objects for the `$destination`
- * parameter, and thus avoid memory allocations entirely.)
- */
- public static function multiply_into(FixedPoint64 $fp64a, FixedPoint64 $fp64b, FixedPoint64 $destination, int $rounding_mode = RoundingMode::DEFAULT) : FixedPoint64
- {
- Testing::_assert(isset($destination));
- $tmp = FixedPoint64::multiply_raw($fp64a, $fp64b, $destination->denominator, $rounding_mode);
- $destination->numerator = $tmp;
- return $destination;
- }
- private static function multiply_raw(FixedPoint64 $fp64a, FixedPoint64 $fp64b, int $divisor, int $rounding_mode = RoundingMode::DEFAULT) : int
- {
- // TODO: Verify that divisors are being chosen correctly.
- // (Actually the below seems to be pretty well argued, now that it's
- // all been written down. Still, we should make sure it all gets tested.)
- // TODO: Make sure unittests cover all of the branches in this function.
- // ---=== Design Notes ===---
- // I am currently reasoning like so:
- //
- // Suppose we multiply two numbers, `a` and `b`.
- // `a` has a denominator of 2^16, giving it 16 bits of precision.
- // `b` has a denominator of 2^8, giving it 8 bits of precision.
- //
- // The product `a * b` will then have 24 bits of precision, with a denominator of 2^24.
- //
- // If we want to store the product into `a'`, which has the same precision as `a`,
- // then we will need to divide the product by 2^8 (the denominator of `b`)
- // to acquire a value with 16 bits of precision (because `16 = 24 - 8`).
- //
- // If we want to store the product into `b'`, which has the same precision as `b`,
- // then we will need to divide the product by 2^16 (the denominator of `a`)
- // to acquire a value with 8 bits of precision (because `8 = 24 - 16`).
- //
- // If we want to store the product into `c`, which has N bits of precision,
- // then we will need to divide by the number of bits required to reach
- // that: `24 - x = N`, so `x = 24 - N`.
- // We divide by `2^(24 - N)` to reach the precision for `c`.
- //
- // But wait, what if `N>24`? Well, that's a good thing to notice,
- // because the destination may actually have more precision than
- // is given by the product of the multiplicands' denominators.
- // In that case, we will need to multiply instead of divide!
- //
- // Now, what if `a` has `P` bits of precision, and `b` has `Q` bits of precision?
- // We will need to adjust our formula slightly:
- // `x = (P+Q) - N`.
- //
- // Now, what if `a` has an arbitrary denominator `A` and `b`, likewise has `B`?
- // The full (wide) product will have a denominator of `AB = A * B`.
- //
- // To store into `a'`, we'll need to find `A` in terms of `AB` and `B`.
- // We write `A` like so: `A = (A * B) / B`.
- // Thus, `A = AB / B` (by substitution.).
- //
- // To store into `b'`, we'll need to find `B` in terms of `AB` and `A`.
- // We write `B` like so: `B = (A * B) / A`.
- // Thus, `B = AB / A` (by substitution.).
- //
- // To store into `c`, we'll write `C` in terms of `AB` and `X`, then find `X`.
- // `C = AB/X`
- // `C*X = AB`
- // `X = AB/C`
- // In the code, we will already know `C` because it is the denominator of `c`.
- // We will need to find `X` so that we can call `divide_128x64` with it.
- // Now we know how to do that. (I hope.)
- //
- // Another algebraic expression for how `c`'s numerator (`cn`) would be calculated:
- // cn/cd = (an/ad) * (bn/bd)
- // cn = (an/ad) * (bn/bd) * cd
- // cn = ((an*bn) / (ad*bd)) * cd
- // cn = (an * bn * cd) / (ad * bd)
- // cn = (an * bn) * (cd / (ad * bd)) <- if( cd < (ad*bd) ) { do this? }
- // cn = (an * bn) / ((ad * bd) / cd) <- if( (ad*bd) < cd ) { do this? }
- Testing::_assert($divisor > 0);
- Testing::_assert($divisor <= PHP_INT_MAX);
- $a = $fp64a->numerator;
- $b = $fp64b->numerator;
- $a_div = $fp64a->denominator;
- $b_div = $fp64b->denominator;
- // ---=== Multiplication ===---
- // Multiply 64bit * 64bit to yield 128bit value.
- $lo = (int)0;
- $hi = (int)0;
- Int128::multiply_64x64($a, $b, $hi,$lo);
- // Do the same for the denominators/divisor.
- $div_lo = (int)0;
- $div_hi = (int)0;
- Int128::multiply_64x64($a_div, $b_div, $div_hi,$div_lo);
- // ---=== Division: Denominator ===---
- // We need to figure out what number to divide or new numerator by,
- // so that it ends up with the precision described by `$divisor`.
- // This will be `($a->denominator * $b->denominator) / $divisor`.
- $scale_direction = (int)0;
- $div_quotient_lo = (int)0;
- $div_quotient_hi = (int)0;
- $div_remainder = (int)0;
- if ( $divisor === 0 || $divisor === 1 ) {
- Testing::_assert($scale_direction === 0);
- $div_quotient_lo = $div_lo;
- $div_quotient_hi = $div_hi;
- Testing::_assert($div_remainder === 0);
- } else
- if ( 0 === $div_hi && $divisor > $div_lo ){
- $scale_direction = 1;
- // TODO: Division here is not guaranteed to be twos-complement compatible.
- $div_quotient_lo = intdiv($divisor, $div_lo);
- Testing::_assert($div_quotient_hi === 0);
- $div_remainder = $divisor % $div_lo;
- // TODO: What to do with $div_remainder?
- }
- else {
- $scale_direction = -1;
- $div_remainder = Int128::divide_128x64($div_hi,$div_lo, $divisor, $div_quotient_hi,$div_quotient_lo);
- // TODO: This limitation isn't strictly necessary;
- // this is something that CAN happen with valid inputs,
- // and there is going to be a way to handle it.
- // It just seems kinda unlikely, and I don't have the time to write it right now.
- if ( $div_quotient_hi !== 0 ) {
- // TODO: Better error message; put parameters and details about the denominators.
- $class_name = __CLASS__;
- $func_name = __FUNCTION__;
- throw new \Exception(
- "$class_name::$func_name: Unimplemented combination of input parameters; ".
- "The product of the inputs' denominators divided by the destination's denominator ".
- "must be a number that fits within PHP_INT_MAX.");
- }
- // TODO: What to do with $div_remainder?
- }
- // TODO: $div_remainder is a smell suggesting that we haven't done enough math or something.
- // In particular, this codepath is making it clear that handling arbitrary
- // input denominators leads to situations where there is no common factor
- // to divide by.
- //
- // Like, if `A = 2^16` and `B = 2^8`, then either `C = 2^16` or `C = 2^8`
- // will both work fine because `AB = 2^24` which has even factors of both 2^16 and 2^8.
- // `C` could be any power-of-2 in that situation, though powers greater than 23
- // will either cause us to do zero scaling or scale up (multiply the result and zero-fill the LSBs).
- //
- // However, if `A = 3` and `B = 5`, then `C` has to be some multiple
- // of either 3 or 5 in order for the scaling to happen cleanly.
- // If we plug in `C = 2`, then we get `X = 15/2`, which is clearly not an integer.
- // (It might be possible to get around this by using those remainders
- // in the later scaling expressions, but it is clear how in my current sleepy state.)
- // ---=== Rounding ===---
- // Round the 128bit value, modulo the `$divisor` parameter.
- // (We really only need to do this for the lower 64 bits, because $divisor can't be bigger than that.)
- // TODO: Implement more rounding modes? (Also `banker_round` is pretty sus at the moment b/c sleep not enough.)
- if ( $scale_direction < 0 )
- {
- if ( $rounding_mode == RoundingMode::HALF_TOWARDS_EVEN ) {
- $lo = self::banker_round($lo, $divisor);
- } else
- if ( $rounding_mode != RoundingMode::DEFAULT ) {
- throw new \Exception("Unimplemented rounding mode ".RoundingMode::to_string($rounding_mode));
- }
- }
- // ---=== Division (or multiplication): Numerator ===---
- // Shrink the 128bit value until it is at the desired precision according to `$divisor`.
- // This will ready it for overflow checking.
- $quotient_lo = (int)0;
- $quotient_hi = (int)0;
- $remainder = (int)0;
- if ( $scale_direction === 0 ) {
- $quotient_lo = $lo;
- $quotient_hi = $hi;
- Testing::_assert($remainder === 0);
- } else
- if ( $scale_direction > 0 ) {
- // Least likely case of all of them, but also the most complicated.
- $tmp1_out_lo = (int)0;
- $tmp1_out_hi = (int)0;
- $tmp2_out_lo = (int)0;
- $tmp2_out_hi = (int)0;
- Int128::multiply_64x64($lo, $div_quotient_lo, $tmp1_out_hi,$tmp1_out_lo);
- Int128::multiply_64x64($hi, $div_quotient_lo, $tmp2_out_hi,$tmp2_out_lo);
- $quotient_lo = $tmp1_out_lo;
- $quotient_hi = $tmp1_out_hi + $tmp2_out_lo;
- Testing::_assert($tmp2_out_hi === 0);
- } else
- if ( $scale_direction < 0 ) {
- $remainder = Int128::divide_128x64($hi,$lo, $divisor, $quotient_hi,$quotient_lo);
- }
- // ---=== Overflow/Error Handling ===---
- // Now we check for overflow (and maybe do some validation on rounding logic).
- // If there is no overflow, then we can safely discard the high part of the quotient.
- if ( $rounding_mode != RoundingMode::DEFAULT ) {
- Testing::_assert($remainder === 0); // Because rounding should have handled this already.
- }
- if ( 0 !== $quotient_hi ) {
- $class_name = __CLASS__;
- $func_name = __FUNCTION__;
- $rounding_mode_str = RoundingMode::to_string($rounding_mode);
- throw new \ArithmeticError(sprintf(
- "Overflow in `$class_name::$func_name`. ".
- "Parameters: {fp64a->numerator:%016X, fp64a->denominator:%016X, fp64b->numerator:%016X, fp64b->denominator:%016x, divisor:%016X, rounding_mode:$rounding_mode_str}",
- $fp64a->numerator, $fp64a->denominator, $fp64b->numerator, $fp64b->denominator, $divisor
- ));
- }
- // ---=== Return ===---
- return $quotient_lo;
- }
- public static function unittest_multiply() : void
- {
- // Left as exercise for reader.
- echo(__CLASS__."::".__FUNCTION__."...");
- echo(" done.\n");
- }
- public static function unittest() : void
- {
- echo("Unittests for ".__CLASS__."\n");
- // Put unittests for other methods here...
- self::unittest_banker_round();
- // ... or here ...
- self::unittest_multiply();
- // ... or here.
- echo("\n");
- }
- }
- ?>
- <?php
- //declare(strict_types=1);
- // Unittest driver to be run from command-line interface.
- /*use \Kickback\Math\IntNN;
- use \Kickback\Math\Int128;
- use \Kickback\Math\Types\FixedPoint64;
- */
- function unittest_all() : void
- {
- IntNN::unittest();
- Int128::unittest();
- FixedPoint64::unittest();
- }
- unittest_all();
- ?>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement