Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- declare(strict_types=1);
- namespace Kickback\Common\Exceptions;
- use \Kickback\Common\Exceptions\CustomException;
- /*
- * Exception that should be thrown whenever any code is called in the `\Kickback\...`
- * namespace without having been implemented yet.
- */
- class UnimplementedException extends CustomException {}
- ?>
- <?php
- // The below comments are probably halfway obsolete at this point,
- // but mostly because I've been DOING them, and it's been going pretty
- // smoothly. The only catch is that I'm starting to realize the scale
- // of the task at hand and... ugh. The design is looking pretty good,
- // but the implementation will take some work to catch up!
- // 2024-07-15 -- Lily
- //
- // Before 2024-07-15:
- //
- // 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!)
- //
- // final class IntX1
- // {
- // }
- //
- // final Class IntX2
- // {
- // }
- //
- // // Everything from X4 up is probably template-able.
- // final class IntX4
- // {
- // }
- //
- // // Implement this to enable 128-bit multiplies on 32-bit arch.
- // final class IntX8
- // {
- // }
- //
- /*
- declare(strict_types=1);
- namespace Kickback\Common\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\Common\Math\LowLevel;
- */
- // TODO: The WideIntX1 class below will end up templated, so that we can
- // operate on integers of arbitrary width.
- /**
- * `WideIntX1` is the base-case for an implementation of wide integers in PHP.
- *
- * Wide integers are integer types with a fixed-width, but a fixed-width that
- * is longer/wider than the host language's native integer type(s).
- *
- * In PHP, there is only one (native/builtin) integer type (`int`), and
- * its width (presumably) depends on the size of arithmetic integers on
- * the host system (so 32-bit integers when PHP is executed on a 32-bit CPU,
- * or 64-bit integers if PHP is executed on a 64-bit CPU).
- *
- * All operations in the WideIntN family will be implemented according to
- * twos-complement arithmatic. This is, notably, NOT what PHP's built-in
- * integer type does. At least, not always. PHP's `int` type will use
- * twos-complement most of the time, but when an integer expression causes
- * _signed_ overflow (including, informally, "underflow"), then
- *
- */
- final class WideIntX1
- {
- // =========================================================================
- // The below comments relate to the `add_with_carry` function (and, by
- // extension, the `add` function.) These comments are placed outside
- // of the function to reduce clutter between procedural steps, and
- // also to make things faster if the function ever gets reparsed
- // multiple times for any reason (ex: cheap metaprogramming techniques).
- //
- // 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_halves($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_halves(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 halves), 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)
- // halves 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_halves(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_halves: 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_halves: 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::subtract_by_halves($a,$b,$u_borrow);
- }
- }
- // Pretty much the same thing as `add_by_halves`, except for subtraction.
- private static function subtract_by_halves(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_subtract_with_borrow()
- {
- echo(__CLASS__."::".__FUNCTION__."...");
- echo(" UNIMPLEMENTED! (Please fix!)\n");
- }
- /*
- * 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_(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;
- }
- public static function unittest() : void
- {
- echo("Unittests for ".__CLASS__."\n");
- self::unittest_add_with_carry();
- self::unittest_subtract_with_borrow();
- echo("\n");
- }
- }
- ?>
- <?php
- /*
- declare(strict_types=1);
- namespace Kickback\Common\Math\LowLevel;
- use \Kickback\Common\Math\LowLevel\WideIntX1;
- */
- final class WideIntX2
- {
- private const MASK_LO = ((1 << 32) - 1);
- private const MASK_SIGN = (1 << 63);
- // 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 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);
- }
- // 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;
- }
- 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\Common\Math;
- */
- final class Signedness
- {
- public const SIGNED = 0;
- public const UNSIGNED = 1;
- public const AMBIGUOUS = 2;
- public static function to_identifier(int $signedness) : string
- {
- switch($signedness)
- {
- case self::SIGNED : return "SIGNED";
- case self::UNSIGNED : return "UNSIGNED";
- case self::AMBIGUOUS: return "AMBIGUOUS";
- }
- return "ERROR: Unknown signedness ($signedness)";
- }
- public static function to_string(int $signedness) : string {
- return self::to_identifier($signedness);
- }
- // 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\Common\Math;
- */
- // TODO: I forget if it's better to do this, or to use an `enum` type. Whatevs.
- // (At least now we have enough solid functionality that if we try to convert
- // this (and the Signedness class) over to enums, we'll find out very _quickly_
- // if it's a good idea or not! <grin>)
- final class RoundingMode
- {
- // Rounding modes.
- // (I think this is all of the possibilities? TODO: Verify.)
- // TODO: I'm VERY tempted to make BANKERS rounding be the default rounding mode.
- // Like, it's a rounding procedure that cancels accumulated error during
- // successive rounds. How fncking cool is that?
- // Why NOT make that the default?
- // (I mean, it might be a little bit slower than truncation...
- // but probably not in any perceivable way. Like, <1%.
- // And it makes your error accumulate at a rate of sqrt(n) instead of (n),
- // for n operations.
- // And it's going to be used for WideFractions and WideIntegers, which
- // will probably not be slow, but they won't be the FASTEST thing to
- // begin with. (They are really optimized for correctness and exactness,
- // at a probably small-ish expense of speed.))
- /** The default rounding mode shall mimic x86 integer truncation. */
- public const DEFAULT = 0;
- public const ALL_TOWARDS_NEG_INF = 1;
- public const ALL_TOWARDS_POS_INF = 2;
- 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
- // Useful alias.
- public const BANKERS = self::HALF_TOWARDS_EVEN;
- /**
- * If the `$rounding_mode` parameter equals RoundingMode::DEFAULT, then
- * it this function will return the more specific RoundingMode that this
- * refers to.
- *
- * If `$rounding_mode` is any other value, then it that value will be
- * returned unmodified.
- */
- public static function resolve_default(int $rounding_mode = self::DEFAULT) : int
- {
- if ( $rounding_mode === self::DEFAULT ) {
- // This mimics x86 integer truncation. (I think? TODO: Double-check this.)
- return self::ALL_TOWARDS_NEG_INF;
- } else {
- return $rounding_mode;
- }
- }
- public static function to_identifier(int $rounding_mode) : string
- {
- switch($rounding_mode)
- {
- case self::DEFAULT : return "DEFAULT";
- 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 "ERROR: Unknown rounding mode ($rounding_mode)";
- }
- public static function to_string(int $rounding_mode) : string
- {
- if ( $rounding_mode === self::DEFAULT ) {
- $resolved_mode = self::resolve_default();
- return "DEFAULT ($resolved_mode)";
- } else {
- return self::rounding_mode_to_identifier($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\Common\Meta;
- */
- class Template
- {
- // TODO: How to populate this?
- // (DO we need to populate this?)
- private array $parameters_? = null;
- public function parameters() : array
- {
- }
- public function instantiate(string $namespace, string $base_name, array $parameters) : void
- {
- throw new UnimplementedException(__CLASS__."->".__FUNCTION__." hasn't been implemented yet.");
- // TODO: Compute $instance_path;
- // NOTE: $instance_path might, itself, need to contain template parameter subsitutions!
- // That way, we could make a file like WideIntX@{N}.template.php and then know,
- // based on the template parameter N being N=4, that the $base_name should be "WideIntX4" instead of "WideIntX@{N}".
- // Thus, it is unnecessary to ask the template what its class/interface/etc name would be.
- // That's important, because if we have to ask the template, then
- // we won't know the $base_name until we have already used the $base_name to process the template!
- // (Also it might be VERY cool if we could create an idiomatic format
- // for encoding template parameters into class/interface/etc names.
- // Then it'd be possible to have the autoloader (!) instantiate templates (!!).
- // Though, admittedly, it would probably be so ugly that we'd just
- // do instantiation-upon-construction anyways.)
- //
- if ( !file_exists($instance_path ) {
- self::template_instance_codegen($namespace, $base_name, $parameters, $instance_path);
- }
- require_once($instance_path);
- }
- private function template_instance_codegen(string $namespace, string $base_name, array $parameters, string $instance_path) : void
- {
- throw new UnimplementedException(__CLASS__."->".__FUNCTION__." hasn't been implemented yet.");
- // TODO: Write specific code for all of this.
- // TODO: Probably alter some of the path generation instructions so that
- // the instance files and temporary files go some place sane, like:
- // * temporary files: somewhere in (\Kickback\SCRIPT_ROOT . "/tmp/...")
- // * instance files: somewhere in (\Kickback\SCRIPT_ROOT . "/TemplateInstances/...")
- //
- // Generate the file:
- // * $template_file_path = \Kickback\SCRIPT_ROOT ."/". self::classpath_to_filepath($namespace, $base_name) . ".template.php";
- // * (BTW, to make this efficient, the below should probably use something like fread/fwrite line-by-line, and NOT slurp the file using `file_get_contents`)
- // * if ( exists($template_file_path) ) { spin for up to 30 seconds, waiting for the file to be deleted, then move to "Process the template file" instructions. }
- // * Read and process the base_name.template.php ($template_file_path) file:
- // * $base_template_text = file_get_contents($template_file_path);
- // * $base_template_text: Replace [<][?][p][h][p]...[?][>] with @@@php...@@@
- // * $base_template_text: Replace all @@php...@@ with [<][?][p][h][p]...[?][>]
- // * $template_tmp1_file_path = $template_file_path . ".tmp";
- // * Write the results ($base_template_text) to $template_tmp1_file_path
- // * Process the template file:
- // * (Is there any way to stream ob_ content line-by-line, so we don't have to store the whole file in memory?)
- // * (If we stream it, we should stream it to another temporary file.)
- // * If streaming: if ( exists($template_tmp2_path) ) { spin for up to 30 seconds, waiting for the file to be deleteted, then }
- // * ob_start();
- // * require_once($template_tmp1_file_path);
- // * $template_output = ob_get_clean();
- // * $template_output: Replace all @@@php...@@@ with [<][?][p][h][p]...[?][>]
- // * Write $template_output (or $template_tmp2_file_path) out to $instance_path.
- // * Remove file at $template_tmp1_file_path (if needed)
- // * Remove file at $template_tmp2_file_path (if needed)
- }
- }
- ?>
- <?php
- /*
- declare(strict_types=1);
- namespace \Kickback\Common\Meta;
- use \Kickback\Common\Meta\Template;
- */
- class TemplateProcessor
- {
- // TODO:
- // * Implement all of the methods needed for the WideFraction template
- // to be able to generate it's code.
- // * Find some way for the Template class to communicate its instance
- // to the TemplateProcessor class. This is crucial for things like
- // accessing the template parameter list from within the template itself.
- public function echo_header_code() : void
- {
- TODO: implement
- }
- public function echo_instance_base_name() : void
- {
- TODO: implement
- }
- }
- ?>
- <?php
- @@php
- /*
- declare(strict_types=1);
- namespace \Kickback\Common\Math\Types;
- use \Kickback\Common\Meta\Template;
- */
- final class WideFractionPart
- {
- public string $numerator; // The name of the member, not an actual numerical value.
- public string $denominator; // The name of the member, not an actual numerical value.
- }
- final class WideFractionTemplateProcessor extends TemplateProcessor
- {
- // The constant denominator case is nice because it allows us to avoid
- // having to store the additional $denominator member for the WideFractions
- // that have commonly used constant denominators (things like 100, 1000, etc).
- //
- // This can potentially make calculations faster too, not just because
- // "less memory == less cache thrashing", but more specifically because
- // it allows some intermediate quantities to be calculated.
- //
- private bool $has_constant_denominator_ = ;
- public function has_constant_denominator() : bool
- {
- // TODO: Probably this should be done in the constructor, instead of lazily here.
- if ( $this->has_constant_denominator_ === null ) {
- if(($array_key_exists("constant_denominator", $this->parameters()))
- && ($this->parameters["constant_denominator"] !== 0)) {
- $this->has_constant_denominator_ = true;
- } else {
- $this->has_constant_denominator_ = false;
- }
- }
- return $this->has_constant_denominator_;
- }
- private int $number_of_parts_ = 0;
- public function number_of_parts() : int
- {
- // TODO: Probably this should be done in the constructor, instead of lazily here.
- if ( $this->number_of_parts_ === 0 ) {
- $this->number_of_parts_ = $this->parameters()["N"];
- }
- return $this->number_of_parts_;
- }
- private \SplFixedArray $parts_ = $this->make_parts_array();
- private function make_parts_array() : \SplFixedArray
- {
- // TODO: \SplFixedArray syntax check!
- $n = $this->number_of_parts();
- $parts_array = new \SplFixedArray($n);
- for ($i = (int)0; $i < $n; $i++)
- {
- $part = new WideFractionPart();
- $part->$numerator = "numerator" . strval($i);
- $part->$denominator = "denominator" . strval($i);
- $this->parts_[$i] = $part;
- }
- return $parts_array;
- }
- public function parts() : \SplFixedArray
- {
- return $this->parts_;
- }
- public function echo_parameter_list(string $param_name, int $from = 0, int $to = $this->number_of_parts()) : void
- {
- $this->echo_code("string ${param_name}" . strval($from))
- for ($i = $from+1; $i < $to; $i++) {
- $this->echo_code(", string ${param_name}" . strval($i));
- }
- $this->echo_code(" ");
- }
- public function echo_parameter_list_lo(string $param_name) : void
- {
- $n = $this->number_of_parts();
- $this->echo_parameter_list($param_name, 0, $n/2);
- }
- public function echo_parameter_list_hi(string $param_name) : void
- {
- $n = $this->number_of_parts();
- $this->echo_parameter_list($param_name, ($n/2)+1, $n);
- }
- public function echo_member_arg_list(string $member_name, int $from = 0, int $to = $this->number_of_parts()) : void
- {
- $parts = $this->parts();
- $this->echo_code($parts[$from]->$member_name)
- for ($i = $from+1; $i < $to; $i++) {
- $this->echo_code(",".$parts[$i]->$member_name);
- }
- $this->echo_code(" ");
- }
- public function echo_numerator_arg_list_lo() : void
- {
- $n = $this->number_of_parts();
- $this->echo_member_arg_list("numerator", 0, $n/2);
- }
- public function echo_numerator_arg_list_hi() : void
- {
- $n = $this->number_of_parts();
- $this->echo_member_arg_list("numerator", ($n/2)+1, $n);
- }
- public function echo_denominator_arg_list_lo() : void
- {
- $n = $this->number_of_parts();
- $this->echo_member_arg_list("denominator", 0, $n/2);
- }
- public function echo_denominator_arg_list_hi() : void
- {
- $n = $this->number_of_parts();
- $this->echo_member_arg_list("denominator", ($n/2)+1, $n);
- }
- }
- function variable_scoping_function() : void
- {
- // TODO: Figure out better template processor syntax.
- $template = new WideFractionTemplateProcessor(__namespace__, basename(__FILE__, '.template.php'));
- $template->echo_header_code();
- @@
- final class @@php $template->echo_instance_base_name() @@
- {
- @@php
- for ($template->parts() as $part) {
- $numerator_part = $part->numerator;
- $denominator_part = $part->denominator;
- $template->set_indentation(1);
- $template->echo_code("private int \$$numerator_part;\n");
- $template->echo_code("private int \$$denominator_part;\n");
- }
- @@
- 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(WideFraction $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);
- }
- /*
- * 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.
- */
- public static function multiply_into(WideFraction $fp64a, WideFraction $fp64b, WideFraction $destination, int $rounding_mode = RoundingMode::DEFAULT) : WideFraction
- {
- Testing::_assert(isset($destination));
- $tmp = WideFraction::multiply_raw($fp64a, $fp64b, $destination->denominator, $rounding_mode);
- $destination->numerator = $tmp;
- return $destination;
- }
- private static function multiply_raw(WideFraction $fp64a, WideFraction $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 function divide_by_into(WideFraction $denominator, WideFraction &$quotient, WideInt? &$Remainder = null) : void
- {
- throw new UnimplementedException(__CLASS__."::".__FUNCTION__." hasn't been implemented yet.");
- }
- /*
- * Copies all state (ex: numerator and denominator) from one WideFraction
- * to another, with the requirement that both of them must be of the same
- * type, and that type must be the class this method is in.
- *
- * Those constraints lend themselves to a pretty straight-forward
- * implementation that doesn't have many (if any) bounds/validity checks
- * required.
- */
- public static function blit(self $dest, self $source) : void
- {
- @@php
- for ($template->parts() as $part) {
- $numerator_part = $part->numerator;
- $denominator_part = $part->denominator;
- $template->set_indentation(2);
- $template->echo_code("\$dest->$numerator_part = \$source->$numerator_part;\n");
- $template->echo_code("\$dest->$denominator_part = \$source->$denominator_part\n");
- }
- @@
- }
- /*
- * Modifies the given WideFraction by replacing it with its inverse.
- *
- * Mathematically, the inverse of a rational number `n / d` is `d / n`.
- * In other words: inverting a fraction is the same as swapping its
- * numerator and denominator.
- *
- * This probably won't be used much for code that uses wide fractions
- * as fixed-point numbers, but it can be handy in some calculations
- * because it does an operation similar to division
- * (e.g. `$my_frac->invert()` computes `WideFraction::divide(1,$my_frac)`),
- * but with none of the costs of division.
- *
- *
- *
- * @see inverse_into
- * @see inverse_from
- */
- public function invert() : void
- {
- @@php
- for ($template->parts as $part) {
- $numerator_part = $part->numerator;
- $denominator_part = $part->denominator;
- $template->set_indentation(2);
- $template->echo_code("\$temp = \$this->$denominator_part;\n");
- $template->echo_code("\$this->$numerator_part = \$this->$denominator_part;\n");
- $template->echo_code("\$this->$denominator_part = \$temp\n");
- }
- @@
- }
- /*
- * @see invert
- * @see inverse_from
- */
- public function inverse_into(WideFraction $dest) : self
- {
- self::blit($dest,$this);
- $dest->invert();
- return $dest;
- }
- /*
- * @see invert
- * @see inverse_into
- */
- public function inverse_from(WideFraction $src) : self
- {
- self::blit($this,$src);
- $this->invert();
- return $this;
- }
- /*
- * Allocates a new instance of the same class as `$this`, then copies
- * the contents of `$this` into that new instance.
- *
- * @return self The new instance.
- */
- public function dup() : self
- {
- $result = new self();
- self::blit($result,$this);
- return $result;
- }
- 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
- } // variable_scoping_function
- variable_scoping_function();
- @@
- ?>
- <?php
- // WideFixedFraction.php
- /*
- declare(strict_types=1);
- namespace Kickback\Common\Math\Types;
- use \Kickback\Common\Math\Types\Integer;
- use \Kickback\Common\Math\RoundingMode;
- use \Kickback\Common\Math\Signedness;
- */
- /**
- * This is the conventional class to use when constructing a WideFraction
- * with a constant denominator.
- *
- * It is otherwise nearly identical to the WideFraction class (and type).
- *
- * @see WideFraction
- */
- abstract class WideFixedFraction extends WideFraction
- {
- // TODO: Finish designing all of the WideFraction factory methods,
- // then create analogous versions in this class.
- public static function make(
- // runtime parameter(s):
- int $numerator = 0,
- int $denominator = 0, // can be elided (left as 0) if assigning a constant denominator.
- // named template parameters:
- int $numerator_bit_width,
- int $denominator_bit_width = 0, // Either this or $constant_denominator_int must be non-zero.
- int $constant_denominator_int = 0, // Either this or $denominator_bit_width must be non-zero.
- // TODO: constant_denominator_wide?
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- ) : void
- {
- $template = WideFractionTemplate::instantiate(__NAMESPACE__,__CLASS__,
- numerator_width:$bit_width,
- constant_denominator_int:$denominator);
- $template_class_path = $template->class_path();
- if ( $has_constant_config ) {
- $result = new $template_class_path($numerator,$denominator);
- } else {
- $result = new $template_class_path($numerator,$denominator, signedness:$signedness, rounding_mode:$rounding_mode);
- }
- throw new UnimplementedException(__CLASS__."::".__FUNCTION__." hasn't been implemented yet.");
- return $result;
- }
- }
- ?>
- <?php
- // WideFraction.php
- /*
- declare(strict_types=1);
- namespace Kickback\Common\Math\Types;
- use \Kickback\Common\Math\Types\Integer;
- use \Kickback\Common\Math\RoundingMode;
- use \Kickback\Common\Math\Signedness;
- */
- // TODO: I really like being able to see the broad overview above,
- // the list of what exists. I'm tempted to make an interface that
- // just has all of the methods of the WideFraction class, but none
- // of the verbose documentation. WideFraction would then implement
- // that interface to ensure the two remain in sync. There would otherwise
- // be no reason for any calling code to use the interface; it'd just
- // be there to provide synchronization guarantees on
- // "read the source"-style documentation.
- // And then it would be admissible to put really long descriptions on all
- // of the WideFraction methods. It'd become hard to see, from text editor PoV,
- // what methods exist, but then we'd be able to just look at the interface
- // and know (most) of what's available.
- //
- // Update: I wrote the interface. (Untested.) It's below.
- // Yes, this interface will probably just be put into WideFraction.php.
- // Yes, that will make it impossible to autoload this interface.
- // This is perfectly fine. This interface isn't meant to be actually used.
- // It's meant to document what's available in the WideFraction class,
- // for anyone using a text editor that wants a concise reference.
- // (It is very difficult to write decently descriptive comments in the
- // WideFraction class without also making it VERY difficult to see where
- // the definitions are and which definitions are available. Good for detailed
- // reading, but bad for "at a glance". The interface is intended to complement
- // that documentation by providing an "at a glance" version.)
- //
- // In other words:
- // There is no reason for any calling code to use the interface;
- // it's just here to provide synchronization guarantees for
- // "read the source"-style documentation.
- //
- // It is good to put this in WideFraction.php if possible, because the
- // two things will have a circular dependency:
- // WideFractionMethods depends on WideFraction for defining method parameters,
- // while WideFraction depends on WideFractionMethods (well, "implements" it)
- // to keep the method definitions synchronized.
- interface WideFractionMethods
- {
- // TODO: Paste uncommented copies of all of the WideFraction methods
- // here (and remove the `absract` attribute because interfaces don't use that).
- // This interface will then serve as a quick reference (from text-editor perspective)
- // of what's available in the WideFraction class.
- // Here's an example, the comparison methods are already moved over:
- // (Compare to the scene in the WideFraction class!)
- // ---=== Configuration properties ===---
- public function has_constant_denominator() : bool;
- public function has_constant_config() : bool;
- public function numerator_bit_width() : int;
- public function denominator_bit_width() : int;
- public function signedness(int? $new_signedness = null) : int;
- public function rounding_mode(int? $new_rounding_mode = null) : int;
- // ---=== Internal Accessors ===---
- // These are intentionally absent from the interface.
- // (It might not be THAT bad to put them here, but it's the kind of
- // thing we should probably only do if there's a need for it.)
- // ---=== Comparison operations ===---
- /** Default comparisons */
- public function cmp(WideFraction $other) : int;
- public function eq(WideFraction $other) : bool; /** @see cmp */
- public function ne(WideFraction $other) : bool; /** @see cmp */
- public function lt(WideFraction $other) : bool; /** @see cmp */
- public function gt(WideFraction $other) : bool; /** @see cmp */
- public function lte(WideFraction $other) : bool; /** @see cmp */
- public function gte(WideFraction $other) : bool; /** @see cmp */
- /** Default comparisons with integer right-hand-side */
- public function cmp_int(int $other) : int;
- public function eq_int(int $other) : bool; /** @see cmp_int */
- public function ne_int(int $other) : bool; /** @see cmp_int */
- public function lt_int(int $other) : bool; /** @see cmp_int */
- public function gt_int(int $other) : bool; /** @see cmp_int */
- public function lte_int(int $other) : bool; /** @see cmp_int */
- public function gte_int(int $other) : bool; /** @see cmp_int */
- /** Default comparisons with string right-hand-side */
- public function cmp_str(string $other) : int;
- public function eq_str(string $other) : bool; /** @see cmp_str */
- public function ne_str(string $other) : bool; /** @see cmp_str */
- public function lt_str(string $other) : bool; /** @see cmp_str */
- public function gt_str(string $other) : bool; /** @see cmp_str */
- public function lte_str(string $other) : bool; /** @see cmp_str */
- public function gte_str(string $other) : bool; /** @see cmp_str */
- /** (U)nsigned WideFraction comparisons */
- public function ucmp(WideFraction &$other) : int;
- public function ueq(WideFraction &$other) : bool; /** @see ucmp */
- public function une(WideFraction &$other) : bool; /** @see ucmp */
- public function ult(WideFraction &$other) : bool; /** @see ucmp */
- public function ugt(WideFraction &$other) : bool; /** @see ucmp */
- public function ulte(WideFraction &$other) : bool; /** @see ucmp */
- public function ugte(WideFraction &$other) : bool; /** @see ucmp */
- /** (U)nsigned integer comparisons */
- public function ucmp_int(int $other) : int;
- public function ueq_int(int $other) : bool; /** @see ucmp_int */
- public function une_int(int $other) : bool; /** @see ucmp_int */
- public function ult_int(int $other) : bool; /** @see ucmp_int */
- public function ugt_int(int $other) : bool; /** @see ucmp_int */
- public function ulte_int(int $other) : bool; /** @see ucmp_int */
- public function ugte_int(int $other) : bool; /** @see ucmp_int */
- /** (U)nsigned string comparisons */
- public function ucmp_str(string $other) : int;
- public function ueq_str(string $other) : bool; /** @see ucmp_str */
- public function une_str(string $other) : bool; /** @see ucmp_str */
- public function ult_str(string $other) : bool; /** @see ucmp_str */
- public function ugt_str(string $other) : bool; /** @see ucmp_str */
- public function ulte_str(string $other) : bool; /** @see ucmp_str */
- public function ugte_str(string $other) : bool; /** @see ucmp_str */
- /** (S)igned WideFraction comparisons */
- public function scmp(WideFraction &$other) : int;
- public function seq(WideFraction &$other) : bool; /** @see scmp */
- public function sne(WideFraction &$other) : bool; /** @see scmp */
- public function slt(WideFraction &$other) : bool; /** @see scmp */
- public function sgt(WideFraction &$other) : bool; /** @see scmp */
- public function slte(WideFraction &$other) : bool; /** @see scmp */
- public function sgte(WideFraction &$other) : bool; /** @see scmp */
- /** (S)igned integer comparisons */
- public function scmp_int(int $other) : int;
- public function seq_int(int $other) : bool; /** @see scmp_int */
- public function sne_int(int $other) : bool; /** @see scmp_int */
- public function slt_int(int $other) : bool; /** @see scmp_int */
- public function sgt_int(int $other) : bool; /** @see scmp_int */
- public function slte_int(int $other) : bool; /** @see scmp_int */
- public function sgte_int(int $other) : bool; /** @see scmp_int */
- /** (S)igned string comparisons */
- public function scmp_str(string $other) : int;
- public function seq_str(string $other) : bool; /** @see scmp_str */
- public function sne_str(string $other) : bool; /** @see scmp_str */
- public function slt_str(string $other) : bool; /** @see scmp_str */
- public function sgt_str(string $other) : bool; /** @see scmp_str */
- public function slte_str(string $other) : bool; /** @see scmp_str */
- public function sgte_str(string $other) : bool; /** @see scmp_str */
- // ---=== Arithmetic operations ===---
- // TODO: Replace all of the `self` with WideFraction.
- public function add(WideFraction &$a, WideFraction &$b, WideFraction? &$c = null, WideFraction? &$d = null) : self;
- public function add_ww(WideFraction &$a, WideFraction &$b, WideFraction? &$c = null, WideFraction? &$d = null) : self;
- public function add_wi(WideFraction &$a, int $b) : self;
- public function add_iw(int $a, WideFraction &$b) : self;
- public function add_ii(int $a, int $b) : self;
- public function increment_by(WideFraction &$b) : void;
- public function increment() : void;
- public function incr_by(int $b) : void;
- public function incr() : void; // Should do same thing as `increment()`.
- public function subtract(WideFraction &$a, WideFraction &$b) : self;
- public function decrement_by(WideFraction &$b) : void;
- public function decrement() : void;
- public function decr_by(int $b) : void;
- public function decr() : void;
- public function negate() : void;
- public function multiply(WideFraction &$a, WideFraction &$b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function mult(WideFraction &$a, int $b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function multiply_by(WideFraction &$b, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function mult_by(int $b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function divide(WideFraction &$numerator, WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function div(WideFraction &$numerator, int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function divide_by(WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function div_by(int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function modulus(WideFraction &$numerator, WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function mod(WideFraction &$numerator, int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function modulus_by(WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function mod_by(int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function divide_and_modulus(WideFraction &$numerator, WideFraction &$denominator, WideFraction &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function div_n_mod(WideFraction &$numerator, int $denominator, int &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public function divide_and_modulus_by(WideFraction &$denominator, WideFraction &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function div_n_mod_by(int $denominator, int &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public function inverse_of(WideFraction &$a) : self;
- public function invert() : self;
- // ---=== Logical operations ===---
- public function sign() : int;
- }
- // TODO: How to ensure that WideFraction operations are not aliasing results?
- // Ex:
- // $a = WideFraction::from_integer(64, 1, 100);
- // $b = WideFraction::from_integer(64, 2, 100);
- // $c = WideFraction::from_integer(64, 3, 100);
- // $d = WideFraction::from_integer(64, 0, 100);
- // $e = WideFraction::from_integer(64, 0, 100);
- //
- // $e->add($d->add($a,$b),$d->add($a,$c));
- //
- // // Problem: the above expression requires $d to store two addition results
- // // before $e even has a chance to see either result.
- // // Likely, the 2nd one (depending on PHP's order-of-arg-evaluation)
- // // will save its result, and the first will be aliased to that.
- //
- // Bad result #1: $e->equals(6) // $e <- (3 + 3) because $a+$b was the prior add op and overwrote the first add op
- // Bad result #2: $e->equals(8) // $e <- (4 + 4) because $a+$c was the prior add op and overwrote the first add op
- // Good result: $e->equals(7) // $e <- (3 + 4) <- ((1+2) + (1+3))
- //
- // If you're having trouble seeing it, think of the above in terms
- // of builtin variables with normal expressions, except that you're required
- // to always do an assignment with every operation:
- //
- // int $a = 1;
- // int $b = 2;
- // int $c = 3;
- // int $d = 0;
- // int $e = 0;
- //
- // $e = (($d = $a + $b) + ($d = $a + $c));
- //
- // What would you think the above expression would evaluate to?
- // Because unless PHP does something very profound, it won't be 7.
- // (OH SHIT. I just ran it. PHP, uh, did something very profound. How the fnck?)
- //
- // Anyhow...
- //
- // Even if PHP somehow clones variables for us, it'll still probably require
- // memory allocations, which are probably multiple orders of magnitude slower
- // than any of the operations that will be done by these things (including multiplication and division, of course).
- // It would (probably) mean that PHP is silently subverting the whole
- // "preallocate your shit" doctrine that this API is designed to use.
- //
- // And some thoughts about mitigation:
- // Maybe this can at least be caught dynamically with some kind of Exception("Value \$a was assigned but never used!") kind of detection+response?
- // Unfortunately, this will require storing one bit of information. And can't be made constant. Ugh.
- // (And given that the above example PHP returns 7, will it just automatically clone things when a mutating member is called? That would be very hard to detect or anticipate. HMmmmm.)
- // (Actually, I wouldn't be surprised if ref parameters are a way to avoid that kind of undetectable scenario.
- // (They'd even allow $b to detect if the arguments are the same object, and throw an error for that!)
- // (Alright, I'm going to change the API for that really quick, because that's the most likely good scenario.)
- // Oh I will probably call it "Write Protection"! What a throw back! (You know, like the little dinky slide-switch on 3.5" floppies that you accidentally flip, so as to prevent yourself from being able to use your own equipment. Good fun!)
- /**
- * The WideFraction type is represented as a sequence of built-in `int` integers
- * for its numerator, and another such sequence as its denominator, along with
- * an auxiliary single `int` used to store dynamic configuration.
- *
- * The primary intended use-case for this class is as
- * a highly flexible variable-base fixed-point number representation.
- * It is expected to be usually used as a way to do precise and exacting
- * decimal calculations. As an unintended consequence, it may be able to
- * do some other interesting operations too (ex: approximate rational numbers),
- * but that functionality will not be a development priority.
- *
- * The size of these sequences is fixed upon template instantiation.
- *
- * The denominator will conventionally be defined by calling factory methods
- * in the `WideFixedFraction` class.
- * The denominator may also be defined as a constant during template instantiation,
- * by specifying "constant_denominator=(your chosen denominator)" as a template
- * parameter (this is what `WideFixedFraction` does under-the-hood).
- *
- * Using a constant denominator allows the WideFraction instance to elide storing
- * the denominator entirely. It also provides optimization opportunities
- * for some operations where an intermediate can be precomputed when
- * the denominators are constant. Internally, the template will replace
- * all variable references to the denominator with literal values.
- *
- * The WideFraction's configuration information can be made constant as well.
- * This will elide the auxilliary integer used to represent that information.
- * This is done by passing the "constant_config" or "constant_config=true"
- * template parameter, and then specifying values for any configuration
- * that the caller requires to be different from the defaults.
- * These template parameters will have the same name as their property
- * counterparts in the class itself.
- *
- * Attempting to call a config property setter on a WideFraction that has
- * constant properties will result in an Exception being thrown.
- * (TODO: Define a more specific exception class for this.)
- *
- * WideFractions may be signed, unsigned, or ambiguously signed.
- *
- * These states are enumerated in the `Signedness` constant class.
- *
- * When a WideFraction is ambiguously signed, the way the value is handled during
- * some operations (ex: comparison) will depend on which variation of the
- * operation is used. Most operations (ex: addition, subtraction, negation,
- * all logical operations, and so on) are inherently sign agnostic (due to
- * how they work on twos-complement numbers), and will not have separate
- * versions for informing signed-versus-unsigned behavior on
- * ambiguously signed WideFractions.
- *
- * WideFractions may have a default RoundingMode, which can be set through
- * the `rounding_mode` property.
- *
- * (Future direction: if we find it tedious to always specify rounding mode
- * after constructing WideFractions, we might be able to implement a feature
- * where we can instantiate the template separately from object construction,
- * then give the template a "initial_default_rounding_mode" parameter, then
- * use the template's `make_*` factory functions to yield WideFractions
- * that will then always have the desired RoundingMode.)
- *
- * The de facto implementation of this type is implemented by generating a class
- * that simply has all of the integers present in it as plain variables.
- * This ensures type safety and may possibly compute faster, as it requires
- * PHP to do fewer (hopefully zero) memory allocations during normal arithmetic
- * and logical operations, and doesn't add array operations on top of the
- * integer operations that would need to be done in either case.
- *
- * This class template is built upon the static template WideIntegerOperations.
- * The functions in WideIntegerOperations can be difficult or tedious to call
- * (either they require the caller to know how many primitive integers
- * they need to use for an operation in advance, or require the caller
- * to implement a template to abstract that information), so the WideFraction
- * class is not just providing fractional math implementations, but it is
- * also providing a convenient abstraction for interacting with "Wide"
- * number operations.
- *
- * Notably, WideInteger is a special case of WideFraction with
- * a constant denominator that has a value of 1.
- *
- * @see RoundingMode
- * @see Signedness
- * @see WideFixedFraction
- * @see WideIntegerOperations
- */
- abstract class WideFraction implements WideFractionMethods
- {
- // NOTE: Just to be clear: None of the shit in this class is working right now.
- // Almost all of these methods don't exist yet.
- // The thing that DOES exist is some of the low-level logic that will
- // be required to implement them, and that makes me feel better about our odds.
- // TODO: When implementing these methods: pay attention to preventing aliasing of WideFraction parameters!
- // Also make sure that things like $this->cmp($other) check for aliasing $this to &$other.
- // That will not only make things safer, but in a case like cmp, it can be an optimization:
- // an expression is always equal to itself and never less than or greataer than!
- //
- // ---=== Initialization Methods ===---
- // e.g. Object Construction and Template Instantiation
- // TODO: Move this function into a WideFractionTemplate class.
- // It needs to be a static method either way, because templates are
- // supposed to be memoized, and having to `new` them would
- // look wrong in the caller's code, even if the new object
- // were to reuse all of the info from a previous instantiation.
- public static function instantiate(
- string $namespace, string $base_name, string $instance_name,
- // named template parameters:
- int $numerator_bit_width,
- int $denominator_bit_width = 0, // Either this or $constant_denominator_int must be non-zero.
- int $constant_denominator_int = 0, // Either this or $denominator_bit_width must be non-zero.
- // TODO: constant_denominator_wide?
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- ) : void
- {
- TODO: IMPORTANT: This is, of course, necessary for the thing to work at all.
- }
- /**
- * Factory method for creating WideFractions with dynamic denominators.
- *
- * To make a WideFraction with a constant denominator,
- * see the WideFixedFraction class.
- *
- * @see WideFixedFraction
- */
- public static function from_int(
- // required template parameters:
- int $numerator_bit_width,
- int $denominator_bit_width = 0, // defaults to being the same thing as the numerator.
- // runtime parameter(s):
- int $numerator = 0,
- int $denominator,
- // optional template parameters:
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- ) : void
- {
- if ( 0 === $denominator_bit_width ) {
- $denominator_bit_width = $numerator_bit_width;
- }
- $template = WideFractionTemplate::instantiate(
- __NAMESPACE__,__CLASS__,
- numerator_width: $numerator_bit_width,
- denominator_bit_width: $denominator_bit_width,
- has_constant_config: $has_constant_config,
- signedness: $signedness,
- rounding_mode: $rounding_mode
- );
- $template_class_path = $template->class_path();
- $result = new $template_class_path($numerator,$denominator, signedness:$signedness, rounding_mode:$rounding_mode);
- throw new UnimplementedException(__CLASS__."::".__FUNCTION__." hasn't been implemented yet.");
- return $result;
- }
- // TODO: Given recent conversations, it might be a good idea to also
- // allow a percent sign (%) to go after any expressions in the string
- // handed to WideFraction::from_string(). This would allow us to
- // write percentage quantities in a natural way, which is helpful
- // for business programming, and possibly other things too.
- //
- // It miiight be a good idea to limit percentages to decimal sequences.
- // Because I have no idea what 0x93% would mean. Is it under or over 100%?
- // (If we DID do that, I'd be tempted to interpret 0x100% as equaling 1.000, and 0x80% as equaling 0.500)
- //
- // Sidenote: although I'm hoping to allow + and - signs to be used on
- // all types of literals (including binary, hex, octal, base-64),
- // I feel it IS important that it goes before any encoding prefix, ex: `0b, 0o, 0x, 0s64c_-n`.
- // E.g. this is OK: `-0xFF` while this is NOT ok: `0x-FF`.
- // (Because - is not a valid character in a hexadecimal sequence.)
- /**
- * The `WideFraction::from_string(...)` method constructs a WideFraction
- * by using strings to represent the `$numerator` and `$denominator`.
- *
- * If the `$denominator` is not specified, then the fractional part
- * of the value may be described using point notation, like so:
- * ```
- * // Decimal points
- * Testing::_assert(WideFraction::from_string(32, "1.24")->denominator_to_int(), 100);
- * Testing::_assert(WideFraction::from_string(32, "1.2")->denominator_to_int(), 10);
- * Testing::_assert(WideFraction::from_string(32, "1.")->denominator_to_int(), 1);
- *
- * // Hexadecimal points
- * Testing::_assert(WideFraction::from_string(32, "0x1B.F3")->denominator_to_int(), 256); // 0x100
- * Testing::_assert(WideFraction::from_string(32, "0x1B.F")->denominator_to_int(), 16); // 0x10
- * Testing::_assert(WideFraction::from_string(32, "0x1B.")->denominator_to_int(), 1); // 0x1
- *
- * // Binary points
- * Testing::_assert(WideFraction::from_string(32, "0b10.01")->denominator_to_int(), 4); // 0b100
- * Testing::_assert(WideFraction::from_string(32, "0b10.0")->denominator_to_int(), 2); // 0b10
- * Testing::_assert(WideFraction::from_string(32, "0b10.")->denominator_to_int(), 1); // 0b1
- * ```
- *
- * If the `$denominator` _is_ specified, then the fractional portion of
- * the numerator's string must not exceed the precision of the denominator.
- * Attempting to store something too precise will result in an exception being thrown. (TODO: define the exception. Pobably UnderflowSomethingSomething)
- *
- * The `$denominator`'s string may not use point notation. Putting a point
- * into the denominator string will result in an exception being thrown. (TODO: define the exception.)
- *
- * Future directions:
- * If it ever becomes convenient to store WideFractions as Base-64 encoded
- * strings, then it is entirely possible to expand the ::from_string method
- * to have that functionality.
- *
- * I envision base-64 functionality looking like this:
- *
- * In addition to PHP-style integer literals, this method supports
- * using base-64 encoded strings. The prefix for base-64 string literals
- * is as follows:
- * "0s64cXXYn"
- * Where the two characters XX represent the two characters used in the base-64
- * encoding to represent values beyond the reach of the 62 digits and
- * (English) latin characters. The Y represents the character used as padding
- * at the end of the base-64 encoding. All padding characters at the end
- * of the literal will be ignored, and it is not necessary to pad the literal.
- * The Y character may be omitted, in which case the padding character is
- * assumed to be '='.
- *
- * Examples of valid base-64 literals are as such:
- * ```
- * "0s64c,-=nGL0srrt-FV,sdemLeQ"
- * "0s64c,-=nGL0srrt-FV,sdemLeQ=="
- * "0s64c,-nGL0srrt-FV,sdemLeQ=="
- * "0s64c,-#nGL0srrt-FV,sdemLeQ##"
- * "0s64c-_#nGL0srrt_FV-sdemLeQ##"
- * "0s64c-&#nGL0srrt_FV&sdemLeQ##"
- * "0s64c-&#nGL0srrt_FV&sd.emLeQ##" // The dot represents a decimal point.
- * "0s64c.&#nGL0srrt_FV&sd.emLeQ##" // The dot is a base64 character, not a decimal point.
- * ```
- *
- * Note: using '.' as an encoding character will prevent it from being used
- * to represent fractional quantities in the literal. Such encodings can
- * still be used to construct WideFractions as long as the denominator
- * is specified. And depending on how the data is defined, this might
- * be the best way to do things; likewise, being able to represent fractional
- * quantities in base64 literals is of limited value, given that such strings
- * are likely to be difficult to understand with human intuition and sense
- * of scale.
- *
- * (Reminder: All of this stuff about base-64 is unimplemented,
- * and may remain that way until there is a reason to implement it.)
- */
- public static function from_string(
- // required template parameters:
- int $numerator_bit_width,
- int $denominator_bit_width = 0, // defaults to being the same thing as the numerator.
- // runtime parameter(s):
- string $numerator = "0",
- string $denominator = "", // May be elided if the `$numerator` string contains a point (ex: decimal point).
- // optional template parameters:
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- )
- {
- if ( 0 === $denominator_bit_width ) {
- $denominator_bit_width = $numerator_bit_width;
- }
- // TODO: Check to ensure the bitwidths are consistent with the numerator/denominator arguments.
- $template = WideFractionTemplate::instantiate(
- __NAMESPACE__,__CLASS__,
- numerator_width: $numerator_bit_width,
- denominator_bit_width: $denominator_bit_width,
- has_constant_config: $has_constant_config,
- signedness: $signedness,
- rounding_mode: $rounding_mode
- );
- $template_class_path = $template->class_path();
- $result = new $template_class_path(signedness:$signedness, rounding_mode:$rounding_mode);
- // TODO: String parsing functions
- $result->numerator_from_string($numerator);
- $result->denominator_from_string($denominator);
- return $result;
- }
- // TODO: Should I just dictate that the contents of the array always be 32-bit integers?
- // (I mean, PHP doesn't have a dedicated type for that, but they'll fit in 64-bit ints,
- // and it would mean that the caller's code doesn't have to special-path for the
- // different host system integer types.)
- /**
- * The intended use-case for this method is to allow the caller to construct
- * a WideFraction using binary data that was read from another data source.
- *
- * This method of construction will assume that the $numerator and $denominator's
- * elements are stored with native endianness (e.g. NO endianness conversions
- * will be performed inside this constructor).
- *
- * The caller should ensure that their data source's endianness agrees
- * with native endianness, or if it doesn't, perform an endiannes conversion
- * before calling this method.
- */
- public static function from_spl_array(
- // required template parameters:
- int $numerator_bit_width,
- int $denominator_bit_width = 0, // defaults to being the same thing as the numerator.
- // runtime parameter(s):
- int elem_width_bytes,
- \SplFixedArray $numerator, // Array of `int`, each element shall span `elem_width_bytes` bytes (upper bits are ignored, if they exist)
- \SplFixedArray $denominator, // Array of `int`, each element shall span `elem_width_bytes` bytes (upper bits are ignored, if they exist)
- // optional template parameters:
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- )
- {
- if ( 0 === $denominator_bit_width ) {
- $denominator_bit_width = $numerator_bit_width;
- }
- // TODO: Check to ensure the bitwidths are consistent with the numerator/denominator arguments.
- $template = WideFractionTemplate::instantiate(
- __NAMESPACE__,__CLASS__,
- numerator_width: $numerator_bit_width,
- denominator_bit_width: $denominator_bit_width,
- has_constant_config: $has_constant_config,
- signedness: $signedness,
- rounding_mode: $rounding_mode
- );
- $template_class_path = $template->class_path();
- $result = new $template_class_path(signedness:$signedness, rounding_mode:$rounding_mode);
- // TODO: Array unpacking functions
- $result->numerator_from_spl_array(elem_width_bytes, $numerator);
- $result->denominator_from_spl_array(elem_width_bytes, $denominator);
- return $result;
- }
- // Below `make` functions are obsolete.
- // I am hoping to name these things much nicer things, like
- // ::from_int, ::from_string, and ::from_spl_array.
- /**
- * Convenience function for constructing WideFractions that have a constant
- * denominator.
- *
- * The given `$bit_width` will become the numerator's bit width.
- * The denominator will not occupy any bits because it is constant.
- *
- * This version also accepts a numerator as an initial value.
- *
- * This function calls `make` after calculating the appropriate parameters.
- *
- * @see make
- */
- public static function make_fixed_N(
- // Runtime parameters:
- int $bit_width, int $numerator,
- // Constant that will inform template params:
- int $denominator,
- // Template parameters:
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- ) : WideFraction
- {
- return self::make(
- numerator:$numerator,
- numerator_bit_width:$bit_width,
- constant_denominator_int:$denominator,
- has_constant_config:$has_constant_config,
- signedness:$signedness,
- rounding_mode:$rounding_mode,
- );
- }
- /**
- * Convenience function for constructing WideFractions that have a constant
- * denominator.
- *
- * The given `$bit_width` will become the numerator's bit width.
- * The denominator will not occupy any bits because it is constant.
- *
- * This version of `make_fixed_*` will yield a WideFraction with
- * a numerator equal to 0.
- *
- * This function calls `make` after calculating the appropriate parameters.
- *
- * @see make
- */
- public static function make_fixed_0(
- // Runtime parameter:
- int $bit_width,
- // Constant that will inform template params:
- int $denominator,
- // Template parameters:
- bool $has_constant_config = false,
- int $signedness = Signedness::SIGNED,
- int $rounding_mode = RoundingMode::DEFAULT,
- ) : WideFraction
- {
- return self::make(
- numerator:0,
- numerator_bit_width:$bit_width,
- constant_denominator_int:$denominator,
- has_constant_config:$has_constant_config,
- signedness:$signedness,
- rounding_mode:$rounding_mode,
- );
- }
- // ---=== Configuration properties ===---
- /**
- * Constant property that returns whether the WideFraction's template instance
- * was instantiated with a constant denominator.
- *
- * If true, then attempting to alter the WideFraction's denominator will
- * result in a thrown exception.
- */
- public abstract function has_constant_denominator() : bool;
- /**
- * Constant property that returns whether the WideFraction's template instance
- * was instantiated with all configuration parameters being constants.
- *
- * If true, then properties like `signedness()` and `rounding_mode()` will
- * be considered constant, and attempt to set them will throw an exception.
- */
- public abstract function has_constant_config() : bool;
- /**
- * Returns the number of bits used to represent the numerator in this
- * WideFraction.
- *
- * This total will include the sign bit for fractions that are defined as signed.
- *
- * This is value is set at the time of template instantiation and may
- * not be changed (e.g. it is always constant).
- */
- public abstract function numerator_bit_width() : int;
- /**
- * Returns the number of bits used to represent the denominator in this
- * WideFraction.
- *
- * In the case of a constant denominator, the returned value will be
- * the minimum number of bits required to store the denominator.
- *
- * This is value is set at the time of template instantiation and may
- * not be changed (e.g. it is always constant).
- */
- public abstract function denominator_bit_width() : int;
- // (Implementations of config properties will vary depending on template parameters.)
- /**
- * Property for getting or setting the signedness of the WideFraction.
- *
- * The possible values for this property are enumerated
- * in the `Signedness` constant class.
- *
- * If the WideFraction's template instance uses constant configuration,
- * then attempting to set this will result in a thrown exception.
- *
- * This is always safe to call as a getter, to retrieve the WideFraction's
- * currently defined signedness.
- *
- * @See Signedness
- */
- public abstract function signedness(int? $new_signedness = null) : int;
- /**
- * Property for getting or setting the default rounding mode of the WideFraction.
- *
- * The possible values for this property are enumerated
- * in the `RoundingMode` constant class.
- *
- * If this is anything besides RoundingMode::DEFAULT, then passing
- * eliding the rounding mode argument from methods that take, or
- * passing RoundingMode::DEFAULT to said methods, will result in
- * the RoundingMode from this property being used instead.
- *
- * Passing a RoundingMode argument to individual methods/operations
- * will always override this.
- *
- * Graphically, the precedence for rounding modes goes like this:
- * ```
- * template parameter < class property < parameter default < explicit argument at method call
- * ```
- *
- * If the WideFraction's template instance uses constant configuration,
- * then attempting to set this will result in a thrown exception.
- *
- * This is always safe to call as a getter, to retrieve the WideFraction's
- * currently defined RoundingMode.
- */
- public abstract function rounding_mode(int? $new_rounding_mode = null) : int;
- // ---=== Internal Accessors ===---
- /**
- * Returns the number of PHP `int` members that are used to represent
- * this WideFraction's numerator.
- *
- * If the WideFraction is signed, then the most significant integer will be
- * the one that stores signing information. All of the others will be
- * treated internally as if they are unsigned.
- *
- * While this property is foundational for implementing arithmetic
- * operations, it is also very problematic to use in any code
- * that isn't explicitly aware of this particular template's
- * internal representation. As such, it is marked as `protected`,
- * so that various template instances may transit this information
- * between themselves, but it is guaranteed that this implementation
- * detail is not used anywhere else.
- */
- protected abstract function numerator_phpint_width() : int;
- /**
- * Returns the number of PHP `int` members that are used to represent
- * this WideFraction's denominator.
- *
- * In the case of a constant denominator, the returned value will be
- * the minimum number of PHP `int` variables required to store the denominator.
- * (This is actually an important choice, because it allows the template
- * to generate code that enumerates the WideFraction's denominator
- * dynamically whenever there is a width mismatch during arithmetic operations.)
- *
- * While this property is foundational for implementing arithmetic
- * operations, it is also very problematic to use in any code
- * that isn't explicitly aware of this particular template's
- * internal representation. As such, it is marked as `protected`,
- * so that various template instances may transit this information
- * between themselves, but it is guaranteed that this implementation
- * detail is not used anywhere else.
- */
- protected abstract function denominator_phpint_width() : int;
- /**
- * Returns and/or sets this WideFraction's `int` at the given index.
- *
- * The least significant `int` has index 0.
- * The most significant `int` has an index that is one less than `(numerator|denominator)_phpint_width()`.
- *
- * Attempting to get or set an `int` that is out of bounds (less than 0
- * or greater than or equal to `*_phpint_width()`) will result
- * in undefined behavior. (It is possible that no attempt will be made
- * to check for this condition, if it is efficient to do it that way.)
- */
- protected abstract function get_numerator_phpint_at(int $index) : int;
- protected abstract function set_numerator_phpint_at(int $index, int $value) : int; /** ditto */
- protected abstract function get_denominator_phpint_at(int $index) : int; /** ditto */
- protected abstract function set_denominator_phpint_at(int $index, int $value) : int; /** ditto */
- // ---=== Comparison operations ===---
- // TODO: Should there be convenience functions for operations between
- // WideFractions and floating point numbers?
- //
- // It would certainly make the WideFraction type more versatile,
- // but I fear that this will lead to computational inaccuracies as
- // it encourages the use of float literals, which tend to be decimal
- // formatted despite the type being unable to represent most decimal
- // fractions (ugh why).
- //
- // Perhaps a better approach would be to allow string literals on
- // one side of the operation. This would then be convenience syntax
- // for constructing a WideFraction with the same template instance
- // and other attributes as one of the WideFractions involved
- // (probably just use the $this fraction to have a simple rule?)
- // but with a numerator as given by the string.
- // The denominator would be some power of 10, and would, by default,
- // be inherited from the ($this) fraction, as speculated above.
- // However, if the precision in the string literal is lower, then
- // the integer will be assumed to have the lower precision.
- // (That probably usually won't help anything, but for narrowing
- // operations it will allow the string literal to have a higher
- // whole number magnitude whenever the possibility is avialable.)
- /**
- * `cmp` returns -1, 0, or 1 based on a comparing this WideFraction
- * against another number whose parameter is named "$other".
- *
- * `cmp` is defined such that, in shorthand notation, these are true:
- * * a < b implies cmp(a,b) < 0
- * * a > b implies cmp(a,b) > 0
- * * a == b implies cmp(a,b) == 0.
- * In the case of the below comparison methods, the `a` is this WideFraction
- * object and `b` is the `$other` parameter.
- *
- * Usage of `cmp` looks like this:
- * ```PHP
- * $foo = WideFraction::make_fixed_s(32, "4.45");
- * $bar = 4;
- * $qux = 5;
- * Testing::_assert($foo->cmp_int($bar) > 0); // 4.45 > 4
- * Testing::_assert($foo->cmp_int($qux) < 0); // 4.45 < 5
- *
- * $foo = WideFraction::make_fixed_s(32, "4.45");
- * $bar = "4.0";
- * $baz = "4.5";
- * $qux = "5.0";
- *
- * Testing::_assert($foo->cmp_str($bar) > 0); // 4.45 > 4.0
- * Testing::_assert($foo->cmp_str($baz) > 0); // 4.45 < 4.5
- * Testing::_assert($foo->cmp_str($qux) < 0); // 4.45 < 5.0
- *
- * $foo = WideFraction::make_fixed_s(32, "4.45");
- * $bar = WideFraction::make_fixed_s(32, "4.0");
- * $baz = WideFraction::make_fixed_s(32, "4.5");
- * $qux = WideFraction::make_fixed_s(32, "5.0");
- *
- * Testing::_assert($foo->cmp($bar) > 0); // 4.45 > 4.0
- * Testing::_assert($foo->cmp($baz) > 0); // 4.45 < 4.5
- * Testing::_assert($foo->cmp($qux) < 0); // 4.45 < 5.0
- * ```
- *
- * The `cmp` method has variations that accept different right-hand-side (RHS)
- * and variations that assert or inform signdedness. These variations are
- * notated with letters in the prefixes of the method names.
- *
- * The notation for prefixes is as follows:
- * * 'i': The operation accepts an integer (`int` type) argument.
- * * 'w': The operation accepts a (W)ideFraction argument.
- * * 'l': The operation accepts a string (L)iteral argument. ('l' is used instead of 's' to avoid ambiguity with the signed prefix.)
- * * 'u': The operation is unsigned. Values (of the numerator) between `0x8000...0000` and `0xFFFF...FFFF` are assumed to be positive.
- * * 's': The operation is signed. Values (of the numerator) between `0x8000...0000` and `0xFFFF...FFFF` are assumed to be negative.
- *
- * The other comparison operations are `eq`, `ne`, `lt`, `gt`, `lte`, and `gte`,
- * which respectively stand for "EQuals", "Not Equals", "Less Than",
- * "Greater Than", "Less Than or Equal to", and "Greater Than or Equal to".
- *
- * These comparisons simply work as one might expect and directly
- * return a boolean value, so they are more convenient than calling
- * `cmp` in most cases (at the expense of making it more difficult to
- * forward all comparison information into other operations).
- *
- * As with `cmp`, there are variations that take different right-hand-side (RHS)
- * operand types and variations that assert or inform signdedness.
- *
- * The non-prefixed versions of these methods are defined as the "default"
- * methods for comparison, in the sense that they will choose a reasonable
- * default when they are called. Specifically, they use some logic to choose
- * an appropriate underlying prefixed version to use, based on the
- * signedness setting of the operands.
- *
- * The logic used to determine which comparison is used, goes as follows:
- * These methods will perform a signed comparison if both operands are signed.
- * And each will perform an unsigned comparison if both operands are unsigned.
- * If operands do not match in signedness (type-wise), then their sign bits
- * are examined:
- * If both are equal, then a comparison is performed (sign doesn't matter).
- * If one sign bit is set and another not, then a WideOverflowException is thrown.
- * If both operands are ambiguously signed WideFractions, then these will act
- * as signed comparisons.
- * If one operand is ambiguously signed, the comparison done will have
- * the signedness of the non-ambiguous type.
- */
- public abstract function cmp(WideFraction $other) : int;
- public abstract function eq(WideFraction $other) : bool; /** @see cmp */
- public abstract function ne(WideFraction $other) : bool; /** @see cmp */
- public abstract function lt(WideFraction $other) : bool; /** @see cmp */
- public abstract function gt(WideFraction $other) : bool; /** @see cmp */
- public abstract function lte(WideFraction $other) : bool; /** @see cmp */
- public abstract function gte(WideFraction $other) : bool; /** @see cmp */
- /**
- * These comparisons are default comparisons that allow convenient comparing
- * of the WideFraction type against the builtin `int` type.
- *
- * In these comparisons, the `int` operand is treated as being ambiguously signed.
- * This means that the comparison will have the signedness of the left operand
- * (the WideFraction or $this/self object).
- * If the left operand is sign-ambiguous, then the comparison will
- * be performed as if both operands were signed.
- *
- * @see cmp
- */
- public abstract function cmp_int(int $other) : int;
- public abstract function eq_int(int $other) : bool; /** @see cmp_int */
- public abstract function ne_int(int $other) : bool; /** @see cmp_int */
- public abstract function lt_int(int $other) : bool; /** @see cmp_int */
- public abstract function gt_int(int $other) : bool; /** @see cmp_int */
- public abstract function lte_int(int $other) : bool; /** @see cmp_int */
- public abstract function gte_int(int $other) : bool; /** @see cmp_int */
- /**
- * These comparisons do a default comparison of this WideFraction object
- * against string literals that are parsed as being a WideFraction of the
- * same type as the left operand (the WideFraction or $this/self object).
- *
- * The `l` prefix is used instead of `s` to prevent confusion with
- * signed versions of the comparison operators. It stands for (L)iteral,
- * as in "string Literal".
- *
- * In these comparisons:
- * The string operand will be treated as ambiguously-signed if there is no
- * sign present in the string and the string is a decimal string (not hex, octal, or binary).
- * The string operand will be treated as signed if the string leads with
- * a sign (either the '-' or the '+' character). (Signs on exponents do not count.)
- * The string operand will be treated as unsigned if the string is given
- * as any non-decimal literal (such as hexadecimals starting with '0x',
- * octals starting with '0o', or binary literals starting with '0b')
- * and does not have a leading sign character.
- *
- * The comparison then proceeds as it would for the default WideFraction
- * to WideFraction comparisons.
- *
- * This means that the comparison will have the signedness of the left operand
- * (the WideFraction, $this/self).
- * If the left operand is sign-ambiguous, then the comparison will
- * be performed as if both operands were signed.
- *
- * @see cmp
- */
- public abstract function cmp_str(string $other) : int;
- public abstract function eq_str(string $other) : bool; /** @see cmp_str */
- public abstract function ne_str(string $other) : bool; /** @see cmp_str */
- public abstract function lt_str(string $other) : bool; /** @see cmp_str */
- public abstract function gt_str(string $other) : bool; /** @see cmp_str */
- public abstract function lte_str(string $other) : bool; /** @see cmp_str */
- public abstract function gte_str(string $other) : bool; /** @see cmp_str */
- // TODO: versions with string-typed $other.
- /** (U)nsigned WideFraction comparisons */
- public abstract function ucmp(WideFraction &$other) : int;
- public abstract function ueq(WideFraction &$other) : bool; /** @see ucmp */
- public abstract function une(WideFraction &$other) : bool; /** @see ucmp */
- public abstract function ult(WideFraction &$other) : bool; /** @see ucmp */
- public abstract function ugt(WideFraction &$other) : bool; /** @see ucmp */
- public abstract function ulte(WideFraction &$other) : bool; /** @see ucmp */
- public abstract function ugte(WideFraction &$other) : bool; /** @see ucmp */
- /** (U)nsigned integer comparisons */
- public abstract function ucmp_int(int $other) : int;
- public abstract function ueq_int(int $other) : bool; /** @see ucmp_int */
- public abstract function une_int(int $other) : bool; /** @see ucmp_int */
- public abstract function ult_int(int $other) : bool; /** @see ucmp_int */
- public abstract function ugt_int(int $other) : bool; /** @see ucmp_int */
- public abstract function ulte_int(int $other) : bool; /** @see ucmp_int */
- public abstract function ugte_int(int $other) : bool; /** @see ucmp_int */
- /** (U)nsigned string comparisons */
- public abstract function ucmp_str(string $other) : int;
- public abstract function ueq_str(string $other) : bool; /** @see ucmp_str */
- public abstract function une_str(string $other) : bool; /** @see ucmp_str */
- public abstract function ult_str(string $other) : bool; /** @see ucmp_str */
- public abstract function ugt_str(string $other) : bool; /** @see ucmp_str */
- public abstract function ulte_str(string $other) : bool; /** @see ucmp_str */
- public abstract function ugte_str(string $other) : bool; /** @see ucmp_str */
- /** (S)igned WideFraction comparisons */
- public abstract function scmp(WideFraction &$other) : int;
- public abstract function seq(WideFraction &$other) : bool; /** @see scmp */
- public abstract function sne(WideFraction &$other) : bool; /** @see scmp */
- public abstract function slt(WideFraction &$other) : bool; /** @see scmp */
- public abstract function sgt(WideFraction &$other) : bool; /** @see scmp */
- public abstract function slte(WideFraction &$other) : bool; /** @see scmp */
- public abstract function sgte(WideFraction &$other) : bool; /** @see scmp */
- /** (S)igned integer comparisons */
- public abstract function scmp_int(int $other) : int;
- public abstract function seq_int(int $other) : bool; /** @see scmp_int */
- public abstract function sne_int(int $other) : bool; /** @see scmp_int */
- public abstract function slt_int(int $other) : bool; /** @see scmp_int */
- public abstract function sgt_int(int $other) : bool; /** @see scmp_int */
- public abstract function slte_int(int $other) : bool; /** @see scmp_int */
- public abstract function sgte_int(int $other) : bool; /** @see scmp_int */
- /** (S)igned string comparisons */
- public abstract function scmp_str(string $other) : int;
- public abstract function seq_str(string $other) : bool; /** @see scmp_str */
- public abstract function sne_str(string $other) : bool; /** @see scmp_str */
- public abstract function slt_str(string $other) : bool; /** @see scmp_str */
- public abstract function sgt_str(string $other) : bool; /** @see scmp_str */
- public abstract function slte_str(string $other) : bool; /** @see scmp_str */
- public abstract function sgte_str(string $other) : bool; /** @see scmp_str */
- // ---=== Arithmetic operations ===---
- public abstract function add(WideFraction &$a, WideFraction &$b, WideFraction? &$c = null, WideFraction? &$d = null) : self;
- public abstract function add_ww(WideFraction &$a, WideFraction &$b, WideFraction? &$c = null, WideFraction? &$d = null) : self;
- public abstract function add_wi(WideFraction &$a, int $b) : self;
- public abstract function add_iw(int $a, WideFraction &$b) : self;
- public abstract function add_ii(int $a, int $b) : self;
- public abstract function increment_by(WideFraction &$b) : void;
- public abstract function increment() : void;
- public abstract function incr_by(int $b) : void;
- public abstract function incr() : void; // Should do same thing as `increment()`.
- public abstract function subtract(WideFraction &$a, WideFraction &$b) : self;
- public abstract function decrement_by(WideFraction &$b) : void;
- public abstract function decrement() : void;
- public abstract function decr_by(int $b) : void;
- public abstract function decr() : void;
- public abstract function negate() : void;
- public abstract function multiply(WideFraction &$a, WideFraction &$b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function mult(WideFraction &$a, int $b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function multiply_by(WideFraction &$b, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function mult_by(int $b, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function divide(WideFraction &$numerator, WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function div(WideFraction &$numerator, int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function divide_by(WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function div_by(int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function modulus(WideFraction &$numerator, WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function mod(WideFraction &$numerator, int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function modulus_by(WideFraction &$denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function mod_by(int $denominator, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function divide_and_modulus(WideFraction &$numerator, WideFraction &$denominator, WideFraction &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function div_n_mod(WideFraction &$numerator, int $denominator, int &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : self;
- public abstract function divide_and_modulus_by(WideFraction &$denominator, WideFraction &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function div_n_mod_by(int $denominator, int &$out_remainder, int $rounding_mode = RoundingMode::DEFAULT) : void;
- public abstract function inverse_of(WideFraction &$a) : self;
- public abstract function invert() : self;
- // ---=== Logical operations ===---
- /**
- * Extracts the sign bit of the numerator.
- *
- * Note that WideFraction is defined in such a way that the denominator
- * is never negative. The entire fraction is what has a sign, and the
- * sign is stored in the numerator.
- */
- public abstract function sign() : int
- // TODO: This documentation really belongs on the `divide` method.
- // It was written for an earlier version of the method too, so it
- // probably needs editing and correction to make it accurate to the
- // current divide method.
- /*
- * Performs a fixed-point division to evaluate `$numerator / $denominator`.
- *
- * The sense of "fixed-point" means that the intended use-case for this
- * method is for when its operands are being used like "fixed-point"
- * numbers, albeit in an arbitrary base decided by the caller.
- *
- * Examples of fixed-point behavior:
- *
- * $n = WideFraction::make_fixed_0i(64, 100); // 64-bit wide number with 2 decimal digits of precision.
- * $d = WideFraction::make_fixed_0i(64, 100); // ditto
- * $q = WideFraction::make_fixed_0i(64, 100); // ditto
- * $t = WideFraction::make_fixed_0i(64, 100); // ditto
- *
- * $q->divide($n->from_decimal("560.56"), $d->from_decimal("12.32"), $q);
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("45.50"));
- *
- * $q->divide($n->from_decimal("1.01"), $d->from_decimal("1.01"), $q);
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.02")); // Truncated from 1.0201
- *
- * $q->divide($n->from_decimal("1.09"), $d->from_decimal("1.09"), $q);
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.18")); // Truncated from 1.1881
- *
- * $q->divide($n->from_decimal("1.09"), $d->from_decimal("1.09"), RoundingMode::BANKERS);
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.19")); // Rounded from 1.1881
- *
- * It works for other bases too:
- *
- * $n = WideFraction::make_fixed_0i(64, 0x100 ); // 0x100 == 256: 64-bit wide number with 2 HEXadecimal digits of precision.
- * $d = WideFraction::make_fixed_0i(64, 0x100 ); // 0x100 == 256, ditto
- * $q = WideFraction::make_fixed_0i(64, 0x100 ); // 0x100 == 256, ditto
- * $t = WideFraction::make_fixed_0i(64, 0x100 ); // 0x100 == 256, ditto
- *
- * $q->divide($n->from_hexadecimal("DC40A.17"), $d->from_hexadecimal("348.C7"));
- * Testing::_assert(WideFraction::equals($q, $t->from_hexadecimal("4.31"));
- *
- * $q->divide($n->from_decimal("1.01"), $d->from_decimal("1.01"));
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.02")); // Truncated from 1.0201
- *
- * $q->divide($n->from_decimal("1.0F"), $d->from_decimal("1.0F"));
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.1E")); // Truncated from 1.1EE1
- *
- * $q->divide($n->from_decimal("1.0F"), $d->from_decimal("1.0F"), RoundingMode::BANKERS);
- * Testing::_assert(WideFraction::equals($q, $t->from_decimal("1.1F")); // Rounded from 1.1EE1
- */
- }
- ?>
- <?php
- /*
- declare(strict_types=1);
- namespace Kickback\Common\Math\Types;
- use \Kickback\Common\Math\Types\WideFraction;
- use \Kickback\Common\Math\RoundingMode;
- */
- // TODO: Would this even be abstract?
- // I mean, the WideFraction template might end up with things like
- // compile-time signedness or default rounding mode, so it would make sense
- // to templatize this class with those parameters as well (just not denominator).
- // But if we needed a WideInteger type in a hurry, we could chose some
- // mandatory parameters initially, and then implement the template later.
- abstract class WideInteger extends WideFraction
- {
- // TODO:
- // * Restrict inherited constructors to only ones that don't accept fractional elements.
- // * Somehow ensure that all other default construction will not have a denominator?
- // * Will there even be any integer-specific operations?
- // * Implement WideInteger template that overrides some WideFraction
- // methods whenever they can be optimized for the denominator-of-1 case.
- // * This class would presumably be for allowing functions to declare
- // WideInteger parameters (thus rejecting anything with fractional quantities).
- // This would allow the caller to declare enforcable intent in their
- // own functions and methods.
- // * The other use for this class, secondary to the above, is for
- // opportunistic optimization of the D=1 case.
- // This is, to say the least, pretty low priority (at the moment).
- // * [There was something that was going to be put here, but brain did not complete the operation. Darn.]
- }
- ?>
- <?php
- //declare(strict_types=1);
- // Unittest driver to be run from command-line interface.
- /*
- use \Kickback\Common\Math\LowLevel\WideInt; // TODO: Make this template exist.
- use \Kickback\Common\Math\Types\WideFraction;
- */
- function unittest_all() : void
- {
- WideInt::unittest();
- WideFraction::unittest();
- }
- unittest_all();
- ?>
Add Comment
Please, Sign In to add comment