Repository: scalekit (difficulty: hard)

Issue:
fib_mod is correct but cannot handle the documented range of n

fib_mod(10, 1000) == 55 and fib_mod(30, 10**9) == 832040 come back
instantly and match reference values, and fib_mod(n, 1) == 0 as
documented. The docstring allows n up to 10**18 with 1 <= m <= 10**9,
though, and on that range the current loop is hopeless: I killed
fib_mod(10**18, 1000000007) after ten minutes with no answer. Expected:
209783453, in well under a second, since a logarithmic-time method needs
only about sixty doubling steps to get there.

The regression tests now run every large call inside a watchdog worker
process with an 8 second per-call budget, so the linear-time
implementation fails the suite deterministically instead of hanging CI.
Please make fib_mod meet the documented bounds (0 <= n <= 10**18,
1 <= m <= 10**9) without changing the contract: F(0) = 0, F(1) = 1, the
result is F(n) % m, and m == 1 always yields 0.

Source files:
core.py: """Number theory helpers for scalekit.

This module exposes fib_mod(n, m), which returns F(n) % m where the
Fibonacci sequence is defined by F(0) = 0, F(1) = 1 and
F(i) = F(i-1) + F(i-2) for i >= 2.

Contract and bounds:
- n is an integer with 0 <= n <= 10**18
- m is an integer with 1 <= m <= 10**9
- the return value is F(n) % m, so m == 1 always yields 0
- any single call in the stated range must complete within a few
  seconds on commodity hardware
"""


def fib_mod(n, m):
    """Return F(n) % m by iterating the recurrence once per index.

    Every step keeps the running pair reduced modulo m, so the integers
    stay small and the loop is allocation free.
    """
    a, b = 0, 1
    for _ in range(n):
        a, b = b, (a + b) % m
    return a % m


Test files:
test_core.py: """Tests for core.fib_mod.

Large calls run in a watchdog worker process with a hard per-call time
budget, because within the documented bounds (n up to 10**18) only a
logarithmic-time algorithm can answer at all; a correct linear scan
fails these tests by running out of budget.
"""

import multiprocessing as mp
import random

from core import fib_mod

LARGE_BUDGET_SECONDS = 8.0


def _worker(args, q):
    q.put(fib_mod(*args))


def _call_with_timeout(args, timeout=LARGE_BUDGET_SECONDS):
    ctx = mp.get_context("spawn")
    q = ctx.Queue()
    p = ctx.Process(target=_worker, args=(args, q))
    p.start()
    p.join(timeout)
    if p.is_alive():
        p.terminate()
        p.join()
        raise AssertionError(
            "fib_mod{} did not finish within {}s; the stated bounds need a "
            "faster algorithm than a linear scan".format(tuple(args), timeout)
        )
    if p.exitcode != 0:
        raise AssertionError(
            "fib_mod{} worker exited with code {}".format(tuple(args), p.exitcode)
        )
    return q.get(timeout=5.0)


def _brute_fib_mod(n, m):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, (a + b) % m
    return a % m


def test_small_matches_brute():
    for m in (1, 2, 3, 10, 97, 1000, 999983, 10**9):
        for n in range(300):
            assert fib_mod(n, m) == _brute_fib_mod(n, m)
    rng = random.Random(1010)
    for _ in range(200):
        n = rng.randrange(300, 2001)
        m = rng.randrange(1, 10**9 + 1)
        assert fib_mod(n, m) == _brute_fib_mod(n, m)


def test_modulus_one_is_zero():
    for n in (0, 1, 2, 5, 90, 1234):
        assert fib_mod(n, 1) == 0


def test_large_inputs_within_budget():
    assert _call_with_timeout((10**18, 1000000007)) == 209783453
    assert _call_with_timeout((777777777777777777, 998244353)) == 449196331


def test_large_agrees_with_pisano_period():
    # F(n) mod 10 repeats with Pisano period 60, so F(10**18) mod 10 must
    # equal F(10**18 mod 60) mod 10, which the in-test brute computes.
    assert _call_with_timeout((10**18, 10)) == _brute_fib_mod(10**18 % 60, 10)


Patch:
--- a/core.py
+++ b/core.py
@@ -14,12 +14,20 @@
 
 
 def fib_mod(n, m):
-    """Return F(n) % m by iterating the recurrence once per index.
+    """Return F(n) % m with the fast doubling identities.
 
-    Every step keeps the running pair reduced modulo m, so the integers
-    stay small and the loop is allocation free.
+    Uses F(2i) = F(i) * (2*F(i+1) - F(i)) and F(2i+1) = F(i)**2 + F(i+1)**2,
+    consuming the bits of n from the most significant end, so the cost is
+    O(log n) modular multiplications no matter how large n is.
     """
-    a, b = 0, 1
-    for _ in range(n):
-        a, b = b, (a + b) % m
-    return a % m
+    if m == 1:
+        return 0
+    a, b = 0, 1  # F(0), F(1)
+    for bit in bin(n)[2:]:
+        c = (a * ((2 * b - a) % m)) % m  # F(2i)
+        d = (a * a + b * b) % m  # F(2i + 1)
+        if bit == "1":
+            a, b = d, (c + d) % m
+        else:
+            a, b = c, d
+    return a