use std::fmt::{self, Display, Formatter};
use crate::{Axis, ComputeScope, Coverage, Level, Partitioning, Space, Spread};
const LEAF: char = '◦';
const MARGIN: &str = " ";
const LEVEL: &str = " ";
const TIMES: &str = " × ";
const GAP: &str = " ";
pub struct LevelTable<'a> {
partitioning: &'a Partitioning,
labels: &'a [(Axis, &'a str)],
}
impl<'a> LevelTable<'a> {
pub(crate) fn new(partitioning: &'a Partitioning, labels: &'a [(Axis, &'a str)]) -> Self {
LevelTable {
partitioning,
labels,
}
}
fn label(&self, axis: Axis) -> String {
match self.labels.iter().find(|&&(named, _)| named == axis) {
Some(&(_, name)) => name.to_string(),
None => format!("a{}", axis.0),
}
}
fn rows(&self) -> Vec<Row> {
let axes: Vec<Axis> = self.partitioning.space().axes().collect();
let mut space = self.partitioning.space().clone();
let mut rows: Vec<Row> = self
.partitioning
.levels()
.iter()
.map(|level| {
let row = Row::of(level, &space, &axes);
space = level.child(&space);
row
})
.collect();
rows.reverse();
rows.insert(0, Row::leaf(&space, &axes));
rows
}
}
impl Display for LevelTable<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let axes: Vec<Axis> = self.partitioning.space().axes().collect();
let header: Vec<String> = axes.iter().map(|&axis| self.label(axis)).collect();
let rows = self.rows();
let counts = Block::new("count", &header, rows.iter().map(|row| &row.counts));
let tiles = Block::new("tile", &header, rows.iter().map(|row| &row.tile));
let coverage_wide = rows
.iter()
.map(|row| row.coverage.chars().count())
.max()
.unwrap_or(0)
+ GAP.chars().count();
let indent = format!("{MARGIN} {LEVEL}{:coverage_wide$}", "");
writeln!(
f,
"{indent}{}{}{GAP}{}{}",
counts.indent(),
counts.line(&header),
tiles.indent(),
tiles.line(&header)
)?;
writeln!(f)?;
for row in &rows {
writeln!(
f,
"{MARGIN}{}{LEVEL}{:coverage_wide$}{}{}{GAP}{}{}",
row.glyph,
row.coverage,
counts.indent(),
counts.line(&row.counts),
tiles.indent(),
tiles.line(&row.tile)
)?;
}
writeln!(f)?;
write!(f, "{indent}{}{GAP}{}", counts.rule(), tiles.rule())
}
}
impl Display for Partitioning {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.table(&[]).fmt(f)
}
}
struct Row {
glyph: char,
coverage: String,
counts: Vec<String>,
tile: Vec<String>,
}
impl Row {
fn of(level: &Level, space: &Space, axes: &[Axis]) -> Row {
Row {
glyph: Row::glyph(level.coverage()),
coverage: Row::coverage(level, space),
counts: axes
.iter()
.map(|&axis| Row::count(level, space, axis))
.collect(),
tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
}
}
fn leaf(space: &Space, axes: &[Axis]) -> Row {
Row {
glyph: LEAF,
coverage: String::new(),
counts: axes.iter().map(|_| "·".to_string()).collect(),
tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
}
}
fn glyph(coverage: Coverage) -> char {
match coverage {
Coverage::Distribute(ComputeScope::Cube) => '▣',
Coverage::Distribute(ComputeScope::Plane) => '▤',
Coverage::Distribute(ComputeScope::Unit) => '▪',
Coverage::Walk => '↻',
}
}
fn coverage(level: &Level, space: &Space) -> String {
let counts: Vec<Option<usize>> = level
.axes()
.iter()
.map(|&axis| level.tiles_const(space, axis))
.collect();
let total = counts
.iter()
.try_fold(1usize, |acc, count| count.map(|n| acc * n));
let many = match total {
Some(n) => n.to_string(),
None => "?".to_string(),
};
let interleaved = level
.axes()
.iter()
.any(|&axis| level.spread(axis) == Some(Spread::Interleaved));
let mut note = match level.coverage() {
Coverage::Distribute(ComputeScope::Cube) => match level.shared_by() {
Some(cubes) => format!("{cubes} cubes sharing {many} boxes"),
None => format!("{many} cubes"),
},
Coverage::Distribute(ComputeScope::Plane) => match level.shared_by() {
Some(planes) => format!("{planes} planes sharing {many} boxes"),
None => format!("{many} planes a cube"),
},
Coverage::Distribute(ComputeScope::Unit) => format!("{many} units"),
Coverage::Walk => format!("{many} steps"),
};
if interleaved {
note += " interleaved";
}
match level.fillers() {
0 => {}
1 => note += ", 1 filling plane",
n => note += &format!(", {n} filling planes"),
}
note
}
fn count(level: &Level, space: &Space, axis: Axis) -> String {
match level.count(axis) {
None => "·".to_string(),
Some(_) => match level.tiles_const(space, axis) {
Some(tiles) => tiles.to_string(),
None => "?".to_string(),
},
}
}
fn extent(space: &Space, axis: Axis) -> String {
match space.is_dynamic(axis) {
true => "?".to_string(),
false => space.extent(axis).to_string(),
}
}
}
struct Block<'n> {
name: &'n str,
widths: Vec<usize>,
}
impl<'n> Block<'n> {
fn new<'c>(
name: &'n str,
header: &[String],
cells: impl Iterator<Item = &'c Vec<String>>,
) -> Self {
let widths = cells.fold(
header.iter().map(|label| label.chars().count()).collect(),
|widest: Vec<usize>, row| {
widest
.iter()
.zip(row)
.map(|(&widest, cell)| widest.max(cell.chars().count()))
.collect()
},
);
Block { name, widths }
}
fn line(&self, cells: &[String]) -> String {
cells
.iter()
.zip(&self.widths)
.map(|(cell, &width)| format!("{cell:>width$}"))
.collect::<Vec<_>>()
.join(TIMES)
}
fn rule(&self) -> String {
format!(
"└─ {} {}┘",
self.name,
"─".repeat(self.wide() - self.ruled() + 1)
)
}
fn indent(&self) -> String {
" ".repeat(self.wide() - self.spanned())
}
fn wide(&self) -> usize {
self.spanned().max(self.ruled())
}
fn spanned(&self) -> usize {
self.widths.iter().sum::<usize>()
+ TIMES.chars().count() * (self.widths.len().saturating_sub(1))
}
fn ruled(&self) -> usize {
self.name.chars().count() + 6
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Levels, Space};
const B: Axis = Axis(0);
const M: Axis = Axis(1);
const N: Axis = Axis(2);
const K: Axis = Axis(3);
fn staged() -> Partitioning {
Partitioning::new(
Space::new(&[(B, 4), (M, 512), (N, 1024), (K, 4096)]),
Levels::leaf(&[(M, 16), (N, 16), (K, 16)])
.walk(&[(M, 2), (N, 2)])
.walk(&[(K, 2)])
.planes(&[(M, 2), (N, 2)])
.walk_every(&[K])
.cubes(&[M, N])
.batches(&[B])
.build(),
)
}
#[test]
fn a_row_is_the_row_below_it_times_its_count() {
let labels = [(B, "b"), (M, "m"), (N, "n"), (K, "k")];
assert_eq!(
staged().table(&labels).to_string(),
[
" b × m × n × k b × m × n × k",
"",
" ◦ · × · × · × · 1 × 16 × 16 × 16",
" ↻ 4 steps · × 2 × 2 × · 1 × 32 × 32 × 16",
" ↻ 2 steps · × · × · × 2 1 × 32 × 32 × 32",
" ▤ 4 planes a cube · × 2 × 2 × · 1 × 64 × 64 × 32",
" ↻ 128 steps · × · × · × 128 1 × 64 × 64 × 4096",
" ▣ 512 cubes 4 × 8 × 16 × · 4 × 512 × 1024 × 4096",
"",
" └─ count ──────┘ └─ tile ────────────┘",
]
.join("\n")
);
}
#[test]
fn an_unlabelled_axis_prints_its_index() {
let table = staged().to_string();
assert!(table.contains("a0 × a1 × a2 × a3"), "{table}");
}
#[test]
fn a_dynamic_axis_prints_a_question() {
let partitioning = Partitioning::new(
Space::new(&[(M, 512), (K, 4096)]).with_dynamic(&[K]),
Levels::leaf(&[(M, 64), (K, 32)])
.walk_every(&[K])
.cubes(&[M])
.build(),
);
let table = partitioning.table(&[(M, "m"), (K, "k")]).to_string();
assert!(table.lines().any(|line| line.contains("× ?")), "{table}");
}
#[test]
fn a_shared_level_still_prints_as_the_cube_grid() {
let partitioning = Partitioning::new(
Space::new(&[(M, 512), (N, 1024)]),
Levels::leaf(&[(M, 64), (N, 64)])
.cubes(&[M, N])
.shared_by(8)
.build(),
);
assert!(partitioning.to_string().contains('▣'));
}
#[test]
fn an_overhanging_axis_counts_its_partial_tile() {
let partitioning = Partitioning::new(
Space::new(&[(M, 500), (K, 4096)]),
Levels::leaf(&[(M, 128), (K, 32)])
.walk_every(&[K])
.cubes(&[M])
.build(),
);
assert_eq!(
partitioning.table(&[(M, "m"), (K, "k")]).to_string(),
[
" m × k m × k",
"",
" ◦ · × · 128 × 32",
" ↻ 128 steps · × 128 128 × 4096",
" ▣ 4 cubes 4 × · 500 × 4096",
"",
" └─ count ─┘ └─ tile ─┘",
]
.join("\n")
);
}
}