forked from libre-chip/fayalite
		
	add efficient prefix-sums and reductions
This commit is contained in:
		
							parent
							
								
									50c86e18dc
								
							
						
					
					
						commit
						bd75fdfefd
					
				
					 2 changed files with 840 additions and 0 deletions
				
			
		|  | @ -29,4 +29,5 @@ pub use misc::{ | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| pub mod job_server; | pub mod job_server; | ||||||
|  | pub mod prefix_sum; | ||||||
| pub mod ready_valid; | pub mod ready_valid; | ||||||
|  |  | ||||||
							
								
								
									
										839
									
								
								crates/fayalite/src/util/prefix_sum.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										839
									
								
								crates/fayalite/src/util/prefix_sum.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,839 @@ | ||||||
|  | // SPDX-License-Identifier: LGPL-3.0-or-later
 | ||||||
|  | // See Notices.txt for copyright information
 | ||||||
|  | 
 | ||||||
|  | // code derived from:
 | ||||||
|  | // https://web.archive.org/web/20250303054010/https://git.libre-soc.org/?p=nmutil.git;a=blob;f=src/nmutil/prefix_sum.py;hb=effeb28e5848392adddcdad1f6e7a098f2a44c9c
 | ||||||
|  | 
 | ||||||
|  | use crate::intern::{Intern, Interned, Memoize}; | ||||||
|  | use std::{borrow::Cow, num::NonZeroUsize}; | ||||||
|  | 
 | ||||||
|  | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] | ||||||
|  | pub struct PrefixSumOp { | ||||||
|  |     pub lhs_index: usize, | ||||||
|  |     pub rhs_and_dest_index: NonZeroUsize, | ||||||
|  |     pub row: u32, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, PartialEq, Eq, Hash, Debug)] | ||||||
|  | #[non_exhaustive] | ||||||
|  | pub struct DiagramConfig { | ||||||
|  |     pub space: Cow<'static, str>, | ||||||
|  |     pub vertical_bar: Cow<'static, str>, | ||||||
|  |     pub plus: Cow<'static, str>, | ||||||
|  |     pub slant: Cow<'static, str>, | ||||||
|  |     pub connect: Cow<'static, str>, | ||||||
|  |     pub no_connect: Cow<'static, str>, | ||||||
|  |     pub padding: usize, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DiagramConfig { | ||||||
|  |     pub const fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             space: Cow::Borrowed(" "), | ||||||
|  |             vertical_bar: Cow::Borrowed("|"), | ||||||
|  |             plus: Cow::Borrowed("\u{2295}"), // ⊕
 | ||||||
|  |             slant: Cow::Borrowed(r"\"), | ||||||
|  |             connect: Cow::Borrowed("\u{25CF}"), // ●
 | ||||||
|  |             no_connect: Cow::Borrowed("X"), | ||||||
|  |             padding: 1, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn draw(self, ops: impl IntoIterator<Item = PrefixSumOp>, item_count: usize) -> String { | ||||||
|  |         #[derive(Copy, Clone, Debug)] | ||||||
|  |         struct DiagramCell { | ||||||
|  |             slant: bool, | ||||||
|  |             plus: bool, | ||||||
|  |             tee: bool, | ||||||
|  |         } | ||||||
|  |         let mut ops_by_row: Vec<Vec<PrefixSumOp>> = Vec::new(); | ||||||
|  |         let mut last_row = 0; | ||||||
|  |         ops.into_iter().for_each(|op| { | ||||||
|  |             assert!( | ||||||
|  |                 op.lhs_index < op.rhs_and_dest_index.get(), | ||||||
|  |                 "invalid PrefixSumOp! lhs_index must be less \ | ||||||
|  |                 than rhs_and_dest_index: {op:?}",
 | ||||||
|  |             ); | ||||||
|  |             assert!( | ||||||
|  |                 op.row >= last_row, | ||||||
|  |                 "invalid PrefixSumOp! row must \ | ||||||
|  |                 not decrease (row last was: {last_row}): {op:?}",
 | ||||||
|  |             ); | ||||||
|  |             let ops = if op.row > last_row || ops_by_row.is_empty() { | ||||||
|  |                 ops_by_row.push(vec![]); | ||||||
|  |                 ops_by_row.last_mut().expect("just pushed") | ||||||
|  |             } else { | ||||||
|  |                 ops_by_row | ||||||
|  |                     .last_mut() | ||||||
|  |                     .expect("just checked if ops_by_row is empty") | ||||||
|  |             }; | ||||||
|  |             if let Some(last) = ops.last() { | ||||||
|  |                 assert!( | ||||||
|  |                     op.rhs_and_dest_index < last.rhs_and_dest_index, | ||||||
|  |                     "invalid PrefixSumOp! rhs_and_dest_index must strictly \ | ||||||
|  |                     decrease in a row:\nthis op: {op:?}\nlast op: {last:?}",
 | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             ops.push(op); | ||||||
|  |             last_row = op.row; | ||||||
|  |         }); | ||||||
|  |         let blank_row = || { | ||||||
|  |             vec![ | ||||||
|  |                 DiagramCell { | ||||||
|  |                     slant: false, | ||||||
|  |                     plus: false, | ||||||
|  |                     tee: false | ||||||
|  |                 }; | ||||||
|  |                 item_count | ||||||
|  |             ] | ||||||
|  |         }; | ||||||
|  |         let mut cells = vec![blank_row()]; | ||||||
|  |         for ops in ops_by_row { | ||||||
|  |             let max_distance = ops | ||||||
|  |                 .iter() | ||||||
|  |                 .map( | ||||||
|  |                     |&PrefixSumOp { | ||||||
|  |                          lhs_index, | ||||||
|  |                          rhs_and_dest_index, | ||||||
|  |                          .. | ||||||
|  |                      }| { rhs_and_dest_index.get() - lhs_index }, | ||||||
|  |                 ) | ||||||
|  |                 .max() | ||||||
|  |                 .expect("ops is known to be non-empty"); | ||||||
|  |             cells.extend((0..max_distance).map(|_| blank_row())); | ||||||
|  |             for op in ops { | ||||||
|  |                 let mut y = cells.len() - 1; | ||||||
|  |                 assert!( | ||||||
|  |                     op.rhs_and_dest_index.get() < item_count, | ||||||
|  |                     "invalid PrefixSumOp! rhs_and_dest_index must be \ | ||||||
|  |                     less than item_count ({item_count}): {op:?}",
 | ||||||
|  |                 ); | ||||||
|  |                 let mut x = op.rhs_and_dest_index.get(); | ||||||
|  |                 cells[y][x].plus = true; | ||||||
|  |                 x -= 1; | ||||||
|  |                 y -= 1; | ||||||
|  |                 while op.lhs_index < x { | ||||||
|  |                     cells[y][x].slant = true; | ||||||
|  |                     x -= 1; | ||||||
|  |                     y -= 1; | ||||||
|  |                 } | ||||||
|  |                 cells[y][x].tee = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let mut retval = String::new(); | ||||||
|  |         let mut row_text = vec![String::new(); 2 * self.padding + 1]; | ||||||
|  |         for cells_row in cells { | ||||||
|  |             for cell in cells_row { | ||||||
|  |                 // top padding
 | ||||||
|  |                 for y in 0..self.padding { | ||||||
|  |                     // top left padding
 | ||||||
|  |                     for x in 0..self.padding { | ||||||
|  |                         row_text[y] += if x == y && (cell.plus || cell.slant) { | ||||||
|  |                             &self.slant | ||||||
|  |                         } else { | ||||||
|  |                             &self.space | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |                     // top vertical bar
 | ||||||
|  |                     row_text[y] += &self.vertical_bar; | ||||||
|  |                     // top right padding
 | ||||||
|  |                     for _ in 0..self.padding { | ||||||
|  |                         row_text[y] += &self.space; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 // center left padding
 | ||||||
|  |                 for _ in 0..self.padding { | ||||||
|  |                     row_text[self.padding] += &self.space; | ||||||
|  |                 } | ||||||
|  |                 // center
 | ||||||
|  |                 row_text[self.padding] += if cell.plus { | ||||||
|  |                     &self.plus | ||||||
|  |                 } else if cell.tee { | ||||||
|  |                     &self.connect | ||||||
|  |                 } else if cell.slant { | ||||||
|  |                     &self.no_connect | ||||||
|  |                 } else { | ||||||
|  |                     &self.vertical_bar | ||||||
|  |                 }; | ||||||
|  |                 // center right padding
 | ||||||
|  |                 for _ in 0..self.padding { | ||||||
|  |                     row_text[self.padding] += &self.space; | ||||||
|  |                 } | ||||||
|  |                 let bottom_padding_start = self.padding + 1; | ||||||
|  |                 let bottom_padding_last = self.padding * 2; | ||||||
|  |                 // bottom padding
 | ||||||
|  |                 for y in bottom_padding_start..=bottom_padding_last { | ||||||
|  |                     // bottom left padding
 | ||||||
|  |                     for _ in 0..self.padding { | ||||||
|  |                         row_text[y] += &self.space; | ||||||
|  |                     } | ||||||
|  |                     // bottom vertical bar
 | ||||||
|  |                     row_text[y] += &self.vertical_bar; | ||||||
|  |                     // bottom right padding
 | ||||||
|  |                     for x in bottom_padding_start..=bottom_padding_last { | ||||||
|  |                         row_text[y] += if x == y && (cell.tee || cell.slant) { | ||||||
|  |                             &self.slant | ||||||
|  |                         } else { | ||||||
|  |                             &self.space | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             for line in &mut row_text { | ||||||
|  |                 retval += line.trim_end(); | ||||||
|  |                 retval += "\n"; | ||||||
|  |                 line.clear(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         retval | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for DiagramConfig { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self::new() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl PrefixSumOp { | ||||||
|  |     pub fn diagram(ops: impl IntoIterator<Item = Self>, item_count: usize) -> String { | ||||||
|  |         Self::diagram_with_config(ops, item_count, DiagramConfig::new()) | ||||||
|  |     } | ||||||
|  |     pub fn diagram_with_config( | ||||||
|  |         ops: impl IntoIterator<Item = Self>, | ||||||
|  |         item_count: usize, | ||||||
|  |         config: DiagramConfig, | ||||||
|  |     ) -> String { | ||||||
|  |         config.draw(ops, item_count) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] | ||||||
|  | pub enum PrefixSumAlgorithm { | ||||||
|  |     /// Uses the algorithm from:
 | ||||||
|  |     /// https://en.wikipedia.org/wiki/Prefix_sum#Algorithm_1:_Shorter_span,_more_parallel
 | ||||||
|  |     LowLatency, | ||||||
|  |     /// Uses the algorithm from:
 | ||||||
|  |     /// https://en.wikipedia.org/wiki/Prefix_sum#Algorithm_2:_Work-efficient
 | ||||||
|  |     WorkEfficient, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl PrefixSumAlgorithm { | ||||||
|  |     fn ops_impl(self, item_count: usize) -> Vec<PrefixSumOp> { | ||||||
|  |         let mut retval = Vec::new(); | ||||||
|  |         let mut distance = 1; | ||||||
|  |         let mut row = 0; | ||||||
|  |         while distance < item_count { | ||||||
|  |             let double_distance = distance | ||||||
|  |                 .checked_mul(2) | ||||||
|  |                 .expect("prefix-sum item_count is too big"); | ||||||
|  |             let (start, step) = match self { | ||||||
|  |                 Self::LowLatency => (distance, 1), | ||||||
|  |                 Self::WorkEfficient => (double_distance - 1, double_distance), | ||||||
|  |             }; | ||||||
|  |             for rhs_and_dest_index in (start..item_count).step_by(step).rev() { | ||||||
|  |                 let Some(rhs_and_dest_index) = NonZeroUsize::new(rhs_and_dest_index) else { | ||||||
|  |                     unreachable!(); | ||||||
|  |                 }; | ||||||
|  |                 let lhs_index = rhs_and_dest_index.get() - distance; | ||||||
|  |                 retval.push(PrefixSumOp { | ||||||
|  |                     lhs_index, | ||||||
|  |                     rhs_and_dest_index, | ||||||
|  |                     row, | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             distance = double_distance; | ||||||
|  |             row += 1; | ||||||
|  |         } | ||||||
|  |         match self { | ||||||
|  |             Self::LowLatency => {} | ||||||
|  |             Self::WorkEfficient => { | ||||||
|  |                 distance /= 2; | ||||||
|  |                 while distance >= 1 { | ||||||
|  |                     let start = distance | ||||||
|  |                         .checked_mul(3) | ||||||
|  |                         .expect("prefix-sum item_count is too big") | ||||||
|  |                         - 1; | ||||||
|  |                     for rhs_and_dest_index in (start..item_count).step_by(distance * 2).rev() { | ||||||
|  |                         let Some(rhs_and_dest_index) = NonZeroUsize::new(rhs_and_dest_index) else { | ||||||
|  |                             unreachable!(); | ||||||
|  |                         }; | ||||||
|  |                         let lhs_index = rhs_and_dest_index.get() - distance; | ||||||
|  |                         retval.push(PrefixSumOp { | ||||||
|  |                             lhs_index, | ||||||
|  |                             rhs_and_dest_index, | ||||||
|  |                             row, | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                     row += 1; | ||||||
|  |                     distance /= 2; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         retval | ||||||
|  |     } | ||||||
|  |     pub fn ops(self, item_count: usize) -> Interned<[PrefixSumOp]> { | ||||||
|  |         #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] | ||||||
|  |         struct MyMemoize(PrefixSumAlgorithm); | ||||||
|  |         impl Memoize for MyMemoize { | ||||||
|  |             type Input = usize; | ||||||
|  |             type InputOwned = usize; | ||||||
|  |             type Output = Interned<[PrefixSumOp]>; | ||||||
|  | 
 | ||||||
|  |             fn inner(self, item_count: &Self::Input) -> Self::Output { | ||||||
|  |                 Intern::intern_owned(self.0.ops_impl(*item_count)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         MyMemoize(self).get_owned(item_count) | ||||||
|  |     } | ||||||
|  |     pub fn run<T>(self, items: impl IntoIterator<Item = T>, f: impl FnMut(&T, &T) -> T) -> Vec<T> { | ||||||
|  |         let mut items = Vec::from_iter(items); | ||||||
|  |         self.run_on_slice(&mut items, f); | ||||||
|  |         items | ||||||
|  |     } | ||||||
|  |     pub fn run_on_slice<T>(self, items: &mut [T], mut f: impl FnMut(&T, &T) -> T) -> &mut [T] { | ||||||
|  |         self.ops(items.len()).into_iter().for_each( | ||||||
|  |             |PrefixSumOp { | ||||||
|  |                  lhs_index, | ||||||
|  |                  rhs_and_dest_index, | ||||||
|  |                  row: _, | ||||||
|  |              }| { | ||||||
|  |                 items[rhs_and_dest_index.get()] = | ||||||
|  |                     f(&items[lhs_index], &items[rhs_and_dest_index.get()]); | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  |         items | ||||||
|  |     } | ||||||
|  |     pub fn filtered_ops( | ||||||
|  |         self, | ||||||
|  |         item_live_out_flags: impl IntoIterator<Item = bool>, | ||||||
|  |     ) -> Vec<PrefixSumOp> { | ||||||
|  |         let mut item_live_out_flags = Vec::from_iter(item_live_out_flags); | ||||||
|  |         let prefix_sum_ops = self.ops(item_live_out_flags.len()); | ||||||
|  |         let mut ops_live_flags = vec![false; prefix_sum_ops.len()]; | ||||||
|  |         for ( | ||||||
|  |             op_index, | ||||||
|  |             &PrefixSumOp { | ||||||
|  |                 lhs_index, | ||||||
|  |                 rhs_and_dest_index, | ||||||
|  |                 row: _, | ||||||
|  |             }, | ||||||
|  |         ) in prefix_sum_ops.iter().enumerate().rev() | ||||||
|  |         { | ||||||
|  |             let live = item_live_out_flags[rhs_and_dest_index.get()]; | ||||||
|  |             item_live_out_flags[lhs_index] |= live; | ||||||
|  |             ops_live_flags[op_index] = live; | ||||||
|  |         } | ||||||
|  |         prefix_sum_ops | ||||||
|  |             .into_iter() | ||||||
|  |             .zip(ops_live_flags) | ||||||
|  |             .filter_map(|(op, live)| live.then_some(op)) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  |     pub fn reduce_ops(self, item_count: usize) -> Interned<[PrefixSumOp]> { | ||||||
|  |         #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] | ||||||
|  |         struct MyMemoize(PrefixSumAlgorithm); | ||||||
|  |         impl Memoize for MyMemoize { | ||||||
|  |             type Input = usize; | ||||||
|  |             type InputOwned = usize; | ||||||
|  |             type Output = Interned<[PrefixSumOp]>; | ||||||
|  | 
 | ||||||
|  |             fn inner(self, item_count: &Self::Input) -> Self::Output { | ||||||
|  |                 let mut item_live_out_flags = vec![false; *item_count]; | ||||||
|  |                 let Some(last_item_live_out_flag) = item_live_out_flags.last_mut() else { | ||||||
|  |                     return Interned::default(); | ||||||
|  |                 }; | ||||||
|  |                 *last_item_live_out_flag = true; | ||||||
|  |                 Intern::intern_owned(self.0.filtered_ops(item_live_out_flags)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         MyMemoize(self).get_owned(item_count) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn reduce_ops(item_count: usize) -> Interned<[PrefixSumOp]> { | ||||||
|  |     PrefixSumAlgorithm::LowLatency.reduce_ops(item_count) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn reduce<T>(items: impl IntoIterator<Item = T>, mut f: impl FnMut(T, T) -> T) -> Option<T> { | ||||||
|  |     let mut items: Vec<_> = items.into_iter().map(Some).collect(); | ||||||
|  |     for op in reduce_ops(items.len()) { | ||||||
|  |         let (Some(lhs), Some(rhs)) = ( | ||||||
|  |             items[op.lhs_index].take(), | ||||||
|  |             items[op.rhs_and_dest_index.get()].take(), | ||||||
|  |         ) else { | ||||||
|  |             unreachable!(); | ||||||
|  |         }; | ||||||
|  |         items[op.rhs_and_dest_index.get()] = Some(f(lhs, rhs)); | ||||||
|  |     } | ||||||
|  |     items.last_mut().and_then(Option::take) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     fn input_strings() -> [String; 9] { | ||||||
|  |         std::array::from_fn(|i| String::from_utf8(vec![b'a' + i as u8]).unwrap()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_prefix_sum_strings() { | ||||||
|  |         let input = input_strings(); | ||||||
|  |         let expected: Vec<String> = input | ||||||
|  |             .iter() | ||||||
|  |             .scan(String::new(), |l, r| { | ||||||
|  |                 *l += r; | ||||||
|  |                 Some(l.clone()) | ||||||
|  |             }) | ||||||
|  |             .collect(); | ||||||
|  |         println!("expected: {expected:?}"); | ||||||
|  |         assert_eq!( | ||||||
|  |             *PrefixSumAlgorithm::WorkEfficient | ||||||
|  |                 .run_on_slice(&mut input.clone(), |l, r| l.to_string() + r), | ||||||
|  |             *expected | ||||||
|  |         ); | ||||||
|  |         assert_eq!( | ||||||
|  |             *PrefixSumAlgorithm::LowLatency | ||||||
|  |                 .run_on_slice(&mut input.clone(), |l, r| l.to_string() + r), | ||||||
|  |             *expected | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_reduce_string() { | ||||||
|  |         let input = input_strings(); | ||||||
|  |         let expected = input.clone().into_iter().reduce(|l, r| l + &r); | ||||||
|  |         assert_eq!(reduce(input, |l, r| l + &r), expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn op(lhs_index: usize, rhs_and_dest_index: usize, row: u32) -> PrefixSumOp { | ||||||
|  |         PrefixSumOp { | ||||||
|  |             lhs_index, | ||||||
|  |             rhs_and_dest_index: NonZeroUsize::new(rhs_and_dest_index).expect("should be non-zero"), | ||||||
|  |             row, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_reduce_ops_9() { | ||||||
|  |         let expected = vec![ | ||||||
|  |             op(7, 8, 0), | ||||||
|  |             op(5, 6, 0), | ||||||
|  |             op(3, 4, 0), | ||||||
|  |             op(1, 2, 0), | ||||||
|  |             op(6, 8, 1), | ||||||
|  |             op(2, 4, 1), | ||||||
|  |             op(4, 8, 2), | ||||||
|  |             op(0, 8, 3), | ||||||
|  |         ]; | ||||||
|  |         println!("expected: {expected:#?}"); | ||||||
|  |         let ops = reduce_ops(9); | ||||||
|  |         println!("ops: {ops:#?}"); | ||||||
|  |         assert_eq!(*ops, *expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_reduce_ops_8() { | ||||||
|  |         let expected = vec![ | ||||||
|  |             op(6, 7, 0), | ||||||
|  |             op(4, 5, 0), | ||||||
|  |             op(2, 3, 0), | ||||||
|  |             op(0, 1, 0), | ||||||
|  |             op(5, 7, 1), | ||||||
|  |             op(1, 3, 1), | ||||||
|  |             op(3, 7, 2), | ||||||
|  |         ]; | ||||||
|  |         println!("expected: {expected:#?}"); | ||||||
|  |         let ops = reduce_ops(8); | ||||||
|  |         println!("ops: {ops:#?}"); | ||||||
|  |         assert_eq!(*ops, *expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_count_ones() { | ||||||
|  |         for width in 0..=10u32 { | ||||||
|  |             for v in 0..1u32 << width { | ||||||
|  |                 let expected = v.count_ones(); | ||||||
|  |                 assert_eq!( | ||||||
|  |                     reduce((0..width).map(|i| (v >> i) & 1), |l, r| l + r).unwrap_or(0), | ||||||
|  |                     expected, | ||||||
|  |                     "v={v:#X}" | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[track_caller] | ||||||
|  |     fn test_diagram(ops: impl IntoIterator<Item = PrefixSumOp>, item_count: usize, expected: &str) { | ||||||
|  |         let text = PrefixSumOp::diagram_with_config( | ||||||
|  |             ops, | ||||||
|  |             item_count, | ||||||
|  |             DiagramConfig { | ||||||
|  |                 plus: Cow::Borrowed("@"), | ||||||
|  |                 ..Default::default() | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  |         println!("text:\n{text}\n"); | ||||||
|  |         assert_eq!(text, expected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_work_efficient_diagram_16() { | ||||||
|  |         let item_count = 16; | ||||||
|  |         test_diagram( | ||||||
|  |             PrefixSumAlgorithm::WorkEfficient.ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  | | ||||||
|  |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ | | ||||||
|  |  | \|  | \|  | \|  | \|  | \|  | \|  | \|  | \| | ||||||
|  |  |  @  |  @  |  @  |  @  |  @  |  @  |  @  |  @ | ||||||
|  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  |  |\ |  | | ||||||
|  |  |  | \|  |  |  | \|  |  |  | \|  |  |  | \|  | | ||||||
|  |  |  |  X  |  |  |  X  |  |  |  X  |  |  |  X  | | ||||||
|  |  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  |  |\ | | ||||||
|  |  |  |  | \|  |  |  | \|  |  |  | \|  |  |  | \| | ||||||
|  |  |  |  |  @  |  |  |  @  |  |  |  @  |  |  |  @ | ||||||
|  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  X  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  @  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  ●  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  ●  |  |  |  ●  |  |  |  @  |  |  |  | | ||||||
|  |  |  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  | \|  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  X  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | \|  |  |  | \|  |  | | ||||||
|  |  |  ●  |  ●  |  @  |  ●  |  @  |  ●  |  @  |  | | ||||||
|  |  |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  | | ||||||
|  |  |  | \|  | \|  | \|  | \|  | \|  | \|  | \|  | | ||||||
|  |  |  |  @  |  @  |  @  |  @  |  @  |  @  |  @  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_low_latency_diagram_16() { | ||||||
|  |         let item_count = 16; | ||||||
|  |         test_diagram( | ||||||
|  |             PrefixSumAlgorithm::LowLatency.ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  ●  | | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  | \| \| \| \| \| \| \| \| \| \| \| \| \| \| \| | ||||||
|  |  ●  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @ | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |  | | ||||||
|  |  | \| \| \| \| \| \| \| \| \| \| \| \| \| \|  | | ||||||
|  |  |  X  X  X  X  X  X  X  X  X  X  X  X  X  X  | | ||||||
|  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  |  | \| \| \| \| \| \| \| \| \| \| \| \| \| \| | ||||||
|  |  ●  ●  @  @  @  @  @  @  @  @  @  @  @  @  @  @ | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  | | ||||||
|  |  | \| \| \| \| \| \| \| \| \| \| \| \|  |  |  | | ||||||
|  |  |  X  X  X  X  X  X  X  X  X  X  X  X  |  |  | | ||||||
|  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |  |  | | ||||||
|  |  |  | \| \| \| \| \| \| \| \| \| \| \| \|  |  | | ||||||
|  |  |  |  X  X  X  X  X  X  X  X  X  X  X  X  |  | | ||||||
|  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |  | | ||||||
|  |  |  |  | \| \| \| \| \| \| \| \| \| \| \| \|  | | ||||||
|  |  |  |  |  X  X  X  X  X  X  X  X  X  X  X  X  | | ||||||
|  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  |  |  |  | \| \| \| \| \| \| \| \| \| \| \| \| | ||||||
|  |  ●  ●  ●  ●  @  @  @  @  @  @  @  @  @  @  @  @ | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  |  |  |  |  | | ||||||
|  |  | \| \| \| \| \| \| \| \|  |  |  |  |  |  |  | | ||||||
|  |  |  X  X  X  X  X  X  X  X  |  |  |  |  |  |  | | ||||||
|  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  |  |  |  | | ||||||
|  |  |  | \| \| \| \| \| \| \| \|  |  |  |  |  |  | | ||||||
|  |  |  |  X  X  X  X  X  X  X  X  |  |  |  |  |  | | ||||||
|  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  |  |  | | ||||||
|  |  |  |  | \| \| \| \| \| \| \| \|  |  |  |  |  | | ||||||
|  |  |  |  |  X  X  X  X  X  X  X  X  |  |  |  |  | | ||||||
|  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  |  | | ||||||
|  |  |  |  |  | \| \| \| \| \| \| \| \|  |  |  |  | | ||||||
|  |  |  |  |  |  X  X  X  X  X  X  X  X  |  |  |  | | ||||||
|  |  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \| \| \| \| \| \| \| \|  |  |  | | ||||||
|  |  |  |  |  |  |  X  X  X  X  X  X  X  X  |  |  | | ||||||
|  |  |  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  |  | | ||||||
|  |  |  |  |  |  |  | \| \| \| \| \| \| \| \|  |  | | ||||||
|  |  |  |  |  |  |  |  X  X  X  X  X  X  X  X  |  | | ||||||
|  |  |  |  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ |  | | ||||||
|  |  |  |  |  |  |  |  | \| \| \| \| \| \| \| \|  | | ||||||
|  |  |  |  |  |  |  |  |  X  X  X  X  X  X  X  X  | | ||||||
|  |  |  |  |  |  |  |  |  |\ |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  |  |  |  |  |  |  |  | \| \| \| \| \| \| \| \| | ||||||
|  |  |  |  |  |  |  |  |  |  @  @  @  @  @  @  @  @ | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_work_efficient_diagram_9() { | ||||||
|  |         let item_count = 9; | ||||||
|  |         test_diagram( | ||||||
|  |             PrefixSumAlgorithm::WorkEfficient.ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  ●  |  ●  |  ●  |  ●  |  | | ||||||
|  |  |\ |  |\ |  |\ |  |\ |  | | ||||||
|  |  | \|  | \|  | \|  | \|  | | ||||||
|  |  |  @  |  @  |  @  |  @  | | ||||||
|  |  |  |\ |  |  |  |\ |  |  | | ||||||
|  |  |  | \|  |  |  | \|  |  | | ||||||
|  |  |  |  X  |  |  |  X  |  | | ||||||
|  |  |  |  |\ |  |  |  |\ |  | | ||||||
|  |  |  |  | \|  |  |  | \|  | | ||||||
|  |  |  |  |  @  |  |  |  @  | | ||||||
|  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  ●  |  |  |  @  | | ||||||
|  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  ●  |  ●  |  @  |  ●  | | ||||||
|  |  |  |\ |  |\ |  |\ |  |\ | | ||||||
|  |  |  | \|  | \|  | \|  | \| | ||||||
|  |  |  |  @  |  @  |  @  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_low_latency_diagram_9() { | ||||||
|  |         let item_count = 9; | ||||||
|  |         test_diagram( | ||||||
|  |             PrefixSumAlgorithm::LowLatency.ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  ●  ●  ●  ●  ●  ●  ●  ●  | | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  | \| \| \| \| \| \| \| \| | ||||||
|  |  ●  @  @  @  @  @  @  @  @ | ||||||
|  |  |\ |\ |\ |\ |\ |\ |\ |  | | ||||||
|  |  | \| \| \| \| \| \| \|  | | ||||||
|  |  |  X  X  X  X  X  X  X  | | ||||||
|  |  |  |\ |\ |\ |\ |\ |\ |\ | | ||||||
|  |  |  | \| \| \| \| \| \| \| | ||||||
|  |  ●  ●  @  @  @  @  @  @  @ | ||||||
|  |  |\ |\ |\ |\ |\ |  |  |  | | ||||||
|  |  | \| \| \| \| \|  |  |  | | ||||||
|  |  |  X  X  X  X  X  |  |  | | ||||||
|  |  |  |\ |\ |\ |\ |\ |  |  | | ||||||
|  |  |  | \| \| \| \| \|  |  | | ||||||
|  |  |  |  X  X  X  X  X  |  | | ||||||
|  |  |  |  |\ |\ |\ |\ |\ |  | | ||||||
|  |  |  |  | \| \| \| \| \|  | | ||||||
|  |  |  |  |  X  X  X  X  X  | | ||||||
|  |  |  |  |  |\ |\ |\ |\ |\ | | ||||||
|  |  |  |  |  | \| \| \| \| \| | ||||||
|  |  ●  |  |  |  @  @  @  @  @ | ||||||
|  |  |\ |  |  |  |  |  |  |  | | ||||||
|  |  | \|  |  |  |  |  |  |  | | ||||||
|  |  |  X  |  |  |  |  |  |  | | ||||||
|  |  |  |\ |  |  |  |  |  |  | | ||||||
|  |  |  | \|  |  |  |  |  |  | | ||||||
|  |  |  |  X  |  |  |  |  |  | | ||||||
|  |  |  |  |\ |  |  |  |  |  | | ||||||
|  |  |  |  | \|  |  |  |  |  | | ||||||
|  |  |  |  |  X  |  |  |  |  | | ||||||
|  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_reduce_diagram_16() { | ||||||
|  |         let item_count = 16; | ||||||
|  |         test_diagram( | ||||||
|  |             reduce_ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  |  ●  | | ||||||
|  |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ |  |\ | | ||||||
|  |  | \|  | \|  | \|  | \|  | \|  | \|  | \|  | \| | ||||||
|  |  |  @  |  @  |  @  |  @  |  @  |  @  |  @  |  @ | ||||||
|  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  |  |\ |  | | ||||||
|  |  |  | \|  |  |  | \|  |  |  | \|  |  |  | \|  | | ||||||
|  |  |  |  X  |  |  |  X  |  |  |  X  |  |  |  X  | | ||||||
|  |  |  |  |\ |  |  |  |\ |  |  |  |\ |  |  |  |\ | | ||||||
|  |  |  |  | \|  |  |  | \|  |  |  | \|  |  |  | \| | ||||||
|  |  |  |  |  @  |  |  |  @  |  |  |  @  |  |  |  @ | ||||||
|  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  X  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  @  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  X  |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_reduce_diagram_9() { | ||||||
|  |         let item_count = 9; | ||||||
|  |         test_diagram( | ||||||
|  |             reduce_ops(item_count), | ||||||
|  |             item_count, | ||||||
|  |             &r" | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  |  |  ●  |  ●  |  ●  |  ●  | | ||||||
|  |  |  |\ |  |\ |  |\ |  |\ | | ||||||
|  |  |  | \|  | \|  | \|  | \| | ||||||
|  |  |  |  @  |  @  |  @  |  @ | ||||||
|  |  |  |  |\ |  |  |  |\ |  | | ||||||
|  |  |  |  | \|  |  |  | \|  | | ||||||
|  |  |  |  |  X  |  |  |  X  | | ||||||
|  |  |  |  |  |\ |  |  |  |\ | | ||||||
|  |  |  |  |  | \|  |  |  | \| | ||||||
|  |  |  |  |  |  @  |  |  |  @ | ||||||
|  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  |  | \| | ||||||
|  |  ●  |  |  |  |  |  |  |  @ | ||||||
|  |  |\ |  |  |  |  |  |  |  | | ||||||
|  |  | \|  |  |  |  |  |  |  | | ||||||
|  |  |  X  |  |  |  |  |  |  | | ||||||
|  |  |  |\ |  |  |  |  |  |  | | ||||||
|  |  |  | \|  |  |  |  |  |  | | ||||||
|  |  |  |  X  |  |  |  |  |  | | ||||||
|  |  |  |  |\ |  |  |  |  |  | | ||||||
|  |  |  |  | \|  |  |  |  |  | | ||||||
|  |  |  |  |  X  |  |  |  |  | | ||||||
|  |  |  |  |  |\ |  |  |  |  | | ||||||
|  |  |  |  |  | \|  |  |  |  | | ||||||
|  |  |  |  |  |  X  |  |  |  | | ||||||
|  |  |  |  |  |  |\ |  |  |  | | ||||||
|  |  |  |  |  |  | \|  |  |  | | ||||||
|  |  |  |  |  |  |  X  |  |  | | ||||||
|  |  |  |  |  |  |  |\ |  |  | | ||||||
|  |  |  |  |  |  |  | \|  |  | | ||||||
|  |  |  |  |  |  |  |  X  |  | | ||||||
|  |  |  |  |  |  |  |  |\ |  | | ||||||
|  |  |  |  |  |  |  |  | \|  | | ||||||
|  |  |  |  |  |  |  |  |  X  | | ||||||
|  |  |  |  |  |  |  |  |  |\ | | ||||||
|  |  |  |  |  |  |  |  |  | \| | ||||||
|  |  |  |  |  |  |  |  |  |  @ | ||||||
|  |  |  |  |  |  |  |  |  |  | | ||||||
|  | "[1..], // trim newline at start
 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue