Skip to main content

hermes_simd_core/
bitboard.rs

1//! Chess bitboards and sliding attack generation views.
2
3use core::marker::PhantomData;
4
5/// Trait defining the sliding attack generation interface.
6///
7/// Implemented by backend markers in the intrinsics crate.
8///
9/// These methods are safe. Unlike [`SimdKernel`](crate::kernel::SimdKernel),
10/// whose operations are `#[target_feature]`-gated and therefore carry an ISA
11/// precondition, an attack generator either computes with plain integer
12/// arithmetic or selects its ISA-specific path by runtime detection behind its
13/// own safe surface. Table lookups are bounds-checked, so an out-of-range
14/// square panics rather than reading out of bounds.
15pub trait BitBoardKernel: Send + Sync + 'static {
16    /// Generate Rook attacks for a square given occupancy.
17    ///
18    /// # Panics
19    /// If `square` is not a board index below 64.
20    fn rook_attacks(square: u8, occupancy: u64) -> u64;
21
22    /// Generate Bishop attacks for a square given occupancy.
23    ///
24    /// # Panics
25    /// If `square` is not a board index below 64.
26    fn bishop_attacks(square: u8, occupancy: u64) -> u64;
27
28    /// Generate Queen attacks for a square given occupancy.
29    ///
30    /// # Panics
31    /// If `square` is not a board index below 64.
32    #[inline(always)]
33    fn queen_attacks(square: u8, occupancy: u64) -> u64 {
34        Self::rook_attacks(square, occupancy) | Self::bishop_attacks(square, occupancy)
35    }
36}
37
38/// A zero-copy newtype family for chess bitboards.
39///
40/// Parameterized by `Backend` (e.g. `KoggeStone`, `Magic`), `Arch` (SIMD architecture),
41/// and reference typestate `Ref` (e.g. `&'a [u64]` or `&'a mut [u64]`).
42#[repr(transparent)]
43pub struct BitBoardView<'a, Backend, Arch, Ref: 'a = &'a [u64]> {
44    ptr: *mut [u64],
45    _marker: PhantomData<(&'a u64, Backend, Arch, Ref)>,
46}
47
48unsafe impl<'a, Backend, Arch, Ref: 'a> Send for BitBoardView<'a, Backend, Arch, Ref> where Ref: Send
49{}
50unsafe impl<'a, Backend, Arch, Ref: 'a> Sync for BitBoardView<'a, Backend, Arch, Ref> where Ref: Sync
51{}
52
53impl<'a, Backend, Arch> Clone for BitBoardView<'a, Backend, Arch, &'a [u64]> {
54    #[inline(always)]
55    fn clone(&self) -> Self {
56        *self
57    }
58}
59
60impl<'a, Backend, Arch> Copy for BitBoardView<'a, Backend, Arch, &'a [u64]> {}
61
62impl<'a, Backend, Arch> BitBoardView<'a, Backend, Arch, &'a [u64]> {
63    /// Create a shared `BitBoardView` over a slice of bitboards.
64    #[inline(always)]
65    pub fn new(data: &'a [u64]) -> Self {
66        Self {
67            ptr: data as *const [u64] as *mut [u64],
68            _marker: PhantomData,
69        }
70    }
71}
72
73impl<'a, Backend, Arch> BitBoardView<'a, Backend, Arch, &'a mut [u64]> {
74    /// Create a mutable `BitBoardView` over a slice of bitboards.
75    #[inline(always)]
76    pub fn new_mut(data: &'a mut [u64]) -> Self {
77        Self {
78            ptr: data as *mut [u64],
79            _marker: PhantomData,
80        }
81    }
82
83    /// Access the underlying raw mutable slice.
84    #[inline(always)]
85    pub fn as_slice_mut(&mut self) -> &mut [u64] {
86        // SAFETY: this impl is reachable only for `Ref = &'a mut [u64]`, so the
87        // pointer came from `new_mut`'s exclusive borrow and no shared view of
88        // the same data can exist. `&mut self` bounds the reborrow to a span in
89        // which the view itself is exclusively held, and the returned lifetime
90        // is that of `self`, which cannot outlive `'a`.
91        unsafe { &mut *self.ptr }
92    }
93
94    /// Downgrade exclusive mutable view to a shared view.
95    #[inline(always)]
96    pub fn downgrade(self) -> BitBoardView<'a, Backend, Arch, &'a [u64]> {
97        BitBoardView {
98            ptr: self.ptr,
99            _marker: PhantomData,
100        }
101    }
102}
103
104impl<'a, Backend, Arch, Ref: 'a> BitBoardView<'a, Backend, Arch, Ref> {
105    /// Access the underlying raw slice of bitboards.
106    #[inline(always)]
107    pub fn as_slice(&self) -> &[u64] {
108        // SAFETY: the pointer was derived from a borrow of `'a` — shared in
109        // `new`, exclusive in `new_mut` — and the view keeps that borrow alive
110        // through its `PhantomData`. Taking `&self` yields a shared reborrow
111        // bounded by `self`, which cannot outlive `'a`; when `Ref` is the
112        // exclusive typestate, holding `&self` precludes a concurrent
113        // `as_slice_mut`, so no aliasing `&mut` exists.
114        unsafe { &*self.ptr }
115    }
116}
117
118impl<'a, Backend, Arch, Ref: 'a> core::ops::Deref for BitBoardView<'a, Backend, Arch, Ref> {
119    type Target = [u64];
120    #[inline(always)]
121    fn deref(&self) -> &Self::Target {
122        self.as_slice()
123    }
124}
125
126impl<'a, Backend, Arch> core::ops::DerefMut for BitBoardView<'a, Backend, Arch, &'a mut [u64]> {
127    #[inline(always)]
128    fn deref_mut(&mut self) -> &mut Self::Target {
129        self.as_slice_mut()
130    }
131}
132
133impl<'a, Backend, Arch, Ref: 'a> BitBoardView<'a, Backend, Arch, Ref>
134where
135    Backend: BitBoardKernel,
136{
137    /// Generate Rook attacks for a square given occupancy.
138    #[inline(always)]
139    pub fn rook_attacks(&self, square: u8, occupancy: u64) -> u64 {
140        Backend::rook_attacks(square, occupancy)
141    }
142
143    /// Generate Bishop attacks for a square given occupancy.
144    #[inline(always)]
145    pub fn bishop_attacks(&self, square: u8, occupancy: u64) -> u64 {
146        Backend::bishop_attacks(square, occupancy)
147    }
148
149    /// Generate Queen attacks for a square given occupancy.
150    #[inline(always)]
151    pub fn queen_attacks(&self, square: u8, occupancy: u64) -> u64 {
152        Backend::queen_attacks(square, occupancy)
153    }
154
155    /// Generate attacks for a batch of squares under a single occupancy bitboard.
156    ///
157    /// Amortizes loop overhead and permits compiler instruction scheduling / pipelining
158    /// by unrolling the attack queries in blocks of 4.
159    #[inline]
160    pub fn batch_attacks_single_occupancy(
161        &self,
162        squares: &[u8],
163        occupancy: u64,
164        out: &mut [u64],
165        is_rook: bool,
166    ) {
167        assert!(
168            out.len() >= squares.len(),
169            "Output slice too short for batch attacks"
170        );
171
172        let len = squares.len();
173        let mut i = 0;
174
175        let unroll = (len / 4) * 4;
176        while i < unroll {
177            let sq0 = squares[i];
178            let sq1 = squares[i + 1];
179            let sq2 = squares[i + 2];
180            let sq3 = squares[i + 3];
181
182            if is_rook {
183                out[i] = Backend::rook_attacks(sq0, occupancy);
184                out[i + 1] = Backend::rook_attacks(sq1, occupancy);
185                out[i + 2] = Backend::rook_attacks(sq2, occupancy);
186                out[i + 3] = Backend::rook_attacks(sq3, occupancy);
187            } else {
188                out[i] = Backend::bishop_attacks(sq0, occupancy);
189                out[i + 1] = Backend::bishop_attacks(sq1, occupancy);
190                out[i + 2] = Backend::bishop_attacks(sq2, occupancy);
191                out[i + 3] = Backend::bishop_attacks(sq3, occupancy);
192            }
193            i += 4;
194        }
195
196        while i < len {
197            let sq = squares[i];
198            out[i] = if is_rook {
199                Backend::rook_attacks(sq, occupancy)
200            } else {
201                Backend::bishop_attacks(sq, occupancy)
202            };
203            i += 1;
204        }
205    }
206}