Skip to main content

polydat_nodes/
register.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Register-plane nodes — 128-bit SIMD words as values
5//! (type_system_alignment.md §8.4 layer 2).
6//!
7//! A register word is a plain 16-byte value with a lane-typed or
8//! raw view ([`polydat::ast::RegLanes`]). Views are free bitcasts:
9//! the [`RegView`] adapter (auto-inserted by the assembler for
10//! any reg→reg wire) retags without touching bits, so a word can
11//! be `[i64; 2]` for one op, `[u8-ish bytes]` for a shuffle, and
12//! algorithm-defined raw state for a third — at zero cost.
13//!
14//! Families here:
15//!
16//! - **Splats** — float lanes from `f64` (exact for f32 lanes in
17//!   range), integer lanes from a `u64` **bit-level** constructor
18//!   (`k as iN` per lane; range semantics belong to the adapter
19//!   system, constructors are bit tools).
20//! - **Gather / conversions** — `[f32; 4]` ↔ `vec_f32` and a
21//!   bounds-checked window gather from a slice.
22//! - **Lane access** — get/set for f32 lanes, reads for i16/i64.
23//! - **Element-wise arithmetic** — add/sub/mul across every lane
24//!   family; integer ops wrap (lane arithmetic is modular, the
25//!   range-checked story lives in scalar adapters).
26//! - **`reg_dot_f32`** — horizontal dot with a FIXED reduction
27//!   tree `((l0+l1) + (l2+l3))`, part of the op contract so
28//!   results are reproducible everywhere (determinism D2).
29//! - **`reg_shuffle_bytes`** — arbitrary byte permutation of the
30//!   raw word from a 16-entry const mask (the SWAR/state-word
31//!   workhorse).
32
33use polydat::ast::Bits128;
34#[cfg(test)]
35use polydat::ast::{PolydatNode, PortType, RegLanes, Value};
36pub use polydat::library::register_view::{RegView, is_reg_port};
37pub use polydat::numeric::register::{
38    gather_f32, lane_f32, lane_i16, lane_i64, mul_i8, to_reg_f32, with_lane_f32,
39};
40// =================================================================
41// Splats
42// =================================================================
43
44/// `reg_splat_f32(k)` — broadcast `k` (applied at f32 precision)
45/// into all four f32 lanes.
46#[polydat::polydat_node(category = Arithmetic)]
47fn reg_splat_f32(k: f64) -> [f32; 4] {
48    [k as f32; 4]
49}
50
51/// `reg_splat_f64(k)` — broadcast `k` into both f64 lanes.
52#[polydat::polydat_node(category = Arithmetic)]
53fn reg_splat_f64(k: f64) -> [f64; 2] {
54    [k; 2]
55}
56
57/// `reg_splat_i8(k)` — bit-level broadcast: each lane is `k as i8`.
58#[polydat::polydat_node(category = Arithmetic)]
59fn reg_splat_i8(k: u64) -> [i8; 16] {
60    [k as i8; 16]
61}
62
63/// `reg_splat_i16(k)` — bit-level broadcast: each lane is `k as i16`.
64#[polydat::polydat_node(category = Arithmetic)]
65fn reg_splat_i16(k: u64) -> [i16; 8] {
66    [k as i16; 8]
67}
68
69/// `reg_splat_i32(k)` — bit-level broadcast: each lane is `k as i32`.
70#[polydat::polydat_node(category = Arithmetic)]
71fn reg_splat_i32(k: u64) -> [i32; 4] {
72    [k as i32; 4]
73}
74
75/// `reg_splat_i64(k)` — bit-level broadcast: each lane is `k as i64`.
76#[polydat::polydat_node(category = Arithmetic)]
77fn reg_splat_i64(k: u64) -> [i64; 2] {
78    [k as i64; 2]
79}
80
81// =================================================================
82// Gather / conversions (f32 lanes — the embedding workhorse)
83// =================================================================
84
85/// `reg_gather_f32(v, offset)` — load lanes `[offset, offset+4)`
86/// of an f32 slice into a register word. Panics when the window
87/// runs past the end (silent zero-fill would corrupt distance
88/// math downstream).
89#[polydat::polydat_node(category = Arithmetic)]
90fn reg_gather_f32(v: &[f32], offset: u64) -> [f32; 4] {
91    gather_f32(v, offset).lanes_f32()
92}
93
94/// `vec_to_reg_f32(v)` — a length-4 `vec_f32` IS a `reg_f32x4`;
95/// panics on any other length.
96#[polydat::polydat_node(category = Conversions)]
97fn vec_to_reg_f32(v: &[f32]) -> [f32; 4] {
98    to_reg_f32(v).lanes_f32()
99}
100
101// ── The bodies the nodes and their native lowerings share ──────
102
103/// `reg_to_vec_f32(r)` — the inverse projection.
104#[polydat::polydat_node(category = Conversions)]
105fn reg_to_vec_f32(r: [f32; 4]) -> Vec<f32> {
106    r.to_vec()
107}
108
109// =================================================================
110// Lane access
111// =================================================================
112
113/// `reg_lane_f32(r, i)` — read lane `i` (0..4), widened to f64.
114#[polydat::polydat_node(category = Arithmetic)]
115fn reg_lane_f32(r: [f32; 4], i: u64) -> f64 {
116    lane_f32(Bits128::from_lanes_f32(r), i)
117}
118
119/// `reg_with_lane_f32(r, i, v)` — copy of `r` with lane `i`
120/// replaced by `v` (at f32 precision).
121#[polydat::polydat_node(category = Arithmetic)]
122fn reg_with_lane_f32(r: [f32; 4], i: u64, v: f64) -> [f32; 4] {
123    with_lane_f32(Bits128::from_lanes_f32(r), i, v).lanes_f32()
124}
125
126/// `reg_lane_i16(r, i)` — read lane `i` (0..8).
127#[polydat::polydat_node(category = Arithmetic)]
128fn reg_lane_i16(r: [i16; 8], i: u64) -> i16 {
129    lane_i16(Bits128::from_lanes_i16(r), i)
130}
131
132/// `reg_lane_i64(r, i)` — read lane `i` (0..2).
133#[polydat::polydat_node(category = Arithmetic)]
134fn reg_lane_i64(r: [i64; 2], i: u64) -> i64 {
135    lane_i64(Bits128::from_lanes_i64(r), i)
136}
137
138// =================================================================
139// Element-wise arithmetic (integer ops wrap; floats are IEEE)
140// =================================================================
141
142#[polydat::polydat_node(category = Arithmetic)]
143fn reg_add_f32(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
144    core::array::from_fn(|i| a[i] + b[i])
145}
146
147#[polydat::polydat_node(category = Arithmetic)]
148fn reg_sub_f32(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
149    core::array::from_fn(|i| a[i] - b[i])
150}
151
152#[polydat::polydat_node(category = Arithmetic)]
153fn reg_mul_f32(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
154    core::array::from_fn(|i| a[i] * b[i])
155}
156
157#[polydat::polydat_node(category = Arithmetic)]
158fn reg_add_f64(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
159    core::array::from_fn(|i| a[i] + b[i])
160}
161
162#[polydat::polydat_node(category = Arithmetic)]
163fn reg_sub_f64(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
164    core::array::from_fn(|i| a[i] - b[i])
165}
166
167#[polydat::polydat_node(category = Arithmetic)]
168fn reg_mul_f64(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
169    core::array::from_fn(|i| a[i] * b[i])
170}
171
172#[polydat::polydat_node(category = Arithmetic)]
173fn reg_add_i8(a: [i8; 16], b: [i8; 16]) -> [i8; 16] {
174    core::array::from_fn(|i| a[i].wrapping_add(b[i]))
175}
176
177#[polydat::polydat_node(category = Arithmetic)]
178fn reg_sub_i8(a: [i8; 16], b: [i8; 16]) -> [i8; 16] {
179    core::array::from_fn(|i| a[i].wrapping_sub(b[i]))
180}
181
182/// `reg_mul_i8(a, b)` — the wrapping product per lane. x86 has no
183/// byte-lane multiply short of AVX-512 and Cranelift lowers none, so
184/// native code runs this body through a helper.
185#[polydat::polydat_node(category = Arithmetic)]
186fn reg_mul_i8(a: [i8; 16], b: [i8; 16]) -> [i8; 16] {
187    mul_i8(Bits128::from_lanes_i8(a), Bits128::from_lanes_i8(b)).lanes_i8()
188}
189
190#[polydat::polydat_node(category = Arithmetic)]
191fn reg_add_i16(a: [i16; 8], b: [i16; 8]) -> [i16; 8] {
192    core::array::from_fn(|i| a[i].wrapping_add(b[i]))
193}
194
195#[polydat::polydat_node(category = Arithmetic)]
196fn reg_sub_i16(a: [i16; 8], b: [i16; 8]) -> [i16; 8] {
197    core::array::from_fn(|i| a[i].wrapping_sub(b[i]))
198}
199
200#[polydat::polydat_node(category = Arithmetic)]
201fn reg_mul_i16(a: [i16; 8], b: [i16; 8]) -> [i16; 8] {
202    core::array::from_fn(|i| a[i].wrapping_mul(b[i]))
203}
204
205#[polydat::polydat_node(category = Arithmetic)]
206fn reg_add_i32(a: [i32; 4], b: [i32; 4]) -> [i32; 4] {
207    core::array::from_fn(|i| a[i].wrapping_add(b[i]))
208}
209
210#[polydat::polydat_node(category = Arithmetic)]
211fn reg_sub_i32(a: [i32; 4], b: [i32; 4]) -> [i32; 4] {
212    core::array::from_fn(|i| a[i].wrapping_sub(b[i]))
213}
214
215#[polydat::polydat_node(category = Arithmetic)]
216fn reg_mul_i32(a: [i32; 4], b: [i32; 4]) -> [i32; 4] {
217    core::array::from_fn(|i| a[i].wrapping_mul(b[i]))
218}
219
220#[polydat::polydat_node(category = Arithmetic)]
221fn reg_add_i64(a: [i64; 2], b: [i64; 2]) -> [i64; 2] {
222    core::array::from_fn(|i| a[i].wrapping_add(b[i]))
223}
224
225#[polydat::polydat_node(category = Arithmetic)]
226fn reg_sub_i64(a: [i64; 2], b: [i64; 2]) -> [i64; 2] {
227    core::array::from_fn(|i| a[i].wrapping_sub(b[i]))
228}
229
230#[polydat::polydat_node(category = Arithmetic)]
231fn reg_mul_i64(a: [i64; 2], b: [i64; 2]) -> [i64; 2] {
232    core::array::from_fn(|i| a[i].wrapping_mul(b[i]))
233}
234
235// =================================================================
236// Horizontal + raw-word ops
237// =================================================================
238
239/// `reg_dot_f32(a, b)` — horizontal dot product with the FIXED
240/// reduction tree `(l0*r0 + l1*r1) + (l2*r2 + l3*r3)`. The tree
241/// shape is part of the contract (determinism D2): every host and
242/// every execution tier produces bit-identical results.
243#[polydat::polydat_node(category = Arithmetic)]
244fn reg_dot_f32(a: [f32; 4], b: [f32; 4]) -> f64 {
245    let p0 = a[0] * b[0];
246    let p1 = a[1] * b[1];
247    let p2 = a[2] * b[2];
248    let p3 = a[3] * b[3];
249    ((p0 + p1) + (p2 + p3)) as f64
250}
251
252/// `reg_shuffle_bytes(x, mask)` — arbitrary byte permutation of
253/// the raw word: output byte `i` is input byte `mask[i]`. The
254/// mask is a 16-entry const list, each entry < 16 (panic
255/// otherwise, at build time). Duplicate indices broadcast; this
256/// is the SWAR / state-word workhorse for lane rearrangement
257/// under any view. Native code bakes the mask into one `shuffle`
258/// instruction, so the node exposes it as its constants.
259#[polydat::polydat_node(category = Arithmetic, jit_constants = reg_shuffle_bytes_jit_constants)]
260fn reg_shuffle_bytes(x: Bits128, mask: polydat::derive_support::Const<Vec<u64>>) -> Bits128 {
261    let m = &*mask;
262    if m.len() != 16 {
263        panic!(
264            "reg_shuffle_bytes: mask must have exactly 16 entries, got {}",
265            m.len()
266        );
267    }
268    let src = x.to_le_bytes();
269    let mut out = [0u8; 16];
270    for (i, &idx) in m.iter().enumerate() {
271        if idx >= 16 {
272            panic!("reg_shuffle_bytes: mask[{i}] = {idx} out of range 0..16");
273        }
274        out[i] = src[idx as usize];
275    }
276    Bits128::from_le_bytes(out)
277}
278
279fn reg_shuffle_bytes_jit_constants(node: &RegShuffleBytes) -> Vec<u64> {
280    node.mask.clone()
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn eval1<N: PolydatNode>(node: &N, a: Value) -> Value {
288        let mut out = [Value::None];
289        node.eval(&[a], &mut out);
290        out[0].clone()
291    }
292
293    fn eval2<N: PolydatNode>(node: &N, a: Value, b: Value) -> Value {
294        let mut out = [Value::None];
295        node.eval(&[a, b], &mut out);
296        out[0].clone()
297    }
298
299    fn f32x4(l: [f32; 4]) -> Value {
300        Value::Reg128(Bits128::from_lanes_f32(l), RegLanes::F32x4)
301    }
302
303    #[test]
304    fn lane_codecs_round_trip_and_share_bits() {
305        let b = Bits128::from_lanes_f32([1.0, -2.5, 0.0, 4.0]);
306        assert_eq!(b.lanes_f32(), [1.0, -2.5, 0.0, 4.0]);
307        // The same bits under another view round-trip unchanged —
308        // views really are bitcasts.
309        let as_i16 = b.lanes_i16();
310        assert_eq!(Bits128::from_lanes_i16(as_i16), b);
311        assert_eq!(Bits128::from_lanes_i8(b.lanes_i8()), b);
312        assert_eq!(Bits128::from_lanes_i64(b.lanes_i64()), b);
313        assert_eq!(Bits128::from_lanes_f64(b.lanes_f64()), b);
314        assert_eq!(Bits128::from_lanes_f16(b.lanes_f16()), b);
315    }
316
317    #[test]
318    fn reg_view_retags_without_touching_bits() {
319        let word = f32x4([1.0, 2.0, 3.0, 4.0]);
320        let raw = eval1(&RegView::new(PortType::Reg128), word.clone());
321        assert_eq!(raw.as_reg_bits(), word.as_reg_bits());
322        assert!(matches!(raw, Value::Reg128(_, RegLanes::Raw)));
323        let back = eval1(&RegView::new(PortType::RegI16x8), raw);
324        assert!(matches!(back, Value::Reg128(_, RegLanes::I16x8)));
325        assert_eq!(back.as_reg_bits(), word.as_reg_bits());
326    }
327
328    #[test]
329    fn splats_and_lane_access() {
330        let r = eval1(&RegSplatF32::new(), Value::F64(2.5));
331        assert_eq!(r.as_reg_bits().lanes_f32(), [2.5; 4]);
332
333        let r = eval1(&RegSplatI16::new(), Value::U64(0xFFFF));
334        // bit-level constructor: 0xFFFF as i16 = -1 in every lane
335        assert_eq!(r.as_reg_bits().lanes_i16(), [-1; 8]);
336
337        let lane = eval2(
338            &RegLaneF32::new(),
339            f32x4([1.0, 2.0, 3.0, 4.0]),
340            Value::U64(2),
341        );
342        assert_eq!(lane, Value::F64(3.0));
343
344        let mut out = [Value::None];
345        RegWithLaneF32::new().eval(
346            &[f32x4([1.0, 2.0, 3.0, 4.0]), Value::U64(1), Value::F64(9.0)],
347            &mut out,
348        );
349        assert_eq!(out[0].as_reg_bits().lanes_f32(), [1.0, 9.0, 3.0, 4.0]);
350    }
351
352    #[test]
353    fn elementwise_arithmetic_and_wrapping() {
354        let sum = eval2(
355            &RegAddF32::new(),
356            f32x4([1.0, 2.0, 3.0, 4.0]),
357            f32x4([10.0, 20.0, 30.0, 40.0]),
358        );
359        assert_eq!(sum.as_reg_bits().lanes_f32(), [11.0, 22.0, 33.0, 44.0]);
360
361        // i16 lanes wrap (modular lane arithmetic).
362        let a = Value::Reg128(Bits128::from_lanes_i16([i16::MAX; 8]), RegLanes::I16x8);
363        let b = Value::Reg128(Bits128::from_lanes_i16([1; 8]), RegLanes::I16x8);
364        let wrapped = eval2(&RegAddI16::new(), a, b);
365        assert_eq!(wrapped.as_reg_bits().lanes_i16(), [i16::MIN; 8]);
366    }
367
368    #[test]
369    fn dot_uses_fixed_reduction_tree() {
370        let d = eval2(
371            &RegDotF32::new(),
372            f32x4([1.0, 2.0, 3.0, 4.0]),
373            f32x4([5.0, 6.0, 7.0, 8.0]),
374        );
375        // ((5 + 12) + (21 + 32)) = 70 — exact in f32.
376        assert_eq!(d, Value::F64(70.0));
377    }
378
379    #[test]
380    fn gather_and_vec_round_trip() {
381        use polydat::ast::SliceArc;
382        let v = Value::VecF32(SliceArc::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]));
383        let r = eval2(&RegGatherF32::new(), v, Value::U64(2));
384        assert_eq!(r.as_reg_bits().lanes_f32(), [2.0, 3.0, 4.0, 5.0]);
385
386        let back = eval1(&RegToVecF32::new(), r);
387        assert_eq!(back.as_vec_f32(), &[2.0, 3.0, 4.0, 5.0]);
388    }
389
390    #[test]
391    fn raw_state_word_byte_shuffle() {
392        // Reverse all 16 bytes via the const mask — exercising the
393        // raw view as algorithm-defined buffer state.
394        let word = Bits128::from_le_bytes(core::array::from_fn(|i| i as u8));
395        let node = RegShuffleBytes::new((0..16).rev().collect());
396        let mut out = [Value::None];
397        node.eval(&[Value::Reg128(word, RegLanes::Raw)], &mut out);
398        let shuffled = out[0].as_reg_bits().to_le_bytes();
399        assert_eq!(shuffled, core::array::from_fn(|i| 15 - i as u8));
400    }
401
402    #[test]
403    fn reg_flow_p1_p2_equivalence() {
404        // A register dataflow through splat → add → view-retag →
405        // lane read, compiled both ways. P2 rides the two-slot
406        // limb protocol (§8.4 layer 1); results must be
407        // bit-identical with typed eval.
408        let src = r#"
409            input cycle: u64
410            a := reg_splat_i16(cycle)
411            b := reg_splat_i16(3)
412            s := reg_add_i16(a, b)
413            out := reg_lane_i16(s, 7)
414        "#;
415        let p1 = polydat::dsl::compile_polydat(src).unwrap();
416        let asm = polydat::dsl::compile::compile_polydat_to_assembler(src).unwrap();
417        let mut p2 = asm.try_compile_raw().expect("reg nodes are P2-eligible");
418
419        let mut k1 = p1;
420        for cycle in [0u64, 5, 0xFFFF, 0x1_0005] {
421            k1.set_inputs(&[cycle]);
422            let want = k1.pull("out").as_i64();
423            let got = p2.eval_for_slot(&[cycle], p2.resolve_output("out").unwrap());
424            assert_eq!(got as i64, want, "cycle={cycle}");
425        }
426    }
427
428    #[test]
429    fn u128_rides_p2_limb_pairs() {
430        // 128-bit integers ride the same two-slot limb protocol
431        // (§8.4 layer 1): widen u64 → u128, then range-narrow
432        // back, through compiled u64 buffers. Built via the
433        // programmatic API because the widening adapters are
434        // assembler-inserted (`__`-prefixed), not DSL-callable.
435        use polydat::compile::assembly::{PolydatAssembler, WireRef};
436        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
437        asm.add_node(
438            "wide",
439            Box::new(polydat::library::polyfill_128::U64ToU128::new()),
440            vec![WireRef::input("cycle")],
441        );
442        asm.add_node(
443            "back",
444            Box::new(polydat::library::polyfill_128::U128ToU64::new()),
445            vec![WireRef::node("wide")],
446        );
447        asm.add_output("back", WireRef::node("back"));
448        let mut p2 = asm.try_compile_raw().expect("u128 nodes are P2-eligible");
449        for v in [0u64, 1, u64::MAX] {
450            let slot = p2.resolve_output("back").unwrap();
451            assert_eq!(p2.eval_for_slot(&[v], slot), v, "u128 round trip of {v}");
452        }
453    }
454
455    /// P1 ↔ P3 equivalence for every register lane family × op:
456    /// splat → binop kernels compiled to native cranelift SIMD
457    /// must be bit-identical with typed eval. Families whose
458    /// vector lowering the host cranelift declines (e.g. i64x2 /
459    /// i8x16 multiplies on pre-AVX512 x86) fall back to the
460    /// hybrid kernel — which still runs the vector ops as native
461    /// JIT segments where supported — and must match there.
462    #[cfg(feature = "jit")]
463    #[test]
464    fn reg_ops_p1_p3_equivalence_all_lane_families() {
465        // Operands route through `hash(cycle)` rather than
466        // `cycle + N`: a literal operand lowers to a const node
467        // that (pre-existing) classifies Fallback, which would
468        // decline the whole pure-P3 kernel and mask the SIMD
469        // path under test.
470        let cases = [
471            ("i8", "cycle", "hash(cycle)"),
472            ("i16", "cycle", "hash(cycle)"),
473            ("i32", "cycle", "hash(cycle)"),
474            ("i64", "cycle", "hash(cycle)"),
475            ("f32", "unit_interval(cycle)", "unit_interval(hash(cycle))"),
476            ("f64", "unit_interval(cycle)", "unit_interval(hash(cycle))"),
477        ];
478        for (fam, ea, eb) in cases {
479            for op in ["add", "sub", "mul"] {
480                let src = format!(
481                    "input cycle: u64
482                     a := reg_splat_{fam}({ea})
483                     b := reg_splat_{fam}({eb})
484                     out := reg_{op}_{fam}(a, b)"
485                );
486                let mut p1 = polydat::dsl::compile_polydat(&src).unwrap();
487
488                for cycle in [0u64, 5, 0xFFFF, 0xDEAD_BEEF] {
489                    p1.set_inputs(&[cycle]);
490                    let want = p1.pull("out").as_reg_bits();
491
492                    let asm = polydat::dsl::compile::compile_polydat_to_assembler(&src).unwrap();
493                    match asm.try_compile_pure_jit_raw() {
494                        Ok(mut p3) => {
495                            let slot = p3.resolve_output("out").unwrap();
496                            p3.eval(&[cycle]);
497                            let got = Bits128([p3.get_slot(slot), p3.get_slot(slot + 1)]);
498                            assert_eq!(got, want, "P3 reg_{op}_{fam} mismatch at cycle={cycle}");
499                        }
500                        Err(e) => {
501                            // Host cranelift declined the vector
502                            // lowering — hybrid must still agree.
503                            eprintln!("reg_{op}_{fam}: pure-P3 declined ({e}); checking hybrid");
504                            let asm =
505                                polydat::dsl::compile::compile_polydat_to_assembler(&src).unwrap();
506                            let mut hy = asm.compile_hybrid().unwrap();
507                            let slot = hy.resolve_output("out").unwrap();
508                            hy.eval(&[cycle]);
509                            let got = Bits128([hy.get_slot(slot), hy.get_slot(slot + 1)]);
510                            assert_eq!(
511                                got, want,
512                                "hybrid reg_{op}_{fam} mismatch at cycle={cycle}"
513                            );
514                        }
515                    }
516                }
517            }
518        }
519    }
520
521    #[test]
522    fn display_and_json_forms() {
523        let word = f32x4([1.0, 2.0, 3.0, 4.0]);
524        assert_eq!(word.to_display_string(), "[1.0, 2.0, 3.0, 4.0]");
525        assert_eq!(
526            word.to_json_value(),
527            serde_json::json!([1.0, 2.0, 3.0, 4.0])
528        );
529        let raw = Value::Reg128(Bits128::from_u128(0xDEAD), RegLanes::Raw);
530        assert_eq!(raw.to_display_string(), format!("{:032x}", 0xDEADu128));
531    }
532}