The Multiplicative Tier: Geometric Scaling and 64-bit Synergy in Mercury

In our last post, we explored the Additive Tier, establishing how Mercury handles direct linear steps. Today, we step up to the second level of the mathematical architecture: The Multiplicative Tier.

At this tier, we move away from simple counting and enter the realm of geometric scaling.

The Theory of Geometric Scaling

It is common to teach multiplication simply as “fast addition”—and while that is functionally true for small integers, it is the wrong mental model for arbitrary-precision engines. Multiplication applies one value as a scale to the entire magnitude of another.

Because multiplication is symmetrical (the order of factors does not change the product), its inverse operation is used to solve for either of the original variables:

$$C = A \cdot B \implies B = \frac{C}{A} \quad \text{and} \quad A = \frac{C}{B}$$

In our Sigma Language notation, we express this structural relationship linearly:

  • C = A * B; (Multiplicative combination)
  • B = C / A; (Solving for the right ratio)
  • A = C / B; (Solving for the left ratio)

Just as subtraction was the tool to find a missing additive difference, division is strictly the tool to find the missing scale. Before we can look at division, however, we need to look at how Mercury handles that massive geometric expansion in silicon.

32-bit Places in a 64-bit World

When Mercury hands a scaling operation off to mercuryAbsMul, the elegance of using a base-$2^{32}$ positional format shines through.

If we multiply two full 32-bit places together, the largest possible product is still less than 2^64, so a 64-bit register can hold the entire intermediate product. Because modern processors feature 64-bit Arithmetic Logic Units (ALUs), we can multiply two base-$2^{32}$ “places” together and capture the entire result natively, without overflowing the hardware register.

Here is the inner engine of mercuryAbsMul that performs this feat:

// Iterate through the places of variable 'b'
for (int bi = bs; bi <= br; bi++) {
    uint bx = b[2 + bi + bq]; // Load the current place of 'b'
    
    int ai;
    ulong reg = 0; // The 64-bit accumulator register

    // Multiply against the places of variable 'a'
    for (ai = as; ai <= ar; ai++) {
        int place = ai + bi - adj;

        if (place >= 0) {
            uint ax = a[2 + ai + aq]; // Load the current place of 'a'

            // Multiply the two 32-bit places, add the carry, and add the existing scratch value
            reg += (ulong) bx * (ulong) ax + scratch[place];

            // Store the bottom 32 bits as the answer for this place
            scratch[place] = (uint) reg;
            
            // Shift the register right to push the top 32 bits forward as the carry
            reg >>= 32;
        } else {
            reg = 0;
        }
    }
    
    // Process any remaining carry for this row
    int place = ai + bi - adj;
    if (place >= 0) {
        reg += scratch[place];
        scratch[place] = (uint) reg;
    }
}

The Accumulator and Scratch Staging

The magic happens inside that innermost for loop. We are not just multiplying bx and ax. We are accumulating three distinct values into our 64-bit reg:

  1. The product of the two current 32-bit places.
  2. The carry over from the previous place.
  3. The value already sitting in the scratch array from earlier multiplication passes.

Once those are added together, the logic mirrors our Additive Tier perfectly. The bottom 32 bits are saved to the scratch array (scratch[place] = (uint) reg;), and the top 32 bits are shifted forward for the next loop (reg >>= 32;).

Once again, by staging all of this geometric expansion safely inside the scratch stack, Mercury ensures that the output variable (val) is never corrupted mid-calculation.

Why Two 32-bit Places Need 64 Bits

There is a deeper reason that Mercury uses a 64-bit register for multiplication. It is not only because the hardware happens to provide one. It is because multiplication combines magnitudes, and the size of the result is measured by adding the places of the factors.

In a positional number system, a digit does not stand alone. A digit at place i represents:

aᵢ × B^i

and a digit at place j represents:

bⱼ × B^j

When those two places are multiplied, their coefficients multiply, but their places add:

(aᵢ × B^i) × (bⱼ × B^j)
= (aᵢ × bⱼ) × B^(i + j)

That addition in the exponent is not a coincidence. The Multiplicative Tier is built on top of the Additive Tier. Multiplication scales magnitudes, but the placement of the resulting product is still governed by additive displacement.

For Mercury, the base is B = 2^32. Each place can hold any value from 0 to 2^32 - 1. The largest possible single-place product is therefore:

(2^32 - 1) × (2^32 - 1)
= 2^64 - 2^33 + 1

That value is smaller than 2^64, so it fits completely inside an unsigned 64-bit register. Mercury can multiply two full 32-bit places without losing a single bit of the product.

Once the product is staged in the 64-bit register, Mercury follows the same carry discipline introduced in the Additive Tier: the low 32 bits become the current output place, and the high 32 bits move forward as carry.

The Bridge to Division

If multiplication is scaling up by multiplying 32-bit places natively, division is the process of scaling down.

However, division is notoriously expensive for a CPU. Instead of guessing factors and repeatedly subtracting (which would take an eternity for arbitrary-precision numbers), Mercury uses a brilliant algorithmic shortcut to solve the inverse ratio: the nibble-sized precomputed table.

In the next post, we will look at how mercuryAbsDiv pre-computes the entire search space of the dividend, turning massive division problems into highly efficient lookup-and-subtract loops.

 

JWCEssentials on GitHub

JWCEssentials/C/Mercury/Mercury.c

Carrying the One in Base 232: Why Mercury Addition Works

Mercury does not abandon grade-school arithmetic. It changes the size of the digit.

That one idea explains a surprising amount of the Mercury arbitrary-precision engine. When we work in decimal, each digit holds a value from 0 to 9. When a column grows too large, we keep the part that belongs in the current column and carry the rest into the next one.

Mercury does exactly the same thing. The only difference is that Mercury’s “digits” are not decimal digits. They are 32-bit unsigned integer limbs.

In other words, Mercury works in base 232.

Every Base Has Columns

In base 10, the largest single digit is 9. If we add:

9 + 9 + 1 = 19

the current column keeps the 9, and the next column receives a carry of 1.

In base 16, the largest single digit is F, which is decimal 15. If we add:

F + F + 1 = 1F

the current column keeps F, and the next column receives a carry of 1.

Mercury uses the same rule, just with much larger digits:

0xFFFFFFFF + 0xFFFFFFFF + 1 = 0x1FFFFFFFF

The current 32-bit limb keeps:

0xFFFFFFFF

and the next limb receives:

1

That is all a carry is.

Why the Carry Can Only Be 0 or 1

This is not a special trick of binary computers. It is a property of positional number systems.

In any base B, a single digit can only range from:

0 to B - 1

When adding two digits plus an incoming carry, the largest possible value is:

(B - 1) + (B - 1) + 1 = 2B - 1

That means the result may cross into the next column, but it can only cross once. The outgoing carry can only be 0 or 1.

For Mercury, the base is:

B = 2^32

So the largest possible single-limb addition is:

(2^32 - 1) + (2^32 - 1) + 1 = 2^33 - 1

That result needs only 33 bits. A 64-bit register has more than enough room to hold it safely.

The Mercury Addition Loop

This is why Mercury can use a 64-bit register to add two 32-bit limbs:

ulong reg = 0;

for (; i <= bh && i <= h; i++) {
    reg += ((slong) a[2+i-al] + (slong) b[2+i-bl]);

    scratch[i - l] = (uint) reg;

    reg >>= 32;
}

The low 32 bits become the output limb:

scratch[i - l] = (uint) reg;

Then Mercury shifts the register right by 32 bits:

reg >>= 32;

That removes the limb we already stored and leaves only the carry for the next position.

If there was no overflow, the carry is 0. If the limb overflowed, the carry is 1.

That is the whole secret: Mercury uses the processor’s native 64-bit arithmetic as a temporary workspace for base-232 digit arithmetic.

Borrowing Is the Same Story in Reverse

Subtraction follows the same positional logic, but instead of carrying forward a positive overflow, it carries forward a borrow.

Consider the smallest possible underflow in one limb:

0x00000000 - 0x00000001

The current limb does not have enough value to complete the subtraction. So it borrows one unit from the next limb. But one unit in the next limb is worth exactly 232 in the current limb.

So the calculation becomes:

0x100000000 - 0x00000001 = 0xFFFFFFFF

The current limb receives:

0xFFFFFFFF

and the next limb receives a borrow of:

-1

The Mercury Subtraction Loop

That is why Mercury’s subtraction loop uses a signed 64-bit register:

slong reg = 0;

for (; i <= bh && i <= h; i++) {
    reg += ((slong) a[2+i-al] - (slong) b[2+i-bl]);

    if (reg < 0) {
        reg += 0x100000000LL;
        scratch[i - l] = (uint) reg;
        reg = -1;
    } else {
        scratch[i - l] = (uint) reg;
        reg = 0;
    }
}

The signed register makes the borrow visible immediately. If reg drops below zero, Mercury knows the current limb had to borrow from the next place.

To repair the current limb, Mercury adds back:

0x100000000

which is exactly 232. Then it stores the repaired 32-bit limb and carries -1 forward into the next position.

This is not a shortcut around subtraction. It is subtraction in positional notation, written directly in the machine’s natural word size.

The Limb Train

A helpful way to picture this is as a train of limbs. Each car holds one base-232 digit:


[00000001] [FFFFFFFF] [FFFFFFFF]
                    + [00000001]
-------------------------------------
[00000002] [00000000] [00000000]

The rightmost limb overflows first. It rolls from FFFFFFFF to 00000000 and sends a carry to the next limb. That limb also rolls over and sends a carry onward. Finally, the left limb receives the carry and increases from 00000001 to 00000002.

This is the same thing that happens in decimal when:

1999 + 1 = 2000

Mercury simply does it in a base where each digit contains 32 bits of information.

Bounded Overflow

This gives Mercury a very useful design property: bounded overflow.

For addition, the bounds are simple:

32-bit limb + 32-bit limb + carry = at most 33 bits

For multiplication, the next tier, the bounds are larger:

32-bit limb * 32-bit limb = at most 64 bits

That is why base 232 is such a natural foundation for this engine. Addition has room to carry. Multiplication has room to stage a limb product. The machine’s native 64-bit arithmetic becomes the safe workbench for 32-bit arbitrary-precision digits.

Why This Matters

The Additive Tier may look simple, but it establishes the discipline that the rest of Mercury builds on.

A large number is not magic. It is a sequence of bounded digits.

A carry is not magic. It is the part of a column that no longer fits.

A borrow is not magic. It is one unit from the next column, reinterpreted in the current column’s base.

Once those ideas are clear, the rest of arbitrary-precision arithmetic becomes much less mysterious. Multiplication is not a different universe. It is the next tier: many limb products, many carries, and the same positional logic scaled upward.

Mercury does not abandon grade-school arithmetic. It changes the size of the digit.

Up Next

The Additive Tier gives us a stable, linear foundation. Next time, we will step up to the Multiplicative Tier, where we move into geometric scaling and explore how decimal long division perfectly explains Mercury’s nibble-sized pre-multiplication table.

Also, how does multiplying two 32bit values produce precisely 64bits?

JWCEssentials on GitHub

JWCEssentials/C/Mercury/Mercury.c