;;; 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))))