Skip to main content

sha256_rhdl/
lane.rs

1//! Complete, one-block-per-cycle SHA-256 compression lane.
2//!
3//! A lane combines the registered round pipeline with a circular synchronous
4//! memory holding the original 256-bit chaining values. At cycle `t` it writes
5//! the accepted chaining value at pointer `p`. At cycle `t + ROUNDS - 1` it
6//! reads address `p`; the memory's registered output and the round token are
7//! both visible at cycle `t + ROUNDS`, where feed-forward is combinational.
8//!
9//! `ROUNDS` must equal `2^ADDRESS_BITS`. Consequently the current write address
10//! and the read address (`write + 1`) are always distinct for supported lanes,
11//! avoiding the pinned [`rhdl_fpga::core::ram::synchronous::SyncBRAM`]'s explicitly undefined same-address
12//! read/write behavior. The pointer advances through valid tokens and bubbles,
13//! because alignment is measured in cycles rather than accepted-token count.
14//!
15//! All three-lane figures in this module are arithmetic projections. Physical
16//! block-RAM mapping, three-lane integration, resources, and timing remain
17//! synthesis or implementation gates.
18
19use rhdl::{core::circuit::descriptor::SyncKind, prelude::*};
20use rhdl_fpga::core::{
21    dff::DFF,
22    ram::synchronous::{In as SyncBramIn, SyncBRAM, Write as SyncBramWrite},
23};
24
25use crate::{
26    PipelineWord, ROUND_COUNT, RoundPipeline, RoundState, WorkingState, feed_forward_kernel,
27};
28
29/// Address width of the complete 64-round lane's chaining-value memory.
30pub const FULL_LANE_ADDRESS_BITS: usize = 6;
31
32/// Canonical module name of the separately generated complete compression lane.
33///
34/// [`ExternalFullCompressionLane`] emits a typed unresolved reference with this
35/// name. The modular signer build must supply the byte-for-byte RHDL-generated
36/// [`FullCompressionLane`] hierarchy under the same name before external HDL
37/// parsing or synthesis.
38pub const SHA256_COMPRESSION_LANE_MODULE: &str = "sha256_compression_lane";
39
40/// Number of entries in the complete lane's circular chaining-value memory.
41pub const FULL_LANE_DELAY_DEPTH: usize = 1 << FULL_LANE_ADDRESS_BITS;
42
43/// Logical storage bits in one complete lane's chaining-value memory.
44pub const FULL_LANE_CHAINING_STORAGE_BITS: usize = FULL_LANE_DELAY_DEPTH * 8 * 32;
45
46/// Projected logical chaining-memory bits for three complete lanes.
47pub const THREE_LANE_CHAINING_STORAGE_BITS: usize = 3 * FULL_LANE_CHAINING_STORAGE_BITS;
48
49/// Flip-flops a naïve 64-stage chaining-value delay would add per lane.
50///
51/// [`CompressionLane`] replaces this logical delay with
52/// [`rhdl_fpga::core::ram::synchronous::SyncBRAM`]. Whether
53/// the target tools map it to physical block RAM is a synthesis evidence gate.
54pub const AVOIDED_CHAINING_DELAY_FFS: usize = ROUND_COUNT * 8 * 32;
55
56/// Projected flip-flops avoided by three circular chaining memories.
57pub const THREE_LANE_AVOIDED_CHAINING_DELAY_FFS: usize = 3 * AVOIDED_CHAINING_DELAY_FFS;
58
59const _: () = {
60    assert!(FULL_LANE_DELAY_DEPTH == ROUND_COUNT);
61    assert!(FULL_LANE_CHAINING_STORAGE_BITS == 16_384);
62    assert!(THREE_LANE_CHAINING_STORAGE_BITS == 49_152);
63    assert!(AVOIDED_CHAINING_DELAY_FFS == 16_384);
64    assert!(THREE_LANE_AVOIDED_CHAINING_DELAY_FFS == 49_152);
65};
66
67/// Prepared input accepted by a compression lane on every rising edge.
68///
69/// `block` contains the sixteen already-decoded big-endian SHA-256 words.
70/// `tag` is opaque to the lane and is preserved bit-for-bit for its scheduler.
71/// When `valid` is false, every datapath field is ignored and represents a
72/// bubble; there is no backpressure.
73#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
74pub struct CompressionInput {
75    /// Input chaining value `H[0]..H[7]`.
76    pub chaining: [b32; 8],
77    /// Sixteen big-endian words in the 512-bit compression block.
78    pub block: [b32; 16],
79    /// Opaque scheduler-owned metadata.
80    pub tag: b32,
81    /// Marks this cycle as an accepted compression token.
82    pub valid: bool,
83}
84
85/// Feed-forward result produced by a compression lane.
86///
87/// `digest` and `tag` are meaningful only while `valid` is true.
88#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
89pub struct CompressionOutput {
90    /// Eight-word SHA-256 state after round execution and feed-forward.
91    pub digest: [b32; 8],
92    /// Opaque scheduler tag copied from the corresponding input token.
93    pub tag: b32,
94    /// Marks `digest` and `tag` as meaningful.
95    pub valid: bool,
96}
97
98/// Fully occupied SHA-256 compression pipeline with circular base-state delay.
99///
100/// The implementation accepts one input or bubble per cycle without
101/// backpressure. Its logical input-to-output latency is exactly `ROUNDS`
102/// cycles: the synchronous memory read is scheduled during the last round and
103/// therefore does not add a 65th registered transfer.
104///
105/// # Storage contract
106///
107/// - round data and the 32-bit tag use resetless registers;
108/// - only round-valid bits and the circular pointer reset;
109/// - chaining values live in resetless
110///   [`rhdl_fpga::core::ram::synchronous::SyncBRAM`] storage and are ignored
111///   whenever the aligned round-valid bit is false;
112/// - `ROUNDS` must be a power of two equal to `2^ADDRESS_BITS`, and at least two.
113///
114/// The complete implementation is [`FullCompressionLane`]. Smaller power-of-two
115/// specializations are useful for fast lowering-equivalence tests.
116#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
117#[rhdl(dq_no_prefix)]
118pub struct CompressionLane<const ROUNDS: usize, const ADDRESS_BITS: usize>
119where
120    rhdl::bits::W<ROUNDS>: BitWidth,
121    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
122{
123    rounds: RoundPipeline<ROUNDS>,
124    chaining_delay: SyncBRAM<[b32; 8], ADDRESS_BITS>,
125    pointer: DFF<Bits<ADDRESS_BITS>>,
126}
127
128impl<const ROUNDS: usize, const ADDRESS_BITS: usize> Default
129    for CompressionLane<ROUNDS, ADDRESS_BITS>
130where
131    rhdl::bits::W<ROUNDS>: BitWidth,
132    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
133{
134    fn default() -> Self {
135        assert!(ROUNDS >= 2, "a BRAM-backed lane needs at least two stages");
136        assert!(
137            ADDRESS_BITS < usize::BITS as usize,
138            "the address-width shift must fit usize"
139        );
140        assert_eq!(
141            ROUNDS,
142            1_usize << ADDRESS_BITS,
143            "round count must equal the circular-memory depth"
144        );
145        assert!(
146            ROUNDS <= ROUND_COUNT,
147            "a SHA-256 compression lane cannot exceed 64 rounds"
148        );
149        Self {
150            rounds: RoundPipeline::default(),
151            chaining_delay: SyncBRAM::default(),
152            pointer: DFF::new(Bits::default()),
153        }
154    }
155}
156
157impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
158    for CompressionLane<ROUNDS, ADDRESS_BITS>
159where
160    rhdl::bits::W<ROUNDS>: BitWidth,
161    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
162{
163    type I = CompressionInput;
164    type O = CompressionOutput;
165    type Kernel = compression_lane_kernel<ROUNDS, ADDRESS_BITS>;
166}
167
168/// Connectivity and feed-forward kernel for [`CompressionLane`].
169#[kernel]
170pub fn compression_lane_kernel<const ROUNDS: usize, const ADDRESS_BITS: usize>(
171    clock_reset: ClockReset,
172    input: CompressionInput,
173    q: Q<ROUNDS, ADDRESS_BITS>,
174) -> (CompressionOutput, D<ROUNDS, ADDRESS_BITS>)
175where
176    rhdl::bits::W<ROUNDS>: BitWidth,
177    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
178{
179    let _ = clock_reset;
180    let next_pointer = q.pointer + 1;
181    let d = D::<ROUNDS, ADDRESS_BITS> {
182        rounds: PipelineWord {
183            state: RoundState {
184                working: WorkingState {
185                    a: input.chaining[0],
186                    b: input.chaining[1],
187                    c: input.chaining[2],
188                    d: input.chaining[3],
189                    e: input.chaining[4],
190                    f: input.chaining[5],
191                    g: input.chaining[6],
192                    h: input.chaining[7],
193                },
194                schedule: input.block,
195            },
196            tag: input.tag,
197            valid: input.valid,
198        },
199        chaining_delay: SyncBramIn::<[b32; 8], ADDRESS_BITS> {
200            read_addr: next_pointer,
201            write: SyncBramWrite::<[b32; 8], ADDRESS_BITS> {
202                addr: q.pointer,
203                value: input.chaining,
204                enable: input.valid,
205            },
206        },
207        pointer: next_pointer,
208    };
209    let output = CompressionOutput {
210        digest: feed_forward_kernel(q.chaining_delay, q.rounds.state.working),
211        tag: q.rounds.tag,
212        valid: q.rounds.valid,
213    };
214    (output, d)
215}
216
217/// Complete 64-round, 64-entry compression lane.
218pub type FullCompressionLane = CompressionLane<ROUND_COUNT, FULL_LANE_ADDRESS_BITS>;
219
220/// Copyable type-level adapter for an inline [`CompressionLane`].
221///
222/// Pinned RHDL derives place the injected child type itself in the generated
223/// `Copy + PartialEq` bounds for a parent's D/Q records, even though those
224/// records contain only the child's digital input and output. The concrete
225/// [`CompressionLane`] carries elaboration configuration and is intentionally
226/// not `Copy`. This zero-sized adapter preserves the exact native lane state,
227/// simulation, and checked descriptor while satisfying that conservative
228/// derive bound. It does not add a hardware wrapper or alter the emitted lane
229/// hierarchy.
230#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
231pub struct InlineCompressionLane<const ROUNDS: usize, const ADDRESS_BITS: usize>;
232
233impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousDQ
234    for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
235where
236    rhdl::bits::W<ROUNDS>: BitWidth,
237    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
238{
239    type D = ();
240    type Q = ();
241}
242
243impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
244    for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
245where
246    rhdl::bits::W<ROUNDS>: BitWidth,
247    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
248{
249    type I = CompressionInput;
250    type O = CompressionOutput;
251    type Kernel = NoSynchronousKernel<ClockReset, CompressionInput, (), (CompressionOutput, ())>;
252}
253
254impl<const ROUNDS: usize, const ADDRESS_BITS: usize> Synchronous
255    for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
256where
257    rhdl::bits::W<ROUNDS>: BitWidth,
258    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
259{
260    type S = <CompressionLane<ROUNDS, ADDRESS_BITS> as Synchronous>::S;
261
262    fn init(&self) -> Self::S {
263        CompressionLane::<ROUNDS, ADDRESS_BITS>::default().init()
264    }
265
266    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
267        CompressionLane::<ROUNDS, ADDRESS_BITS>::default().sim(clock_reset, input, state)
268    }
269
270    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
271        CompressionLane::<ROUNDS, ADDRESS_BITS>::default().descriptor(scoped_name)
272    }
273}
274
275/// Behavioral full lane with an unresolved external HDL implementation.
276///
277/// Simulation delegates to the exact [`FullCompressionLane`], preserving its
278/// 64-cycle latency, reset flushing, bubbles, tags, and digest arithmetic. HDL
279/// lowering deliberately emits only a typed reference to
280/// [`SHA256_COMPRESSION_LANE_MODULE`]. A modular build must separately generate
281/// that module from [`FullCompressionLane`] and pass the complete source set to
282/// a capable external parser. The unresolved skeleton alone is not checked RTL
283/// evidence and must never be sent to synthesis by itself.
284#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
285pub struct ExternalFullCompressionLane;
286
287impl SynchronousDQ for ExternalFullCompressionLane {
288    type D = ();
289    type Q = ();
290}
291
292impl SynchronousIO for ExternalFullCompressionLane {
293    type I = CompressionInput;
294    type O = CompressionOutput;
295    type Kernel = NoSynchronousKernel<ClockReset, CompressionInput, (), (CompressionOutput, ())>;
296}
297
298impl Synchronous for ExternalFullCompressionLane {
299    type S = <FullCompressionLane as Synchronous>::S;
300
301    fn init(&self) -> Self::S {
302        FullCompressionLane::default().init()
303    }
304
305    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
306        FullCompressionLane::default().sim(clock_reset, input, state)
307    }
308
309    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
310        Descriptor::<SyncKind> {
311            name: scoped_name,
312            input_kind: CompressionInput::static_kind(),
313            output_kind: CompressionOutput::static_kind(),
314            d_kind: Kind::Empty,
315            q_kind: Kind::Empty,
316            kernel: None,
317            netlist: None,
318            hdl: Some(HDLDescriptor {
319                name: SHA256_COMPRESSION_LANE_MODULE.into(),
320                modules: vlog::ModuleList { modules: vec![] },
321            }),
322            _phantom: core::marker::PhantomData,
323        }
324        .with_external_netlist_black_box()
325    }
326}
327
328/// Emits and Icarus-checks a power-of-two lane specialization.
329///
330/// This is intended for small specializations because the pinned RHDL release
331/// emits the complete round array as one monolithic function; Icarus Verilog 13
332/// cannot lex the full 64-stage form.
333///
334/// # Errors
335///
336/// Returns an error if the specialization is invalid, RHDL lowering fails, or
337/// Icarus rejects the generated module hierarchy.
338pub fn compression_lane_checked_verilog<const ROUNDS: usize, const ADDRESS_BITS: usize>()
339-> Result<String, RHDLError>
340where
341    rhdl::bits::W<ROUNDS>: BitWidth,
342    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
343{
344    let lane = CompressionLane::<ROUNDS, ADDRESS_BITS>::default();
345    Ok(lane
346        .descriptor(SHA256_COMPRESSION_LANE_MODULE.into())?
347        .hdl()?
348        .modules
349        .pretty())
350}
351
352/// Renders the full lane while deliberately bypassing its final Icarus check.
353///
354/// The pinned RHDL core is patched locally so nested composition consumes raw
355/// child descriptors rather than re-running Icarus for every level. Icarus
356/// Verilog 13 still overflows its scanner on the monolithic 64-stage function,
357/// so callers of this entry point must pass the returned hierarchy through an
358/// explicit capable parser such as Verilator or Vivado.
359///
360/// Any successfully returned text would be generation-only evidence. It would
361/// not be parse, simulation, equivalence, synthesis, resource, or timing
362/// evidence and would still require Verilator and Vivado gates.
363///
364/// # Errors
365///
366/// Returns an error if RHDL cannot construct or render the full descriptor.
367pub fn try_full_compression_lane_unchecked_verilog() -> Result<String, RHDLError> {
368    let lane = FullCompressionLane::default();
369    let descriptor = lane.descriptor(SHA256_COMPRESSION_LANE_MODULE.into())?;
370    let hdl = descriptor.hdl_unchecked()?;
371    Ok(hdl.modules.pretty())
372}
373
374#[cfg(test)]
375mod tests {
376    use std::collections::BTreeMap;
377
378    use rhdl::core::sim::ResetOrData;
379
380    use super::*;
381    use crate::{ROUND_CONSTANTS_RHDL, compress_block, round_kernel};
382
383    #[derive(Clone, Debug, Default, Synchronous, SynchronousDQ)]
384    struct ExternalLaneParent {
385        lane: ExternalFullCompressionLane,
386    }
387
388    impl SynchronousIO for ExternalLaneParent {
389        type I = CompressionInput;
390        type O = CompressionOutput;
391        type Kernel = external_lane_parent_kernel;
392    }
393
394    #[kernel]
395    fn external_lane_parent_kernel(
396        clock_reset: ClockReset,
397        input: CompressionInput,
398        q: ExternalLaneParentQ,
399    ) -> (CompressionOutput, ExternalLaneParentD) {
400        let _ = clock_reset;
401        (q.lane, ExternalLaneParentD { lane: input })
402    }
403
404    fn word(value: u32) -> b32 {
405        b32(u128::from(value))
406    }
407
408    fn word_u32(value: b32) -> u32 {
409        let bytes = value.raw().to_le_bytes();
410        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
411    }
412
413    fn prepared_input(marker: u32, tag: u32) -> CompressionInput {
414        let chaining = core::array::from_fn(|index| {
415            let index = u32::try_from(index).expect("eight chaining words fit u32");
416            word(
417                0x1020_3040_u32
418                    .wrapping_add(index.wrapping_mul(0x1111_1111))
419                    .rotate_left(index)
420                    ^ marker.rotate_right(index),
421            )
422        });
423        let block = core::array::from_fn(|index| {
424            let index = u32::try_from(index).expect("sixteen block words fit u32");
425            word(
426                marker
427                    .wrapping_mul(index.wrapping_add(1))
428                    .rotate_left(index * 3)
429                    ^ index.wrapping_mul(0x9e37_79b9),
430            )
431        });
432        CompressionInput {
433            chaining,
434            block,
435            tag: word(tag),
436            valid: true,
437        }
438    }
439
440    fn partial_output(input: &CompressionInput, rounds: usize) -> CompressionOutput {
441        let mut state = RoundState {
442            working: WorkingState {
443                a: input.chaining[0],
444                b: input.chaining[1],
445                c: input.chaining[2],
446                d: input.chaining[3],
447                e: input.chaining[4],
448                f: input.chaining[5],
449                g: input.chaining[6],
450                h: input.chaining[7],
451            },
452            schedule: input.block,
453        };
454        for round_constant in ROUND_CONSTANTS_RHDL.into_iter().take(rounds) {
455            state = round_kernel(state, round_constant);
456        }
457        CompressionOutput {
458            digest: feed_forward_kernel(input.chaining, state.working),
459            tag: input.tag,
460            valid: true,
461        }
462    }
463
464    fn expected_outputs(
465        events: &[ResetOrData<CompressionInput>],
466        rounds: usize,
467    ) -> Vec<(usize, CompressionOutput)> {
468        events
469            .iter()
470            .enumerate()
471            .filter_map(|(accepted_cycle, event)| {
472                let ResetOrData::Data(input) = event else {
473                    return None;
474                };
475                if !input.valid {
476                    return None;
477                }
478                let output_cycle = accepted_cycle + rounds;
479                if output_cycle >= events.len()
480                    || events[accepted_cycle + 1..output_cycle]
481                        .iter()
482                        .any(|event| matches!(event, ResetOrData::Reset))
483                {
484                    return None;
485                }
486                Some((output_cycle, partial_output(input, rounds)))
487            })
488            .collect()
489    }
490
491    fn observed_outputs<L>(
492        lane: &L,
493        events: &[ResetOrData<CompressionInput>],
494    ) -> Vec<(usize, CompressionOutput)>
495    where
496        L: Synchronous<I = CompressionInput, O = CompressionOutput>,
497    {
498        lane.run(events.iter().copied().clock_pos_edge(10))
499            .synchronous_sample()
500            .enumerate()
501            .filter_map(|(cycle, sample)| sample.output.valid.then_some((cycle, sample.output)))
502            .collect()
503    }
504
505    fn small_lane_events() -> Vec<ResetOrData<CompressionInput>> {
506        let mut events = vec![ResetOrData::Reset];
507        // Initialize every memory address and every resetless round stage before
508        // the emitted-Verilog test begins comparing complete output buses.
509        events.extend((0..4).map(|index| {
510            ResetOrData::Data(prepared_input(0x1000_0000 + index, 0xa000_0000 + index))
511        }));
512        events.extend([
513            ResetOrData::Data(prepared_input(0x2000_0001, 0xb000_0001)),
514            ResetOrData::Data(prepared_input(0x2000_0002, 0xb000_0002)),
515            ResetOrData::Data(CompressionInput::default()),
516            ResetOrData::Data(prepared_input(0x2000_0003, 0xb000_0003)),
517            // This reset kills tags B0000002/B0000003 while they are in flight.
518            ResetOrData::Reset,
519        ]);
520        for index in 0..13_u32 {
521            let input = if index % 5 == 3 {
522                CompressionInput::default()
523            } else {
524                prepared_input(0x3000_0000 + index, 0xc000_0000 + index)
525            };
526            events.push(ResetOrData::Data(input));
527        }
528        events.extend(std::iter::repeat_n(
529            ResetOrData::Data(CompressionInput::default()),
530            4,
531        ));
532        events
533    }
534
535    #[test]
536    fn small_lane_handles_consecutive_tokens_bubbles_reset_and_wraparound() -> Result<(), RHDLError>
537    {
538        let lane = CompressionLane::<4, 2>::default();
539        let events = small_lane_events();
540        let observed = observed_outputs(&lane, &events);
541        let expected = expected_outputs(&events, 4);
542        assert_eq!(observed, expected);
543        assert!(observed.windows(2).any(|pair| pair[1].0 == pair[0].0 + 1));
544        assert!(
545            !observed
546                .iter()
547                .any(|(_, output)| output.tag == word(0xb000_0002))
548        );
549        assert!(events.len() > 2 * 4);
550
551        let test_bench = lane
552            .run(events.into_iter().clock_pos_edge(10))
553            .collect::<SynchronousTestBench<_, _>>();
554        test_bench
555            .rtl(&lane, &TestBenchOptions::default().skip(18))?
556            .run_iverilog()?;
557        Ok(())
558    }
559
560    #[test]
561    fn full_lane_matches_compression_for_more_than_two_pointer_wraps() {
562        let mut events = vec![ResetOrData::Reset];
563        for cycle in 1..=20_u32 {
564            events.push(ResetOrData::Data(prepared_input(
565                0x4000_0000 + cycle,
566                0xd000_0000 + cycle,
567            )));
568        }
569        events.push(ResetOrData::Reset);
570        for cycle in 0..145_u32 {
571            let input = if cycle % 11 == 7 {
572                CompressionInput::default()
573            } else {
574                prepared_input(0x5000_0000 + cycle, 0xe000_0000 + cycle)
575            };
576            events.push(ResetOrData::Data(input));
577        }
578        events.extend(std::iter::repeat_n(
579            ResetOrData::Data(CompressionInput::default()),
580            ROUND_COUNT,
581        ));
582
583        let lane = FullCompressionLane::default();
584        let observed = observed_outputs(&lane, &events);
585        let expected = expected_outputs(&events, ROUND_COUNT);
586        assert_eq!(observed, expected);
587        assert!(observed.len() > FULL_LANE_DELAY_DEPTH);
588
589        let input_by_tag = events
590            .iter()
591            .filter_map(|event| match event {
592                ResetOrData::Data(input) if input.valid => Some((word_u32(input.tag), *input)),
593                _ => None,
594            })
595            .collect::<BTreeMap<_, _>>();
596        for (_, output) in observed {
597            let input = input_by_tag
598                .get(&word_u32(output.tag))
599                .expect("every output tag came from one prepared input");
600            let chaining = input.chaining.map(word_u32);
601            let block = input.block.map(word_u32);
602            assert_eq!(output.digest.map(word_u32), compress_block(chaining, block));
603        }
604    }
605
606    #[test]
607    fn external_full_lane_behavior_delegates_to_the_native_full_lane() {
608        let mut events = vec![ResetOrData::Reset];
609        for cycle in 0..70_u32 {
610            let input = if cycle.is_multiple_of(9) {
611                CompressionInput::default()
612            } else {
613                prepared_input(0x6000_0000 + cycle, 0xf000_0000 + cycle)
614            };
615            events.push(ResetOrData::Data(input));
616        }
617        events.push(ResetOrData::Reset);
618        events.push(ResetOrData::Data(prepared_input(0x7000_0001, 0x0bad_f00d)));
619        events.extend(std::iter::repeat_n(
620            ResetOrData::Data(CompressionInput::default()),
621            ROUND_COUNT,
622        ));
623
624        let native = observed_outputs(&FullCompressionLane::default(), &events);
625        let external = observed_outputs(&ExternalFullCompressionLane, &events);
626        assert_eq!(external, native);
627        assert_eq!(external.len(), 7);
628        assert_eq!(
629            external.last().expect("post-reset result").1.tag,
630            word(0x0bad_f00d)
631        );
632    }
633
634    #[test]
635    fn external_full_lane_descriptor_is_typed_and_unresolved() -> Result<(), RHDLError> {
636        let lane = ExternalFullCompressionLane;
637        let descriptor = lane.descriptor("ignored_parent_scope".into())?;
638        assert_eq!(descriptor.name.to_string(), "ignored_parent_scope");
639        assert_eq!(descriptor.input_kind, CompressionInput::static_kind());
640        assert_eq!(descriptor.output_kind, CompressionOutput::static_kind());
641        assert_eq!(descriptor.input_kind.bits(), 801);
642        assert_eq!(descriptor.output_kind.bits(), 289);
643        assert!(descriptor.hdl().is_err());
644        assert!(descriptor.hdl_unchecked()?.modules.modules.is_empty());
645
646        let wrapper = descriptor
647            .netlist()?
648            .as_vlog("sha256_external_lane_wrapper")?
649            .modules
650            .pretty();
651        assert!(wrapper.contains("sha256_compression_lane bb_0("));
652        assert!(!wrapper.contains("module sha256_compression_lane("));
653        Ok(())
654    }
655
656    #[test]
657    fn external_reference_resolves_against_separate_checked_lane_source() -> Result<(), RHDLError> {
658        let descriptor =
659            ExternalLaneParent::default().descriptor("sha256_external_lane_wrapper".into())?;
660        assert!(descriptor.hdl().is_err());
661        let wrapper = descriptor.hdl_unchecked()?.modules.pretty();
662        assert!(wrapper.contains("module sha256_external_lane_wrapper("));
663        assert!(
664            wrapper.contains("sha256_compression_lane c0("),
665            "unexpected modular wrapper:\n{wrapper}"
666        );
667        assert!(!wrapper.contains("module sha256_compression_lane("));
668        let lane = compression_lane_checked_verilog::<4, 2>()?;
669        assert_eq!(
670            wrapper.matches("module sha256_compression_lane(").count()
671                + lane.matches("module sha256_compression_lane(").count(),
672            1
673        );
674
675        let wrapper_path = std::env::temp_dir().join(format!(
676            "hashsigs_sha256_external_lane_wrapper_{}.v",
677            std::process::id()
678        ));
679        let lane_path = std::env::temp_dir().join(format!(
680            "hashsigs_sha256_external_lane_source_{}.v",
681            std::process::id()
682        ));
683        std::fs::write(&wrapper_path, wrapper).expect("write temporary modular wrapper");
684        std::fs::write(&lane_path, lane).expect("write temporary generated lane source");
685        let result = std::process::Command::new("iverilog")
686            .args(["-g2012", "-s", "sha256_external_lane_wrapper", "-t", "null"])
687            .arg(&wrapper_path)
688            .arg(&lane_path)
689            .output()
690            .expect("Icarus Verilog must be installed for RHDL RTL gates");
691        std::fs::remove_file(&wrapper_path).expect("remove temporary modular wrapper");
692        std::fs::remove_file(&lane_path).expect("remove temporary generated lane source");
693        assert!(
694            result.status.success(),
695            "separately generated lane did not resolve the external reference:\n{}\n{}",
696            String::from_utf8_lossy(&result.stdout),
697            String::from_utf8_lossy(&result.stderr)
698        );
699        Ok(())
700    }
701
702    #[test]
703    fn checked_small_lane_generator_exposes_resetless_memory() -> Result<(), RHDLError> {
704        let hdl = compression_lane_checked_verilog::<4, 2>()?;
705        assert!(hdl.contains("module sha256_compression_lane"));
706        assert!(hdl.contains("reg [255:0] mem[3:0]"));
707        assert_eq!(hdl.matches("if (reset)").count(), 5);
708        Ok(())
709    }
710
711    #[test]
712    #[ignore = "slow explicit external Verilator parser gate"]
713    fn full_lane_unchecked_render_passes_verilator_lint() -> Result<(), RHDLError> {
714        let hdl = try_full_compression_lane_unchecked_verilog()?;
715        assert!(hdl.contains("module sha256_compression_lane"));
716        assert!(hdl.contains("reg [255:0] mem[63:0]"));
717        let path = std::env::temp_dir().join(format!(
718            "hashsigs_sha256_full_lane_{}.v",
719            std::process::id()
720        ));
721        std::fs::write(&path, hdl).expect("write temporary full-lane Verilog");
722        let result = std::process::Command::new("verilator")
723            .args([
724                "--lint-only",
725                "--Wno-fatal",
726                "--top-module",
727                "sha256_compression_lane",
728            ])
729            .arg(&path)
730            .output()
731            .expect("Verilator must be installed for this explicitly ignored gate");
732        std::fs::remove_file(&path).expect("remove temporary full-lane Verilog");
733        assert!(
734            result.status.success(),
735            "Verilator lint failed:\n{}\n{}",
736            String::from_utf8_lossy(&result.stdout),
737            String::from_utf8_lossy(&result.stderr)
738        );
739        Ok(())
740    }
741}