Skip to main content

wots_rhdl/
fabric.rs

1//! Historical nine-context, three-lane SHA transport fabric.
2//!
3//! This module is deliberately limited to transport. It arbitrates complete
4//! [`CompressionInput`] requests onto three fixed-throughput SHA lanes and
5//! routes their fixed-latency [`CompressionOutput`] responses back to context
6//! slots. It does **not** decide which WOTS task is dependency-ready, store
7//! intermediate hashes, or prove signature throughput.
8//!
9//! The issue side scans nine context slots from a four-bit round-robin cursor,
10//! accepts at most three distinct canonical requests, and advances past the
11//! last accepted slot. A valid request is canonical only when its task tag is
12//! well formed and the tag's context field names the slot carrying it. Since a
13//! slot is visited once per scan, one context can never occupy two lanes in a
14//! cycle.
15//!
16//! The return side validates each tag before indexing a context. Malformed and
17//! out-of-range tags are rejected. If two lanes nevertheless return to the
18//! same context in one cycle, every member of that collision is rejected and
19//! the context receives no response. Equal-latency lanes combined with the
20//! distinct-issue invariant make that condition unreachable in a correctly
21//! integrated design, but detecting it turns an integration fault into an
22//! explicit signal rather than silent data loss.
23//!
24//! Only the round-robin cursor is registered and reset. Request payloads,
25//! response payloads, tags, and router state are purely combinational here.
26//!
27//! # Architecture status
28//!
29//! This central 9-by-3 fabric is a preserved foundation from an earlier
30//! architecture study. The selected production signer does not instantiate
31//! [`ShaTransportFabric`]. Instead, each [`crate::cluster::LaneLocalCluster`]
32//! keeps four contexts, one lane, response routing, and result framing local;
33//! [`crate::top::Sha256SignerTop`] replicates that cluster three times. The
34//! local topology prevents wide context memory ports from crossing the global
35//! boundary and provides twelve total contexts.
36//!
37//! Tests of this module establish its own issue/routing behavior only. They do
38//! not establish production-signer topology, WOTS dependency scheduling,
39//! sustained signature throughput, synthesis, timing, resources, or hardware
40//! behavior.
41
42use rhdl::prelude::*;
43use rhdl_fpga::core::dff::DFF;
44use sha256_rhdl::lane::{CompressionInput, CompressionOutput};
45
46use crate::tag::{decode_tag_kernel, decode_task_tag, tag_is_well_formed_kernel};
47
48/// Number of context slots in this historical central fabric.
49pub const CONTEXT_COUNT: usize = 9;
50/// Number of compression-lane slots in this historical central fabric.
51pub const LANE_COUNT: usize = 3;
52
53const _: () = {
54    assert!(CONTEXT_COUNT <= 16);
55    assert!(LANE_COUNT <= CONTEXT_COUNT);
56};
57
58/// Nine context-owned requests presented to the round-robin issue kernel.
59#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
60pub struct IssueInput {
61    /// One stable request slot per scheduler context.
62    pub contexts: [CompressionInput; CONTEXT_COUNT],
63}
64
65/// Compact valid/tag view consumed by the emitted-RTL arbitration gate.
66///
67/// Separating control from the 7,000-plus-bit request payload keeps standalone
68/// Icarus checks small. [`issue_requests_kernel`] uses this exact kernel before
69/// muxing the selected payloads.
70#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
71pub struct IssueControlInput {
72    /// Request-valid bits by context slot.
73    pub valid: [bool; CONTEXT_COUNT],
74    /// Complete opaque request tags by context slot.
75    pub tags: [b32; CONTEXT_COUNT],
76}
77
78/// Compact arbitration decision used to drive the payload mux.
79#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
80pub struct IssueControlOutput {
81    /// Selected context for each lane; meaningful when `lane_valid` is true.
82    pub lane_contexts: [b4; LANE_COUNT],
83    /// Valid grant for each lane.
84    pub lane_valid: [bool; LANE_COUNT],
85    /// Per-context selected requests.
86    pub accepted: [bool; CONTEXT_COUNT],
87    /// Per-context malformed or wrongly owned requests.
88    pub rejected: [bool; CONTEXT_COUNT],
89    /// Number of selected contexts.
90    pub grant_count: b2,
91    /// Cursor for the following cycle.
92    pub next_context: b4,
93}
94
95/// Issue grants and the three lane inputs for one cycle.
96#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
97pub struct IssueOutput {
98    /// Compression tokens for lanes zero through two.
99    pub lanes: [CompressionInput; LANE_COUNT],
100    /// Per-context grant/ready indications.
101    ///
102    /// The compression lanes have no backpressure, so `ready` is asserted only
103    /// for a valid request selected in this cycle.
104    pub ready: [bool; CONTEXT_COUNT],
105    /// Per-context request handshakes. This equals `ready` for the current
106    /// always-ready lane interface and remains explicit for scheduler wiring.
107    pub accepted: [bool; CONTEXT_COUNT],
108    /// Valid requests rejected because the tag is malformed, names a context
109    /// outside the fabric, or does not name the slot carrying the request.
110    pub rejected: [bool; CONTEXT_COUNT],
111    /// Number of valid lane tokens produced this cycle.
112    pub grant_count: b2,
113    /// Cursor value to register for the next cycle.
114    pub next_context: b4,
115}
116
117/// Three compression-lane responses presented to the return router.
118#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
119pub struct ResponseRouterInput {
120    /// Fixed-latency outputs from lanes zero through two.
121    pub lanes: [CompressionOutput; LANE_COUNT],
122}
123
124/// Compact valid/tag view consumed by the emitted-RTL response gate.
125#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
126pub struct ResponseControlInput {
127    /// Response-valid bits by lane.
128    pub valid: [bool; LANE_COUNT],
129    /// Complete opaque response tags by lane.
130    pub tags: [b32; LANE_COUNT],
131}
132
133/// Compact validated response-routing decision.
134#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
135pub struct ResponseControlOutput {
136    /// Decoded context destination by lane.
137    pub lane_contexts: [b4; LANE_COUNT],
138    /// Lanes accepted after validation and collision detection.
139    pub lane_accepted: [bool; LANE_COUNT],
140    /// Contexts receiving exactly one response.
141    pub context_accepted: [bool; CONTEXT_COUNT],
142    /// Valid lanes with noncanonical task tags.
143    pub malformed: [bool; LANE_COUNT],
144    /// Valid canonical lanes naming contexts `9..=15`.
145    pub out_of_range: [bool; LANE_COUNT],
146    /// Every member of a same-context collision.
147    pub collision: [bool; LANE_COUNT],
148    /// Union of all rejection classes.
149    pub rejected: [bool; LANE_COUNT],
150}
151
152/// Per-context routed responses and fail-closed lane status.
153#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
154pub struct ResponseRouterOutput {
155    /// One response slot per scheduler context.
156    pub contexts: [CompressionOutput; CONTEXT_COUNT],
157    /// Per-context response handshakes.
158    pub accepted: [bool; CONTEXT_COUNT],
159    /// Lanes whose response was routed to exactly one context.
160    pub lane_accepted: [bool; LANE_COUNT],
161    /// Valid lanes rejected for a noncanonical task tag.
162    pub malformed: [bool; LANE_COUNT],
163    /// Valid, canonical lanes rejected because context is `9..=15`.
164    pub out_of_range: [bool; LANE_COUNT],
165    /// Canonical in-range lanes rejected because another lane names the same
166    /// context in this cycle. Every colliding lane is rejected.
167    pub collision: [bool; LANE_COUNT],
168    /// Union of malformed, out-of-range, and collision failures.
169    pub rejected: [bool; LANE_COUNT],
170}
171
172/// Combined request/response ports for [`ShaTransportFabric`].
173#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
174pub struct TransportInput {
175    /// Context-owned compression requests.
176    pub issue: IssueInput,
177    /// Fixed-latency lane responses.
178    pub returns: ResponseRouterInput,
179}
180
181/// Combined issue and return results for [`ShaTransportFabric`].
182#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
183pub struct TransportOutput {
184    /// Lane inputs and request handshakes.
185    pub issue: IssueOutput,
186    /// Routed responses and explicit rejection status.
187    pub returns: ResponseRouterOutput,
188}
189
190/// Return the request named by a four-bit context selector.
191///
192/// Callers keep the selector in `0..9`; the default arm is defensive and maps
193/// every other value to slot eight rather than synthesizing a large mux with
194/// an undefined output.
195#[kernel]
196fn select_request_kernel(
197    requests: [CompressionInput; CONTEXT_COUNT],
198    context: b4,
199) -> CompressionInput {
200    match context {
201        Bits::<4>(0) => requests[0],
202        Bits::<4>(1) => requests[1],
203        Bits::<4>(2) => requests[2],
204        Bits::<4>(3) => requests[3],
205        Bits::<4>(4) => requests[4],
206        Bits::<4>(5) => requests[5],
207        Bits::<4>(6) => requests[6],
208        Bits::<4>(7) => requests[7],
209        _ => requests[8],
210    }
211}
212
213/// Increment a valid context number modulo nine.
214#[kernel]
215fn increment_context_kernel(context: b4) -> b4 {
216    if context == b4(8) {
217        b4(0)
218    } else {
219        context + b4(1)
220    }
221}
222
223/// Convert a nine-bit mask to explicit per-context handshake wires.
224#[kernel]
225fn context_mask_kernel(mask: b9) -> [bool; CONTEXT_COUNT] {
226    [
227        mask & b9(0x001) != b9(0),
228        mask & b9(0x002) != b9(0),
229        mask & b9(0x004) != b9(0),
230        mask & b9(0x008) != b9(0),
231        mask & b9(0x010) != b9(0),
232        mask & b9(0x020) != b9(0),
233        mask & b9(0x040) != b9(0),
234        mask & b9(0x080) != b9(0),
235        mask & b9(0x100) != b9(0),
236    ]
237}
238
239/// Fairly select at most three distinct canonical context tags.
240///
241/// This is the area-small control kernel used by the complete payload fabric
242/// and by emitted-Verilog tests.
243#[kernel]
244#[allow(clippy::assign_op_pattern)] // Compound assignment is not supported by pinned RHDL.
245pub fn issue_control_kernel(input: IssueControlInput, cursor: b4) -> IssueControlOutput {
246    let mut lane_contexts = [b4(0); LANE_COUNT];
247    let mut lane_valid = [false; LANE_COUNT];
248    let mut accepted_mask = b9(0);
249    let mut rejected_mask = b9(0);
250    let mut eligible_mask = b9(0);
251    let mut grant_count = b2(0);
252    let mut scan_context = cursor;
253    let mut next_context = cursor;
254
255    // Validate each tag once at its statically known owner slot. The rotating
256    // scan below then selects one-bit eligibility rather than replicating a
257    // 32-bit tag mux and validator for every priority position.
258    let fields = [
259        decode_tag_kernel(input.tags[0]),
260        decode_tag_kernel(input.tags[1]),
261        decode_tag_kernel(input.tags[2]),
262        decode_tag_kernel(input.tags[3]),
263        decode_tag_kernel(input.tags[4]),
264        decode_tag_kernel(input.tags[5]),
265        decode_tag_kernel(input.tags[6]),
266        decode_tag_kernel(input.tags[7]),
267        decode_tag_kernel(input.tags[8]),
268    ];
269    let canonical = [
270        tag_is_well_formed_kernel(input.tags[0]) && fields[0].context == b4(0),
271        tag_is_well_formed_kernel(input.tags[1]) && fields[1].context == b4(1),
272        tag_is_well_formed_kernel(input.tags[2]) && fields[2].context == b4(2),
273        tag_is_well_formed_kernel(input.tags[3]) && fields[3].context == b4(3),
274        tag_is_well_formed_kernel(input.tags[4]) && fields[4].context == b4(4),
275        tag_is_well_formed_kernel(input.tags[5]) && fields[5].context == b4(5),
276        tag_is_well_formed_kernel(input.tags[6]) && fields[6].context == b4(6),
277        tag_is_well_formed_kernel(input.tags[7]) && fields[7].context == b4(7),
278        tag_is_well_formed_kernel(input.tags[8]) && fields[8].context == b4(8),
279    ];
280    if input.valid[0] && canonical[0] {
281        eligible_mask = eligible_mask | b9(0x001);
282    }
283    if input.valid[1] && canonical[1] {
284        eligible_mask = eligible_mask | b9(0x002);
285    }
286    if input.valid[2] && canonical[2] {
287        eligible_mask = eligible_mask | b9(0x004);
288    }
289    if input.valid[3] && canonical[3] {
290        eligible_mask = eligible_mask | b9(0x008);
291    }
292    if input.valid[4] && canonical[4] {
293        eligible_mask = eligible_mask | b9(0x010);
294    }
295    if input.valid[5] && canonical[5] {
296        eligible_mask = eligible_mask | b9(0x020);
297    }
298    if input.valid[6] && canonical[6] {
299        eligible_mask = eligible_mask | b9(0x040);
300    }
301    if input.valid[7] && canonical[7] {
302        eligible_mask = eligible_mask | b9(0x080);
303    }
304    if input.valid[8] && canonical[8] {
305        eligible_mask = eligible_mask | b9(0x100);
306    }
307    if input.valid[0] && !canonical[0] {
308        rejected_mask = rejected_mask | b9(0x001);
309    }
310    if input.valid[1] && !canonical[1] {
311        rejected_mask = rejected_mask | b9(0x002);
312    }
313    if input.valid[2] && !canonical[2] {
314        rejected_mask = rejected_mask | b9(0x004);
315    }
316    if input.valid[3] && !canonical[3] {
317        rejected_mask = rejected_mask | b9(0x008);
318    }
319    if input.valid[4] && !canonical[4] {
320        rejected_mask = rejected_mask | b9(0x010);
321    }
322    if input.valid[5] && !canonical[5] {
323        rejected_mask = rejected_mask | b9(0x020);
324    }
325    if input.valid[6] && !canonical[6] {
326        rejected_mask = rejected_mask | b9(0x040);
327    }
328    if input.valid[7] && !canonical[7] {
329        rejected_mask = rejected_mask | b9(0x080);
330    }
331    if input.valid[8] && !canonical[8] {
332        rejected_mask = rejected_mask | b9(0x100);
333    }
334
335    for _offset in 0..CONTEXT_COUNT {
336        let context_bit = b9(1) << scan_context;
337        let eligible = eligible_mask & context_bit != b9(0);
338        if eligible && grant_count < b2(3) {
339            match grant_count {
340                Bits::<2>(0) => {
341                    lane_contexts[0] = scan_context;
342                    lane_valid[0] = true;
343                }
344                Bits::<2>(1) => {
345                    lane_contexts[1] = scan_context;
346                    lane_valid[1] = true;
347                }
348                _ => {
349                    lane_contexts[2] = scan_context;
350                    lane_valid[2] = true;
351                }
352            }
353            accepted_mask = accepted_mask | context_bit;
354            grant_count = grant_count + b2(1);
355            next_context = increment_context_kernel(scan_context);
356        }
357        scan_context = increment_context_kernel(scan_context);
358    }
359
360    let accepted = context_mask_kernel(accepted_mask);
361    IssueControlOutput {
362        lane_contexts,
363        lane_valid,
364        accepted,
365        rejected: context_mask_kernel(rejected_mask),
366        grant_count,
367        next_context,
368    }
369}
370
371/// Fairly select at most three distinct canonical context requests.
372///
373/// `cursor` must be in `0..9`. [`ShaTransportFabric`] maintains that invariant
374/// across reset and wraparound. A selected request is copied wholesale, so all
375/// chaining words, block words, tag bits, and `valid` are preserved.
376#[kernel]
377pub fn issue_requests_kernel(input: IssueInput, cursor: b4) -> IssueOutput {
378    let control = issue_control_kernel(
379        IssueControlInput {
380            valid: [
381                input.contexts[0].valid,
382                input.contexts[1].valid,
383                input.contexts[2].valid,
384                input.contexts[3].valid,
385                input.contexts[4].valid,
386                input.contexts[5].valid,
387                input.contexts[6].valid,
388                input.contexts[7].valid,
389                input.contexts[8].valid,
390            ],
391            tags: [
392                input.contexts[0].tag,
393                input.contexts[1].tag,
394                input.contexts[2].tag,
395                input.contexts[3].tag,
396                input.contexts[4].tag,
397                input.contexts[5].tag,
398                input.contexts[6].tag,
399                input.contexts[7].tag,
400                input.contexts[8].tag,
401            ],
402        },
403        cursor,
404    );
405    let mut lanes = [CompressionInput::default(); LANE_COUNT];
406    if control.lane_valid[0] {
407        lanes[0] = select_request_kernel(input.contexts, control.lane_contexts[0]);
408    }
409    if control.lane_valid[1] {
410        lanes[1] = select_request_kernel(input.contexts, control.lane_contexts[1]);
411    }
412    if control.lane_valid[2] {
413        lanes[2] = select_request_kernel(input.contexts, control.lane_contexts[2]);
414    }
415    IssueOutput {
416        lanes,
417        ready: control.accepted,
418        accepted: control.accepted,
419        rejected: control.rejected,
420        grant_count: control.grant_count,
421        next_context: control.next_context,
422    }
423}
424
425/// Choose the accepted lane response for one context.
426#[kernel]
427fn select_response_kernel(
428    lanes: [CompressionOutput; LANE_COUNT],
429    lane_contexts: [b4; LANE_COUNT],
430    lane_accepted: [bool; LANE_COUNT],
431    context: b4,
432) -> CompressionOutput {
433    let mut response = CompressionOutput::default();
434    if lane_accepted[0] && lane_contexts[0] == context {
435        response = lanes[0];
436    }
437    if lane_accepted[1] && lane_contexts[1] == context {
438        response = lanes[1];
439    }
440    if lane_accepted[2] && lane_contexts[2] == context {
441        response = lanes[2];
442    }
443    response
444}
445
446/// Validate three lane tags and compute their context destinations.
447///
448/// This small control kernel is shared by the full payload router and the
449/// emitted-Verilog gate. Collision handling is fail closed.
450#[kernel]
451pub fn response_control_kernel(input: ResponseControlInput) -> ResponseControlOutput {
452    let fields = [
453        decode_tag_kernel(input.tags[0]),
454        decode_tag_kernel(input.tags[1]),
455        decode_tag_kernel(input.tags[2]),
456    ];
457    let well_formed = [
458        tag_is_well_formed_kernel(input.tags[0]),
459        tag_is_well_formed_kernel(input.tags[1]),
460        tag_is_well_formed_kernel(input.tags[2]),
461    ];
462    let lane_contexts = [fields[0].context, fields[1].context, fields[2].context];
463    let malformed = [
464        input.valid[0] && !well_formed[0],
465        input.valid[1] && !well_formed[1],
466        input.valid[2] && !well_formed[2],
467    ];
468    let out_of_range = [
469        input.valid[0] && well_formed[0] && lane_contexts[0] >= b4(9),
470        input.valid[1] && well_formed[1] && lane_contexts[1] >= b4(9),
471        input.valid[2] && well_formed[2] && lane_contexts[2] >= b4(9),
472    ];
473    let eligible = [
474        input.valid[0] && well_formed[0] && lane_contexts[0] < b4(9),
475        input.valid[1] && well_formed[1] && lane_contexts[1] < b4(9),
476        input.valid[2] && well_formed[2] && lane_contexts[2] < b4(9),
477    ];
478    let collision = [
479        eligible[0]
480            && ((eligible[1] && lane_contexts[0] == lane_contexts[1])
481                || (eligible[2] && lane_contexts[0] == lane_contexts[2])),
482        eligible[1]
483            && ((eligible[0] && lane_contexts[1] == lane_contexts[0])
484                || (eligible[2] && lane_contexts[1] == lane_contexts[2])),
485        eligible[2]
486            && ((eligible[0] && lane_contexts[2] == lane_contexts[0])
487                || (eligible[1] && lane_contexts[2] == lane_contexts[1])),
488    ];
489    let lane_accepted = [
490        eligible[0] && !collision[0],
491        eligible[1] && !collision[1],
492        eligible[2] && !collision[2],
493    ];
494    let context_accepted = [
495        (lane_accepted[0] && lane_contexts[0] == b4(0))
496            || (lane_accepted[1] && lane_contexts[1] == b4(0))
497            || (lane_accepted[2] && lane_contexts[2] == b4(0)),
498        (lane_accepted[0] && lane_contexts[0] == b4(1))
499            || (lane_accepted[1] && lane_contexts[1] == b4(1))
500            || (lane_accepted[2] && lane_contexts[2] == b4(1)),
501        (lane_accepted[0] && lane_contexts[0] == b4(2))
502            || (lane_accepted[1] && lane_contexts[1] == b4(2))
503            || (lane_accepted[2] && lane_contexts[2] == b4(2)),
504        (lane_accepted[0] && lane_contexts[0] == b4(3))
505            || (lane_accepted[1] && lane_contexts[1] == b4(3))
506            || (lane_accepted[2] && lane_contexts[2] == b4(3)),
507        (lane_accepted[0] && lane_contexts[0] == b4(4))
508            || (lane_accepted[1] && lane_contexts[1] == b4(4))
509            || (lane_accepted[2] && lane_contexts[2] == b4(4)),
510        (lane_accepted[0] && lane_contexts[0] == b4(5))
511            || (lane_accepted[1] && lane_contexts[1] == b4(5))
512            || (lane_accepted[2] && lane_contexts[2] == b4(5)),
513        (lane_accepted[0] && lane_contexts[0] == b4(6))
514            || (lane_accepted[1] && lane_contexts[1] == b4(6))
515            || (lane_accepted[2] && lane_contexts[2] == b4(6)),
516        (lane_accepted[0] && lane_contexts[0] == b4(7))
517            || (lane_accepted[1] && lane_contexts[1] == b4(7))
518            || (lane_accepted[2] && lane_contexts[2] == b4(7)),
519        (lane_accepted[0] && lane_contexts[0] == b4(8))
520            || (lane_accepted[1] && lane_contexts[1] == b4(8))
521            || (lane_accepted[2] && lane_contexts[2] == b4(8)),
522    ];
523    ResponseControlOutput {
524        lane_contexts,
525        lane_accepted,
526        context_accepted,
527        malformed,
528        out_of_range,
529        collision,
530        rejected: [
531            malformed[0] || out_of_range[0] || collision[0],
532            malformed[1] || out_of_range[1] || collision[1],
533            malformed[2] || out_of_range[2] || collision[2],
534        ],
535    }
536}
537
538/// Validate and route three fixed-latency lane responses to nine contexts.
539///
540/// Collision handling is fail closed: no response involved in a same-context
541/// collision reaches a context slot.
542#[kernel]
543pub fn route_responses_kernel(input: ResponseRouterInput) -> ResponseRouterOutput {
544    let control = response_control_kernel(ResponseControlInput {
545        valid: [
546            input.lanes[0].valid,
547            input.lanes[1].valid,
548            input.lanes[2].valid,
549        ],
550        tags: [input.lanes[0].tag, input.lanes[1].tag, input.lanes[2].tag],
551    });
552    let contexts = [
553        select_response_kernel(
554            input.lanes,
555            control.lane_contexts,
556            control.lane_accepted,
557            b4(0),
558        ),
559        select_response_kernel(
560            input.lanes,
561            control.lane_contexts,
562            control.lane_accepted,
563            b4(1),
564        ),
565        select_response_kernel(
566            input.lanes,
567            control.lane_contexts,
568            control.lane_accepted,
569            b4(2),
570        ),
571        select_response_kernel(
572            input.lanes,
573            control.lane_contexts,
574            control.lane_accepted,
575            b4(3),
576        ),
577        select_response_kernel(
578            input.lanes,
579            control.lane_contexts,
580            control.lane_accepted,
581            b4(4),
582        ),
583        select_response_kernel(
584            input.lanes,
585            control.lane_contexts,
586            control.lane_accepted,
587            b4(5),
588        ),
589        select_response_kernel(
590            input.lanes,
591            control.lane_contexts,
592            control.lane_accepted,
593            b4(6),
594        ),
595        select_response_kernel(
596            input.lanes,
597            control.lane_contexts,
598            control.lane_accepted,
599            b4(7),
600        ),
601        select_response_kernel(
602            input.lanes,
603            control.lane_contexts,
604            control.lane_accepted,
605            b4(8),
606        ),
607    ];
608    ResponseRouterOutput {
609        contexts,
610        accepted: control.context_accepted,
611        lane_accepted: control.lane_accepted,
612        malformed: control.malformed,
613        out_of_range: control.out_of_range,
614        collision: control.collision,
615        rejected: control.rejected,
616    }
617}
618
619/// Historical stateful nine-context arbiter plus combinational response router.
620///
621/// This device remains a focused transport oracle and is not a child of the
622/// active [`crate::top::Sha256SignerTop`].
623#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
624#[rhdl(dq_no_prefix)]
625pub struct ShaTransportFabric {
626    cursor: DFF<b4>,
627}
628
629impl Default for ShaTransportFabric {
630    fn default() -> Self {
631        Self {
632            cursor: DFF::new(b4(0)),
633        }
634    }
635}
636
637impl SynchronousIO for ShaTransportFabric {
638    type I = TransportInput;
639    type O = TransportOutput;
640    type Kernel = sha_transport_fabric_kernel;
641}
642
643/// Connectivity kernel for [`ShaTransportFabric`].
644#[kernel]
645#[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
646pub fn sha_transport_fabric_kernel(
647    _clock_reset: ClockReset,
648    input: TransportInput,
649    q: Q,
650) -> (TransportOutput, D) {
651    let issue = issue_requests_kernel(input.issue, q.cursor);
652    let returns = route_responses_kernel(input.returns);
653    (
654        TransportOutput { issue, returns },
655        D {
656            cursor: issue.next_context,
657        },
658    )
659}
660
661fn word_u32(word: b32) -> u32 {
662    let bytes = word.raw().to_le_bytes();
663    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
664}
665
666fn host_request_is_canonical(request: &CompressionInput, slot: usize) -> bool {
667    if !request.valid {
668        return false;
669    }
670    decode_task_tag(word_u32(request.tag)).is_ok_and(|tag| usize::from(tag.context) == slot)
671}
672
673/// Independent host oracle for one issue-arbitration cycle.
674///
675/// This model uses host integer indexing and [`decode_task_tag`] rather than
676/// the RHDL muxes, masks, and validation kernel. Tests use it as a scoreboard
677/// for VM traces.
678///
679/// # Panics
680///
681/// Panics if `cursor` does not name one of the nine contexts.
682#[must_use]
683pub fn host_issue_oracle(input: IssueInput, cursor: usize) -> IssueOutput {
684    assert!(cursor < CONTEXT_COUNT, "host cursor must name a context");
685    let mut output = IssueOutput {
686        next_context: b4(u128::try_from(cursor).expect("nine contexts fit b4")),
687        ..IssueOutput::default()
688    };
689    let mut granted = 0_usize;
690    for offset in 0..CONTEXT_COUNT {
691        let slot = (cursor + offset) % CONTEXT_COUNT;
692        let request = input.contexts[slot];
693        let canonical = host_request_is_canonical(&request, slot);
694        if request.valid && !canonical {
695            output.rejected[slot] = true;
696        }
697        if canonical && granted < LANE_COUNT {
698            output.lanes[granted] = request;
699            output.ready[slot] = true;
700            output.accepted[slot] = true;
701            granted += 1;
702            output.next_context =
703                b4(u128::try_from((slot + 1) % CONTEXT_COUNT).expect("nine contexts fit b4"));
704        }
705    }
706    output.grant_count = b2(u128::try_from(granted).expect("three grants fit b2"));
707    output
708}
709
710/// Independent host oracle for one response-routing cycle.
711///
712/// Like the hardware router, this rejects every member of a collision.
713#[must_use]
714pub fn host_response_router_oracle(input: ResponseRouterInput) -> ResponseRouterOutput {
715    let mut output = ResponseRouterOutput::default();
716    let mut decoded_contexts = [None; LANE_COUNT];
717
718    for (lane, response) in input.lanes.into_iter().enumerate() {
719        if !response.valid {
720            continue;
721        }
722        match decode_task_tag(word_u32(response.tag)) {
723            Err(_) => output.malformed[lane] = true,
724            Ok(tag) if usize::from(tag.context) >= CONTEXT_COUNT => {
725                output.out_of_range[lane] = true;
726            }
727            Ok(tag) => decoded_contexts[lane] = Some(usize::from(tag.context)),
728        }
729    }
730
731    for left in 0..LANE_COUNT {
732        for right in (left + 1)..LANE_COUNT {
733            if decoded_contexts[left].is_some() && decoded_contexts[left] == decoded_contexts[right]
734            {
735                output.collision[left] = true;
736                output.collision[right] = true;
737            }
738        }
739    }
740
741    for (lane, decoded_context) in decoded_contexts.into_iter().enumerate() {
742        output.rejected[lane] =
743            output.malformed[lane] || output.out_of_range[lane] || output.collision[lane];
744        if let Some(context) = decoded_context
745            && !output.rejected[lane]
746        {
747            output.contexts[context] = input.lanes[lane];
748            output.accepted[context] = true;
749            output.lane_accepted[lane] = true;
750        }
751    }
752    output
753}