lumbda/quantum/mod-solinas.lsp
russell@unturf.com 1665893321
factory + quantum + sweep-doctrine: AGPLv3 share-back from foxhop ecdsa
29 new files publish factory infra (V2 autoscaler with live VRAM
sampling + EWMA peak tracking, HUGE solo-dispatch, two-tier DLQ/rDLQ
classifier + retry), general quantum circuit primitives (Cuccaro
ripple-carry adder, Clifford gate library, Clifford tableau simulator,
mod-arith family, dialog GCD reversible inverse, Karatsuba multiplier,
Solinas fast reduction), and a TCRAUDT reducer harness. Originally
developed in ~/git/www.foxhop.net/ecdsa/ for secp256k1 attack-surface
research; published upstream as obligated by AGPLv3.

Parametrization contract at factory/CONTRACT.md. Consumers export
LUMBDA_REPO_DIR + LUMBDA_QUEUE_DIR + LUMBDA_BACKEND_CMD + LUMBDA_EMITTER_CMD
then exec factory scripts. No fork-and-modify; single source of truth
upstream.

Integration tests gate 7 V2 defect classes that wedged a live factory
on 2026-06-12 (skewed-demand starve, zero-floor reservation,
multi-tier greedy, +-25%% damping, cold-start ramp, DLQ surge halve,
post-damp CPU ceiling) + 28 DLQ classifier cases (auto-retry vs
escalate partition) + bash -n syntax lint across every script.

GPU backend stays in consumer trees; rationale in
factory/GPU-BACKEND-NOTE.md. Bend wire protocol + gpu-worker.lsp
already upstream at examples/cuda-fanout/.

make factory-lint                bash -n on every factory/*.sh
make test-integration            V2 reducer + DLQ classifier + syntax gate
make sweep-doctrine              TCRAUDT reducer gate (serial)
make sweep-doctrine-parallel     xargs -P fan-out

Verified on neoblanka: factory-lint 12 scripts PASS; test-integration
14 V2 cases + 28 DLQ classifier cases + 12 syntax cases all PASS.
2026-06-14 10:37:35 -04:00

595 lines
29 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; mod-solinas.lsp — Phase B step 7: Solinas-style fast reduction
;;; for mod-mul, primes of form p = 2^n - c with sparse c.
;;;
;;; Cuts mod-mul Stage 2 from O(n²) (2n bit-loops × O(n) mod-add per bit)
;;; to O(popcount(c) × n) — a small constant × n. For our test primes
;;; c = 5 = 2² + 2⁰ → popcount=2 → 2 mod-adds + 2 mod-doubles ≈ 4 cuccaro
;;; operations instead of 2n. Speedup at n=8 (p=251): ~16× fewer cuccaros.
;;;
;;; Mirrors upstream `mod_mul_write_into_zero_acc_schoolbook` at
;;; ~/git/ecdsafail-challenge/src/point_add/mod.rs:4044 — the same pattern
;;; of walking hi through doublings and folding ±-add into acc keyed on
;;; the bits of c.
;;;
;;; Math:
;;; x*y splits as [lo | hi] where lo = bits[0..n), hi = bits[n..2n).
;;; Then x*y = hi*2^n + lo. Since p = 2^n - c, 2^n ≡ c (mod p).
;;; So (x*y) mod p = (lo + c*hi) mod p.
;;; With c = Σ sign·2^k_j, this becomes lo + Σ sign·(hi · 2^k_j) mod p.
;;;
;;; Calling convention (same shape as mod-mul!):
;;; a-reg, b-reg, out-reg : (n+1)-wide; top bit |0>.
;;; c-expansion : Scheme list of (sign . shift) pairs encoding
;;; c = Σ sign · 2^shift. Caller computes once.
;;; e.g. for p=11/p=251 (c=5):
;;; '((1 . 2) (1 . 0))
;;; Pairs must be sorted by ASCENDING shift
;;; because we cumulatively double hi.
;;; cin, tmp, flag, red-tmp : same scratch suite as mod-mul!.
;;;
;;; Internal ancillae (Litinski + Solinas):
;;; sb-tmp-ext (2n bits) wide product accumulator (Stage 1 / 3)
;;; sb-low (1 bit) wide[0] for Stage 1 / 3
;;; sb-xext (n+1 bits) Stage 1 / 3 add-subtract scratch
;;; sb-const-tmp (n+1 bits) Stage 1 / 3 constant-loading scratch
;;; sol-lo-ext (n+1 bits) Stage-2 lo-as-extended-reg copy
;;; sol-hi-ext (n+1 bits) Stage-2 hi-as-extended-reg copy (walks via doublings)
(load "quantum/gates.lsp")
(load "quantum/adder.lsp")
(load "quantum/mod-arith.lsp")
;;; ── from-zero specialization lever ────────────────────────────────
;;;
;;; *mod-mul-from-zero-first-add* — when #t, mod-mul-solinas-from-zero!
;;; (& its callers) replace Stage 2a's first mod-add! with
;;; mod-add-from-zero!. Saves n CCX per fresh-multiply call. Caller
;;; opts in by invoking mod-mul-from-zero! (mod-arith.lsp) instead of
;;; mod-mul!. Default OFF so opt-out is trivial.
;;;
;;; Port of HEAD's `mod_add_qq_fast_from_zero` specialization
;;; (mod.rs:961-1038) lifted to the Solinas Stage 2 entry point.
(define *mod-mul-from-zero-first-add* #f)
;;; ── helper: apply n consecutive doublings to a register ──
(define (mod-double-n! c v-reg n+1 p k
cin-reg cin-idx tmp-reg flag-reg flag-idx)
"Apply mod-double-inplace! k times. v := v * 2^k mod p. k >= 0."
(let loop ((i 0))
(when (< i k)
(mod-double-inplace! c v-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(loop (+ i 1)))))
(define (mod-halve-n! c v-reg n+1 p k
cin-reg cin-idx tmp-reg flag-reg flag-idx)
"Apply mod-halve-inplace! k times. v := v * 2^(-k) mod p."
(let loop ((i 0))
(when (< i k)
(mod-halve-inplace! c v-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(loop (+ i 1)))))
;;; ── helper: CX-copy a slice of one register into the low bits of another ──
(define (cx-copy-slice! c src-reg src-off tgt-reg width)
"tgt[k] ^= src[src-off + k] for k in [0, width). Self-inverse."
(let loop ((k 0))
(when (< k width)
(gate-cx! c src-reg (+ src-off k) tgt-reg k)
(loop (+ k 1)))))
;;; ── mod-mul-solinas! — Litinski schoolbook + Solinas Stage-2 reduce ──
(define (mod-mul-solinas! c a-reg b-reg out-reg n+1 p c-expansion
cin-reg cin-idx tmp-reg flag-reg flag-idx
red-tmp-reg)
"out := (a * b) mod p using Solinas fast reduction.
c-expansion encodes c = 2^n - p as ((sign . shift) ...) sorted by
ASCENDING shift. See file header for full calling convention.
red-tmp-reg unused here (kept for signature parity with mod-mul!);
may be passed as any (n+1)-wide ancilla |0> in/out (caller still
allocs to keep the call site uniform with mod-mul!)."
(let ((n (- n+1 1)))
(cond
((= n 0) #t)
(else
;; ── Stage 0: alloc Litinski scratch ──
;;
;; sweep-036: when *sub-x-from-wide-host-alloc* is on, hoist
;; sb-xfull alloc to OUTERMOST Stage-0 step so it lands at the
;; lowest free 513-wide base inside the mod-mul scope. Width
;; matches sub-x-from-wide!'s internal compute:
;; xfull-width = 2n+1 by default
;; = 4n+1 when *cuccaro-callers-fast* AND
;; *sub-x-from-wide-cas-fast* both on.
;; *host-sb-xfull-reg* communicates the name to sub-x-from-wide!
;; & add-x-into-wide! (mod-arith.lsp) without changing their
;; signatures.
(when *sub-x-from-wide-host-alloc*
(let* ((use-fast? (and *cuccaro-callers-fast*
*sub-x-from-wide-cas-fast*))
(joint-width (+ (* 2 n) 1))
(xfull-width (cond (use-fast? (+ joint-width
(- joint-width 1)))
(else joint-width))))
(alloc! c (quote sb-xfull-host) xfull-width)
(set! *host-sb-xfull-reg* (quote sb-xfull-host))))
(alloc! c (quote sb-tmp-ext) (* 2 n))
(alloc! c (quote sb-low) 1)
(alloc! c (quote sb-xext) (+ n 1))
(alloc! c (quote sb-const-tmp) (+ n 1))
;; Optional CAS-borrowed carries register for HMR dispatch in
;; cuccaro-add-offset! (mod-arith.lsp). Activated by setting
;; *cuccaro-callers-fast* #t at emit driver level. Borrows from
;; red-tmp-reg (caller-passed, unused per Solinas signature
;; convention — see file header). ZERO qubit cost.
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* red-tmp-reg)
(set! *cas-borrowed-carries-offset* 0)
(set! *cas-borrowed-bit-base* 200000)
;; red-tmp-reg is (n+1) wide; fast-borrowed dispatch consumes
;; (width-1) carries lanes. Set width=n+1 so joint dispatch
;; falls through for sub-x-from-wide (width 2n+1, needs 2n
;; slots) — only schoolbook-row-k0 (width n+1, needs n slots)
;; activates. Offset variants (width n+1) always fit.
(set! *cas-borrowed-carries-width* (+ n 1)))
;; ── Stage 1: compute wide product sb-tmp-ext := a * b ──
(schoolbook-mul-into-addsub! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
;; ── Stage 2: reduce via Solinas walk ──
;; 2a. out += lo mod p
;; 2b. for each (sign . shift) in c-expansion (ascending):
;; advance hi by delta-shift doublings; out += sign * hi mod p
;; 2c. walk hi all the way back via halvings to clear the copy.
;;
;; alg-11 mirror — classical Stage-2 tracker. When both a-reg and
;; b-reg carry classical values (find-classical-value via
;; bind-input!/rebind-mirror! channel), derive the classical
;; product & track lo / hi / out across the Stage-2 walk so the
;; in-line mod-add!/mod-sub!/mod-double-n! dispatchers can
;; route pseudo-Mersenne on safe inputs.
(let* ((a-cl (find-classical-value c a-reg))
(b-cl (find-classical-value c b-reg))
(out-cl (find-classical-value c out-reg))
(track? (and a-cl b-cl out-cl))
(prod (and track? (* a-cl b-cl)))
(lo-cl (and track? (modulo prod (expt 2 n))))
(hi-cl (and track? (quotient prod (expt 2 n)))))
;; sol-lo-ext (n+1) holds a CX-copy of sb-tmp-ext[0..n) in low n
;; bits; top bit stays |0>.
(alloc! c (quote sol-lo-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(when track?
(rebind-mirror! c (quote sol-lo-ext) lo-cl))
(mod-add! c (quote sol-lo-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(when track?
(rebind-mirror! c out-reg (modulo (+ out-cl lo-cl) p)))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(free! c (quote sol-lo-ext))
;; sol-hi-ext: copy of sb-tmp-ext[n..2n) in low n bits.
(alloc! c (quote sol-hi-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl))
;; Walk hi via mod-doublings. Track cumulative shift via current-shift.
(let loop ((rest c-expansion)
(current-shift 0)
(cur-hi (and track? hi-cl))
(cur-out (and track? (modulo (+ out-cl lo-cl) p))))
(cond
((null? rest)
;; Walk hi back to its original value via halvings.
(mod-halve-n! c (quote sol-hi-ext) n+1 p current-shift
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl)))
(else
(let* ((pair (car rest))
(sign (car pair))
(shift (cdr pair))
(delta (- shift current-shift)))
(when (< delta 0)
(error "mod-mul-solinas: c-expansion must be ascending"
(list current-shift shift)))
(mod-double-n! c (quote sol-hi-ext) n+1 p delta
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(let* ((hi-after (and track?
(modulo (* cur-hi (expt 2 delta)) p)))
(next-out (cond
((not track?) #f)
((= sign 1)
(modulo (+ cur-out hi-after) p))
(else
(modulo (- cur-out hi-after) p)))))
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-after))
(cond
((= sign 1)
(mod-add! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
((= sign -1)
(mod-sub! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
(else
(error "mod-mul-solinas: sign must be ±1" sign)))
(when track?
(rebind-mirror! c out-reg next-out))
(loop (cdr rest) shift hi-after next-out)))))))
;; Uncopy hi from sb-tmp-ext.
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(free! c (quote sol-hi-ext))
;; ── Stage 3: uncompute sb-tmp-ext back to |0> ──
(schoolbook-mul-into-addsub-inverse! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
;; ── Stage 4: free Litinski scratch ──
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* #f))
(free! c (quote sb-const-tmp))
(free! c (quote sb-xext))
(free! c (quote sb-low))
(free! c (quote sb-tmp-ext))
;; sweep-036: free host-allocated sb-xfull (if hoisted).
(when *sub-x-from-wide-host-alloc*
(set! *host-sb-xfull-reg* #f)
(free! c (quote sb-xfull-host)))))))
;;; ── mod-mul-solinas-sub! — out := out - (a*b) mod p via Solinas ──
;;;
;;; Mirror of mod-mul-sub! using Solinas Stage 2. Needed so we can swap
;;; in mod-mul-solinas! through the Roetteler point-add (which calls
;;; mod-mul-sub! at steps 5, 10, 12c, 12d).
(define (mod-mul-solinas-sub! c a-reg b-reg out-reg n+1 p c-expansion
cin-reg cin-idx tmp-reg flag-reg flag-idx
red-tmp-reg)
"out := (out - a*b) mod p via Solinas. Mirror of mod-mul-solinas! with
sign-inverted Stage-2 mod-add/mod-sub."
(let ((n (- n+1 1)))
(cond
((= n 0) #t)
(else
;; sweep-036: hoist sb-xfull alloc when opt-in flag is set.
(when *sub-x-from-wide-host-alloc*
(let* ((use-fast? (and *cuccaro-callers-fast*
*sub-x-from-wide-cas-fast*))
(joint-width (+ (* 2 n) 1))
(xfull-width (cond (use-fast? (+ joint-width
(- joint-width 1)))
(else joint-width))))
(alloc! c (quote sb-xfull-host) xfull-width)
(set! *host-sb-xfull-reg* (quote sb-xfull-host))))
(alloc! c (quote sb-tmp-ext) (* 2 n))
(alloc! c (quote sb-low) 1)
(alloc! c (quote sb-xext) (+ n 1))
(alloc! c (quote sb-const-tmp) (+ n 1))
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* red-tmp-reg)
(set! *cas-borrowed-carries-offset* 0)
(set! *cas-borrowed-bit-base* 200000)
;; red-tmp-reg is (n+1) wide; fast-borrowed dispatch consumes
;; (width-1) carries lanes. Set width=n+1 so joint dispatch
;; falls through for sub-x-from-wide (width 2n+1, needs 2n
;; slots) — only schoolbook-row-k0 (width n+1, needs n slots)
;; activates. Offset variants (width n+1) always fit.
(set! *cas-borrowed-carries-width* (+ n 1)))
(schoolbook-mul-into-addsub! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
;; Stage 2 with SIGN INVERTED on every fold (mod-add ↔ mod-sub).
;; alg-11 classical-mirror tracker — same shape as mod-mul-solinas!
;; but sign is flipped for the running out accumulator.
(let* ((a-cl (find-classical-value c a-reg))
(b-cl (find-classical-value c b-reg))
(out-cl (find-classical-value c out-reg))
(track? (and a-cl b-cl out-cl))
(prod (and track? (* a-cl b-cl)))
(lo-cl (and track? (modulo prod (expt 2 n))))
(hi-cl (and track? (quotient prod (expt 2 n)))))
(alloc! c (quote sol-lo-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(when track?
(rebind-mirror! c (quote sol-lo-ext) lo-cl))
(mod-sub! c (quote sol-lo-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(when track?
(rebind-mirror! c out-reg (modulo (- out-cl lo-cl) p)))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(free! c (quote sol-lo-ext))
(alloc! c (quote sol-hi-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl))
(let loop ((rest c-expansion)
(current-shift 0)
(cur-hi (and track? hi-cl))
(cur-out (and track? (modulo (- out-cl lo-cl) p))))
(cond
((null? rest)
(mod-halve-n! c (quote sol-hi-ext) n+1 p current-shift
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl)))
(else
(let* ((pair (car rest))
(sign (car pair))
(shift (cdr pair))
(delta (- shift current-shift)))
(when (< delta 0)
(error "mod-mul-solinas-sub: c-expansion must be ascending"
(list current-shift shift)))
(mod-double-n! c (quote sol-hi-ext) n+1 p delta
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(let* ((hi-after (and track?
(modulo (* cur-hi (expt 2 delta)) p)))
(next-out (cond
((not track?) #f)
((= sign 1)
(modulo (- cur-out hi-after) p))
(else
(modulo (+ cur-out hi-after) p)))))
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-after))
(cond
((= sign 1)
(mod-sub! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
((= sign -1)
(mod-add! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
(else
(error "mod-mul-solinas-sub: sign must be ±1" sign)))
(when track?
(rebind-mirror! c out-reg next-out))
(loop (cdr rest) shift hi-after next-out)))))))
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(free! c (quote sol-hi-ext))
(schoolbook-mul-into-addsub-inverse! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* #f))
(free! c (quote sb-const-tmp))
(free! c (quote sb-xext))
(free! c (quote sb-low))
(free! c (quote sb-tmp-ext))
;; sweep-036: free host-allocated sb-xfull (if hoisted).
(when *sub-x-from-wide-host-alloc*
(set! *host-sb-xfull-reg* #f)
(free! c (quote sb-xfull-host)))))))
;;; ── mod-mul-solinas-from-zero! — out=|0> specialization ──────────
;;;
;;; Caller-explicit specialization of mod-mul-solinas! for the case
;;; where out-reg is guaranteed to be |0> on entry. Mirror of
;;; mod-mul-solinas! verbatim except Stage 2a's mod-add! swaps to
;;; mod-add-from-zero! — saves n CCX per call.
;;;
;;; Caller responsibility: out-reg MUST be |0> across all n+1 bits on
;;; entry. Typical sites: mod-square! (out alloc'd same line) and
;;; mod-inv-by Fermat ladder's r-next / b-next freshly-alloc'd registers.
;;;
;;; Port mirrors HEAD's `mod_add_qq_fast_from_zero` (mod.rs:961-1038)
;;; lifted to the Solinas Stage 2 entry boundary. Stage 2b+ uses plain
;;; mod-add! / mod-sub! because out-reg is no longer zero after 2a.
(define (mod-mul-solinas-from-zero! c a-reg b-reg out-reg n+1 p c-expansion
cin-reg cin-idx tmp-reg flag-reg flag-idx
red-tmp-reg)
"out := (a * b) mod p assuming out is |0> on entry. Same calling
convention as mod-mul-solinas!; Stage 2a uses mod-add-from-zero!
for n CCX savings vs mod-mul-solinas!."
(let ((n (- n+1 1)))
(cond
((= n 0) #t)
(else
;; ── Stage 0: alloc Litinski scratch (same as mod-mul-solinas!) ──
(when *sub-x-from-wide-host-alloc*
(let* ((use-fast? (and *cuccaro-callers-fast*
*sub-x-from-wide-cas-fast*))
(joint-width (+ (* 2 n) 1))
(xfull-width (cond (use-fast? (+ joint-width
(- joint-width 1)))
(else joint-width))))
(alloc! c (quote sb-xfull-host) xfull-width)
(set! *host-sb-xfull-reg* (quote sb-xfull-host))))
(alloc! c (quote sb-tmp-ext) (* 2 n))
(alloc! c (quote sb-low) 1)
(alloc! c (quote sb-xext) (+ n 1))
(alloc! c (quote sb-const-tmp) (+ n 1))
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* red-tmp-reg)
(set! *cas-borrowed-carries-offset* 0)
(set! *cas-borrowed-bit-base* 200000)
(set! *cas-borrowed-carries-width* (+ n 1)))
;; ── Stage 1: wide product (unchanged) ──
(schoolbook-mul-into-addsub! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
;; ── Stage 2a: out += lo mod p — FROM-ZERO PATH ──
;; alg-11 classical-mirror tracker — same shape as mod-mul-solinas!
;; but out starts at 0 per the from-zero contract.
(let* ((a-cl (find-classical-value c a-reg))
(b-cl (find-classical-value c b-reg))
(track? (and a-cl b-cl))
(prod (and track? (* a-cl b-cl)))
(lo-cl (and track? (modulo prod (expt 2 n))))
(hi-cl (and track? (quotient prod (expt 2 n)))))
(alloc! c (quote sol-lo-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(when track?
(rebind-mirror! c (quote sol-lo-ext) lo-cl))
(cond
(*mod-mul-from-zero-first-add*
(mod-add-from-zero! c (quote sol-lo-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
(else
(mod-add! c (quote sol-lo-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx)))
(when track?
(rebind-mirror! c out-reg (modulo lo-cl p)))
(cx-copy-slice! c (quote sb-tmp-ext) 0 (quote sol-lo-ext) n)
(free! c (quote sol-lo-ext))
;; ── Stage 2b+: out is no longer zero — use plain mod-add!. ──
(alloc! c (quote sol-hi-ext) (+ n 1))
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl))
(let loop ((rest c-expansion)
(current-shift 0)
(cur-hi (and track? hi-cl))
(cur-out (and track? (modulo lo-cl p))))
(cond
((null? rest)
(mod-halve-n! c (quote sol-hi-ext) n+1 p current-shift
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-cl)))
(else
(let* ((pair (car rest))
(sign (car pair))
(shift (cdr pair))
(delta (- shift current-shift)))
(when (< delta 0)
(error "mod-mul-solinas-from-zero: c-expansion must be ascending"
(list current-shift shift)))
(mod-double-n! c (quote sol-hi-ext) n+1 p delta
cin-reg cin-idx tmp-reg flag-reg flag-idx)
(let* ((hi-after (and track?
(modulo (* cur-hi (expt 2 delta)) p)))
(next-out (cond
((not track?) #f)
((= sign 1)
(modulo (+ cur-out hi-after) p))
(else
(modulo (- cur-out hi-after) p)))))
(when track?
(rebind-mirror! c (quote sol-hi-ext) hi-after))
(cond
((= sign 1)
(mod-add! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
((= sign -1)
(mod-sub! c (quote sol-hi-ext) out-reg n+1 p
cin-reg cin-idx tmp-reg flag-reg flag-idx))
(else
(error "mod-mul-solinas-from-zero: sign must be ±1" sign)))
(when track?
(rebind-mirror! c out-reg next-out))
(loop (cdr rest) shift hi-after next-out)))))))
(cx-copy-slice! c (quote sb-tmp-ext) n (quote sol-hi-ext) n)
(free! c (quote sol-hi-ext))
;; ── Stage 3: uncompute wide product (unchanged) ──
(schoolbook-mul-into-addsub-inverse! c a-reg b-reg n
(quote sb-low) 0
(quote sb-tmp-ext)
(quote sb-xext)
cin-reg cin-idx
(quote sb-const-tmp))
;; ── Stage 4: free scratch (unchanged) ──
(when *cuccaro-callers-fast*
(set! *cas-borrowed-carries-reg* #f))
(free! c (quote sb-const-tmp))
(free! c (quote sb-xext))
(free! c (quote sb-low))
(free! c (quote sb-tmp-ext))
(when *sub-x-from-wide-host-alloc*
(set! *host-sb-xfull-reg* #f)
(free! c (quote sb-xfull-host)))))))
;;; ── classical helper: compute c-expansion for p = 2^n - c ──
;;;
;;; Returns ((1 . k0) (1 . k1) ...) for c = Σ 2^k_i (i.e. naive bit-set
;;; expansion, sorted by ascending shift). For sparse c with consecutive
;;; runs the *solinas-c-naf* lever (sweep-092) emits NAF ±1 expansion
;;; which drops popcount on runs (e.g. secp256k1 c = 2^32 + 977 goes
;;; 7 -> 5 terms; saves 2 mod-add/sub + 2 mod-double-n calls per
;;; mod-mul-solinas! invocation).
;;; *solinas-c-naf* — sweep-092 lever. Port of HEAD's R84_QPROD_NAF
;;; rewrite of the round84 quotient * c product. HEAD ed94ad2
;;; (2026-06-09 13:43 UTC). When #t, compute-c-expansion emits the
;;; non-adjacent-form (NAF) ±1 expansion of c instead of the naive
;;; bit-set expansion. Both Stage-2 loops in mod-mul-solinas! /
;;; -sub! / -from-zero! already handle (sign . shift) pairs with
;;; sign in {+1, -1}, so this is purely a classical-helper change.
;;;
;;; NAF properties: any integer has a unique NAF with no two
;;; consecutive non-zero digits. For runs of consecutive 1 bits
;;; like 977 = 0b1111010001 the NAF replaces a run length L with
;;; 2 terms (one -1 at the run's low end, one +1 at the next zero
;;; above the run). For secp256k1's c = 2^32 + 977:
;;; naive: (1.0) (1.4) (1.6) (1.7) (1.8) (1.9) (1.32) ; 7 terms
;;; NAF : (1.0) (1.4) (-1.6) (1.10) (1.32) ; 5 terms
;;;
;;; Default #f preserves naive byte-identity (HEAD's pre-ed94ad2 form).
;;; When #t adds the cost of one NAF computation per Solinas mod-mul
;;; (constant time on n+1 = 257; negligible vs the circuit emit cost).
(define *solinas-c-naf* #f)
(define (compute-c-expansion-naive p n)
"Naive bit-set expansion (pre-sweep-092 form)."
(let ((c-val (- (expt 2 n) p)))
(when (<= c-val 0)
(error "compute-c-expansion-naive: p must satisfy p < 2^n" (list p n)))
(let loop ((i 0) (acc (quote ())))
(cond
((>= i n)
(reverse acc))
((bit-set? c-val i)
(loop (+ i 1) (cons (cons 1 i) acc)))
(else
(loop (+ i 1) acc))))))
(define (compute-c-expansion-naf p n)
"NAF (non-adjacent form) ±1 expansion of c = 2^n - p. Returns a
list of (sign . shift) pairs sorted by ascending shift, with
sign in {+1, -1}. NAF guarantees no two consecutive shifts share
a non-zero digit, so over runs of consecutive 1-bits the
popcount drops from L to 2."
(let ((c-val (- (expt 2 n) p)))
(when (<= c-val 0)
(error "compute-c-expansion-naf: p must satisfy p < 2^n" (list p n)))
;; Standard NAF: while c > 0, if c odd then z = 2 - (c mod 4),
;; subtract z from c, else z = 0; emit z at current shift; c >>= 1.
;; We use quotient (not arithmetic-shift) for C-tier compatibility.
(let loop ((c c-val) (i 0) (acc (quote ())))
(cond
((= c 0)
(reverse acc))
((= (remainder c 2) 1)
(let ((z (- 2 (remainder c 4))))
(loop (quotient (- c z) 2) (+ i 1)
(cons (cons z i) acc))))
(else
(loop (quotient c 2) (+ i 1) acc))))))
(define (compute-c-expansion p n)
"Compute the c = 2^n - p expansion as an ascending list of
(sign . shift) pairs. Caller can pre-compute and pass into
mod-mul-solinas!. When *solinas-c-naf* is #t emits the NAF
±1 form (sweep-092); else emits the naive +1 bit-set form."
(cond
(*solinas-c-naf* (compute-c-expansion-naf p n))
(else (compute-c-expansion-naive p n))))