Skip to main content

rhdl_ed25519_scalable_core/
machine.rs

1//! The barrel-threaded field machine: the engine that does the arithmetic.
2//!
3//! A [`FieldMachine`] owns one 255-bit multiplier ([`FieldMulPar`](crate::fe17::FieldMulPar), 225 DSPs, or the 150-DSP [`FieldMulKara`](crate::fe17_kara::FieldMulKara) with `ED25519_MULTIPLIER=LEAN150`,
4//! a new product every clock, 23 clocks of latency), one adder
5//! ([`FieldAddSub`]), one table scanner, a program ROM, and `K = 2^KB`
6//! hardware **contexts**: independent jobs (signatures) resident at once.
7//!
8//! # Why barrel threading
9//!
10//! A pipelined multiplier only helps if something always has a multiply
11//! ready. Instead of finding parallelism inside one job, the machine runs `K`
12//! jobs that take turns: in clock `t`, context `t mod K` issues one bundle.
13//! A context's next turn is `K >= 32` clocks later, after every result it
14//! could need is back (the longest write-back is 25 clocks). So:
15//!
16//! - programs need no latency scheduling and the hardware no hazard logic;
17//! - the multiplier is busy every clock the program has a multiply;
18//! - each context's registers live in block RAM, addressed
19//!   `register * K + context`, so contexts cost BRAM rather than LUTs.
20//!
21#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/barrel-machine.svg"), "</div>")]
22//!
23//! # Pipeline
24//!
25//! ```text
26//! cycle t-1       ROM prefetch for the next head context
27//! cycle t     S1  head context issues: bank, table-row and digit addresses
28//! cycle t+1   S2  operands selected; scan accumulator updated
29//! cycle t+2   S3  M and A units issue; OUT captured; lookup negated
30//! cycle t+3   S4  bank L (one of two slots) committed
31//! t+2+23          multiplier write-back       t+2+5  adder write-back
32//! ```
33//!
34//! # Registers
35//!
36//! Each bank has exactly one writer, so there are no write-port conflicts:
37//! **M** (multiplier results), **A** (adder results), **I** (job inputs from
38//! the loader), **C** (the program's constants, one copy for all contexts),
39//! and **L** (the point selected by the last table lookup, two slots per
40//! context so one lookup can commit while the previous one is still being
41//! read). Banks are replicated once per read port (four operands per
42//! bundle). An operand in bank I with index 96 or more (`CONST_BASE`) reads C.
43//!
44//! Register addresses interleave contexts: register `r` of context `c` is
45//! word `r * K + c` (`r << KB | c`). Multiplier and adder tags carry
46//! `c << 7 | r` so a result returning 23 or 5 cycles later finds its own
47//! word without any bookkeeping.
48//!
49//! # Where the memories live, and why
50//!
51//! The first layout put every bank in block RAM, and on a full chip block
52//! RAM ran out before LUTs or DSPs: a fixed-base machine used 72 BRAM36
53//! (banks M and A 16 each, bank I 16, base-point tables 22, program 2.5),
54//! and the compress machine 289, of which 228 were its input bank. Two
55//! observations fixed most of it:
56//!
57//! - **Banks M, A and I are written at run time and never preloaded**, which
58//!   is exactly what UltraRAM can hold; the chip barely used its 960
59//!   UltraRAMs. Constants used to sit in bank I as preloaded contents (one
60//!   copy per context), which is what kept bank I in block RAM. So
61//!   constants moved to their own 32-entry ROM C (a few LUTs, the same for
62//!   every context), and the compress stage writes its identity defaults at
63//!   reset instead of preloading them.
64//! - **Machines without inputs** (fixed-base, verify) never write bank I, so
65//!   it shrinks to the minimum and lives in LUT RAM.
66//!
67//! [`MachineMemory::SPECIALIZED`] (the default) is that layout;
68//! [`MachineMemory::AS_BUILT`] reproduces the one mapped in Vivado on
69//! 2026-10-14. The new layout is projected at about 24 BRAM36 + 32 URAM per
70//! fixed-base machine; it is **not yet mapped**, and UltraRAM timing at
71//! 205 MHz is the thing to check.
72//!
73//! # Instructions
74//!
75//! An [`InstrW`] bundle has three slots, all usable at once: **M** (multiply;
76//! 7-bit destination, two operands of bank + 7-bit index), **A** (add,
77//! subtract, or `OUT`), and **T** (one table-scan step: digit, entry pair,
78//! 9-bit table row, bank-L slot). Bundles are produced by
79//! [`microcode::Builder`](crate::microcode::Builder); [`encode_bundle`] packs
80//! them into ROM words.
81//!
82//! # Constant-address table lookup
83//!
84//! A lookup of a signed digit runs one T step per pair of table entries. Each
85//! step reads both entries at a public row; the context's accumulator keeps
86//! the one whose number equals the digit's magnitude, and the last step
87//! negates it if the digit is negative and commits it to bank L. No secret
88//! value forms an address, an enable or a branch. (A logical constant-time
89//! property, not a power or EM guarantee.) The tables hold `e · 2^offset(i) · B`
90//! for each of the 54 digits, 372 rows in two BRAM images.
91//!
92//! # Backpressure
93//!
94//! An `OUT` bundle issues only when the single output register and the
95//! pipeline hold no other output; otherwise the context retries on its next
96//! turn. This is the machine's only stall.
97//!
98//! # Parameters and cost
99//!
100//! `FieldMachine<KB, K, RA, IA, PB, TB, KT>`: contexts `K = 2^KB`, bank
101//! address widths `RA` (M/A) and `IA` (I), program-counter width `PB`,
102//! base-point table rows `2^TB`, key-table rows `2^KT`.
103//! [`FbMachine`](crate::sign_core::FbMachine) is `<5, 32, 9, 5, 10, 9, 1>`,
104//! [`CpMachine`](crate::sign_core::CpMachine) is `<6, 64, 12, 13, 9, 1, 1>`.
105//! Vivado (as-built layout, routed at 4.878 ns) put a fixed-base machine at
106//! 10,110 LUT, 13,795 FF, 225 DSP and 72 BRAM36, with +0.407 ns of slack.
107
108use rhdl::prelude::*;
109use rhdl_fpga::core::{
110    constant::Constant,
111    dff::DFF,
112    ram::synchronous::{In as RamIn, RamStyle, SyncBRAM, Write as RamWrite},
113};
114
115use super::{
116    fe17::{
117        Fe17, FieldAddIn, FieldAddSub, FieldMulIn, limbs_from_digits, p_minus,
118        digits_from_limbs,
119    },
120    host::{DIGITS, Fp, Niels, TABLE_ROWS, basepoint_table, digit_entries, table_row},
121    microcode::{AOp, Bank, Bundle, Operand, Program},
122};
123
124// Generated by `build.rs` from `ED25519_MULTIPLIER`: `FieldMul`, the
125// multiplier every field machine of this build uses.
126include!(concat!(env!("OUT_DIR"), "/multiplier.rs"));
127// Generated by `build.rs` from `ED25519_URAM_MACHINES`: `URAM_MACHINES`.
128include!(concat!(env!("OUT_DIR"), "/placement.rs"));
129
130/// Bank encodings in [`OperandW::bank`]: bank M (multiplier results).
131pub const BANK_M: u128 = 0;
132/// Bank A (adder results).
133pub const BANK_A: u128 = 1;
134/// Bank I (inputs, indices below [`CONST_BASE`]) and the constant ROM C
135/// (indices [`CONST_BASE`] and up). They share a code so the operand field
136/// stays two bits wide.
137pub const BANK_I: u128 = 2;
138/// Bank L (selected lookup point).
139pub const BANK_L: u128 = 3;
140
141/// Encoded operand: which bank to read and which register in it.
142#[derive(Copy, Clone, Debug, PartialEq, Digital)]
143pub struct OperandW {
144    /// Bank code ([`BANK_M`], [`BANK_A`], [`BANK_I`] or [`BANK_L`]).
145    pub bank: b2,
146    /// Register index within the context (bank I: 64 and up selects
147    /// constant `index - 64`; bank L: `4 * slot + coordinate`).
148    pub index: b7,
149}
150
151/// Encoded VLIW bundle stored in program ROM, one word per program counter.
152///
153/// The three slots are independent; a disabled slot's other fields are
154/// ignored. Operands are read in S1/S2 and the units issue in S3.
155#[derive(Copy, Clone, Debug, PartialEq, Digital)]
156pub struct InstrW {
157    /// Multiply slot enabled.
158    pub m_en: bool,
159    /// Multiply destination register (bank M).
160    pub m_dst: b7,
161    /// First factor (read port 0; never bank L).
162    pub m_a: OperandW,
163    /// Second factor (read port 1; the only operand programs point at bank L).
164    pub m_b: OperandW,
165    /// Add slot enabled.
166    pub a_en: bool,
167    /// 0 add, 1 subtract, 2 output (`OUT` sends `a_a` to the output port
168    /// instead of using the adder).
169    pub a_op: b2,
170    /// Add destination register (bank A); unused by `OUT`.
171    pub a_dst: b7,
172    /// First operand (read port 2), also the value an `OUT` emits.
173    pub a_a: OperandW,
174    /// Second operand (read port 3).
175    pub a_b: OperandW,
176    /// Program output index reported with an `OUT` event.
177    pub out_index: b6,
178    /// Scan slot enabled.
179    pub t_en: bool,
180    /// Digit slot 0..128 (base-point scalar 0..54, key scalar 64..118).
181    pub t_digit: b7,
182    /// Scan the key table instead of the base-point table.
183    pub t_key: bool,
184    /// Entry pair 0..8: this step compares the digit magnitude with
185    /// entries `2 * pair + 1` and `2 * pair + 2`.
186    pub t_pair: b3,
187    /// Table row of the pair (`table_row(digit) + pair`), a public constant.
188    pub t_row: b9,
189    /// Bank-L slot committed by `t_finish`.
190    pub t_lslot: bool,
191    /// Reset the accumulator to the identity first (first step of a lookup).
192    pub t_clear: bool,
193    /// Commit the accumulator, negated if the digit is negative, to bank L
194    /// (last step of a lookup).
195    pub t_finish: bool,
196    /// Last bundle of its program; the context retires after issuing it.
197    pub end: bool,
198}
199
200/// Affine Niels point in radix-`2^17` digits: the form table entries are
201/// stored in, ready for a mixed addition.
202#[derive(Copy, Clone, Debug, PartialEq, Digital)]
203pub struct Niels17 {
204    /// `y + x`.
205    pub ypx: Fe17,
206    /// `y - x`.
207    pub ymx: Fe17,
208    /// `2dxy`.
209    pub xy2d: Fe17,
210}
211
212/// Bits of a key-slot number: 3 (8 public keys) by default, 4 (16) with the
213/// feature `key-slots-16` (uBFT: a replica verifies up to 11 peers). Each slot
214/// owns 512 rows of every verify machine's key table, so 16 slots double the
215/// key-table memory.
216#[cfg(not(feature = "key-slots-16"))]
217pub const KEY_SLOT_BITS: usize = 3;
218/// Bits of a key-slot number (feature `key-slots-16`: 16 slots).
219#[cfg(feature = "key-slots-16")]
220pub const KEY_SLOT_BITS: usize = 4;
221/// Public keys a known-key verifier holds at once.
222pub const KEY_SLOTS: usize = 1 << KEY_SLOT_BITS;
223/// Rows of a verify machine's key table: 512 per slot.
224pub const KEY_TABLE_BITS: usize = 9 + KEY_SLOT_BITS;
225/// A key-slot number.
226pub type KeySlot = Bits<KEY_SLOT_BITS>;
227
228/// Key slot 0, for requests that look up no key. A kernel generic over a
229/// width (`CB`) cannot write `bits(0)` here: rustc would infer that width.
230#[kernel]
231pub fn no_key() -> KeySlot {
232    bits(0)
233}
234
235/// Start request: context `ctx` (idle, inputs loaded) begins at bundle
236/// `entry`.
237///
238/// The request is latched as pending and takes effect on the context's next
239/// turn; the caller may assert it on any cycle.
240#[derive(Copy, Clone, Debug, PartialEq, Digital)]
241pub struct StartReq<const KB: usize>
242where
243    rhdl::bits::W<KB>: BitWidth,
244{
245    /// Request valid this cycle.
246    pub valid: bool,
247    /// Context to start (`KB` bits).
248    pub ctx: Bits<KB>,
249    /// Opaque job tag reported with outputs.
250    pub tag: b16,
251    /// Key-table slot for key lookups (row `key * 512 + row`).
252    pub key: KeySlot,
253    /// First bundle (programs share one ROM; see `microcode::concat`).
254    pub entry: b12,
255}
256
257/// Digits write for the table scanner (sign-magnitude: bit 5 = sign).
258///
259/// Writes all 128 digit slots of one context at once, so a context's scalar
260/// recoding is one memory word read by every T step.
261#[derive(Copy, Clone, Debug, PartialEq, Digital)]
262pub struct DigitsWrite<const KB: usize>
263where
264    rhdl::bits::W<KB>: BitWidth,
265{
266    /// Write valid this cycle.
267    pub valid: bool,
268    /// Context whose digits are replaced.
269    pub ctx: Bits<KB>,
270    /// Sign-magnitude digits, magnitude (0..=16) in bits 4..0: slots 0..54
271    /// for the base-point scalar, 64..118 for the key scalar, zero elsewhere.
272    pub digits: [b6; 128],
273}
274
275/// One key-table coordinate write (the key-loading path): one coordinate
276/// of one entry per cycle, so a full entry pair takes six writes.
277#[derive(Copy, Clone, Debug, PartialEq, Digital)]
278pub struct TableWrite {
279    /// Write valid this cycle.
280    pub valid: bool,
281    /// Row: `key * 512 + table row` (truncated to the `KT`-bit table).
282    pub row: Bits<KEY_TABLE_BITS>,
283    /// Odd image (entries `2*pair + 2`) instead of the even one
284    /// (entries `2*pair + 1`).
285    pub odd: bool,
286    /// Coordinate: 0 `y+x`, 1 `y-x`, 2 `2dxy` (3 writes nothing).
287    pub coord: b2,
288    /// Coordinate value.
289    pub value: Fe17,
290}
291
292/// Bank-I write: one input register of one context, from the loader.
293#[derive(Copy, Clone, Debug, PartialEq, Digital)]
294pub struct InputWrite<const KB: usize>
295where
296    rhdl::bits::W<KB>: BitWidth,
297{
298    /// Write valid this cycle.
299    pub valid: bool,
300    /// Context.
301    pub ctx: Bits<KB>,
302    /// Input register index (below [`CONST_BASE`]).
303    pub index: b7,
304    /// Value.
305    pub value: Fe17,
306}
307
308/// Machine inputs, all sampled every cycle and independent of each other.
309#[derive(Copy, Clone, Debug, PartialEq, Digital)]
310pub struct MachineIn<const KB: usize>
311where
312    rhdl::bits::W<KB>: BitWidth,
313{
314    /// Start a context.
315    pub start: StartReq<KB>,
316    /// Load scan digits.
317    pub digits: DigitsWrite<KB>,
318    /// Load an input register.
319    pub input: InputWrite<KB>,
320    /// Load a key-table entry.
321    pub table: TableWrite,
322    /// The consumer takes [`MachineOut::out`] this cycle. The output
323    /// register is freed for new `OUT` bundles from the next cycle on.
324    pub out_accept: bool,
325}
326
327/// Output-port event from an `OUT` bundle slot.
328///
329/// The machine has one output register, which is why `OUT` bundles are the
330/// only ones that can stall.
331#[derive(Copy, Clone, Debug, PartialEq, Digital)]
332pub struct OutEvent<const KB: usize>
333where
334    rhdl::bits::W<KB>: BitWidth,
335{
336    /// Event valid; stays set until [`MachineIn::out_accept`].
337    pub valid: bool,
338    /// Context that emitted it.
339    pub ctx: Bits<KB>,
340    /// Job tag given at start.
341    pub tag: b16,
342    /// Program output index (`InstrW::out_index`).
343    pub index: b6,
344    /// Emitted value.
345    pub value: Fe17,
346}
347
348/// Machine outputs.
349#[derive(Copy, Clone, Debug, PartialEq, Digital)]
350pub struct MachineOut<const KB: usize>
351where
352    rhdl::bits::W<KB>: BitWidth,
353{
354    /// Output event, held until accepted.
355    pub out: OutEvent<KB>,
356    /// Context retired its last bundle this cycle (one-cycle pulse when that
357    /// bundle reaches S3). All of its earlier `OUT`s have been captured; an
358    /// `OUT` in the last bundle itself appears on `out` one cycle later.
359    pub done: bool,
360    /// Retired context.
361    pub done_ctx: Bits<KB>,
362    /// Retired job tag.
363    pub done_tag: b16,
364}
365
366/// Per-context thread state, held in the rotating ring.
367///
368/// Entries carry no context number: the ring rotates by one each clock, so
369/// `ring[k]` always belongs to context `slot + k (mod K)`.
370#[derive(Copy, Clone, Debug, PartialEq, Digital)]
371#[doc(hidden)]
372pub struct Ring<const PB: usize>
373where
374    rhdl::bits::W<PB>: BitWidth,
375{
376    /// The context is running a program.
377    active: bool,
378    /// Program counter of the context's next bundle.
379    pc: Bits<PB>,
380    /// Job tag of the running program.
381    tag: b16,
382}
383
384/// Control word that travels with a bundle down stages S2 and S3.
385#[derive(Copy, Clone, Debug, PartialEq, Digital)]
386#[doc(hidden)]
387pub struct Stage<const KB: usize, const PB: usize>
388where
389    rhdl::bits::W<KB>: BitWidth,
390    rhdl::bits::W<PB>: BitWidth,
391{
392    /// A bundle issued (false for stalled or idle turns).
393    valid: bool,
394    /// The bundle is its program's last (`InstrW::end`).
395    last: bool,
396    /// Issuing context.
397    ctx: Bits<KB>,
398    /// Job tag of the issuing context.
399    tag: b16,
400}
401
402/// All flip-flop state of a [`FieldMachine`] outside its memories and units.
403#[derive(Copy, Clone, Debug, PartialEq, Digital)]
404#[doc(hidden)]
405pub struct MachineRegs<const KB: usize, const PB: usize, const K: usize>
406where
407    rhdl::bits::W<KB>: BitWidth,
408    rhdl::bits::W<PB>: BitWidth,
409{
410    /// Context whose turn it is this cycle (owner of `ring[0]`); wraps mod K.
411    slot: Bits<KB>,
412    /// Thread state of every context, rotated one place per clock.
413    ring: [Ring<PB>; K],
414    /// A start request is waiting for the context's next turn.
415    pending: [bool; K],
416    /// Job tag of the pending start.
417    pending_tag: [b16; K],
418    /// Key-table slot of each context, latched at start.
419    key: [KeySlot; K],
420    /// Entry bundle of the pending start.
421    pending_entry: [b12; K],
422    /// The single output register.
423    out: OutEvent<KB>,
424    /// S2 control (bundle issued last cycle).
425    s2: Stage<KB, PB>,
426    /// S2 bundle; slot enables are cleared when the bundle did not issue.
427    s2_instr: InstrW,
428    /// S2 scan digit, picked in S1 from the digit word read a cycle ahead.
429    /// (Until 2026-10-17 the 128:1 pick sat in S2 in front of the
430    /// magnitude compare and the 765-bit select: the 8-machine signer's
431    /// worst routed path, -0.581 ns, 87 % route.)
432    s2_digit: b6,
433    /// This cycle's head got its start or its digit word one cycle before
434    /// its turn, after its bundle and digit word were read ahead: a start
435    /// waits a turn.
436    dig_fresh: bool,
437    /// The head's bundle, read from the ROM two slots ahead and registered
438    /// here, so S1 decodes from flip-flops. (Until 2026-10-17 S1 used the ROM
439    /// output directly; with the digit pick in S1 that was the 6-machine
440    /// verifier's worst routed path, -0.370 ns, ROM -> t_digit -> 128:1 pick.)
441    s1_instr: InstrW,
442    /// S3 control.
443    s3: Stage<KB, PB>,
444    /// S3 bundle.
445    s3_instr: InstrW,
446    /// S3 operands, in port order: `m_a`, `m_b`, `a_a`, `a_b`.
447    s3_ops: [Fe17; 4],
448    /// S3 copy of the scan accumulator after this bundle's T step.
449    s3_acc: Niels17,
450    /// S3 sign of the digit the T step scanned.
451    s3_negative: bool,
452    /// S4: commit `s4_l` to bank L this cycle.
453    s4_commit: bool,
454    /// S4: bank-L slot to commit to.
455    s4_lslot: bool,
456    /// S4: context to commit for.
457    s4_ctx: Bits<KB>,
458    /// S4: the selected point, already negated if the digit was negative.
459    s4_l: Niels17,
460    /// The write-only inputs (start, digits, input and table writes),
461    /// registered on arrival and acted on one cycle later, in order. The
462    /// controller's dispatch (a priority pick over machines and contexts)
463    /// and the machine's wide latching (`pending[ctx]` and friends) then
464    /// sit in different cycles. (Until 2026-10-17 they shared one: the
465    /// 8-machine signer's worst routed path, -1.338 ns, 90 % route, from
466    /// `fb_free` to a machine's clock enables.) `out_accept` is a handshake
467    /// and stays combinational.
468    in_start: StartReq<KB>,
469    /// Registered digit write.
470    in_digits: DigitsWrite<KB>,
471    /// Registered input write.
472    in_input: InputWrite<KB>,
473    /// Registered table write.
474    in_table: TableWrite,
475}
476
477/// Constants a program can address (operand index `CONST_BASE + c`): 32,
478/// the size of ROM C, which fills the 7-bit index field above the inputs.
479pub const CONST_SLOTS: usize = 32;
480/// Bank-I operand index of constant 0; lower indices are inputs. 96 leaves
481/// room for the compress program's 96 inputs (32 points x 3 coordinates).
482pub const CONST_BASE: usize = 96;
483
484/// The constant ROM C: [`CONST_SLOTS`] constants as a literal and a
485/// registered read, with the ports of a [`SyncBRAM`] (the write port is
486/// ignored).
487///
488/// This was a `SyncBRAM` with its write disabled, which Vivado 2023.2
489/// mapped to RAM32M16 LUT RAM. In the aux machine (constants 1, d, √−1,
490/// …) synthesis trimmed the bit columns that are zero in every entry,
491/// kept the INIT strings in the original layout, and wired the output
492/// flip-flops packed: the card's netlist read d with 16 wrong bits and
493/// rejected valid keys (DSig, build v3, 2026-10-19). A literal and a mux
494/// leave no RAM to infer.
495#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
496pub struct ConstRom {
497    /// The constants, slot `c` at index `c`.
498    table: Constant<[Fe17; CONST_SLOTS]>,
499    /// The read register (one clock of latency, as a `SyncBRAM`).
500    out: DFF<Fe17>,
501}
502
503impl ConstRom {
504    /// A ROM holding `consts` (slot, value); missing slots read zero.
505    fn new(consts: &[(Bits<5>, Fe17)]) -> Self {
506        let mut table = [Fe17::zero(); CONST_SLOTS];
507        for &(slot, value) in consts {
508            table[slot.raw() as usize] = value;
509        }
510        Self { table: Constant::new(table), out: DFF::new(Fe17::zero()) }
511    }
512}
513
514/// Port types of [`ConstRom`]: those of a 32-word `SyncBRAM`.
515impl SynchronousIO for ConstRom {
516    type I = RamIn<Fe17, 5>;
517    type O = Fe17;
518    type Kernel = const_rom_kernel;
519}
520
521/// Read slot `read_addr` into the register.
522#[kernel]
523pub fn const_rom_kernel(_cr: ClockReset, i: RamIn<Fe17, 5>, q: ConstRomQ) -> (Fe17, ConstRomD) {
524    let mut d = ConstRomD::dont_care();
525    d.table = ();
526    d.out = q.table[i.read_addr];
527    (q.out, d)
528}
529
530/// Register banks M, A and I and the constant ROM C, one copy per read
531/// port. Bank I holds per-context inputs; C holds the program's constants
532/// once for all contexts (32 entries, LUT ROM).
533///
534/// Each memory has one read and one write port; four copies of each bank
535/// with identical writes give the four read ports a bundle needs. Reads have
536/// one clock of latency.
537#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
538pub struct RegBanks<const RA: usize, const IA: usize>
539where
540    rhdl::bits::W<RA>: BitWidth,
541    rhdl::bits::W<IA>: BitWidth,
542{
543    /// Bank M copies, one per read port (`2^RA` words).
544    m: [SyncBRAM<Fe17, RA>; 4],
545    /// Bank A copies, one per read port (`2^RA` words).
546    a: [SyncBRAM<Fe17, RA>; 4],
547    /// Bank I copies, one per read port (`2^IA` words).
548    i: [SyncBRAM<Fe17, IA>; 4],
549    /// Constant ROM C copies, one per read port (32 words).
550    c: [ConstRom; 4],
551}
552
553/// Port bundle of [`RegBanks`].
554#[derive(Copy, Clone, Debug, PartialEq, Digital)]
555pub struct RegBanksIn<const RA: usize, const IA: usize>
556where
557    rhdl::bits::W<RA>: BitWidth,
558    rhdl::bits::W<IA>: BitWidth,
559{
560    /// Bank M ports, indexed by read port.
561    pub m: [RamIn<Fe17, RA>; 4],
562    /// Bank A ports.
563    pub a: [RamIn<Fe17, RA>; 4],
564    /// Bank I ports.
565    pub i: [RamIn<Fe17, IA>; 4],
566    /// Constant ROM ports.
567    pub c: [RamIn<Fe17, 5>; 4],
568}
569
570/// Read data of [`RegBanks`], one cycle after the read address.
571#[derive(Copy, Clone, Debug, PartialEq, Digital)]
572pub struct RegBanksOut {
573    /// Bank M copies.
574    pub m: [Fe17; 4],
575    /// Bank A copies.
576    pub a: [Fe17; 4],
577    /// Bank I copies.
578    pub i: [Fe17; 4],
579    /// Constant ROM copies.
580    pub c: [Fe17; 4],
581}
582
583/// Port types of [`RegBanks`].
584impl<const RA: usize, const IA: usize> SynchronousIO for RegBanks<RA, IA>
585where
586    rhdl::bits::W<RA>: BitWidth,
587    rhdl::bits::W<IA>: BitWidth,
588{
589    type I = RegBanksIn<RA, IA>;
590    type O = RegBanksOut;
591    type Kernel = reg_banks_kernel<RA, IA>;
592}
593
594/// Wiring for [`RegBanks`]: ports pass straight through to the memories.
595#[kernel]
596pub fn reg_banks_kernel<const RA: usize, const IA: usize>(
597    _cr: ClockReset,
598    i: RegBanksIn<RA, IA>,
599    q: RegBanksQ<RA, IA>,
600) -> (RegBanksOut, RegBanksD<RA, IA>)
601where
602    rhdl::bits::W<RA>: BitWidth,
603    rhdl::bits::W<IA>: BitWidth,
604{
605    let mut d = RegBanksD::<RA, IA>::dont_care();
606    d.m = i.m;
607    d.a = i.a;
608    d.i = i.i;
609    d.c = i.c;
610    (
611        RegBanksOut {
612            m: q.m,
613            a: q.a,
614            i: q.i,
615            c: q.c,
616        },
617        d,
618    )
619}
620
621/// Table-scan memories: bank L, the scan accumulator, digits, the base-point
622/// table (`2^TB` rows) and the key table (`2^KT` rows). A machine that never
623/// scans uses `TB = KT = 1`, and one without key lookups `KT = 1`.
624///
625/// The per-context memories (`bank_l0`, `bank_l1`, `acc`, `digits`) have
626/// one word per context, addressed by context number.
627#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
628pub struct ScanMems<const KB: usize, const TB: usize, const KT: usize>
629where
630    rhdl::bits::W<KB>: BitWidth,
631    rhdl::bits::W<TB>: BitWidth,
632    rhdl::bits::W<KT>: BitWidth,
633{
634    /// Bank L, slot 0: one committed lookup result per context.
635    bank_l0: SyncBRAM<Niels17, KB>,
636    /// Bank L, slot 1.
637    bank_l1: SyncBRAM<Niels17, KB>,
638    /// Scan accumulator: the entry selected so far by the current lookup.
639    acc: SyncBRAM<Niels17, KB>,
640    /// 128 sign-magnitude digits per context.
641    digits: SyncBRAM<[b6; 128], KB>,
642    /// Base-point table, even image (entries `2 * pair + 1`); preloaded ROM.
643    table0: SyncBRAM<Niels17, TB>,
644    /// Base-point table, odd image (entries `2 * pair + 2`); preloaded ROM.
645    table1: SyncBRAM<Niels17, TB>,
646    /// Per-key tables written at run time.
647    key: KeyTables<KT>,
648}
649
650/// Port bundle of [`ScanMems`].
651#[derive(Copy, Clone, Debug, PartialEq, Digital)]
652pub struct ScanMemsIn<const KB: usize, const TB: usize, const KT: usize>
653where
654    rhdl::bits::W<KB>: BitWidth,
655    rhdl::bits::W<TB>: BitWidth,
656    rhdl::bits::W<KT>: BitWidth,
657{
658    /// Bank L, slot 0.
659    pub bank_l0: RamIn<Niels17, KB>,
660    /// Bank L, slot 1.
661    pub bank_l1: RamIn<Niels17, KB>,
662    /// Accumulator.
663    pub acc: RamIn<Niels17, KB>,
664    /// Digits.
665    pub digits: RamIn<[b6; 128], KB>,
666    /// Even base-point table entries.
667    pub table0: RamIn<Niels17, TB>,
668    /// Odd base-point table entries.
669    pub table1: RamIn<Niels17, TB>,
670    /// Key tables.
671    pub key: KeyTablesIn<KT>,
672}
673
674/// Read data of [`ScanMems`].
675#[derive(Copy, Clone, Debug, PartialEq, Digital)]
676pub struct ScanMemsOut {
677    /// Bank L, slot 0.
678    pub bank_l0: Niels17,
679    /// Bank L, slot 1.
680    pub bank_l1: Niels17,
681    /// Accumulator.
682    pub acc: Niels17,
683    /// Digits.
684    pub digits: [b6; 128],
685    /// Even base-point table entry.
686    pub table0: Niels17,
687    /// Odd base-point table entry.
688    pub table1: Niels17,
689    /// Key-table entries (even, odd).
690    pub key: [Niels17; 2],
691}
692
693/// Port types of [`ScanMems`].
694impl<const KB: usize, const TB: usize, const KT: usize> SynchronousIO for ScanMems<KB, TB, KT>
695where
696    rhdl::bits::W<KB>: BitWidth,
697    rhdl::bits::W<TB>: BitWidth,
698    rhdl::bits::W<KT>: BitWidth,
699{
700    type I = ScanMemsIn<KB, TB, KT>;
701    type O = ScanMemsOut;
702    type Kernel = scan_mems_kernel<KB, TB, KT>;
703}
704
705/// Wiring for [`ScanMems`]: ports pass straight through to the memories.
706#[kernel]
707pub fn scan_mems_kernel<const KB: usize, const TB: usize, const KT: usize>(
708    _cr: ClockReset,
709    i: ScanMemsIn<KB, TB, KT>,
710    q: ScanMemsQ<KB, TB, KT>,
711) -> (ScanMemsOut, ScanMemsD<KB, TB, KT>)
712where
713    rhdl::bits::W<KB>: BitWidth,
714    rhdl::bits::W<TB>: BitWidth,
715    rhdl::bits::W<KT>: BitWidth,
716{
717    let mut d = ScanMemsD::<KB, TB, KT>::dont_care();
718    d.bank_l0 = i.bank_l0;
719    d.bank_l1 = i.bank_l1;
720    d.acc = i.acc;
721    d.digits = i.digits;
722    d.table0 = i.table0;
723    d.table1 = i.table1;
724    d.key = i.key;
725    (
726        ScanMemsOut {
727            bank_l0: q.bank_l0,
728            bank_l1: q.bank_l1,
729            acc: q.acc,
730            digits: q.digits,
731            table0: q.table0,
732            table1: q.table1,
733            key: q.key,
734        },
735        d,
736    )
737}
738
739/// Key tables: two images (even/odd entries) of three coordinate RAMs each,
740/// written one coordinate at a time.
741///
742/// Splitting by coordinate keeps the write port one [`Fe17`] wide, so the
743/// loader needs no read-modify-write of a full Niels entry.
744#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
745pub struct KeyTables<const KT: usize>
746where
747    rhdl::bits::W<KT>: BitWidth,
748{
749    /// `ram[0..3]`: even image `y+x`, `y-x`, `2dxy`; `ram[3..6]`: odd image.
750    ram: [SyncBRAM<Fe17, KT>; 6],
751}
752
753/// Key tables with the default memory style (left to synthesis).
754impl<const KT: usize> Default for KeyTables<KT>
755where
756    rhdl::bits::W<KT>: BitWidth,
757{
758    fn default() -> Self {
759        KeyTables { ram: std::array::from_fn(|_| SyncBRAM::default()) }
760    }
761}
762
763impl<const KT: usize> KeyTables<KT>
764where
765    rhdl::bits::W<KT>: BitWidth,
766{
767    /// Key tables in UltraRAM (written at run time, never preloaded).
768    pub fn ultra() -> Self {
769        KeyTables { ram: std::array::from_fn(|_| SyncBRAM::default().with_style(RamStyle::Ultra)) }
770    }
771}
772
773/// Port bundle of [`KeyTables`]: one shared read row, one coordinate write.
774#[derive(Copy, Clone, Debug, PartialEq, Digital)]
775pub struct KeyTablesIn<const KT: usize>
776where
777    rhdl::bits::W<KT>: BitWidth,
778{
779    /// Row read by both images.
780    pub read_addr: Bits<KT>,
781    /// One coordinate write.
782    pub write: TableWrite,
783}
784
785/// Port types of [`KeyTables`]: the read returns the (even, odd) entries.
786impl<const KT: usize> SynchronousIO for KeyTables<KT>
787where
788    rhdl::bits::W<KT>: BitWidth,
789{
790    type I = KeyTablesIn<KT>;
791    type O = [Niels17; 2];
792    type Kernel = key_tables_kernel<KT>;
793}
794
795/// Wiring for [`KeyTables`].
796#[kernel]
797pub fn key_tables_kernel<const KT: usize>(
798    _cr: ClockReset,
799    i: KeyTablesIn<KT>,
800    q: KeyTablesQ<KT>,
801) -> ([Niels17; 2], KeyTablesD<KT>)
802where
803    rhdl::bits::W<KT>: BitWidth,
804{
805    let mut d = KeyTablesD::<KT>::dont_care();
806    let w = i.write;
807    // RAM number = image base (0 even, 3 odd) + coordinate; exactly one of
808    // the six RAMs is write-enabled, all share the address and data.
809    let image = if w.odd { b3(3) } else { b3(0) };
810    for k in 0..6 {
811        d.ram[k] = RamIn::<Fe17, KT> {
812            read_addr: i.read_addr,
813            write: RamWrite::<Fe17, KT> {
814                addr: w.row.resize::<KT>(),
815                value: w.value,
816                enable: w.valid && (b3(k as u128) == image + w.coord.resize::<3>()),
817            },
818        };
819    }
820    (
821        [
822            Niels17 { ypx: q.ram[0], ymx: q.ram[1], xy2d: q.ram[2] },
823            Niels17 { ypx: q.ram[3], ymx: q.ram[4], xy2d: q.ram[5] },
824        ],
825        d,
826    )
827}
828
829/// Barrel field machine. `K = 2^KB` contexts; `RA`/`IA` are bank-M/A and
830/// bank-I address widths (`index * K + ctx`); `PB` is the program-counter width.
831/// `TB` and `KT` are the base-point and key table address widths. See the
832/// module docs for the pipeline and bank layout.
833#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
834#[rhdl(dq_no_prefix)]
835pub struct FieldMachine<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
836where
837    rhdl::bits::W<KB>: BitWidth,
838    rhdl::bits::W<RA>: BitWidth,
839    rhdl::bits::W<IA>: BitWidth,
840    rhdl::bits::W<PB>: BitWidth,
841    rhdl::bits::W<TB>: BitWidth,
842    rhdl::bits::W<KT>: BitWidth,
843{
844    /// Thread ring, pipeline registers and the output register.
845    regs: DFF<MachineRegs<KB, PB, K>>,
846    /// Program ROM (`2^PB` bundles, preloaded); read one cycle ahead of issue.
847    rom: SyncBRAM<InstrW, PB>,
848    /// Register banks M, A, I and constant ROM C.
849    banks: RegBanks<RA, IA>,
850    /// Bank L, scan accumulator, digits and lookup tables.
851    scan: ScanMems<KB, TB, KT>,
852    /// The field multiplier, fed from S3: the 225-DSP
853    /// [`FieldMulPar`](crate::fe17::FieldMulPar) or, with `ED25519_MULTIPLIER=LEAN150`, the 150-DSP
854    /// [`FieldMulKara`](crate::fe17_kara::FieldMulKara) (same ports and
855    /// latency).
856    mul: FieldMul,
857    /// The adder/subtractor, fed from S3.
858    add: FieldAddSub,
859}
860
861/// Encode one operand reference. Bank C shares bank I's code with the index
862/// offset by [`CONST_BASE`]. Panics if the encoded index exceeds 7 bits.
863/// `program` is not used.
864fn encode_operand(o: Operand, _program: &Program) -> OperandW {
865    let (bank, index) = match o.bank {
866        Bank::M => (BANK_M, o.index as usize),
867        Bank::A => (BANK_A, o.index as usize),
868        Bank::I => (BANK_I, o.index as usize),
869        Bank::C => (BANK_I, CONST_BASE + o.index as usize),
870        Bank::L => (BANK_L, o.index as usize),
871    };
872    assert!(index < 128, "register index exceeds the 7-bit operand field");
873    OperandW {
874        bank: b2(bank),
875        index: b7(index as u128),
876    }
877}
878
879/// Encode a compiled bundle as a ROM word.
880///
881/// Empty slots are encoded disabled with harmless operands. Panics if the
882/// first multiply factor is in bank L (the instruction set reserves bank L
883/// for the second factor) or an operand index does not fit seven bits.
884pub fn encode_bundle(b: &Bundle, program: &Program) -> InstrW {
885    // Unused operands read input register 0: a valid address in any bank I.
886    let none = OperandW {
887        bank: b2(BANK_I),
888        index: b7(0),
889    };
890    let mut w = InstrW {
891        m_en: false,
892        m_dst: b7(0),
893        m_a: none,
894        m_b: none,
895        a_en: false,
896        a_op: b2(0),
897        a_dst: b7(0),
898        a_a: none,
899        a_b: none,
900        out_index: b6(0),
901        t_en: false,
902        t_digit: b7(0),
903        t_key: false,
904        t_pair: b3(0),
905        t_row: b9(0),
906        t_lslot: false,
907        t_clear: false,
908        t_finish: false,
909        end: false,
910    };
911    if let Some(m) = b.m {
912        w.m_en = true;
913        w.m_dst = b7(m.dst as u128);
914        w.m_a = encode_operand(m.a, program);
915        w.m_b = encode_operand(m.b, program);
916        assert!(m.a.bank != Bank::L);
917    }
918    if let Some(a) = b.a {
919        w.a_en = true;
920        w.a_dst = b7(a.dst as u128);
921        w.a_a = encode_operand(a.a, program);
922        w.a_b = encode_operand(a.b, program);
923        w.a_op = b2(match a.op {
924            AOp::Add => 0,
925            AOp::Sub => 1,
926            AOp::Out(index) => {
927                w.out_index = b6(index as u128);
928                2
929            }
930        });
931    }
932    if let Some(t) = b.t {
933        w.t_en = true;
934        w.t_digit = b7(t.digit as u128);
935        w.t_key = t.key;
936        w.t_pair = b3(t.pair as u128);
937        w.t_row = b9(t.row as u128);
938        w.t_lslot = t.l_slot == 1;
939        w.t_clear = t.clear;
940        w.t_finish = t.finish;
941    }
942    w.end = b.end;
943    w
944}
945
946/// A fixed-base table as two BRAM images (even and odd entries of each
947/// pair), row `table_row(i) + pair`.
948///
949/// Returns `(address, entry)` preload lists for `table0` and `table1`. The
950/// scanner reads both images at one row, so one T step tests two entries.
951pub fn table_images(table: &[Vec<Niels>]) -> (Vec<(usize, Niels17)>, Vec<(usize, Niels17)>) {
952    let mut even = Vec::new();
953    let mut odd = Vec::new();
954    for i in 0..DIGITS {
955        for e in 0..digit_entries(i) {
956            let n = table[i][e];
957            let entry = Niels17 {
958                ypx: Fe17::from_fp(n.ypx),
959                ymx: Fe17::from_fp(n.ymx),
960                xy2d: Fe17::from_fp(n.xy2d),
961            };
962            // Row table_row(i) + pair. `e` is zero-based, so entry number
963            // (magnitude) 2*pair+1 has even e = 2*pair and goes to `even`.
964            let address = table_row(i) + e / 2;
965            if e % 2 == 0 {
966                even.push((address, entry));
967            } else {
968                odd.push((address, entry));
969            }
970        }
971    }
972    // Every digit has an even entry count, so each row has both images.
973    assert_eq!(even.len(), TABLE_ROWS);
974    (even, odd)
975}
976
977/// A per-context scanner memory: LUT RAM when `lean` (the program never
978/// scans), otherwise left to synthesis.
979fn scan_mem<T: Digital, const N: usize>(lean: bool) -> SyncBRAM<T, N>
980where
981    rhdl::bits::W<N>: BitWidth,
982{
983    if lean { SyncBRAM::default().with_style(RamStyle::Distributed) } else { SyncBRAM::default() }
984}
985
986/// One copy of bank M or A: UltraRAM when `ultra`, LUT RAM when `lut`,
987/// otherwise left to synthesis (block RAM).
988fn bank<const RA: usize>(ultra: bool, lut: bool) -> SyncBRAM<Fe17, RA>
989where
990    rhdl::bits::W<RA>: BitWidth,
991{
992    if ultra {
993        SyncBRAM::default().with_style(RamStyle::Ultra)
994    } else if lut {
995        SyncBRAM::default().with_style(RamStyle::Distributed)
996    } else {
997        SyncBRAM::default()
998    }
999}
1000
1001/// Where a field machine keeps its memories.
1002///
1003/// The as-built machines put everything in block RAM, which is what runs
1004/// out first on a full chip (a fixed-base machine used 72 BRAM36: banks M
1005/// and A 16 each, bank I 16, base-point tables 22, program 2.5). The
1006/// specialized layout moves the run-time-written banks to UltraRAM, which
1007/// the design barely uses, and turns a constants-only bank I into LUT ROM.
1008#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1009pub struct MachineMemory {
1010    /// Banks M and A and the key tables in UltraRAM (`ram_style = "ultra"`).
1011    /// Bank I also goes to UltraRAM when the program has inputs and no
1012    /// presets (UltraRAM cannot be preloaded).
1013    pub ultra_banks: bool,
1014    /// Bank I as LUT ROM (`ram_style = "distributed"`) when the program
1015    /// takes no per-context inputs, so the bank only holds constants. Also
1016    /// puts the constant ROM C, and the scanner's per-context memories of
1017    /// programs with no T steps, in LUT RAM.
1018    pub rom_consts: bool,
1019    /// Banks M and A in LUT RAM (`ram_style = "distributed"`) when
1020    /// `ultra_banks` is off: the layout that turns spare LUTs into
1021    /// machines once UltraRAM and block RAM are spent. Key tables stay in
1022    /// UltraRAM.
1023    pub lut_banks: bool,
1024}
1025
1026impl MachineMemory {
1027    /// Every memory left to synthesis (block RAM), as mapped in Vivado on
1028    /// 2026-10-14.
1029    pub const AS_BUILT: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: false, lut_banks: false };
1030    /// UltraRAM banks and LUT-ROM constants (the default for [`FieldMachine::new`]).
1031    pub const SPECIALIZED: MachineMemory = MachineMemory { ultra_banks: true, rom_consts: true, lut_banks: false };
1032    /// The block-RAM fallback for a machine that UltraRAM cannot hold:
1033    /// banks M and A (and key tables) in block RAM, constants still in LUT
1034    /// ROM. What the planner places when a build runs out of UltraRAM.
1035    pub const BRAM_BANKS: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: true, lut_banks: false };
1036    /// The LUT-RAM fallback: banks M and A in LUT RAM, key tables in
1037    /// UltraRAM, constants in LUT ROM.
1038    pub const LUT_BANKS: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: true, lut_banks: true };
1039}
1040
1041/// The layout of machine `k` of a core's replicated machines (fixed-base
1042/// machines in a signer, verify machines in a verifier) in this build:
1043/// [`MachineMemory::SPECIALIZED`] for the first [`URAM_MACHINES`],
1044/// [`MachineMemory::LUT_BANKS`] for the next [`LUT_MACHINES`], and
1045/// [`MachineMemory::BRAM_BANKS`] for the rest.
1046pub fn placement(k: usize) -> MachineMemory {
1047    if k < URAM_MACHINES {
1048        MachineMemory::SPECIALIZED
1049    } else if k - URAM_MACHINES < LUT_MACHINES {
1050        MachineMemory::LUT_BANKS
1051    } else {
1052        MachineMemory::BRAM_BANKS
1053    }
1054}
1055
1056/// Constructors: preload the program ROM, constants and tables, and pick
1057/// memory styles for the program.
1058impl<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
1059    FieldMachine<KB, K, RA, IA, PB, TB, KT>
1060where
1061    rhdl::bits::W<KB>: BitWidth,
1062    rhdl::bits::W<RA>: BitWidth,
1063    rhdl::bits::W<IA>: BitWidth,
1064    rhdl::bits::W<PB>: BitWidth,
1065    rhdl::bits::W<TB>: BitWidth,
1066    rhdl::bits::W<KT>: BitWidth,
1067{
1068    /// Build a machine running `program` (specialized memory layout).
1069    pub fn new(program: &Program) -> Self {
1070        Self::with_presets(program, &[])
1071    }
1072
1073    /// Build a machine running `program` with the given memory layout.
1074    pub fn with_memory(program: &Program, memory: MachineMemory) -> Self {
1075        Self::build(program, &[], memory)
1076    }
1077
1078    /// Build a machine whose bank-I registers `index` start as `value` in
1079    /// every context (used for identity defaults in partial batches).
1080    pub fn with_presets(program: &Program, presets: &[(usize, Fp)]) -> Self {
1081        Self::build(program, presets, MachineMemory::SPECIALIZED)
1082    }
1083
1084    /// Shared constructor. Checks that the parameters fit the program, then
1085    /// builds every memory's preload image and style.
1086    fn build(program: &Program, presets: &[(usize, Fp)], memory: MachineMemory) -> Self {
1087        // K and KB must agree; K >= 32 is what makes hazard logic unnecessary.
1088        assert_eq!(K, 1 << KB);
1089        assert!(K >= 32, "barrel depth must cover the 25-cycle write-back");
1090        // Result tags are `ctx << 7 | dst` in 16 bits.
1091        assert!(KB <= 9, "result tags hold at most 9 context bits");
1092        assert!(program.bundles.len() <= 1 << PB);
1093        assert!(program.regs_m.max(program.regs_a) << KB <= 1 << RA);
1094        assert!(program.regs_i <= CONST_BASE && program.consts.len() <= CONST_SLOTS);
1095        assert!(program.regs_i << KB <= 1 << IA, "bank I too small");
1096        // Program ROM image: bundle `pc` at address `pc`.
1097        let rom: Vec<(Bits<PB>, InstrW)> = program
1098            .bundles
1099            .iter()
1100            .enumerate()
1101            .map(|(pc, b)| (bits(pc as u128), encode_bundle(b, program)))
1102            .collect();
1103        let consts: Vec<(Bits<5>, Fe17)> =
1104            program.consts.iter().enumerate().map(|(c, value)| (bits::<5>(c as u128), Fe17::from_fp(*value))).collect();
1105        // Bank-I preload: the same value at word `index << KB | ctx` for every
1106        // context.
1107        let mut preset_words = Vec::new();
1108        for &(index, value) in presets {
1109            assert!(index < program.regs_i);
1110            for ctx in 0..K {
1111                preset_words.push((bits::<IA>(((index << KB) | ctx) as u128), Fe17::from_fp(value)));
1112            }
1113        }
1114        // Bank I: never written without inputs (LUT RAM, nearly free); run-time
1115        // inputs without presets can live in UltraRAM.
1116        let no_inputs = memory.rom_consts && program.regs_i == 0 && presets.is_empty();
1117        let ultra_inputs = memory.ultra_banks && program.regs_i > 0 && presets.is_empty();
1118        // Machines that never scan the base-point table keep a 2-row stub.
1119        let scans = program.bundles.iter().any(|b| b.t.is_some_and(|t| !t.key));
1120        // Programs with no table steps at all never touch the scanner's
1121        // memories: keep them in LUT RAM instead of block RAM.
1122        let lean_scan = memory.rom_consts && !program.bundles.iter().any(|b| b.t.is_some());
1123        let (even, odd) = if scans {
1124            assert!(TABLE_ROWS <= 1 << TB, "base-point table needs {TABLE_ROWS} rows");
1125            let (e, o) = table_images(basepoint_table());
1126            (
1127                e.into_iter().map(|(a, v)| (bits::<TB>(a as u128), v)).collect::<Vec<_>>(),
1128                o.into_iter().map(|(a, v)| (bits::<TB>(a as u128), v)).collect::<Vec<_>>(),
1129            )
1130        } else {
1131            (Vec::new(), Vec::new())
1132        };
1133        // Reset state: all contexts idle, pipeline empty.
1134        let ring = Ring {
1135            active: false,
1136            pc: bits(0),
1137            tag: b16(0),
1138        };
1139        let stage = Stage {
1140            valid: false,
1141            last: false,
1142            ctx: bits(0),
1143            tag: b16(0),
1144        };
1145        // An all-slots-disabled bundle for the empty S2/S3 stages.
1146        let idle = encode_bundle(&Bundle::default(), program);
1147        let zero = Fe17::zero();
1148        let niels = Niels17 {
1149            ypx: zero,
1150            ymx: zero,
1151            xy2d: zero,
1152        };
1153        Self {
1154            regs: DFF::new(MachineRegs {
1155                slot: bits(0),
1156                ring: [ring; K],
1157                pending: [false; K],
1158                pending_tag: [b16(0); K],
1159                key: [bits(0); K],
1160                pending_entry: [b12(0); K],
1161                out: OutEvent {
1162                    valid: false,
1163                    ctx: bits(0),
1164                    tag: b16(0),
1165                    index: b6(0),
1166                    value: zero,
1167                },
1168                s2: stage,
1169                s2_instr: idle,
1170                s2_digit: b6(0),
1171                dig_fresh: false,
1172                s1_instr: idle,
1173                s3: stage,
1174                s3_instr: idle,
1175                s3_ops: [zero; 4],
1176                s3_acc: niels,
1177                s3_negative: false,
1178                s4_commit: false,
1179                s4_lslot: false,
1180                s4_ctx: bits(0),
1181                s4_l: niels,
1182                in_start: StartReq { valid: false, ctx: bits(0), tag: b16(0), key: bits(0), entry: b12(0) },
1183                in_digits: DigitsWrite { valid: false, ctx: bits(0), digits: [b6(0); 128] },
1184                in_input: InputWrite { valid: false, ctx: bits(0), index: b7(0), value: zero },
1185                in_table: TableWrite { valid: false, row: bits(0), odd: false, coord: b2(0), value: zero },
1186            }),
1187            rom: SyncBRAM::new(rom),
1188            banks: RegBanks {
1189                m: std::array::from_fn(|_| bank(memory.ultra_banks, memory.lut_banks)),
1190                a: std::array::from_fn(|_| bank(memory.ultra_banks, memory.lut_banks)),
1191                i: std::array::from_fn(|_| {
1192                    let bank = SyncBRAM::new(preset_words.clone());
1193                    if no_inputs {
1194                        bank.with_style(RamStyle::Distributed)
1195                    } else if ultra_inputs {
1196                        bank.with_style(RamStyle::Ultra)
1197                    } else {
1198                        bank
1199                    }
1200                }),
1201                c: std::array::from_fn(|_| ConstRom::new(&consts)),
1202            },
1203            scan: ScanMems {
1204                bank_l0: scan_mem(lean_scan),
1205                bank_l1: scan_mem(lean_scan),
1206                acc: scan_mem(lean_scan),
1207                digits: scan_mem(lean_scan),
1208                table0: SyncBRAM::new(even),
1209                table1: SyncBRAM::new(odd),
1210                // KT = 1 is a stub table; UltraRAM is only worth it for real ones.
1211                key: if (memory.ultra_banks || memory.lut_banks) && KT > 1 { KeyTables::ultra() } else { KeyTables::default() },
1212            },
1213            mul: FieldMul::default(),
1214            add: FieldAddSub::default(),
1215        }
1216    }
1217}
1218
1219/// Port types of [`FieldMachine`].
1220impl<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
1221    SynchronousIO for FieldMachine<KB, K, RA, IA, PB, TB, KT>
1222where
1223    rhdl::bits::W<KB>: BitWidth,
1224    rhdl::bits::W<RA>: BitWidth,
1225    rhdl::bits::W<IA>: BitWidth,
1226    rhdl::bits::W<PB>: BitWidth,
1227    rhdl::bits::W<TB>: BitWidth,
1228    rhdl::bits::W<KT>: BitWidth,
1229{
1230    type I = MachineIn<KB>;
1231    type O = MachineOut<KB>;
1232    type Kernel = field_machine_kernel<KB, K, RA, IA, PB, TB, KT>;
1233}
1234
1235/// The group identity (the point `(0, 1)`) in Niels form: `y + x = 1`,
1236/// `y - x = 1`, `2dxy = 0`. A lookup of digit 0 returns it, and adding it is
1237/// a no-op, so zero digits need no special case in the program.
1238#[kernel]
1239pub fn niels_identity() -> Niels17 {
1240    let mut one = [b17(0); 15];
1241    one[0] = b17(1);
1242    Niels17 {
1243        ypx: Fe17 { d: one },
1244        ymx: Fe17 { d: one },
1245        xy2d: Fe17 { d: [b17(0); 15] },
1246    }
1247}
1248
1249/// `-x mod p` for canonical `x` (zero stays zero).
1250///
1251/// Combinational. [`p_minus`] alone would map 0 to `p`, which is not
1252/// canonical, hence the zero test.
1253#[kernel]
1254pub fn field_neg(x: Fe17) -> Fe17 {
1255    // OR-reduce the digits.
1256    let mut zero = true;
1257    for k in 0..15 {
1258        if x.d[k] != b17(0) {
1259            zero = false;
1260        }
1261    }
1262    if zero {
1263        x
1264    } else {
1265        Fe17 {
1266            d: digits_from_limbs(p_minus(limbs_from_digits(x.d))),
1267        }
1268    }
1269}
1270
1271/// A memory port bundle that reads `addr` and never writes.
1272///
1273/// The write side still needs defined values, so it reuses `addr` and a
1274/// caller-supplied `filler` (usually the memory's current output) rather
1275/// than leaving them as don't-cares.
1276#[kernel]
1277fn read_disabled<T: Digital, const N: usize>(addr: Bits<N>, filler: T) -> RamIn<T, N>
1278where
1279    rhdl::bits::W<N>: BitWidth,
1280{
1281    RamIn::<T, N> {
1282        read_addr: addr,
1283        write: RamWrite::<T, N> {
1284            addr: addr,
1285            value: filler,
1286            enable: false,
1287        },
1288    }
1289}
1290
1291/// State transition for [`FieldMachine`].
1292///
1293/// One call handles every pipeline stage at once, each working on a
1294/// different bundle: S1 for the context whose turn it is, S2 and S3 for the
1295/// bundles issued one and two cycles earlier, S4 for the bank-L commit, and
1296/// the write-back of whatever the units return this cycle. Values read from
1297/// `q` are memory outputs addressed in the previous cycle; values written to
1298/// `d` take effect at the next edge.
1299#[kernel]
1300pub fn field_machine_kernel<
1301    const KB: usize,
1302    const K: usize,
1303    const RA: usize,
1304    const IA: usize,
1305    const PB: usize,
1306    const TB: usize,
1307    const KT: usize,
1308>(
1309    _cr: ClockReset,
1310    i: MachineIn<KB>,
1311    q: Q<KB, K, RA, IA, PB, TB, KT>,
1312) -> (MachineOut<KB>, D<KB, K, RA, IA, PB, TB, KT>)
1313where
1314    rhdl::bits::W<KB>: BitWidth,
1315    rhdl::bits::W<RA>: BitWidth,
1316    rhdl::bits::W<IA>: BitWidth,
1317    rhdl::bits::W<PB>: BitWidth,
1318    rhdl::bits::W<TB>: BitWidth,
1319    rhdl::bits::W<KT>: BitWidth,
1320{
1321    let r = q.regs;
1322    // Next register state; starts as a copy so untouched fields hold.
1323    let mut n = r;
1324    let mut d = D::<KB, K, RA, IA, PB, TB, KT>::dont_care();
1325    // Register the write-only inputs and act on last cycle's (see
1326    // `MachineRegs::in_start`); the output handshake stays direct.
1327    n.in_start = i.start;
1328    n.in_digits = i.digits;
1329    n.in_input = i.input;
1330    n.in_table = i.table;
1331    let i = MachineIn::<KB> { start: r.in_start, digits: r.in_digits, input: r.in_input, table: r.in_table, out_accept: i.out_accept };
1332    // ---- External start requests: latch them; the context starts on its
1333    // next turn.
1334    if i.start.valid {
1335        n.pending[i.start.ctx] = true;
1336        n.pending_tag[i.start.ctx] = i.start.tag;
1337        n.key[i.start.ctx] = i.start.key;
1338        n.pending_entry[i.start.ctx] = i.start.entry;
1339    }
1340
1341    // ---- Output register handshake.
1342    if i.out_accept {
1343        n.out.valid = false;
1344    }
1345    // An OUT already in S2 or S3 will claim the register when it reaches S3,
1346    // so a new OUT may issue only if neither stage holds one. Stalled
1347    // bundles have a_en cleared and do not count.
1348    let out_in_flight = (r.s2_instr.a_en && r.s2_instr.a_op == b2(2))
1349        || (r.s3_instr.a_en && r.s3_instr.a_op == b2(2));
1350    // Uses the registered valid, so an accept frees the register one cycle
1351    // later.
1352    let out_free = !r.out.valid && !out_in_flight;
1353
1354    // ---- S1: the head context issues its prefetched bundle.
1355    let slot = r.slot;
1356    let head = r.ring[0];
1357    // This context's bundle: read two cycles ago, registered last cycle.
1358    let instr = r.s1_instr;
1359    n.s1_instr = q.rom;
1360    // An idle context with a pending start begins now at its entry bundle,
1361    // unless the start came too late for the read-ahead (dig_fresh).
1362    let starting = !head.active && r.pending[slot] && !r.dig_fresh;
1363    let wants = head.active || starting;
1364    let is_out = instr.a_en && instr.a_op == b2(2);
1365    // The only stall: an OUT with the output register busy waits a full turn.
1366    let issue = wants && (!is_out || out_free);
1367    let pc = if head.active { head.pc } else { r.pending_entry[slot].resize::<PB>() };
1368    let tag = if starting { r.pending_tag[slot] } else { head.tag };
1369    if starting && issue {
1370        n.pending[slot] = false;
1371    }
1372    let last = instr.end;
1373    // Rotate: the head moves to the tail with its advanced program counter.
1374    for k in 1..K {
1375        n.ring[k - 1] = r.ring[k];
1376    }
1377    n.ring[K - 1] = if issue {
1378        Ring::<PB> {
1379            // Retiring after the last bundle returns the context to idle.
1380            active: !last,
1381            pc: if last { bits::<PB>(0) } else { pc + bits::<PB>(1) },
1382            tag: tag,
1383        }
1384    } else {
1385        // Not issued: keep the state unchanged and retry next turn.
1386        head
1387    };
1388    // Wraps at K = 2^KB.
1389    n.slot = slot + bits::<KB>(1);
1390    // Prefetch the bundle of the context two turns ahead: the ROM read and
1391    // s1_instr take a cycle each. Its pc is final, since it last issued
1392    // K - 2 >= 1 cycles ago.
1393    let following = r.ring[2];
1394    let next_slot = slot + bits::<KB>(1);
1395    let ahead_slot = slot + bits::<KB>(2);
1396    let ahead_pc = if following.active {
1397        following.pc
1398    } else if i.start.valid && i.start.ctx == ahead_slot {
1399        // A start arriving this cycle is not yet in pending_entry; bypass it.
1400        i.start.entry.resize::<PB>()
1401    } else {
1402        r.pending_entry[ahead_slot].resize::<PB>()
1403    };
1404    d.rom = read_disabled::<InstrW, PB>(ahead_pc, instr);
1405
1406    // S1 register-file addresses: word `index << KB | ctx` in M, A and I,
1407    // and the low six index bits in C. All four are read; S2 picks the bank.
1408    let ctx1 = slot;
1409    let operands = [instr.m_a, instr.m_b, instr.a_a, instr.a_b];
1410    for p in 0..4 {
1411        let index = operands[p].index;
1412        let ra = (index.resize::<RA>() << (KB as u128)) | ctx1.resize::<RA>();
1413        let ia = (index.resize::<IA>() << (KB as u128)) | ctx1.resize::<IA>();
1414        d.banks.m[p].read_addr = ra;
1415        d.banks.a[p].read_addr = ra;
1416        d.banks.i[p].read_addr = ia;
1417        // Constant `c` sits at operand index CONST_BASE + c (the subtraction
1418        // wraps for input indices, whose ROM read is not used).
1419        d.banks.c[p].read_addr = (index - b7(CONST_BASE as u128)).resize::<5>();
1420    }
1421    // Per-context scanner words, and both table images at the bundle's
1422    // public row.
1423    d.scan.bank_l0.read_addr = ctx1;
1424    d.scan.bank_l1.read_addr = ctx1;
1425    d.scan.acc.read_addr = ctx1;
1426    // The digit word is read a cycle ahead, for the next head, so this
1427    // bundle's digit is picked here and S2 sees a 6-bit register. Digits
1428    // are written only together with a start, to an idle context, so the
1429    // word changes under a running context never; a write in the same
1430    // cycle as the read-ahead holds that start back one turn.
1431    d.scan.digits.read_addr = next_slot;
1432    n.s2_digit = q.scan.digits[instr.t_digit];
1433    // A start for the next head arriving now missed its bundle's read-ahead.
1434    n.dig_fresh = (i.digits.valid && i.digits.ctx == next_slot) || (i.start.valid && i.start.ctx == next_slot);
1435    d.scan.table0 = read_disabled::<Niels17, TB>(instr.t_row.resize::<TB>(), q.scan.table0);
1436    d.scan.table1 = read_disabled::<Niels17, TB>(instr.t_row.resize::<TB>(), q.scan.table1);
1437    // Key rows: key * 512 + row, truncated to KT bits.
1438    let key_row = ((r.key[slot].resize::<16>() << 9) | instr.t_row.resize::<16>()).resize::<KT>();
1439    d.scan.key.read_addr = key_row;
1440    n.s2 = Stage::<KB, PB> {
1441        valid: issue,
1442        last: last,
1443        ctx: ctx1,
1444        tag: tag,
1445    };
1446    n.s2_instr = instr;
1447    // A bundle that did not issue becomes a bubble: no unit, write or OUT.
1448    if !issue {
1449        n.s2_instr.m_en = false;
1450        n.s2_instr.a_en = false;
1451        n.s2_instr.t_en = false;
1452    }
1453
1454    // ---- S2: operand selection and the constant-address scan.
1455    // Bank read data addressed in S1 is on q.banks now.
1456    let ins2 = r.s2_instr;
1457    let ops2 = [ins2.m_a, ins2.m_b, ins2.a_a, ins2.a_b];
1458    for p in 0..4 {
1459        let bank = ops2[p].bank;
1460        n.s3_ops[p] = if bank == b2(BANK_M) {
1461            q.banks.m[p]
1462        } else if bank == b2(BANK_A) {
1463            q.banks.a[p]
1464        } else if bank == b2(BANK_I) {
1465            // Indices from CONST_BASE up select the constant ROM.
1466            if ops2[p].index >= b7(CONST_BASE as u128) { q.banks.c[p] } else { q.banks.i[p] }
1467        } else {
1468            // Bank L. Programs only use it as the second multiply factor
1469            // (encode_bundle and the Builder enforce this), but the select
1470            // is generated for every port. Index bit 2 picks the slot and
1471            // bits 1..0 the coordinate (3 also reads 2dxy).
1472            let index = ops2[p].index;
1473            let l = if (index & b7(4)) != b7(0) {
1474                q.scan.bank_l1
1475            } else {
1476                q.scan.bank_l0
1477            };
1478            let coordinate = index & b7(3);
1479            if coordinate == b7(0) {
1480                l.ypx
1481            } else if coordinate == b7(1) {
1482                l.ymx
1483            } else {
1484                l.xy2d
1485            }
1486        };
1487    }
1488    // Constant-address scan step. The digit was selected (in S1) from the
1489    // context's digit word by the public slot number; its value only steers
1490    // data multiplexers, never an address or enable.
1491    let digit = r.s2_digit;
1492    let magnitude = digit & b6(31);
1493    let negative = (digit & b6(32)) != b6(0);
1494    // Entry numbers this step covers: first (even image) and first + 1
1495    // (odd image).
1496    let first = (ins2.t_pair.resize::<6>() << 1) + b6(1);
1497    // The first step of a lookup starts from the identity, so magnitude 0
1498    // selects the identity.
1499    let mut acc = if ins2.t_clear { niels_identity() } else { q.scan.acc };
1500    let (entry0, entry1) = if ins2.t_key {
1501        (q.scan.key[0], q.scan.key[1])
1502    } else {
1503        (q.scan.table0, q.scan.table1)
1504    };
1505    if magnitude == first {
1506        acc = entry0;
1507    }
1508    if magnitude == first + b6(1) {
1509        acc = entry1;
1510    }
1511    // The accumulator is written back every T step, one cycle later (in
1512    // S3, below) from the registered `s3_acc`, so the RAM's write port is
1513    // fed straight from a register: the digit mux, the compare and the
1514    // 765-bit select end at flip-flops, not at the LUT-RAM. The context's
1515    // next step reads it K >= 2 cycles after this step, so the write still
1516    // lands first, and the read in S1 is another context's.
1517    n.s3_acc = acc;
1518    n.s3_negative = negative;
1519    n.s3 = r.s2;
1520    n.s3_instr = ins2;
1521
1522    // ---- S3: issue the arithmetic units, emit outputs, commit bank L.
1523    let ins3 = r.s3_instr;
1524    let ctx3 = r.s3.ctx;
1525    // The scan accumulator write of the step that was in S2 last cycle.
1526    d.scan.acc.write = RamWrite::<Niels17, KB> {
1527        addr: ctx3,
1528        value: r.s3_acc,
1529        enable: ins3.t_en,
1530    };
1531    let ops3 = r.s3_ops;
1532    // Tags: ctx << 7 | destination register. The write-back below unpacks
1533    // them into the bank address, so results need no other tracking.
1534    let mul_tag = (ctx3.resize::<16>() << 7) | ins3.m_dst.resize::<16>();
1535    d.mul = FieldMulIn {
1536        valid: ins3.m_en,
1537        a: ops3[0],
1538        b: ops3[1],
1539        tag: mul_tag,
1540    };
1541    let add_tag = (ctx3.resize::<16>() << 7) | ins3.a_dst.resize::<16>();
1542    // OUT uses the A slot's operand but not the adder.
1543    d.add = FieldAddIn {
1544        valid: ins3.a_en && ins3.a_op != b2(2),
1545        sub: ins3.a_op == b2(1),
1546        a: ops3[2],
1547        b: ops3[3],
1548        tag: add_tag,
1549    };
1550    // Conditional negation gets its own stage; bank L commits at S4.
1551    // Negating a Niels point swaps y+x and y-x and negates 2dxy.
1552    n.s4_commit = ins3.t_en && ins3.t_finish;
1553    n.s4_lslot = ins3.t_lslot;
1554    n.s4_ctx = ctx3;
1555    n.s4_l = if r.s3_negative {
1556        Niels17 {
1557            ypx: r.s3_acc.ymx,
1558            ymx: r.s3_acc.ypx,
1559            xy2d: field_neg(r.s3_acc.xy2d),
1560        }
1561    } else {
1562        r.s3_acc
1563    };
1564    // S4: commit the point registered last cycle into the chosen slot. Two
1565    // slots let a program start the next lookup while it still reads the
1566    // previous result.
1567    d.scan.bank_l0.write = RamWrite::<Niels17, KB> {
1568        addr: r.s4_ctx,
1569        value: r.s4_l,
1570        enable: r.s4_commit && !r.s4_lslot,
1571    };
1572    d.scan.bank_l1.write = RamWrite::<Niels17, KB> {
1573        addr: r.s4_ctx,
1574        value: r.s4_l,
1575        enable: r.s4_commit && r.s4_lslot,
1576    };
1577    // OUT: capture a_a into the output register. S1's out_free check
1578    // guarantees the register is empty.
1579    if ins3.a_en && ins3.a_op == b2(2) {
1580        n.out = OutEvent::<KB> {
1581            valid: true,
1582            ctx: ctx3,
1583            tag: r.s3.tag,
1584            index: ins3.out_index,
1585            value: ops3[2],
1586        };
1587    }
1588
1589    // ---- Write-back: one writer per bank, replicated to every read copy.
1590    // Unit results arriving this cycle: unpack tag (ctx << 7 | dst) into
1591    // word dst << KB | ctx.
1592    let m = q.mul;
1593    let m_addr = ((m.tag & b16(127)).resize::<RA>() << (KB as u128)) | (m.tag >> 7).resize::<RA>();
1594    let a = q.add;
1595    let a_addr = ((a.tag & b16(127)).resize::<RA>() << (KB as u128)) | (a.tag >> 7).resize::<RA>();
1596    // Loader writes to bank I use the same interleaved layout.
1597    let i_addr = (i.input.index.resize::<IA>() << (KB as u128)) | i.input.ctx.resize::<IA>();
1598    for p in 0..4 {
1599        d.banks.m[p].write = RamWrite::<Fe17, RA> {
1600            addr: m_addr,
1601            value: m.r,
1602            enable: m.valid,
1603        };
1604        d.banks.a[p].write = RamWrite::<Fe17, RA> {
1605            addr: a_addr,
1606            value: a.r,
1607            enable: a.valid,
1608        };
1609        d.banks.i[p].write = RamWrite::<Fe17, IA> {
1610            addr: i_addr,
1611            value: i.input.value,
1612            enable: i.input.valid,
1613        };
1614        // The constant ROM is never written.
1615        d.banks.c[p].write = RamWrite::<Fe17, 5> {
1616            addr: b5(0),
1617            value: i.input.value,
1618            enable: false,
1619        };
1620    }
1621    // Loader writes to the digit memory and key tables; the base-point
1622    // tables are ROMs.
1623    d.scan.digits.write = RamWrite::<[b6; 128], KB> {
1624        addr: i.digits.ctx,
1625        value: i.digits.digits,
1626        enable: i.digits.valid,
1627    };
1628    d.scan.table0.write.enable = false;
1629    d.scan.table1.write.enable = false;
1630    d.scan.key.write = i.table;
1631    d.regs = n;
1632    // done pulses when the last bundle of a program is in S3.
1633    let output = MachineOut::<KB> {
1634        out: r.out,
1635        done: r.s3.valid && r.s3.last,
1636        done_ctx: ctx3,
1637        done_tag: r.s3.tag,
1638    };
1639    (output, d)
1640}
1641
1642/// Host-side helper: pack signed digits into the hardware sign-magnitude form
1643/// (base-point scalar in slots 0..54; slots 64.. stay zero). For machines
1644/// without key lookups.
1645pub fn pack_digits(digits: &[i8; 64]) -> [b6; 128] {
1646    pack_two(digits, &[0; 64])
1647}
1648
1649/// Host-side helper: base-point digits in slots 0..54 and key digits in
1650/// slots 64..118.
1651///
1652/// Panics if either scalar has nonzero digits at or above [`DIGITS`]. Each
1653/// slot is `|d|` in bits 4..0 with bit 5 set for negative `d`.
1654pub fn pack_two(base: &[i8; 64], key: &[i8; 64]) -> [b6; 128] {
1655    assert!(base[DIGITS..].iter().all(|&d| d == 0) && key[DIGITS..].iter().all(|&d| d == 0));
1656    std::array::from_fn(|k| {
1657        let d = if k < 64 { base[k] } else { key[k - 64] };
1658        let magnitude = d.unsigned_abs() as u128;
1659        let sign = if d < 0 { 32 } else { 0 };
1660        b6(magnitude | sign)
1661    })
1662}