Skip to main content

rhdl_ed25519_fast_core/
sha512_parallel.rs

1//! Four-lane, three-phase SHA-512 compression fabric.
2//!
3//! The signing pipeline hashes secret expansion, nonce material, and challenge
4//! material concurrently.  A single iterative compressor would serialize those
5//! independent jobs, so this module keeps four complete compression contexts.
6//! Each lane performs one SHA-512 round in three registered phases and accepts a
7//! fresh 1024-bit block whenever it is idle.  The 16-word circular schedule is
8//! held locally; neither message data nor schedule words leave the core.
9
10use rhdl::prelude::*;
11use rhdl_fpga::core::dff::DFF;
12
13/// Request accepted by one compression lane when [`Sha512CompressOut::ready`]
14/// is asserted.
15#[derive(Copy, Clone, Debug, PartialEq, Digital)]
16pub struct Sha512CompressIn {
17    /// Starts compression of one block.
18    pub start: bool,
19    /// Incoming eight-word SHA-512 chaining state, in FIPS word order.
20    pub state: [b64; 8],
21    /// Sixteen big-endian message words.
22    pub block: [b64; 16],
23}
24
25/// Result and flow-control signals from one compression lane.
26#[derive(Copy, Clone, Debug, PartialEq, Digital)]
27pub struct Sha512CompressOut {
28    /// The lane can accept a request in this cycle.
29    pub ready: bool,
30    /// [`Self::state`] contains a newly completed chaining state.
31    pub valid: bool,
32    /// Completed eight-word chaining state.
33    pub state: [b64; 8],
34}
35
36#[derive(Copy, Clone, Debug, Default, PartialEq, Digital)]
37enum Phase {
38    #[default]
39    Idle,
40    A,
41    B,
42    C,
43    Accumulate,
44}
45
46#[derive(Copy, Clone, Debug, PartialEq, Digital)]
47#[doc(hidden)]
48pub struct Sha512CompressRegs {
49    phase: Phase,
50    round: b7,
51    chain: [b64; 8],
52    work: [b64; 8],
53    schedule: [b64; 16],
54    schedule_left: b64,
55    schedule_right: b64,
56    round_word: b64,
57    sigma1_ch: b64,
58    sigma0_maj: b64,
59    h_plus_k: b64,
60    t1_base: b64,
61    t2: b64,
62    valid: bool,
63    result: [b64; 8],
64}
65
66fn reset_regs() -> Sha512CompressRegs {
67    Sha512CompressRegs {
68        phase: Phase::Idle,
69        round: b7(0),
70        chain: [b64(0); 8],
71        work: [b64(0); 8],
72        schedule: [b64(0); 16],
73        schedule_left: b64(0),
74        schedule_right: b64(0),
75        round_word: b64(0),
76        sigma1_ch: b64(0),
77        sigma0_maj: b64(0),
78        h_plus_k: b64(0),
79        t1_base: b64(0),
80        t2: b64(0),
81        valid: false,
82        result: [b64(0); 8],
83    }
84}
85
86/// A 242-cycle SHA-512 compressor with a 16-word circular schedule.
87#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
88#[rhdl(dq_no_prefix)]
89pub struct Sha512Compress {
90    regs: DFF<Sha512CompressRegs>,
91}
92
93impl Default for Sha512Compress {
94    fn default() -> Self {
95        Self {
96            regs: DFF::new(reset_regs()),
97        }
98    }
99}
100
101impl SynchronousIO for Sha512Compress {
102    type I = Sha512CompressIn;
103    type O = Sha512CompressOut;
104    type Kernel = sha512_compress_kernel;
105}
106
107/// Rotate right.
108#[kernel]
109pub fn rotr(value: b64, amount: b7) -> b64 {
110    (value >> amount) | (value << (b7(64) - amount))
111}
112
113/// SHA-512 message-schedule sigma0.
114#[kernel]
115pub fn small_sigma0(value: b64) -> b64 {
116    rotr(value, b7(1)) ^ rotr(value, b7(8)) ^ (value >> 7)
117}
118
119/// SHA-512 message-schedule sigma1.
120#[kernel]
121pub fn small_sigma1(value: b64) -> b64 {
122    rotr(value, b7(19)) ^ rotr(value, b7(61)) ^ (value >> 6)
123}
124
125/// SHA-512 round Sigma0.
126#[kernel]
127pub fn big_sigma0(value: b64) -> b64 {
128    rotr(value, b7(28)) ^ rotr(value, b7(34)) ^ rotr(value, b7(39))
129}
130
131/// SHA-512 round Sigma1.
132#[kernel]
133pub fn big_sigma1(value: b64) -> b64 {
134    rotr(value, b7(14)) ^ rotr(value, b7(18)) ^ rotr(value, b7(41))
135}
136
137// A case ROM is deliberate here.  It is only 5 Kib, has combinational read
138// semantics matching the three-phase schedule, and does not consume a BRAM
139// read-latency slot on the round path.
140/// SHA-512 round constant `K_t`.
141#[kernel]
142pub fn round_constant(round: b7) -> b64 {
143    match round {
144        Bits::<7>(0) => b64(0x428a2f98d728ae22),
145        Bits::<7>(1) => b64(0x7137449123ef65cd),
146        Bits::<7>(2) => b64(0xb5c0fbcfec4d3b2f),
147        Bits::<7>(3) => b64(0xe9b5dba58189dbbc),
148        Bits::<7>(4) => b64(0x3956c25bf348b538),
149        Bits::<7>(5) => b64(0x59f111f1b605d019),
150        Bits::<7>(6) => b64(0x923f82a4af194f9b),
151        Bits::<7>(7) => b64(0xab1c5ed5da6d8118),
152        Bits::<7>(8) => b64(0xd807aa98a3030242),
153        Bits::<7>(9) => b64(0x12835b0145706fbe),
154        Bits::<7>(10) => b64(0x243185be4ee4b28c),
155        Bits::<7>(11) => b64(0x550c7dc3d5ffb4e2),
156        Bits::<7>(12) => b64(0x72be5d74f27b896f),
157        Bits::<7>(13) => b64(0x80deb1fe3b1696b1),
158        Bits::<7>(14) => b64(0x9bdc06a725c71235),
159        Bits::<7>(15) => b64(0xc19bf174cf692694),
160        Bits::<7>(16) => b64(0xe49b69c19ef14ad2),
161        Bits::<7>(17) => b64(0xefbe4786384f25e3),
162        Bits::<7>(18) => b64(0x0fc19dc68b8cd5b5),
163        Bits::<7>(19) => b64(0x240ca1cc77ac9c65),
164        Bits::<7>(20) => b64(0x2de92c6f592b0275),
165        Bits::<7>(21) => b64(0x4a7484aa6ea6e483),
166        Bits::<7>(22) => b64(0x5cb0a9dcbd41fbd4),
167        Bits::<7>(23) => b64(0x76f988da831153b5),
168        Bits::<7>(24) => b64(0x983e5152ee66dfab),
169        Bits::<7>(25) => b64(0xa831c66d2db43210),
170        Bits::<7>(26) => b64(0xb00327c898fb213f),
171        Bits::<7>(27) => b64(0xbf597fc7beef0ee4),
172        Bits::<7>(28) => b64(0xc6e00bf33da88fc2),
173        Bits::<7>(29) => b64(0xd5a79147930aa725),
174        Bits::<7>(30) => b64(0x06ca6351e003826f),
175        Bits::<7>(31) => b64(0x142929670a0e6e70),
176        Bits::<7>(32) => b64(0x27b70a8546d22ffc),
177        Bits::<7>(33) => b64(0x2e1b21385c26c926),
178        Bits::<7>(34) => b64(0x4d2c6dfc5ac42aed),
179        Bits::<7>(35) => b64(0x53380d139d95b3df),
180        Bits::<7>(36) => b64(0x650a73548baf63de),
181        Bits::<7>(37) => b64(0x766a0abb3c77b2a8),
182        Bits::<7>(38) => b64(0x81c2c92e47edaee6),
183        Bits::<7>(39) => b64(0x92722c851482353b),
184        Bits::<7>(40) => b64(0xa2bfe8a14cf10364),
185        Bits::<7>(41) => b64(0xa81a664bbc423001),
186        Bits::<7>(42) => b64(0xc24b8b70d0f89791),
187        Bits::<7>(43) => b64(0xc76c51a30654be30),
188        Bits::<7>(44) => b64(0xd192e819d6ef5218),
189        Bits::<7>(45) => b64(0xd69906245565a910),
190        Bits::<7>(46) => b64(0xf40e35855771202a),
191        Bits::<7>(47) => b64(0x106aa07032bbd1b8),
192        Bits::<7>(48) => b64(0x19a4c116b8d2d0c8),
193        Bits::<7>(49) => b64(0x1e376c085141ab53),
194        Bits::<7>(50) => b64(0x2748774cdf8eeb99),
195        Bits::<7>(51) => b64(0x34b0bcb5e19b48a8),
196        Bits::<7>(52) => b64(0x391c0cb3c5c95a63),
197        Bits::<7>(53) => b64(0x4ed8aa4ae3418acb),
198        Bits::<7>(54) => b64(0x5b9cca4f7763e373),
199        Bits::<7>(55) => b64(0x682e6ff3d6b2b8a3),
200        Bits::<7>(56) => b64(0x748f82ee5defb2fc),
201        Bits::<7>(57) => b64(0x78a5636f43172f60),
202        Bits::<7>(58) => b64(0x84c87814a1f0ab72),
203        Bits::<7>(59) => b64(0x8cc702081a6439ec),
204        Bits::<7>(60) => b64(0x90befffa23631e28),
205        Bits::<7>(61) => b64(0xa4506cebde82bde9),
206        Bits::<7>(62) => b64(0xbef9a3f7b2c67915),
207        Bits::<7>(63) => b64(0xc67178f2e372532b),
208        Bits::<7>(64) => b64(0xca273eceea26619c),
209        Bits::<7>(65) => b64(0xd186b8c721c0c207),
210        Bits::<7>(66) => b64(0xeada7dd6cde0eb1e),
211        Bits::<7>(67) => b64(0xf57d4f7fee6ed178),
212        Bits::<7>(68) => b64(0x06f067aa72176fba),
213        Bits::<7>(69) => b64(0x0a637dc5a2c898a6),
214        Bits::<7>(70) => b64(0x113f9804bef90dae),
215        Bits::<7>(71) => b64(0x1b710b35131c471b),
216        Bits::<7>(72) => b64(0x28db77f523047d84),
217        Bits::<7>(73) => b64(0x32caab7b40c72493),
218        Bits::<7>(74) => b64(0x3c9ebe0a15c9bebc),
219        Bits::<7>(75) => b64(0x431d67c49c100d4c),
220        Bits::<7>(76) => b64(0x4cc5d4becb3e42b6),
221        Bits::<7>(77) => b64(0x597f299cfc657e2a),
222        Bits::<7>(78) => b64(0x5fcb6fab3ad6faec),
223        _ => b64(0x6c44198c4a475817),
224    }
225}
226
227/// State transition for [`Sha512Compress`].
228#[kernel]
229pub fn sha512_compress_kernel(
230    _cr: ClockReset,
231    i: Sha512CompressIn,
232    q: Q,
233) -> (Sha512CompressOut, D) {
234    let r = q.regs;
235    let mut n = r;
236    let output = Sha512CompressOut {
237        ready: r.phase == Phase::Idle,
238        valid: r.valid,
239        state: r.result,
240    };
241    n.valid = false;
242
243    match r.phase {
244        Phase::Idle => {
245            if i.start {
246                n.chain = i.state;
247                n.work = i.state;
248                n.schedule = i.block;
249                n.round = b7(0);
250                n.phase = Phase::A;
251            }
252        }
253        Phase::A => {
254            let index = (r.round & b7(15)).resize::<4>();
255            if r.round < b7(16) {
256                n.round_word = r.schedule[index];
257            } else {
258                let index1 = ((r.round + b7(1)) & b7(15)).resize::<4>();
259                let index9 = ((r.round + b7(9)) & b7(15)).resize::<4>();
260                let index14 = ((r.round + b7(14)) & b7(15)).resize::<4>();
261                n.schedule_left = r.schedule[index] + small_sigma0(r.schedule[index1]);
262                n.schedule_right = r.schedule[index9] + small_sigma1(r.schedule[index14]);
263            }
264            let a = r.work[0];
265            let b = r.work[1];
266            let c = r.work[2];
267            let e = r.work[4];
268            let f = r.work[5];
269            let g = r.work[6];
270            n.sigma1_ch = big_sigma1(e) + ((e & f) ^ ((!e) & g));
271            n.sigma0_maj = big_sigma0(a) + ((a & b) ^ (a & c) ^ (b & c));
272            n.h_plus_k = r.work[7] + round_constant(r.round);
273            n.phase = Phase::B;
274        }
275        Phase::B => {
276            if r.round >= b7(16) {
277                let word = r.schedule_left + r.schedule_right;
278                n.round_word = word;
279                n.schedule[(r.round & b7(15)).resize::<4>()] = word;
280            }
281            n.t1_base = r.h_plus_k + r.sigma1_ch;
282            n.t2 = r.sigma0_maj;
283            n.phase = Phase::C;
284        }
285        Phase::C => {
286            let t1 = r.t1_base + r.round_word;
287            n.work = [
288                t1 + r.t2,
289                r.work[0],
290                r.work[1],
291                r.work[2],
292                r.work[3] + t1,
293                r.work[4],
294                r.work[5],
295                r.work[6],
296            ];
297            if r.round == b7(79) {
298                n.phase = Phase::Accumulate;
299            } else {
300                n.round = r.round + b7(1);
301                n.phase = Phase::A;
302            }
303        }
304        Phase::Accumulate => {
305            for index in 0..8 {
306                n.result[index] = r.chain[index] + r.work[index];
307            }
308            n.valid = true;
309            n.phase = Phase::Idle;
310        }
311    }
312
313    (output, D { regs: n })
314}
315
316/// One tagged request port of [`Sha512Pool`].
317#[derive(Copy, Clone, Debug, PartialEq, Digital)]
318pub struct Sha512PoolRequest {
319    /// Marks this request valid.
320    pub valid: bool,
321    /// Caller-defined transaction tag returned with the result.
322    pub tag: b9,
323    /// Incoming chaining state.
324    pub state: [b64; 8],
325    /// Message block to compress.
326    pub block: [b64; 16],
327}
328
329/// Two-request input accepted by [`Sha512Pool`].
330#[derive(Copy, Clone, Debug, PartialEq, Digital)]
331pub struct Sha512PoolIn {
332    /// First request port; it has allocation priority when only one lane is free.
333    pub request0: Sha512PoolRequest,
334    /// Second independent request port.
335    pub request1: Sha512PoolRequest,
336}
337
338/// One tagged completion port of [`Sha512Pool`].
339#[derive(Copy, Clone, Debug, PartialEq, Digital)]
340pub struct Sha512PoolResult {
341    /// Marks this result valid.
342    pub valid: bool,
343    /// Tag captured when the corresponding request was accepted.
344    pub tag: b9,
345    /// Completed chaining state.
346    pub state: [b64; 8],
347}
348
349/// Flow control and two completion ports from [`Sha512Pool`].
350#[derive(Copy, Clone, Debug, PartialEq, Digital)]
351pub struct Sha512PoolOut {
352    /// First request port can transact this cycle.
353    pub request0_ready: bool,
354    /// Second request port can transact this cycle.
355    pub request1_ready: bool,
356    /// Lowest-numbered completed lane.
357    pub result0: Sha512PoolResult,
358    /// Second completed lane, when two lanes finish together.
359    pub result1: Sha512PoolResult,
360}
361
362#[derive(Copy, Clone, Debug, PartialEq, Digital)]
363#[doc(hidden)]
364pub struct Sha512PoolRegs {
365    allocate: b2,
366    tags: [b9; 4],
367}
368
369/// Four independent SHA-512 compression lanes with dual issue and dual retire.
370///
371/// At most two blocks are accepted per cycle. Allocation rotates across the
372/// four lanes, while retirement is stable in lane-number order. A tag remains
373/// attached to its lane for the full 242-cycle compression interval.
374#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
375pub struct Sha512Pool {
376    regs: DFF<Sha512PoolRegs>,
377    lane0: Sha512Compress,
378    lane1: Sha512Compress,
379    lane2: Sha512Compress,
380    lane3: Sha512Compress,
381}
382
383impl Default for Sha512Pool {
384    fn default() -> Self {
385        Self {
386            regs: DFF::new(Sha512PoolRegs {
387                allocate: b2(0),
388                tags: [b9(0); 4],
389            }),
390            lane0: Sha512Compress::default(),
391            lane1: Sha512Compress::default(),
392            lane2: Sha512Compress::default(),
393            lane3: Sha512Compress::default(),
394        }
395    }
396}
397
398impl SynchronousIO for Sha512Pool {
399    type I = Sha512PoolIn;
400    type O = Sha512PoolOut;
401    type Kernel = sha512_pool_kernel;
402}
403
404#[kernel]
405fn empty_compress_input() -> Sha512CompressIn {
406    Sha512CompressIn {
407        start: false,
408        state: [b64(0); 8],
409        block: [b64(0); 16],
410    }
411}
412
413#[kernel]
414fn empty_pool_result() -> Sha512PoolResult {
415    Sha512PoolResult {
416        valid: false,
417        tag: b9(0),
418        state: [b64(0); 8],
419    }
420}
421
422/// State transition and lane crossbar for [`Sha512Pool`].
423#[kernel]
424pub fn sha512_pool_kernel(
425    _cr: ClockReset,
426    i: Sha512PoolIn,
427    q: Sha512PoolQ,
428) -> (Sha512PoolOut, Sha512PoolD) {
429    let ready = [q.lane0.ready, q.lane1.ready, q.lane2.ready, q.lane3.ready];
430    let valid = [q.lane0.valid, q.lane1.valid, q.lane2.valid, q.lane3.valid];
431    let states = [q.lane0.state, q.lane1.state, q.lane2.state, q.lane3.state];
432    let mut request0_found = false;
433    let mut request1_found = false;
434    let mut request0_lane = b2(0);
435    let mut request1_lane = b2(0);
436
437    for offset in 0..4 {
438        let candidate = q.regs.allocate + b2(offset);
439        if !request0_found && ready[candidate] {
440            request0_found = true;
441            request0_lane = candidate;
442        }
443    }
444    for offset in 0..4 {
445        let candidate = q.regs.allocate + b2(offset);
446        let reserved = i.request0.valid && request0_found && candidate == request0_lane;
447        if !request1_found && ready[candidate] && !reserved {
448            request1_found = true;
449            request1_lane = candidate;
450        }
451    }
452
453    let take0 = [
454        i.request0.valid && request0_found && request0_lane == b2(0),
455        i.request0.valid && request0_found && request0_lane == b2(1),
456        i.request0.valid && request0_found && request0_lane == b2(2),
457        i.request0.valid && request0_found && request0_lane == b2(3),
458    ];
459    let take1 = [
460        i.request1.valid && request1_found && request1_lane == b2(0),
461        i.request1.valid && request1_found && request1_lane == b2(1),
462        i.request1.valid && request1_found && request1_lane == b2(2),
463        i.request1.valid && request1_found && request1_lane == b2(3),
464    ];
465    let mut lane_inputs = [empty_compress_input(); 4];
466    for lane in 0..4 {
467        lane_inputs[lane].start = take0[lane] || take1[lane];
468        if take0[lane] {
469            lane_inputs[lane].state = i.request0.state;
470            lane_inputs[lane].block = i.request0.block;
471        } else {
472            lane_inputs[lane].state = i.request1.state;
473            lane_inputs[lane].block = i.request1.block;
474        }
475    }
476
477    let mut result0 = empty_pool_result();
478    let mut result1 = empty_pool_result();
479    for lane in 0..4 {
480        if valid[lane] && !result0.valid {
481            result0.valid = true;
482            result0.tag = q.regs.tags[lane];
483            result0.state = states[lane];
484        } else if valid[lane] && !result1.valid {
485            result1.valid = true;
486            result1.tag = q.regs.tags[lane];
487            result1.state = states[lane];
488        }
489    }
490
491    let mut n = q.regs;
492    for lane in 0..4 {
493        if take0[lane] {
494            n.tags[lane] = i.request0.tag;
495        } else if take1[lane] {
496            n.tags[lane] = i.request1.tag;
497        }
498    }
499    if i.request1.valid && request1_found {
500        n.allocate = request1_lane + b2(1);
501    } else if i.request0.valid && request0_found {
502        n.allocate = request0_lane + b2(1);
503    }
504
505    let output = Sha512PoolOut {
506        request0_ready: request0_found,
507        request1_ready: request1_found,
508        result0,
509        result1,
510    };
511    let mut d = Sha512PoolD::dont_care();
512    d.regs = n;
513    d.lane0 = lane_inputs[0];
514    d.lane1 = lane_inputs[1];
515    d.lane2 = lane_inputs[2];
516    d.lane3 = lane_inputs[3];
517    (output, d)
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use std::iter;
524
525    fn idle() -> Sha512CompressIn {
526        Sha512CompressIn {
527            start: false,
528            state: [b64(0); 8],
529            block: [b64(0); 16],
530        }
531    }
532
533    fn iv() -> [b64; 8] {
534        [
535            b64(0x6a09e667f3bcc908),
536            b64(0xbb67ae8584caa73b),
537            b64(0x3c6ef372fe94f82b),
538            b64(0xa54ff53a5f1d36f1),
539            b64(0x510e527fade682d1),
540            b64(0x9b05688c2b3e6c1f),
541            b64(0x1f83d9abfb41bd6b),
542            b64(0x5be0cd19137e2179),
543        ]
544    }
545
546    #[test]
547    fn compiles_to_rhdl_netlist() -> miette::Result<()> {
548        Sha512Compress::default()
549            .descriptor(ScopedName::top())?
550            .hdl()?;
551        Sha512Pool::default().descriptor(ScopedName::top())?.hdl()?;
552        Ok(())
553    }
554
555    #[test]
556    fn compresses_padded_abc_in_242_cycles() {
557        let mut request = idle();
558        request.start = true;
559        request.state = iv();
560        request.block[0] = b64(0x6162638000000000);
561        request.block[15] = b64(24);
562
563        let samples: Vec<_> = Sha512Compress::default()
564            .run(
565                iter::once(idle())
566                    .chain(iter::once(request))
567                    .chain(iter::repeat_n(idle(), 260))
568                    .with_reset(1)
569                    .clock_pos_edge(100),
570            )
571            .synchronous_sample()
572            .map(|sample| sample.output)
573            .collect();
574        let completion = samples
575            .iter()
576            .position(|output| output.valid)
577            .expect("SHA-512 completion");
578        let expected = [
579            0xddaf35a193617abau64,
580            0xcc417349ae204131u64,
581            0x12e6fa4e89a97ea2u64,
582            0x0a9eeee64b55d39au64,
583            0x2192992a274fc1a8u64,
584            0x36ba3c23a3feebbdu64,
585            0x454d4423643ce80eu64,
586            0x2a9ac94fa54ca49fu64,
587        ];
588        for (actual, expected) in samples[completion].state.iter().zip(expected) {
589            assert_eq!(actual.raw(), expected as u128);
590        }
591        assert_eq!(completion, 244, "reset sample plus 242-cycle lane latency");
592    }
593
594    #[test]
595    fn pool_accepts_and_retires_four_independent_blocks() {
596        fn pool_idle() -> Sha512PoolIn {
597            Sha512PoolIn {
598                request0: Sha512PoolRequest {
599                    valid: false,
600                    tag: b9(0),
601                    state: [b64(0); 8],
602                    block: [b64(0); 16],
603                },
604                request1: Sha512PoolRequest {
605                    valid: false,
606                    tag: b9(0),
607                    state: [b64(0); 8],
608                    block: [b64(0); 16],
609                },
610            }
611        }
612        let mut first = pool_idle();
613        first.request0.valid = true;
614        first.request0.tag = b9(10);
615        first.request0.state = iv();
616        first.request0.block[0] = b64(0x6162638000000000);
617        first.request0.block[15] = b64(24);
618        first.request1 = first.request0;
619        first.request1.tag = b9(11);
620        let mut second = first;
621        second.request0.tag = b9(12);
622        second.request1.tag = b9(13);
623
624        let completions: Vec<_> = Sha512Pool::default()
625            .run(
626                iter::once(pool_idle())
627                    .chain([first, second])
628                    .chain(iter::repeat_n(pool_idle(), 270))
629                    .with_reset(1)
630                    .clock_pos_edge(100),
631            )
632            .synchronous_sample()
633            .flat_map(|sample| [sample.output.result0, sample.output.result1])
634            .filter(|result| result.valid)
635            .collect();
636        assert_eq!(completions.len(), 4);
637        assert_eq!(
638            completions
639                .iter()
640                .map(|result| result.tag.raw())
641                .collect::<Vec<_>>(),
642            vec![10, 11, 12, 13]
643        );
644    }
645}