From 6f4c109845558ea4df07db52e2b2dff12d8fc753 Mon Sep 17 00:00:00 2001 From: coastalwhite Date: Wed, 15 May 2024 13:40:16 +0200 Subject: [PATCH] refactor: remove old DSL describe code --- crates/polars-lazy/src/frame/mod.rs | 3 +- .../src/physical_plan/expressions/apply.rs | 2 +- .../src/physical_plan/expressions/mod.rs | 4 +- .../src/physical_plan/streaming/tree.rs | 11 +- crates/polars-plan/src/dot.rs | 477 ---------- crates/polars-plan/src/dsl/expr.rs | 12 +- crates/polars-plan/src/dsl/meta.rs | 8 +- crates/polars-plan/src/lib.rs | 1 - .../polars-plan/src/logical_plan/alp/dot.rs | 16 +- .../src/logical_plan/alp/format.rs | 82 +- .../polars-plan/src/logical_plan/alp/mod.rs | 4 +- .../src/logical_plan/alp/tree_format.rs | 11 +- .../src/logical_plan/conversion/dsl_to_ir.rs | 2 +- crates/polars-plan/src/logical_plan/debug.rs | 2 +- .../src/logical_plan/functions/mod.rs | 5 +- crates/polars-plan/src/logical_plan/mod.rs | 30 +- .../src/logical_plan/tree_format.rs | 814 ------------------ 17 files changed, 117 insertions(+), 1367 deletions(-) delete mode 100644 crates/polars-plan/src/dot.rs delete mode 100644 crates/polars-plan/src/logical_plan/tree_format.rs diff --git a/crates/polars-lazy/src/frame/mod.rs b/crates/polars-lazy/src/frame/mod.rs index 18e549a5d682..4f09aa0ac48c 100644 --- a/crates/polars-lazy/src/frame/mod.rs +++ b/crates/polars-lazy/src/frame/mod.rs @@ -520,8 +520,7 @@ impl LazyFrame { } pub fn to_alp(self) -> PolarsResult { - let (node, lp_arena, expr_arena) = self.logical_plan.to_alp()?; - Ok(IRPlan::new(node, lp_arena, expr_arena)) + self.logical_plan.to_alp() } pub(crate) fn optimize_with_scratch( diff --git a/crates/polars-lazy/src/physical_plan/expressions/apply.rs b/crates/polars-lazy/src/physical_plan/expressions/apply.rs index 0b75510b6ac6..b510f8471370 100644 --- a/crates/polars-lazy/src/physical_plan/expressions/apply.rs +++ b/crates/polars-lazy/src/physical_plan/expressions/apply.rs @@ -38,7 +38,7 @@ impl ApplyExpr { ) -> Self { #[cfg(debug_assertions)] if matches!(options.collect_groups, ApplyOptions::ElementWise) && options.returns_scalar { - panic!("expr {} is not implemented correctly. 'returns_scalar' and 'elementwise' are mutually exclusive", expr) + panic!("expr {:?} is not implemented correctly. 'returns_scalar' and 'elementwise' are mutually exclusive", expr) } Self { diff --git a/crates/polars-lazy/src/physical_plan/expressions/mod.rs b/crates/polars-lazy/src/physical_plan/expressions/mod.rs index 6d496e82b716..1158268c7da6 100644 --- a/crates/polars-lazy/src/physical_plan/expressions/mod.rs +++ b/crates/polars-lazy/src/physical_plan/expressions/mod.rs @@ -341,7 +341,7 @@ impl<'a> AggregationContext<'a> { (true, &DataType::List(_)) => { if series.len() != self.groups.len() { let fmt_expr = if let Some(e) = expr { - format!("'{e}' ") + format!("'{e:?}' ") } else { String::new() }; @@ -589,7 +589,7 @@ impl Display for &dyn PhysicalExpr { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.as_expression() { None => Ok(()), - Some(e) => write!(f, "{e}"), + Some(e) => write!(f, "{e:?}"), } } } diff --git a/crates/polars-lazy/src/physical_plan/streaming/tree.rs b/crates/polars-lazy/src/physical_plan/streaming/tree.rs index e10643d20d70..db25b429bfe3 100644 --- a/crates/polars-lazy/src/physical_plan/streaming/tree.rs +++ b/crates/polars-lazy/src/physical_plan/streaming/tree.rs @@ -177,8 +177,15 @@ pub(super) fn dbg_tree(tree: Tree, lp_arena: &Arena, expr_arena: &Arena PolarsResult { - let mut s = String::with_capacity(512); - self.dot_viz(&mut s, (0, 0), "").expect("io error"); - s.push_str("\n}"); - Ok(s) - } - - fn write_dot( - &self, - acc_str: &mut String, - prev_node: &str, - current_node: &str, - id: usize, - ) -> std::fmt::Result { - if id == 0 { - writeln!(acc_str, "graph expr {{") - } else { - writeln!( - acc_str, - "\"{}\" -- \"{}\"", - prev_node.replace('"', r#"\""#), - current_node.replace('"', r#"\""#) - ) - } - } - - fn dot_viz( - &self, - acc_str: &mut String, - id: (usize, usize), - prev_node: &str, - ) -> std::fmt::Result { - let (mut branch, id) = id; - - match self { - Expr::BinaryExpr { left, op, right } => { - let current_node = format!( - r#"BINARY - left _; - op {op:?}; - right: _ [{branch},{id}]"#, - ); - - self.write_dot(acc_str, prev_node, ¤t_node, id)?; - for input in [left, right] { - input.dot_viz(acc_str, (branch, id + 1), ¤t_node)?; - branch += 1; - } - Ok(()) - }, - _ => self.write_dot(acc_str, prev_node, &format!("{branch}{id}"), id), - } - } -} - -#[derive(Copy, Clone)] -pub struct DotNode<'a> { - pub branch: usize, - pub id: usize, - pub fmt: &'a str, -} - -impl DslPlan { - fn write_single_node(&self, acc_str: &mut String, node: DotNode) -> std::fmt::Result { - let fmt_node = node.fmt.replace('"', r#"\""#); - writeln!(acc_str, "graph polars_query {{\n\"[{fmt_node}]\"")?; - Ok(()) - } - - fn write_dot( - &self, - acc_str: &mut String, - prev_node: DotNode, - current_node: DotNode, - id_map: &mut PlHashMap, - ) -> std::fmt::Result { - if current_node.id == 0 && current_node.branch == 0 { - writeln!(acc_str, "graph polars_query {{") - } else { - let fmt_prev_node = prev_node.fmt.replace('"', r#"\""#); - let fmt_current_node = current_node.fmt.replace('"', r#"\""#); - - let id_prev_node = format!( - "\"{} [{:?}]\"", - &fmt_prev_node, - (prev_node.branch, prev_node.id) - ); - let id_current_node = format!( - "\"{} [{:?}]\"", - &fmt_current_node, - (current_node.branch, current_node.id) - ); - - writeln!(acc_str, "{} -- {}", &id_prev_node, &id_current_node)?; - - id_map.insert(id_current_node, fmt_current_node); - id_map.insert(id_prev_node, fmt_prev_node); - - Ok(()) - } - } - - fn is_single(&self, branch: usize, id: usize) -> bool { - id == 0 && branch == 0 - } - - /// - /// # Arguments - /// `id` - (branch, id) - /// Used to make sure that the dot boxes are distinct. - /// branch is an id per join/union branch - /// id is incremented by the depth traversal of the tree. - pub fn dot( - &self, - acc_str: &mut String, - id: (usize, usize), - prev_node: DotNode, - id_map: &mut PlHashMap, - ) -> std::fmt::Result { - use DslPlan::*; - let (mut branch, id) = id; - - match self { - Union { inputs, .. } => { - let current_node = DotNode { - branch, - id, - fmt: "UNION", - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - for input in inputs { - input.dot(acc_str, (branch, id + 1), current_node, id_map)?; - branch += 1; - } - Ok(()) - }, - HConcat { inputs, .. } => { - let current_node = DotNode { - branch, - id, - fmt: "HCONCAT", - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - for input in inputs { - input.dot(acc_str, (branch, id + 1), current_node, id_map)?; - branch += 1; - } - Ok(()) - }, - Cache { - input, - id: cache_id, - cache_hits, - } => { - // Always increment cache ids as the `DotNode[0, 0]` will insert a new graph, which we don't want. - let cache_id = cache_id.saturating_add(1); - let fmt = if *cache_hits == UNLIMITED_CACHE { - Cow::Borrowed("CACHE") - } else { - Cow::Owned(format!("CACHE: {} times", *cache_hits)) - }; - let current_node = DotNode { - branch: cache_id, - id: cache_id, - fmt: &fmt, - }; - // here we take the cache id, to ensure the same cached subplans get the same ids - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (cache_id, cache_id + 1), current_node, id_map) - }, - Filter { predicate, input } => { - let pred = fmt_predicate(Some(predicate)); - let fmt = format!("FILTER BY {pred}"); - - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - #[cfg(feature = "python")] - PythonScan { options } => self.write_scan( - acc_str, - prev_node, - "PYTHON", - &[], - options.with_columns.as_ref().map(|s| s.as_slice()), - Some(options.schema.len()), - &options.predicate, - branch, - id, - id_map, - ), - Select { expr, input, .. } => { - let schema = input.compute_schema().map_err(|_| { - eprintln!("could not determine schema"); - std::fmt::Error - })?; - - let fmt = format!("π {}/{}", expr.len(), schema.len()); - - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - Sort { - input, by_column, .. - } => { - let fmt = format!("SORT BY {by_column:?}"); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - GroupBy { - input, keys, aggs, .. - } => { - let mut s_keys = String::with_capacity(128); - s_keys.push('['); - for key in keys.iter() { - write!(s_keys, "{key:?},")? - } - s_keys.pop(); - s_keys.push(']'); - let fmt = format!("AGG {:?}\nBY\n{} [{:?}]", aggs, s_keys, (branch, id)); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - HStack { input, exprs, .. } => { - let mut fmt = String::with_capacity(128); - fmt.push_str("WITH COLUMNS ["); - for e in exprs { - if let Expr::Alias(_, name) = e { - write!(fmt, "\"{name}\",")? - } else { - for name in expr_to_leaf_column_names(e).iter().take(1) { - write!(fmt, "\"{name}\",")? - } - } - } - fmt.pop(); - fmt.push(']'); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - Slice { input, offset, len } => { - let fmt = format!("SLICE offset: {offset}; len: {len}"); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - Distinct { input, options, .. } => { - let mut fmt = String::with_capacity(128); - fmt.push_str("DISTINCT"); - if let Some(subset) = &options.subset { - fmt.push_str(" BY "); - for name in subset.iter() { - write!(fmt, "{name}")? - } - } - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - DataFrameScan { - schema, - projection, - selection, - .. - } => { - let total_columns = schema.len(); - let mut n_columns = "*".to_string(); - if let Some(columns) = projection { - n_columns = format!("{}", columns.len()); - } - - let pred = fmt_predicate(selection.as_ref()); - let fmt = format!("TABLE\nπ {n_columns}/{total_columns};\nσ {pred}"); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - if self.is_single(branch, id) { - self.write_single_node(acc_str, current_node) - } else { - self.write_dot(acc_str, prev_node, current_node, id_map) - } - }, - Scan { - paths, - file_info, - predicate, - scan_type, - file_options: options, - } => { - let name: &str = scan_type.into(); - - self.write_scan( - acc_str, - prev_node, - name, - paths.as_ref(), - options.with_columns.as_ref().map(|cols| cols.as_slice()), - file_info.as_ref().map(|fi| fi.schema.len()), - predicate, - branch, - id, - id_map, - ) - }, - Join { - input_left, - input_right, - left_on, - right_on, - options, - .. - } => { - let fmt = format!( - r#"JOIN {} - left: {:?}; - right: {:?}"#, - options.args.how, left_on, right_on - ); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input_left.dot(acc_str, (branch + 100, id + 1), current_node, id_map)?; - input_right.dot(acc_str, (branch + 200, id + 1), current_node, id_map) - }, - MapFunction { - input, function, .. - } => { - let fmt = format!("{function}"); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - ExtContext { input, .. } => { - let current_node = DotNode { - branch, - id, - fmt: "EXTERNAL_CONTEXT", - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - Sink { input, payload, .. } => { - let current_node = DotNode { - branch, - id, - fmt: match payload { - SinkType::Memory => "SINK (MEMORY)", - SinkType::File { .. } => "SINK (FILE)", - #[cfg(feature = "cloud")] - SinkType::Cloud { .. } => "SINK (CLOUD)", - }, - }; - self.write_dot(acc_str, prev_node, current_node, id_map)?; - input.dot(acc_str, (branch, id + 1), current_node, id_map) - }, - } - } - - #[allow(clippy::too_many_arguments)] - fn write_scan( - &self, - acc_str: &mut String, - prev_node: DotNode, - name: &str, - path: &[PathBuf], - with_columns: Option<&[String]>, - total_columns: Option, - predicate: &Option

, - branch: usize, - id: usize, - id_map: &mut PlHashMap, - ) -> std::fmt::Result { - let mut n_columns_fmt = "*".to_string(); - if let Some(columns) = with_columns { - n_columns_fmt = format!("{}", columns.len()); - } - - let path_fmt = match path.len() { - 1 => path[0].to_string_lossy(), - 0 => "".into(), - _ => Cow::Owned(format!( - "{} files: first file: {}", - path.len(), - path[0].to_string_lossy() - )), - }; - - let pred = fmt_predicate(predicate.as_ref()); - let total_columns = total_columns - .map(|v| format!("{v}")) - .unwrap_or_else(|| "?".to_string()); - let fmt = format!( - "{name} SCAN {};\nπ {}/{};\nσ {}", - path_fmt, n_columns_fmt, total_columns, pred, - ); - let current_node = DotNode { - branch, - id, - fmt: &fmt, - }; - if self.is_single(branch, id) { - self.write_single_node(acc_str, current_node) - } else { - self.write_dot(acc_str, prev_node, current_node, id_map) - } - } -} - -fn fmt_predicate(predicate: Option<&P>) -> String { - if let Some(predicate) = predicate { - let n = 25; - let mut pred_fmt = format!("{predicate}"); - pred_fmt = pred_fmt.replace('[', ""); - pred_fmt = pred_fmt.replace(']', ""); - if pred_fmt.len() > n { - pred_fmt.truncate(n); - pred_fmt.push_str("...") - } - pred_fmt - } else { - "-".to_string() - } -} diff --git a/crates/polars-plan/src/dsl/expr.rs b/crates/polars-plan/src/dsl/expr.rs index a4a81d56582a..b78c0dc45c9b 100644 --- a/crates/polars-plan/src/dsl/expr.rs +++ b/crates/polars-plan/src/dsl/expr.rs @@ -1,4 +1,4 @@ -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; use polars_core::prelude::*; @@ -315,6 +315,16 @@ impl Expr { } } +impl fmt::Debug for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // @NOTE: It is maybe not that nice to convert to IR in the Debug impl, but since this + // debug impl is used a lot. It it nice to have well formatted information. + let mut expr_arena = Arena::with_capacity(16); + let ir = to_expr_ir(self.clone(), &mut expr_arena); + ExprIRDisplay::new(&ir, &expr_arena).fmt(f) + } +} + #[derive(Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Operator { diff --git a/crates/polars-plan/src/dsl/meta.rs b/crates/polars-plan/src/dsl/meta.rs index 5cab9a3f36df..940a0689c6f6 100644 --- a/crates/polars-plan/src/dsl/meta.rs +++ b/crates/polars-plan/src/dsl/meta.rs @@ -2,8 +2,8 @@ use std::fmt::Display; use std::ops::BitAnd; use super::*; +use crate::logical_plan::alp::tree_format::TreeFmtVisitor; use crate::logical_plan::expr_expansion::is_regex_projection; -use crate::logical_plan::tree_format::TreeFmtVisitor; use crate::logical_plan::visitor::{AexprNode, TreeWalker}; /// Specialized expressions for Categorical dtypes. @@ -85,7 +85,7 @@ impl MetaNameSpace { } Ok(Expr::Selector(s)) } else { - polars_bail!(ComputeError: "expected selector, got {}", self.0) + polars_bail!(ComputeError: "expected selector, got {:?}", self.0) } } @@ -98,7 +98,7 @@ impl MetaNameSpace { } Ok(Expr::Selector(s)) } else { - polars_bail!(ComputeError: "expected selector, got {}", self.0) + polars_bail!(ComputeError: "expected selector, got {:?}", self.0) } } @@ -111,7 +111,7 @@ impl MetaNameSpace { } Ok(Expr::Selector(s)) } else { - polars_bail!(ComputeError: "expected selector, got {}", self.0) + polars_bail!(ComputeError: "expected selector, got {:?}", self.0) } } diff --git a/crates/polars-plan/src/lib.rs b/crates/polars-plan/src/lib.rs index 071cca71e247..5cad9759d823 100644 --- a/crates/polars-plan/src/lib.rs +++ b/crates/polars-plan/src/lib.rs @@ -5,7 +5,6 @@ extern crate core; pub mod constants; -pub mod dot; pub mod dsl; pub mod frame; pub mod global; diff --git a/crates/polars-plan/src/logical_plan/alp/dot.rs b/crates/polars-plan/src/logical_plan/alp/dot.rs index 393a1c4e6242..62b162349c1a 100644 --- a/crates/polars-plan/src/logical_plan/alp/dot.rs +++ b/crates/polars-plan/src/logical_plan/alp/dot.rs @@ -138,20 +138,20 @@ impl<'a> IRDotDisplay<'a> { Sort { input, by_column, .. } => { - let by_column = self.display_exprs(&by_column); + let by_column = self.display_exprs(by_column); self.with_root(*input)._format(f, Some(id), last)?; write_label(f, id, |f| write!(f, "SORT BY {by_column}"))?; }, GroupBy { input, keys, aggs, .. } => { - let keys = self.display_exprs(&keys); - let aggs = self.display_exprs(&aggs); + let keys = self.display_exprs(keys); + let aggs = self.display_exprs(aggs); self.with_root(*input)._format(f, Some(id), last)?; write_label(f, id, |f| write!(f, "AGG {aggs}\nBY\n{keys}"))?; }, HStack { input, exprs, .. } => { - let exprs = self.display_exprs(&exprs); + let exprs = self.display_exprs(exprs); self.with_root(*input)._format(f, Some(id), last)?; write_label(f, id, |f| write!(f, "WITH COLUMNS {exprs}"))?; }, @@ -231,8 +231,8 @@ impl<'a> IRDotDisplay<'a> { self.with_root(*input_left)._format(f, Some(id), last)?; self.with_root(*input_right)._format(f, Some(id), last)?; - let left_on = self.display_exprs(&left_on); - let right_on = self.display_exprs(&right_on); + let left_on = self.display_exprs(left_on); + let right_on = self.display_exprs(right_on); write_label(f, id, |f| { write!( @@ -270,7 +270,9 @@ impl<'a> IRDotDisplay<'a> { let columns = ColumnsDisplay(columns.as_ref()); self.with_root(*input)._format(f, Some(id), last)?; - write_label(f, id, |f| write!(f, "simple π {num_columns}/{total_columns}\n[{columns}]"))?; + write_label(f, id, |f| { + write!(f, "simple π {num_columns}/{total_columns}\n[{columns}]") + })?; }, Invalid => write_label(f, id, |f| f.write_str("INVALID"))?, } diff --git a/crates/polars-plan/src/logical_plan/alp/format.rs b/crates/polars-plan/src/logical_plan/alp/format.rs index b43e0ea44faf..945c96fe18a4 100644 --- a/crates/polars-plan/src/logical_plan/alp/format.rs +++ b/crates/polars-plan/src/logical_plan/alp/format.rs @@ -3,6 +3,7 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::path::PathBuf; +use polars_core::datatypes::AnyValue; use polars_core::schema::Schema; use recursive::recursive; @@ -310,7 +311,11 @@ impl<'a> IRDisplay<'a> { let total_columns = self.0.lp_arena.get(*input).schema(self.0.lp_arena).len(); let columns = ColumnsDisplay(columns.as_ref()); - write!(f, "{:indent$}simple π {num_columns}/{total_columns} [{columns}]", "")?; + write!( + f, + "{:indent$}simple π {num_columns}/{total_columns} [{columns}]", + "" + )?; self.with_root(*input)._format(f, sub_indent) }, @@ -345,6 +350,14 @@ impl<'a> IRDisplay<'a> { } impl<'a> ExprIRDisplay<'a> { + pub(crate) fn new(expr_ir: &'a ExprIR, expr_arena: &'a Arena) -> Self { + Self { + node: expr_ir.node(), + output_name: expr_ir.output_name_inner(), + expr_arena, + } + } + fn with_slice(&self, exprs: &'a [T]) -> ExprIRSliceDisplay<'a, T> { ExprIRSliceDisplay { exprs, @@ -608,7 +621,7 @@ impl fmt::Display for ColumnsDisplay<'_> { if let Some(fst) = iter_names.next() { write!(f, "\"{fst}\"")?; - + if len > 0 { write!(f, ", ... {len} other columns")?; } @@ -618,37 +631,36 @@ impl fmt::Display for ColumnsDisplay<'_> { } } +impl fmt::Debug for Operator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} -// impl fmt::Debug for Operator { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// Display::fmt(self, f) -// } -// } -// -// impl fmt::Debug for LiteralValue { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// use LiteralValue::*; -// -// match self { -// Binary(_) => write!(f, "[binary value]"), -// Range { low, high, .. } => write!(f, "range({low}, {high})"), -// Series(s) => { -// let name = s.name(); -// if name.is_empty() { -// write!(f, "Series") -// } else { -// write!(f, "Series[{name}]") -// } -// }, -// Float(v) => { -// let av = AnyValue::Float64(*v); -// write!(f, "dyn float: {}", av) -// }, -// Int(v) => write!(f, "dyn int: {}", v), -// _ => { -// let av = self.to_any_value().unwrap(); -// write!(f, "{av}") -// }, -// } -// } -// } +impl fmt::Debug for LiteralValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use LiteralValue::*; + + match self { + Binary(_) => write!(f, "[binary value]"), + Range { low, high, .. } => write!(f, "range({low}, {high})"), + Series(s) => { + let name = s.name(); + if name.is_empty() { + write!(f, "Series") + } else { + write!(f, "Series[{name}]") + } + }, + Float(v) => { + let av = AnyValue::Float64(*v); + write!(f, "dyn float: {}", av) + }, + Int(v) => write!(f, "dyn int: {}", v), + _ => { + let av = self.to_any_value().unwrap(); + write!(f, "{av}") + }, + } + } +} diff --git a/crates/polars-plan/src/logical_plan/alp/mod.rs b/crates/polars-plan/src/logical_plan/alp/mod.rs index 967de00c8c33..dd55f598a944 100644 --- a/crates/polars-plan/src/logical_plan/alp/mod.rs +++ b/crates/polars-plan/src/logical_plan/alp/mod.rs @@ -2,7 +2,7 @@ mod dot; mod format; mod inputs; mod schema; -mod tree_format; +pub(crate) mod tree_format; use std::borrow::Cow; use std::fmt; @@ -158,7 +158,7 @@ impl IRPlan { self.lp_arena.get(self.lp_top) } - fn as_ref(&self) -> IRPlanRef { + pub fn as_ref(&self) -> IRPlanRef { IRPlanRef { lp_top: self.lp_top, lp_arena: &self.lp_arena, diff --git a/crates/polars-plan/src/logical_plan/alp/tree_format.rs b/crates/polars-plan/src/logical_plan/alp/tree_format.rs index 7c6c410af602..a271e6c3b4d9 100644 --- a/crates/polars-plan/src/logical_plan/alp/tree_format.rs +++ b/crates/polars-plan/src/logical_plan/alp/tree_format.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::fmt; use polars_core::error::*; @@ -34,7 +33,7 @@ fn with_header(header: &Option, text: &str) -> String { } #[cfg(feature = "regex")] -fn multiline_expression(expr: &str) -> Cow<'_, str> { +fn multiline_expression(expr: &str) -> std::borrow::Cow<'_, str> { let re = Regex::new(r"([\)\]])(\.[a-z0-9]+\()").unwrap(); re.replace_all(expr, "$1\n $2") } @@ -313,8 +312,12 @@ impl Visitor for TreeFmtVisitor { node: &Self::Node, arena: &Self::Arena, ) -> PolarsResult { - let ae = node.to_aexpr(arena); - let repr = format!("{:E}", ae); + let expr = ExprIRDisplay { + node: node.node(), + output_name: &OutputName::None, + expr_arena: arena, + }; + let repr = expr.to_string(); if self.levels.len() <= self.depth { self.levels.push(vec![]) diff --git a/crates/polars-plan/src/logical_plan/conversion/dsl_to_ir.rs b/crates/polars-plan/src/logical_plan/conversion/dsl_to_ir.rs index 2bb58dc83127..b5ba674f45d7 100644 --- a/crates/polars-plan/src/logical_plan/conversion/dsl_to_ir.rs +++ b/crates/polars-plan/src/logical_plan/conversion/dsl_to_ir.rs @@ -579,7 +579,7 @@ fn expand_filter(predicate: Expr, input: Node, lp_arena: &Arena) -> PolarsRe _ => { let mut expanded = String::new(); for e in rewritten.iter().take(5) { - expanded.push_str(&format!("\t{e},\n")) + expanded.push_str(&format!("\t{e:?},\n")) } // pop latest comma expanded.pop(); diff --git a/crates/polars-plan/src/logical_plan/debug.rs b/crates/polars-plan/src/logical_plan/debug.rs index fac0e7c75600..c4f4690b86b4 100644 --- a/crates/polars-plan/src/logical_plan/debug.rs +++ b/crates/polars-plan/src/logical_plan/debug.rs @@ -7,7 +7,7 @@ pub fn dbg_nodes(nodes: &[Node], arena: &Arena) { println!("["); for node in nodes { let e = node_to_expr(*node, arena); - println!("{e}") + println!("{e:?}") } println!("]"); } diff --git a/crates/polars-plan/src/logical_plan/functions/mod.rs b/crates/polars-plan/src/logical_plan/functions/mod.rs index 7de90431ab17..07ec434ce999 100644 --- a/crates/polars-plan/src/logical_plan/functions/mod.rs +++ b/crates/polars-plan/src/logical_plan/functions/mod.rs @@ -327,8 +327,11 @@ impl Display for FunctionNode { MergeSorted { .. } => write!(f, "MERGE SORTED"), Pipeline { original, .. } => { if let Some(original) = original { + let ir_plan = original.as_ref().clone().to_alp().unwrap(); + let ir_display = ir_plan.display(); + writeln!(f, "--- STREAMING")?; - write!(f, "{:?}", original.as_ref())?; + write!(f, "{ir_display}")?; let indent = 2; writeln!(f, "{:indent$}--- END STREAMING", "") } else { diff --git a/crates/polars-plan/src/logical_plan/mod.rs b/crates/polars-plan/src/logical_plan/mod.rs index e403c6dc84ab..c4059749d112 100644 --- a/crates/polars-plan/src/logical_plan/mod.rs +++ b/crates/polars-plan/src/logical_plan/mod.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::fmt::Debug; use std::path::PathBuf; use std::sync::Arc; @@ -22,7 +23,6 @@ pub(crate) mod debug; pub(crate) mod expr_expansion; pub mod expr_ir; mod file_scan; -mod format; mod functions; pub(super) mod hive; pub(crate) mod iterator; @@ -33,7 +33,6 @@ mod projection_expr; #[cfg(feature = "python")] mod pyarrow; mod schema; -pub(crate) mod tree_format; pub mod visitor; pub use aexpr::*; @@ -54,8 +53,6 @@ pub use schema::*; use serde::{Deserialize, Serialize}; use strum_macros::IntoStaticStr; -use self::tree_format::{TreeFmtNode, TreeFmtVisitor}; - pub type ColumnName = Arc; #[derive(Clone, Copy, Debug)] @@ -219,22 +216,31 @@ impl Default for DslPlan { } impl DslPlan { - pub fn describe(&self) -> String { - format!("{self:#?}") + pub fn describe(&self) -> PolarsResult { + Ok(self.clone().to_alp()?.describe()) + } + + pub fn describe_tree_format(&self) -> PolarsResult { + Ok(self.clone().to_alp()?.describe_tree_format()) } - pub fn describe_tree_format(&self) -> String { - let mut visitor = TreeFmtVisitor::default(); - TreeFmtNode::root_logical_plan(self).traverse(&mut visitor); - format!("{visitor:#?}") + pub fn display(&self) -> PolarsResult { + struct DslPlanDisplay(IRPlan); + impl fmt::Display for DslPlanDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.as_ref().display().fmt(f) + } + } + Ok(DslPlanDisplay(self.clone().to_alp()?)) } - pub fn to_alp(self) -> PolarsResult<(Node, Arena, Arena)> { + pub fn to_alp(self) -> PolarsResult { let mut lp_arena = Arena::with_capacity(16); let mut expr_arena = Arena::with_capacity(16); let node = to_alp(self, &mut expr_arena, &mut lp_arena, true, true)?; + let plan = IRPlan::new(node, lp_arena, expr_arena); - Ok((node, lp_arena, expr_arena)) + Ok(plan) } } diff --git a/crates/polars-plan/src/logical_plan/tree_format.rs b/crates/polars-plan/src/logical_plan/tree_format.rs deleted file mode 100644 index 5a227a5660db..000000000000 --- a/crates/polars-plan/src/logical_plan/tree_format.rs +++ /dev/null @@ -1,814 +0,0 @@ -use std::borrow::Cow; -use std::fmt::{Debug, Display, Formatter, UpperExp}; - -use polars_core::error::*; -#[cfg(feature = "regex")] -use regex::Regex; - -use crate::constants::LEN; -use crate::logical_plan::visitor::{VisitRecursion, Visitor}; -use crate::prelude::visitor::AexprNode; -use crate::prelude::*; - -/// Hack UpperExpr trait to get a kind of formatting that doesn't traverse the nodes. -/// So we can format with {foo:E} -impl UpperExp for AExpr { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = match self { - AExpr::Explode(_) => "explode", - AExpr::Alias(_, name) => return write!(f, "alias({})", name.as_ref()), - AExpr::Column(name) => return write!(f, "col({})", name.as_ref()), - AExpr::Literal(lv) => return write!(f, "lit({lv:?})"), - AExpr::BinaryExpr { op, .. } => return write!(f, "binary: {}", op), - AExpr::Cast { - data_type, strict, .. - } => { - return if *strict { - write!(f, "strict cast({})", data_type) - } else { - write!(f, "cast({})", data_type) - } - }, - AExpr::Sort { options, .. } => { - return write!( - f, - "sort: {}{}{}", - options.descending as u8, options.nulls_last as u8, options.multithreaded as u8 - ) - }, - AExpr::Gather { .. } => "gather", - AExpr::SortBy { sort_options, .. } => { - write!(f, "sort_by:")?; - for i in &sort_options.descending { - write!(f, "{}", *i as u8)?; - } - write!( - f, - "{}{}", - sort_options.nulls_last as u8, sort_options.multithreaded as u8 - )?; - return Ok(()); - }, - AExpr::Filter { .. } => "filter", - AExpr::Agg(a) => { - let s: &str = a.into(); - return write!(f, "{}", s.to_lowercase()); - }, - AExpr::Ternary { .. } => "ternary", - AExpr::AnonymousFunction { options, .. } => { - return write!(f, "anonymous_function: {}", options.fmt_str) - }, - AExpr::Function { function, .. } => return write!(f, "function: {function}"), - AExpr::Window { .. } => "window", - AExpr::Wildcard => "*", - AExpr::Slice { .. } => "slice", - AExpr::Len => LEN, - AExpr::Nth(v) => return write!(f, "nth({})", v), - }; - - write!(f, "{s}") - } -} - -pub enum TreeFmtNode<'a> { - Expression(Option, &'a Expr), - LogicalPlan(Option, &'a DslPlan), -} - -struct TreeFmtNodeData<'a>(String, Vec>); - -fn with_header(header: &Option, text: &str) -> String { - if let Some(header) = header { - format!("{header}\n{text}") - } else { - text.to_string() - } -} - -#[cfg(feature = "regex")] -fn multiline_expression(expr: &str) -> Cow<'_, str> { - let re = Regex::new(r"([\)\]])(\.[a-z0-9]+\()").unwrap(); - re.replace_all(expr, "$1\n $2") -} - -impl<'a> TreeFmtNode<'a> { - pub fn root_logical_plan(lp: &'a DslPlan) -> Self { - Self::LogicalPlan(None, lp) - } - - pub fn traverse(&self, visitor: &mut TreeFmtVisitor) { - let TreeFmtNodeData(title, child_nodes) = self.node_data(); - - if visitor.levels.len() <= visitor.depth { - visitor.levels.push(vec![]); - } - - let row = visitor.levels.get_mut(visitor.depth).unwrap(); - row.resize(visitor.width + 1, "".to_string()); - - row[visitor.width] = title; - visitor.prev_depth = visitor.depth; - visitor.depth += 1; - - for child in &child_nodes { - child.traverse(visitor); - } - - visitor.depth -= 1; - visitor.width += if visitor.prev_depth == visitor.depth { - 1 - } else { - 0 - }; - } - - fn node_data(&self) -> TreeFmtNodeData<'_> { - use DslPlan::*; - use TreeFmtNode::{Expression as NE, LogicalPlan as NL}; - use {with_header as wh, TreeFmtNodeData as ND}; - - match self { - #[cfg(feature = "regex")] - NE(h, expr) => ND(wh(h, &multiline_expression(&format!("{expr:?}"))), vec![]), - #[cfg(not(feature = "regex"))] - NE(h, expr) => ND(wh(h, &format!("{expr:?}")), vec![]), - #[cfg(feature = "python")] - NL(h, lp @ PythonScan { .. }) => ND(wh(h, &format!("{lp:?}",)), vec![]), - NL(h, lp @ Scan { .. }) => ND(wh(h, &format!("{lp:?}",)), vec![]), - NL( - h, - DataFrameScan { - schema, - projection, - selection, - .. - }, - ) => ND( - wh( - h, - &format!( - "DF {:?}\nPROJECT {}/{} COLUMNS", - schema.iter_names().take(4).collect::>(), - if let Some(columns) = projection { - format!("{}", columns.len()) - } else { - "*".to_string() - }, - schema.len() - ), - ), - if let Some(expr) = selection { - vec![NE(Some("SELECTION:".to_string()), expr)] - } else { - vec![] - }, - ), - NL(h, Union { inputs, .. }) => ND( - wh( - h, - // THis is commented out, but must be restored when we convert to IR's. - // &(if let Some(slice) = options.slice { - // format!("SLICED UNION: {slice:?}") - // } else { - // "UNION".to_string() - // }), - "UNION", - ), - inputs - .iter() - .enumerate() - .map(|(i, lp)| NL(Some(format!("PLAN {i}:")), lp)) - .collect(), - ), - NL(h, HConcat { inputs, .. }) => ND( - wh(h, "HCONCAT"), - inputs - .iter() - .enumerate() - .map(|(i, lp)| NL(Some(format!("PLAN {i}:")), lp)) - .collect(), - ), - NL( - h, - Cache { - input, - id, - cache_hits, - }, - ) => ND( - wh( - h, - &format!("CACHE[id: {:x}, cache_hits: {}]", *id, *cache_hits), - ), - vec![NL(None, input)], - ), - NL(h, Filter { input, predicate }) => ND( - wh(h, "FILTER"), - vec![ - NE(Some("predicate:".to_string()), predicate), - NL(Some("FROM:".to_string()), input), - ], - ), - NL(h, Select { expr, input, .. }) => ND( - wh(h, "SELECT"), - expr.iter() - .map(|expr| NE(Some("expression:".to_string()), expr)) - .chain([NL(Some("FROM:".to_string()), input)]) - .collect(), - ), - NL( - h, - DslPlan::Sort { - input, by_column, .. - }, - ) => ND( - wh(h, "SORT BY"), - by_column - .iter() - .map(|expr| NE(Some("expression:".to_string()), expr)) - .chain([NL(None, input)]) - .collect(), - ), - NL( - h, - GroupBy { - input, keys, aggs, .. - }, - ) => ND( - wh(h, "AGGREGATE"), - aggs.iter() - .map(|expr| NE(Some("expression:".to_string()), expr)) - .chain( - keys.iter() - .map(|expr| NE(Some("aggregate by:".to_string()), expr)), - ) - .chain([NL(Some("FROM:".to_string()), input)]) - .collect(), - ), - NL( - h, - Join { - input_left, - input_right, - left_on, - right_on, - options, - .. - }, - ) => ND( - wh(h, &format!("{} JOIN", options.args.how)), - left_on - .iter() - .map(|expr| NE(Some("left on:".to_string()), expr)) - .chain([NL(Some("LEFT PLAN:".to_string()), input_left)]) - .chain( - right_on - .iter() - .map(|expr| NE(Some("right on:".to_string()), expr)), - ) - .chain([NL(Some("RIGHT PLAN:".to_string()), input_right)]) - .collect(), - ), - NL(h, HStack { input, exprs, .. }) => ND( - wh(h, "WITH_COLUMNS"), - exprs - .iter() - .map(|expr| NE(Some("expression:".to_string()), expr)) - .chain([NL(None, input)]) - .collect(), - ), - NL(h, Distinct { input, options }) => ND( - wh( - h, - &format!( - "UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}", - options.maintain_order, options.keep_strategy, options.subset - ), - ), - vec![NL(None, input)], - ), - NL(h, DslPlan::Slice { input, offset, len }) => ND( - wh(h, &format!("SLICE[offset: {offset}, len: {len}]")), - vec![NL(None, input)], - ), - NL(h, MapFunction { input, function }) => { - ND(wh(h, &format!("{function}")), vec![NL(None, input)]) - }, - NL(h, ExtContext { input, .. }) => ND(wh(h, "EXTERNAL_CONTEXT"), vec![NL(None, input)]), - NL(h, Sink { input, payload }) => ND( - wh( - h, - match payload { - SinkType::Memory => "SINK (memory)", - SinkType::File { .. } => "SINK (file)", - #[cfg(feature = "cloud")] - SinkType::Cloud { .. } => "SINK (cloud)", - }, - ), - vec![NL(None, input)], - ), - } - } -} - -#[derive(Default)] -pub(crate) struct TreeFmtVisitor { - levels: Vec>, - prev_depth: usize, - depth: usize, - width: usize, -} - -impl Visitor for TreeFmtVisitor { - type Node = AexprNode; - type Arena = Arena; - - /// Invoked before any children of `node` are visited. - fn pre_visit( - &mut self, - node: &Self::Node, - arena: &Self::Arena, - ) -> PolarsResult { - let ae = node.to_aexpr(arena); - let repr = format!("{:E}", ae); - - if self.levels.len() <= self.depth { - self.levels.push(vec![]) - } - - // the post-visit ensures the width of this node is known - let row = self.levels.get_mut(self.depth).unwrap(); - - // set default values to ensure we format at the right width - row.resize(self.width + 1, "".to_string()); - row[self.width] = repr; - - // before entering a depth-first branch we preserve the depth to control the width increase - // in the post-visit - self.prev_depth = self.depth; - - // we will enter depth first, we enter child so depth increases - self.depth += 1; - - Ok(VisitRecursion::Continue) - } - - fn post_visit( - &mut self, - _node: &Self::Node, - _arena: &Self::Arena, - ) -> PolarsResult { - // we finished this branch so we decrease in depth, back the caller node - self.depth -= 1; - - // because we traverse depth first - // the width is increased once after one or more depth-first branches - // this way we avoid empty columns in the resulting tree representation - self.width += if self.prev_depth == self.depth { 1 } else { 0 }; - - Ok(VisitRecursion::Continue) - } -} - -/// Calculates the number of digits in a `usize` number -/// Useful for the alignment of `usize` values when they are displayed -fn digits(n: usize) -> usize { - if n == 0 { - 1 - } else { - f64::log10(n as f64) as usize + 1 - } -} - -/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree -#[derive(Clone, Default, Debug)] -struct TreeViewColumn { - offset: usize, - width: usize, - center: usize, -} - -/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree -#[derive(Clone, Default, Debug)] -struct TreeViewRow { - offset: usize, - height: usize, - center: usize, -} - -/// Meta-info of a cell in a populated `TreeFmtVisitor` -#[derive(Clone, Default, Debug)] -struct TreeViewCell<'a> { - text: Vec<&'a str>, - /// A `Vec` of indices of `TreeViewColumn`-s stored elsewhere in another `Vec` - /// For a cell on a row `i` these indices point to the columns that contain child-cells on a - /// row `i + 1` (if the latter exists) - /// NOTE: might warrant a rethink should this code become used broader - children_columns: Vec, -} - -/// The complete intermediate representation of a `TreeFmtVisitor` that can be drawn on a `Canvas` -/// down the line -#[derive(Default, Debug)] -struct TreeView<'a> { - n_rows: usize, - n_rows_width: usize, - matrix: Vec>>, - /// NOTE: `TreeViewCell`'s `children_columns` field contains indices pointing at the elements - /// of this `Vec` - columns: Vec, - rows: Vec, -} - -// NOTE: the code below this line is full of hardcoded integer offsets which may not be a big -// problem as long as it remains the private implementation of the pretty-print -/// The conversion from a reference to `levels` field of a `TreeFmtVisitor` -impl<'a> From<&'a [Vec]> for TreeView<'a> { - #[allow(clippy::needless_range_loop)] - fn from(value: &'a [Vec]) -> Self { - let n_rows = value.len(); - let n_cols = value.iter().map(|row| row.len()).max().unwrap_or(0); - if n_rows == 0 || n_cols == 0 { - return TreeView::default(); - } - // the character-width of the highest index of a row - let n_rows_width = digits(n_rows - 1); - - let mut matrix = vec![vec![TreeViewCell::default(); n_cols]; n_rows]; - for i in 0..n_rows { - for j in 0..n_cols { - if j < value[i].len() && !value[i][j].is_empty() { - matrix[i][j].text = value[i][j].split('\n').collect(); - if i < n_rows - 1 { - if j < value[i + 1].len() && !value[i + 1][j].is_empty() { - matrix[i][j].children_columns.push(j); - } - for k in j + 1..n_cols { - if (k >= value[i].len() || value[i][k].is_empty()) - && k < value[i + 1].len() - { - if !value[i + 1][k].is_empty() { - matrix[i][j].children_columns.push(k); - } - } else { - break; - } - } - } - } - } - } - - let mut y_offset = 3; - let mut rows = vec![TreeViewRow::default(); n_rows]; - for i in 0..n_rows { - let mut height = 0; - for j in 0..n_cols { - height = [matrix[i][j].text.len(), height].into_iter().max().unwrap(); - } - height += 2; - rows[i].offset = y_offset; - rows[i].height = height; - rows[i].center = height / 2; - y_offset += height + 3; - } - - let mut x_offset = n_rows_width + 4; - let mut columns = vec![TreeViewColumn::default(); n_cols]; - // the two nested loops below are those `needless_range_loop`s - // more readable this way to my taste - for j in 0..n_cols { - let mut width = 0; - for i in 0..n_rows { - width = [ - matrix[i][j].text.iter().map(|l| l.len()).max().unwrap_or(0), - width, - ] - .into_iter() - .max() - .unwrap(); - } - width += 6; - columns[j].offset = x_offset; - columns[j].width = width; - columns[j].center = width / 2 + width % 2; - x_offset += width; - } - - Self { - n_rows, - n_rows_width, - matrix, - columns, - rows, - } - } -} - -/// The basic charset that's used for drawing lines and boxes on a `Canvas` -struct Glyphs { - void: char, - vertical_line: char, - horizontal_line: char, - top_left_corner: char, - top_right_corner: char, - bottom_left_corner: char, - bottom_right_corner: char, - tee_down: char, - tee_up: char, -} - -impl Default for Glyphs { - fn default() -> Self { - Self { - void: ' ', - vertical_line: '│', - horizontal_line: '─', - top_left_corner: '╭', - top_right_corner: '╮', - bottom_left_corner: '╰', - bottom_right_corner: '╯', - tee_down: '┬', - tee_up: '┴', - } - } -} - -/// A `Point` on a `Canvas` -#[derive(Clone, Copy)] -struct Point(usize, usize); - -/// The orientation of a line on a `Canvas` -#[derive(Clone, Copy)] -enum Orientation { - Vertical, - Horizontal, -} - -/// `Canvas` -struct Canvas { - width: usize, - height: usize, - canvas: Vec>, - glyphs: Glyphs, -} - -impl Canvas { - fn new(width: usize, height: usize, glyphs: Glyphs) -> Self { - Self { - width, - height, - canvas: vec![vec![glyphs.void; width]; height], - glyphs, - } - } - - /// Draws a single `symbol` on the `Canvas` - /// NOTE: The `Point`s that lay outside of the `Canvas` are quietly ignored - fn draw_symbol(&mut self, point: Point, symbol: char) { - let Point(x, y) = point; - if x < self.width && y < self.height { - self.canvas[y][x] = symbol; - } - } - - /// Draws a line of `length` from an `origin` along the `orientation` - fn draw_line(&mut self, origin: Point, orientation: Orientation, length: usize) { - let Point(x, y) = origin; - if let Orientation::Vertical = orientation { - let mut down = 0; - while down < length { - self.draw_symbol(Point(x, y + down), self.glyphs.vertical_line); - down += 1; - } - } else if let Orientation::Horizontal = orientation { - let mut right = 0; - while right < length { - self.draw_symbol(Point(x + right, y), self.glyphs.horizontal_line); - right += 1; - } - } - } - - /// Draws a box of `width` and `height` with an `origin` being the top left corner - fn draw_box(&mut self, origin: Point, width: usize, height: usize) { - let Point(x, y) = origin; - self.draw_symbol(origin, self.glyphs.top_left_corner); - self.draw_symbol(Point(x + width - 1, y), self.glyphs.top_right_corner); - self.draw_symbol(Point(x, y + height - 1), self.glyphs.bottom_left_corner); - self.draw_symbol( - Point(x + width - 1, y + height - 1), - self.glyphs.bottom_right_corner, - ); - self.draw_line(Point(x + 1, y), Orientation::Horizontal, width - 2); - self.draw_line( - Point(x + 1, y + height - 1), - Orientation::Horizontal, - width - 2, - ); - self.draw_line(Point(x, y + 1), Orientation::Vertical, height - 2); - self.draw_line( - Point(x + width - 1, y + 1), - Orientation::Vertical, - height - 2, - ); - } - - /// Draws a box of height `2 + text.len()` containing a left-aligned text - fn draw_label_centered(&mut self, center: Point, text: &[&str]) { - if !text.is_empty() { - let Point(x, y) = center; - let text_width = text.iter().map(|l| l.len()).max().unwrap(); - let half_width = text_width / 2 + text_width % 2; - let half_height = text.len() / 2; - if x >= half_width + 2 && y > half_height { - self.draw_box( - Point(x - half_width - 2, y - half_height - 1), - text_width + 4, - text.len() + 2, - ); - for (i, line) in text.iter().enumerate() { - for (j, c) in line.chars().enumerate() { - self.draw_symbol(Point(x - half_width + j, y - half_height + i), c); - } - } - } - } - } - - /// Draws branched lines from a `Point` to multiple `Point`s below - /// NOTE: the shape of these connections is very specific for this particular kind of the - /// representation of a tree - fn draw_connections(&mut self, from: Point, to: &[Point], branching_offset: usize) { - let mut start_with_corner = true; - let Point(mut x_from, mut y_from) = from; - for (i, Point(x, y)) in to.iter().enumerate() { - if *x >= x_from && *y >= y_from - 1 { - self.draw_symbol(Point(*x, *y), self.glyphs.tee_up); - if *x == x_from { - // if the first connection goes straight below - self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down); - self.draw_line(Point(x_from, y_from), Orientation::Vertical, *y - y_from); - x_from += 1; - } else { - if start_with_corner { - // if the first or the second connection steers to the right - self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down); - self.draw_line( - Point(x_from, y_from), - Orientation::Vertical, - branching_offset, - ); - y_from += branching_offset; - self.draw_symbol(Point(x_from, y_from), self.glyphs.bottom_left_corner); - start_with_corner = false; - x_from += 1; - } - let length = *x - x_from; - self.draw_line(Point(x_from, y_from), Orientation::Horizontal, length); - x_from += length; - if i == to.len() - 1 { - self.draw_symbol(Point(x_from, y_from), self.glyphs.top_right_corner); - } else { - self.draw_symbol(Point(x_from, y_from), self.glyphs.tee_down); - } - self.draw_line( - Point(x_from, y_from + 1), - Orientation::Vertical, - *y - y_from - 1, - ); - x_from += 1; - } - } - } - } -} - -/// The actual drawing happens in the conversion of the intermediate `TreeView` into `Canvas` -impl From> for Canvas { - fn from(value: TreeView<'_>) -> Self { - let width = value.n_rows_width + 3 + value.columns.iter().map(|c| c.width).sum::(); - let height = - 3 + value.rows.iter().map(|r| r.height).sum::() + 3 * (value.n_rows - 1); - let mut canvas = Canvas::new(width, height, Glyphs::default()); - - // Axles - let (x, y) = (value.n_rows_width + 2, 1); - canvas.draw_symbol(Point(x, y), '┌'); - canvas.draw_line(Point(x + 1, y), Orientation::Horizontal, width - x); - canvas.draw_line(Point(x, y + 1), Orientation::Vertical, height - y); - - // Row and column indices - for (i, row) in value.rows.iter().enumerate() { - // the prefix `Vec` of spaces compensates for the row indices that are shorter than the - // highest index, effectively, row indices are right-aligned - for (j, c) in vec![' '; value.n_rows_width - digits(i)] - .into_iter() - .chain(format!("{i}").chars()) - .enumerate() - { - canvas.draw_symbol(Point(j + 1, row.offset + row.center), c); - } - } - for (j, col) in value.columns.iter().enumerate() { - let j_width = digits(j); - let start = col.offset + col.center - (j_width / 2 + j_width % 2); - for (k, c) in format!("{j}").chars().enumerate() { - canvas.draw_symbol(Point(start + k, 0), c); - } - } - - // Non-empty cells (nodes) and their connections (edges) - for (i, row) in value.matrix.iter().enumerate() { - for (j, cell) in row.iter().enumerate() { - if !cell.text.is_empty() { - canvas.draw_label_centered( - Point( - value.columns[j].offset + value.columns[j].center, - value.rows[i].offset + value.rows[i].center, - ), - &cell.text, - ); - } - } - } - - fn even_odd(a: usize, b: usize) -> usize { - if a % 2 == 0 && b % 2 == 1 { - 1 - } else { - 0 - } - } - - for (i, row) in value.matrix.iter().enumerate() { - for (j, cell) in row.iter().enumerate() { - if !cell.text.is_empty() && i < value.rows.len() - 1 { - let children_points = cell - .children_columns - .iter() - .map(|k| { - let child_total_padding = - value.rows[i + 1].height - value.matrix[i + 1][*k].text.len() - 2; - let even_cell_in_odd_row = even_odd( - value.matrix[i + 1][*k].text.len(), - value.rows[i + 1].height, - ); - Point( - value.columns[*k].offset + value.columns[*k].center - 1, - value.rows[i + 1].offset - + child_total_padding / 2 - + child_total_padding % 2 - - even_cell_in_odd_row, - ) - }) - .collect::>(); - - let parent_total_padding = - value.rows[i].height - value.matrix[i][j].text.len() - 2; - let even_cell_in_odd_row = - even_odd(value.matrix[i][j].text.len(), value.rows[i].height); - - canvas.draw_connections( - Point( - value.columns[j].offset + value.columns[j].center - 1, - value.rows[i].offset + value.rows[i].height - - parent_total_padding / 2 - - even_cell_in_odd_row, - ), - &children_points, - parent_total_padding / 2 + 1 + even_cell_in_odd_row, - ); - } - } - } - - canvas - } -} - -impl Display for Canvas { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for row in &self.canvas { - writeln!(f, "{}", row.iter().collect::().trim_end())?; - } - - Ok(()) - } -} - -impl Display for TreeFmtVisitor { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self, f) - } -} - -impl Debug for TreeFmtVisitor { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let tree_view: TreeView<'_> = self.levels.as_slice().into(); - let canvas: Canvas = tree_view.into(); - write!(f, "{canvas}")?; - - Ok(()) - } -}