/home/noah/src/trueno/src/brick/simd_config.rs
Line | Count | Source |
1 | | //! SIMD Configuration and Lazy Initialization |
2 | | //! |
3 | | //! LCP-07: Lazy AMX/SIMD tile configuration for expensive state setup. |
4 | | //! LCP-13: Unroll-and-tail vectorization patterns. |
5 | | |
6 | | use super::ComputeBackend; |
7 | | |
8 | | // ---------------------------------------------------------------------------- |
9 | | // LCP-07: Lazy AMX Tile Config |
10 | | // ---------------------------------------------------------------------------- |
11 | | |
12 | | /// SIMD backend state for lazy initialization. |
13 | | /// |
14 | | /// AMX (Advanced Matrix Extensions) and AVX-512 require tile configuration |
15 | | /// that's expensive to set up. This tracks whether initialization has occurred. |
16 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
17 | | pub enum SimdBackendState { |
18 | | /// Not initialized - will configure on first use |
19 | | #[default] |
20 | | Uninitialized, |
21 | | /// Configuration in progress |
22 | | Configuring, |
23 | | /// Ready to use |
24 | | Ready, |
25 | | /// Failed to initialize (fallback to scalar) |
26 | | Failed, |
27 | | } |
28 | | |
29 | | /// Lazy SIMD tile configuration manager. |
30 | | /// |
31 | | /// Defers expensive SIMD state setup until actually needed. |
32 | | #[derive(Debug)] |
33 | | pub struct LazySimdConfig { |
34 | | /// Current state |
35 | | state: SimdBackendState, |
36 | | /// Best available backend |
37 | | best_backend: ComputeBackend, |
38 | | /// Whether AMX is supported |
39 | | amx_supported: bool, |
40 | | /// Tile configuration (for AMX) |
41 | | tile_config: Option<AmxTileConfig>, |
42 | | } |
43 | | |
44 | | /// AMX tile configuration (8x8 tile palette). |
45 | | #[derive(Debug, Clone, Copy, Default)] |
46 | | pub struct AmxTileConfig { |
47 | | /// Palette ID (0-1) |
48 | | pub palette: u8, |
49 | | /// Start row |
50 | | pub start_row: u8, |
51 | | /// Number of rows per tile |
52 | | pub rows: u8, |
53 | | /// Bytes per row |
54 | | pub bytes_per_row: u16, |
55 | | } |
56 | | |
57 | | impl LazySimdConfig { |
58 | | /// Create new lazy config, detecting best backend. |
59 | | #[must_use] |
60 | 0 | pub fn new() -> Self { |
61 | 0 | Self { |
62 | 0 | state: SimdBackendState::Uninitialized, |
63 | 0 | best_backend: Self::detect_best_backend(), |
64 | 0 | amx_supported: Self::detect_amx(), |
65 | 0 | tile_config: None, |
66 | 0 | } |
67 | 0 | } |
68 | | |
69 | | /// Detect best available SIMD backend. |
70 | 0 | fn detect_best_backend() -> ComputeBackend { |
71 | | #[cfg(target_arch = "x86_64")] |
72 | | { |
73 | 0 | if is_x86_feature_detected!("avx512f") { |
74 | 0 | return ComputeBackend::Avx512; |
75 | 0 | } |
76 | 0 | if is_x86_feature_detected!("avx2") { |
77 | 0 | return ComputeBackend::Avx2; |
78 | 0 | } |
79 | 0 | if is_x86_feature_detected!("sse2") { |
80 | 0 | return ComputeBackend::Sse2; |
81 | 0 | } |
82 | | } |
83 | | #[cfg(target_arch = "aarch64")] |
84 | | { |
85 | | // NEON is always available on aarch64 |
86 | | return ComputeBackend::Neon; |
87 | | } |
88 | 0 | ComputeBackend::Scalar |
89 | 0 | } |
90 | | |
91 | | /// Detect AMX support (Intel Sapphire Rapids+). |
92 | 0 | fn detect_amx() -> bool { |
93 | | #[cfg(target_arch = "x86_64")] |
94 | | { |
95 | | // AMX requires specific CPUID checks |
96 | | // For now, return false as AMX is rare |
97 | 0 | false |
98 | | } |
99 | | #[cfg(not(target_arch = "x86_64"))] |
100 | | { |
101 | | false |
102 | | } |
103 | 0 | } |
104 | | |
105 | | /// Ensure SIMD is configured, initializing lazily if needed. |
106 | 0 | pub fn ensure_ready(&mut self) -> Result<ComputeBackend, SimdBackendState> { |
107 | 0 | match self.state { |
108 | 0 | SimdBackendState::Ready => Ok(self.best_backend), |
109 | 0 | SimdBackendState::Failed => Err(SimdBackendState::Failed), |
110 | 0 | SimdBackendState::Configuring => Err(SimdBackendState::Configuring), |
111 | | SimdBackendState::Uninitialized => { |
112 | 0 | self.state = SimdBackendState::Configuring; |
113 | | |
114 | | // Configure AMX tiles if supported |
115 | 0 | if self.amx_supported { |
116 | 0 | self.tile_config = Some(AmxTileConfig { |
117 | 0 | palette: 1, |
118 | 0 | start_row: 0, |
119 | 0 | rows: 16, |
120 | 0 | bytes_per_row: 64, |
121 | 0 | }); |
122 | 0 | // In real implementation, would call LDTILECFG here |
123 | 0 | } |
124 | | |
125 | 0 | self.state = SimdBackendState::Ready; |
126 | 0 | Ok(self.best_backend) |
127 | | } |
128 | | } |
129 | 0 | } |
130 | | |
131 | | /// Get current state. |
132 | | #[must_use] |
133 | 0 | pub fn state(&self) -> SimdBackendState { |
134 | 0 | self.state |
135 | 0 | } |
136 | | |
137 | | /// Get best backend without initializing. |
138 | | #[must_use] |
139 | 0 | pub fn best_backend(&self) -> ComputeBackend { |
140 | 0 | self.best_backend |
141 | 0 | } |
142 | | |
143 | | /// Check if AMX is supported. |
144 | | #[must_use] |
145 | 0 | pub fn has_amx(&self) -> bool { |
146 | 0 | self.amx_supported |
147 | 0 | } |
148 | | |
149 | | /// Reset to uninitialized state. |
150 | 0 | pub fn reset(&mut self) { |
151 | 0 | self.state = SimdBackendState::Uninitialized; |
152 | 0 | self.tile_config = None; |
153 | 0 | } |
154 | | } |
155 | | |
156 | | impl Default for LazySimdConfig { |
157 | 0 | fn default() -> Self { |
158 | 0 | Self::new() |
159 | 0 | } |
160 | | } |
161 | | |
162 | | // ---------------------------------------------------------------------------- |
163 | | // LCP-13: Unroll-and-Tail Vectorization |
164 | | // ---------------------------------------------------------------------------- |
165 | | |
166 | | /// Unroll factor for SIMD loops. |
167 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
168 | | pub enum UnrollFactor { |
169 | | /// No unrolling (1x) |
170 | | None, |
171 | | /// 2x unroll |
172 | | X2, |
173 | | /// 4x unroll |
174 | | X4, |
175 | | /// 8x unroll (AVX-512) |
176 | | X8, |
177 | | } |
178 | | |
179 | | impl UnrollFactor { |
180 | | /// Get numeric factor. |
181 | | #[must_use] |
182 | 0 | pub fn value(&self) -> usize { |
183 | 0 | match self { |
184 | 0 | UnrollFactor::None => 1, |
185 | 0 | UnrollFactor::X2 => 2, |
186 | 0 | UnrollFactor::X4 => 4, |
187 | 0 | UnrollFactor::X8 => 8, |
188 | | } |
189 | 0 | } |
190 | | |
191 | | /// Get optimal factor for backend. |
192 | | #[must_use] |
193 | 0 | pub fn for_backend(backend: ComputeBackend) -> Self { |
194 | 0 | match backend { |
195 | 0 | ComputeBackend::Avx512 => UnrollFactor::X8, |
196 | 0 | ComputeBackend::Avx2 => UnrollFactor::X4, |
197 | 0 | ComputeBackend::Sse2 | ComputeBackend::Neon => UnrollFactor::X2, |
198 | 0 | _ => UnrollFactor::None, |
199 | | } |
200 | 0 | } |
201 | | } |
202 | | |
203 | | /// Helper for unroll-and-tail loop pattern. |
204 | | /// |
205 | | /// Processes data in unrolled chunks, then handles the tail. |
206 | | #[derive(Debug)] |
207 | | pub struct UnrollTailIterator { |
208 | | /// Total elements |
209 | | total: usize, |
210 | | /// Current position |
211 | | position: usize, |
212 | | /// Elements per unrolled iteration |
213 | | chunk_size: usize, |
214 | | } |
215 | | |
216 | | impl UnrollTailIterator { |
217 | | /// Create iterator for given size and unroll factor. |
218 | 0 | pub fn new(total: usize, factor: UnrollFactor) -> Self { |
219 | 0 | Self { |
220 | 0 | total, |
221 | 0 | position: 0, |
222 | 0 | chunk_size: factor.value(), |
223 | 0 | } |
224 | 0 | } |
225 | | |
226 | | /// Get number of full unrolled iterations. |
227 | | #[must_use] |
228 | 0 | pub fn full_iterations(&self) -> usize { |
229 | 0 | self.total / self.chunk_size |
230 | 0 | } |
231 | | |
232 | | /// Get tail size (remainder). |
233 | | #[must_use] |
234 | 0 | pub fn tail_size(&self) -> usize { |
235 | 0 | self.total % self.chunk_size |
236 | 0 | } |
237 | | |
238 | | /// Check if there's a tail to process. |
239 | | #[must_use] |
240 | 0 | pub fn has_tail(&self) -> bool { |
241 | 0 | self.tail_size() > 0 |
242 | 0 | } |
243 | | |
244 | | /// Get next chunk range for unrolled iteration. |
245 | 0 | pub fn next_chunk(&mut self) -> Option<(usize, usize)> { |
246 | 0 | if self.position + self.chunk_size <= self.total { |
247 | 0 | let start = self.position; |
248 | 0 | self.position += self.chunk_size; |
249 | 0 | Some((start, start + self.chunk_size)) |
250 | | } else { |
251 | 0 | None |
252 | | } |
253 | 0 | } |
254 | | |
255 | | /// Get tail range (call after all chunks consumed). |
256 | 0 | pub fn tail_range(&self) -> Option<(usize, usize)> { |
257 | 0 | let tail_start = self.full_iterations() * self.chunk_size; |
258 | 0 | if tail_start < self.total { |
259 | 0 | Some((tail_start, self.total)) |
260 | | } else { |
261 | 0 | None |
262 | | } |
263 | 0 | } |
264 | | } |
265 | | |
266 | | /// Process a slice with unroll-and-tail pattern. |
267 | | /// |
268 | | /// # Example |
269 | | /// ```ignore |
270 | | /// let result = unroll_tail_process( |
271 | | /// &data, |
272 | | /// UnrollFactor::X4, |
273 | | /// |chunk| chunk.iter().sum::<f32>(), // Unrolled body |
274 | | /// |elem| *elem, // Tail body |
275 | | /// ); |
276 | | /// ``` |
277 | 0 | pub fn unroll_tail_process<T, U, F, G>( |
278 | 0 | data: &[T], |
279 | 0 | factor: UnrollFactor, |
280 | 0 | mut process_chunk: F, |
281 | 0 | mut process_elem: G, |
282 | 0 | ) -> Vec<U> |
283 | 0 | where |
284 | 0 | F: FnMut(&[T]) -> U, |
285 | 0 | G: FnMut(&T) -> U, |
286 | | { |
287 | 0 | let mut iter = UnrollTailIterator::new(data.len(), factor); |
288 | 0 | let mut results = |
289 | 0 | Vec::with_capacity(iter.full_iterations() + if iter.has_tail() { 1 } else { 0 }); |
290 | | |
291 | | // Process full chunks |
292 | 0 | while let Some((start, end)) = iter.next_chunk() { |
293 | 0 | results.push(process_chunk(&data[start..end])); |
294 | 0 | } |
295 | | |
296 | | // Process tail |
297 | 0 | if let Some((start, end)) = iter.tail_range() { |
298 | 0 | for elem in &data[start..end] { |
299 | 0 | results.push(process_elem(elem)); |
300 | 0 | } |
301 | 0 | } |
302 | | |
303 | 0 | results |
304 | 0 | } |
305 | | |
306 | | #[cfg(test)] |
307 | | mod tests { |
308 | | use super::*; |
309 | | |
310 | | // ========================================================================= |
311 | | // SimdBackendState Tests |
312 | | // ========================================================================= |
313 | | |
314 | | #[test] |
315 | | fn test_simd_backend_state_default() { |
316 | | let state = SimdBackendState::default(); |
317 | | assert_eq!(state, SimdBackendState::Uninitialized); |
318 | | } |
319 | | |
320 | | #[test] |
321 | | fn test_simd_backend_state_variants() { |
322 | | let states = [ |
323 | | SimdBackendState::Uninitialized, |
324 | | SimdBackendState::Configuring, |
325 | | SimdBackendState::Ready, |
326 | | SimdBackendState::Failed, |
327 | | ]; |
328 | | // All variants are distinct |
329 | | for i in 0..states.len() { |
330 | | for j in (i + 1)..states.len() { |
331 | | assert_ne!(states[i], states[j]); |
332 | | } |
333 | | } |
334 | | } |
335 | | |
336 | | // ========================================================================= |
337 | | // LazySimdConfig Tests |
338 | | // ========================================================================= |
339 | | |
340 | | #[test] |
341 | | fn test_lazy_simd_config_new() { |
342 | | let config = LazySimdConfig::new(); |
343 | | assert_eq!(config.state(), SimdBackendState::Uninitialized); |
344 | | } |
345 | | |
346 | | #[test] |
347 | | fn test_lazy_simd_config_best_backend() { |
348 | | let config = LazySimdConfig::new(); |
349 | | // Best backend should be detected and not crash |
350 | | let _backend = config.best_backend(); |
351 | | } |
352 | | |
353 | | #[test] |
354 | | fn test_lazy_simd_config_ensure_ready() { |
355 | | let mut config = LazySimdConfig::new(); |
356 | | let result = config.ensure_ready(); |
357 | | assert!(result.is_ok()); |
358 | | assert_eq!(config.state(), SimdBackendState::Ready); |
359 | | } |
360 | | |
361 | | #[test] |
362 | | fn test_lazy_simd_config_ensure_ready_idempotent() { |
363 | | let mut config = LazySimdConfig::new(); |
364 | | |
365 | | let result1 = config.ensure_ready(); |
366 | | let result2 = config.ensure_ready(); |
367 | | |
368 | | assert_eq!(result1, result2); |
369 | | } |
370 | | |
371 | | #[test] |
372 | | fn test_lazy_simd_config_reset() { |
373 | | let mut config = LazySimdConfig::new(); |
374 | | let _ = config.ensure_ready(); |
375 | | assert_eq!(config.state(), SimdBackendState::Ready); |
376 | | |
377 | | config.reset(); |
378 | | assert_eq!(config.state(), SimdBackendState::Uninitialized); |
379 | | } |
380 | | |
381 | | #[test] |
382 | | fn test_lazy_simd_config_has_amx() { |
383 | | let config = LazySimdConfig::new(); |
384 | | // AMX is typically not available, but this shouldn't crash |
385 | | let _has_amx = config.has_amx(); |
386 | | } |
387 | | |
388 | | #[test] |
389 | | fn test_lazy_simd_config_default() { |
390 | | let config = LazySimdConfig::default(); |
391 | | assert_eq!(config.state(), SimdBackendState::Uninitialized); |
392 | | } |
393 | | |
394 | | // ========================================================================= |
395 | | // AmxTileConfig Tests |
396 | | // ========================================================================= |
397 | | |
398 | | #[test] |
399 | | fn test_amx_tile_config_default() { |
400 | | let config = AmxTileConfig::default(); |
401 | | assert_eq!(config.palette, 0); |
402 | | assert_eq!(config.start_row, 0); |
403 | | assert_eq!(config.rows, 0); |
404 | | assert_eq!(config.bytes_per_row, 0); |
405 | | } |
406 | | |
407 | | #[test] |
408 | | fn test_amx_tile_config_custom() { |
409 | | let config = AmxTileConfig { |
410 | | palette: 1, |
411 | | start_row: 0, |
412 | | rows: 16, |
413 | | bytes_per_row: 64, |
414 | | }; |
415 | | assert_eq!(config.palette, 1); |
416 | | assert_eq!(config.rows, 16); |
417 | | assert_eq!(config.bytes_per_row, 64); |
418 | | } |
419 | | |
420 | | // ========================================================================= |
421 | | // UnrollFactor Tests |
422 | | // ========================================================================= |
423 | | |
424 | | #[test] |
425 | | fn test_unroll_factor_value() { |
426 | | assert_eq!(UnrollFactor::None.value(), 1); |
427 | | assert_eq!(UnrollFactor::X2.value(), 2); |
428 | | assert_eq!(UnrollFactor::X4.value(), 4); |
429 | | assert_eq!(UnrollFactor::X8.value(), 8); |
430 | | } |
431 | | |
432 | | #[test] |
433 | | fn test_unroll_factor_for_backend() { |
434 | | assert_eq!(UnrollFactor::for_backend(ComputeBackend::Avx512), UnrollFactor::X8); |
435 | | assert_eq!(UnrollFactor::for_backend(ComputeBackend::Avx2), UnrollFactor::X4); |
436 | | assert_eq!(UnrollFactor::for_backend(ComputeBackend::Sse2), UnrollFactor::X2); |
437 | | assert_eq!(UnrollFactor::for_backend(ComputeBackend::Neon), UnrollFactor::X2); |
438 | | assert_eq!(UnrollFactor::for_backend(ComputeBackend::Scalar), UnrollFactor::None); |
439 | | } |
440 | | |
441 | | // ========================================================================= |
442 | | // UnrollTailIterator Tests |
443 | | // ========================================================================= |
444 | | |
445 | | #[test] |
446 | | fn test_unroll_tail_iterator_basic() { |
447 | | let iter = UnrollTailIterator::new(10, UnrollFactor::X4); |
448 | | assert_eq!(iter.full_iterations(), 2); // 10 / 4 = 2 |
449 | | assert_eq!(iter.tail_size(), 2); // 10 % 4 = 2 |
450 | | assert!(iter.has_tail()); |
451 | | } |
452 | | |
453 | | #[test] |
454 | | fn test_unroll_tail_iterator_no_tail() { |
455 | | let iter = UnrollTailIterator::new(12, UnrollFactor::X4); |
456 | | assert_eq!(iter.full_iterations(), 3); |
457 | | assert_eq!(iter.tail_size(), 0); |
458 | | assert!(!iter.has_tail()); |
459 | | } |
460 | | |
461 | | #[test] |
462 | | fn test_unroll_tail_iterator_next_chunk() { |
463 | | let mut iter = UnrollTailIterator::new(10, UnrollFactor::X4); |
464 | | |
465 | | assert_eq!(iter.next_chunk(), Some((0, 4))); |
466 | | assert_eq!(iter.next_chunk(), Some((4, 8))); |
467 | | assert_eq!(iter.next_chunk(), None); // No more full chunks |
468 | | } |
469 | | |
470 | | #[test] |
471 | | fn test_unroll_tail_iterator_tail_range() { |
472 | | let iter = UnrollTailIterator::new(10, UnrollFactor::X4); |
473 | | assert_eq!(iter.tail_range(), Some((8, 10))); |
474 | | } |
475 | | |
476 | | #[test] |
477 | | fn test_unroll_tail_iterator_tail_range_no_tail() { |
478 | | let iter = UnrollTailIterator::new(8, UnrollFactor::X4); |
479 | | assert_eq!(iter.tail_range(), None); |
480 | | } |
481 | | |
482 | | #[test] |
483 | | fn test_unroll_tail_iterator_empty() { |
484 | | let iter = UnrollTailIterator::new(0, UnrollFactor::X4); |
485 | | assert_eq!(iter.full_iterations(), 0); |
486 | | assert_eq!(iter.tail_size(), 0); |
487 | | assert!(!iter.has_tail()); |
488 | | } |
489 | | |
490 | | // ========================================================================= |
491 | | // unroll_tail_process Tests |
492 | | // ========================================================================= |
493 | | |
494 | | #[test] |
495 | | fn test_unroll_tail_process_basic() { |
496 | | let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; |
497 | | let results = unroll_tail_process( |
498 | | &data, |
499 | | UnrollFactor::X4, |
500 | | |chunk| chunk.iter().sum::<f32>(), |
501 | | |elem| *elem, |
502 | | ); |
503 | | |
504 | | // 2 full chunks: [1,2,3,4] -> 10.0, [5,6,7,8] -> 26.0 |
505 | | // Tail: 9.0, 10.0 |
506 | | assert_eq!(results.len(), 4); |
507 | | assert_eq!(results[0], 10.0); // 1+2+3+4 |
508 | | assert_eq!(results[1], 26.0); // 5+6+7+8 |
509 | | assert_eq!(results[2], 9.0); |
510 | | assert_eq!(results[3], 10.0); |
511 | | } |
512 | | |
513 | | #[test] |
514 | | fn test_unroll_tail_process_no_tail() { |
515 | | let data = vec![1, 2, 3, 4, 5, 6, 7, 8]; |
516 | | let results = unroll_tail_process( |
517 | | &data, |
518 | | UnrollFactor::X4, |
519 | | |chunk| chunk.iter().sum::<i32>(), |
520 | | |elem| *elem, |
521 | | ); |
522 | | |
523 | | assert_eq!(results.len(), 2); |
524 | | assert_eq!(results[0], 10); // 1+2+3+4 |
525 | | assert_eq!(results[1], 26); // 5+6+7+8 |
526 | | } |
527 | | |
528 | | #[test] |
529 | | fn test_unroll_tail_process_empty() { |
530 | | let data: Vec<i32> = vec![]; |
531 | | let results = unroll_tail_process(&data, UnrollFactor::X4, |_| 0, |_| 0); |
532 | | assert!(results.is_empty()); |
533 | | } |
534 | | |
535 | | #[test] |
536 | | fn test_unroll_tail_process_small() { |
537 | | let data = vec![1, 2, 3]; // Smaller than chunk size |
538 | | let results = unroll_tail_process(&data, UnrollFactor::X4, |_| 0, |elem| *elem); |
539 | | |
540 | | // No full chunks, only tail |
541 | | assert_eq!(results.len(), 3); |
542 | | assert_eq!(results, vec![1, 2, 3]); |
543 | | } |
544 | | |
545 | | // ========================================================================= |
546 | | // FALSIFICATION TESTS |
547 | | // ========================================================================= |
548 | | |
549 | | /// FALSIFICATION TEST: UnrollFactor values must be powers of 2 |
550 | | #[test] |
551 | | fn test_falsify_unroll_factor_powers_of_two() { |
552 | | let factors = [ |
553 | | UnrollFactor::None, |
554 | | UnrollFactor::X2, |
555 | | UnrollFactor::X4, |
556 | | UnrollFactor::X8, |
557 | | ]; |
558 | | |
559 | | for factor in &factors { |
560 | | let value = factor.value(); |
561 | | assert!( |
562 | | value.is_power_of_two(), |
563 | | "FALSIFICATION FAILED: UnrollFactor {:?} value {} is not power of 2", |
564 | | factor, |
565 | | value |
566 | | ); |
567 | | } |
568 | | } |
569 | | |
570 | | /// FALSIFICATION TEST: UnrollTailIterator must cover all elements exactly once |
571 | | #[test] |
572 | | fn test_falsify_unroll_tail_covers_all() { |
573 | | for total in [1, 7, 8, 10, 100, 1000] { |
574 | | for factor in [UnrollFactor::None, UnrollFactor::X2, UnrollFactor::X4, UnrollFactor::X8] |
575 | | { |
576 | | let mut iter = UnrollTailIterator::new(total, factor); |
577 | | let mut covered = 0usize; |
578 | | |
579 | | // Count elements in full chunks |
580 | | while let Some((start, end)) = iter.next_chunk() { |
581 | | covered += end - start; |
582 | | } |
583 | | |
584 | | // Count elements in tail |
585 | | if let Some((start, end)) = iter.tail_range() { |
586 | | covered += end - start; |
587 | | } |
588 | | |
589 | | assert_eq!( |
590 | | covered, total, |
591 | | "FALSIFICATION FAILED: UnrollTailIterator({}, {:?}) covered {} elements, expected {}", |
592 | | total, factor, covered, total |
593 | | ); |
594 | | } |
595 | | } |
596 | | } |
597 | | |
598 | | /// FALSIFICATION TEST: LazySimdConfig state transitions must be valid |
599 | | #[test] |
600 | | fn test_falsify_simd_config_state_transitions() { |
601 | | let mut config = LazySimdConfig::new(); |
602 | | |
603 | | // Initial state must be Uninitialized |
604 | | assert_eq!( |
605 | | config.state(), |
606 | | SimdBackendState::Uninitialized, |
607 | | "FALSIFICATION FAILED: Initial state should be Uninitialized" |
608 | | ); |
609 | | |
610 | | // After ensure_ready, state must be Ready |
611 | | let result = config.ensure_ready(); |
612 | | assert!( |
613 | | result.is_ok(), |
614 | | "FALSIFICATION FAILED: ensure_ready should succeed" |
615 | | ); |
616 | | assert_eq!( |
617 | | config.state(), |
618 | | SimdBackendState::Ready, |
619 | | "FALSIFICATION FAILED: State should be Ready after ensure_ready" |
620 | | ); |
621 | | |
622 | | // After reset, state must be Uninitialized again |
623 | | config.reset(); |
624 | | assert_eq!( |
625 | | config.state(), |
626 | | SimdBackendState::Uninitialized, |
627 | | "FALSIFICATION FAILED: State should be Uninitialized after reset" |
628 | | ); |
629 | | } |
630 | | } |