From 16f0479a4e43a58c04796e9aa4d43d92b2655024 Mon Sep 17 00:00:00 2001 From: tslil Date: Sat, 18 Oct 2025 20:19:32 +0100 Subject: Add display implementations for Composite, and a BQN-like explainer --- rprt-engine/Cargo.toml | 4 +- rprt-engine/src/explainer.rs | 204 ++++++++++++++++++++++++++++++++++++++++ rprt-engine/src/expression.rs | 213 ++++++++++++++++++++++++++++++++---------- rprt-engine/src/parser.rs | 2 +- 4 files changed, 372 insertions(+), 51 deletions(-) create mode 100644 rprt-engine/src/explainer.rs (limited to 'rprt-engine') diff --git a/rprt-engine/Cargo.toml b/rprt-engine/Cargo.toml index d705b5e..179c095 100644 --- a/rprt-engine/Cargo.toml +++ b/rprt-engine/Cargo.toml @@ -10,4 +10,6 @@ either = "1.15.0" logos = "0.15.1" regex = "1.12.2" - +[[bin]] +name = "explainer" +path = "src/explainer.rs" diff --git a/rprt-engine/src/explainer.rs b/rprt-engine/src/explainer.rs new file mode 100644 index 0000000..9c847f2 --- /dev/null +++ b/rprt-engine/src/explainer.rs @@ -0,0 +1,204 @@ +use rprt_engine::{expression::*, parser::*, token::*}; +use std::env; +use std::collections::BTreeSet; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let input = &args[1]; + + let tokens = match tokenise(input) { + Ok(t) => t, + Err(e) => { + eprintln!("Tokenization error: {}", e); + std::process::exit(1); + } + }; + + let parsed = match parse(tokens) { + Ok(p) => p, + Err(e) => { + eprintln!("Parse error: {:?}", e); + std::process::exit(1); + } + }; + + let formatted = format!("{}", parsed); + let tree = generate_tree(&formatted, &parsed); + print!("{}", tree); +} + +struct TreeNode { + pos: usize, + depth: usize, + children: Vec, + is_leaf: bool, +} + +fn generate_tree(formatted: &str, expr: &Composite) -> String { + let width = formatted.chars().count(); + + let mut nodes = Vec::new(); + build_tree_from_expr(expr, 0, 0, &mut nodes); + + let max_depth = nodes.iter().map(|n| n.depth).max().unwrap_or(0); + + let mut output = String::new(); + output.push_str(formatted); + output.push('\n'); + + let mut active_bars: BTreeSet = nodes + .iter() + .filter(|n| n.is_leaf) + .map(|n| n.pos) + .collect(); + + let mut prev_active = active_bars.clone(); + + for current_depth in (0..=max_depth).rev() { + let interior: Vec<_> = nodes + .iter() + .filter(|n| n.depth == current_depth && !n.is_leaf) + .collect(); + + let mut next_active = active_bars.clone(); + for node in &interior { + for &child in &node.children { + next_active.remove(&child); + } + next_active.insert(node.pos); + } + + let mut horizontal_spans = Vec::new(); + for node in &interior { + if !node.children.is_empty() { + let left = *node.children.iter().min().unwrap(); + let right = *node.children.iter().max().unwrap(); + horizontal_spans.push((left, right)); + } + } + + let mut line = vec![' '; width]; + for i in 0..width { + let up = prev_active.contains(&i); + let down = next_active.contains(&i); + let in_span = horizontal_spans.iter().any(|(l, r)| *l <= i && i <= *r); + let left = i > 0 && horizontal_spans.iter().any(|(l, r)| *l <= i - 1 && i - 1 <= *r); + let right = i + 1 < width && horizontal_spans.iter().any(|(l, r)| *l <= i + 1 && i + 1 <= *r); + + if up || down || in_span { + line[i] = box_char(up, down, left, right); + } + } + + output.push_str(&line.iter().collect::()); + output.push('\n'); + + prev_active = next_active.clone(); + active_bars = next_active; + } + + if !nodes.is_empty() { + let mut final_line = vec![' '; width]; + if let Some(&pos) = prev_active.iter().next() { + if pos < width { + final_line[pos] = '│'; + } + } + output.push_str(&final_line.iter().collect::()); + output.push('\n'); + } + + output +} + +fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &mut Vec) -> usize { + match expr { + Composite::SelectionFunction { .. } | Composite::TextFunction { .. } => { + nodes.push(TreeNode { + pos: offset, + depth, + children: vec![], + is_leaf: true, + }); + offset + } + Composite::Train2 { f, g } => { + let f_offset = offset + 1; + let f_pos = build_tree_from_expr(f, depth + 1, f_offset, nodes); + + let f_str = format!("{}", f); + let g_offset = f_offset + f_str.chars().count() + 1; + let g_pos = build_tree_from_expr(g, depth + 1, g_offset, nodes); + + nodes.push(TreeNode { + pos: f_pos, + depth, + children: vec![f_pos, g_pos], + is_leaf: false, + }); + f_pos + } + Composite::Train3 { f, g, h } => { + let f_offset = offset + 1; + let f_pos = build_tree_from_expr(f, depth + 1, f_offset, nodes); + + let f_str = format!("{}", f); + let g_offset = f_offset + f_str.chars().count() + 1; + let g_pos = build_tree_from_expr(g, depth + 1, g_offset, nodes); + + let g_str = format!("{}", g); + let h_offset = g_offset + g_str.chars().count() + 1; + let h_pos = build_tree_from_expr(h, depth + 1, h_offset, nodes); + + nodes.push(TreeNode { + pos: g_pos, + depth, + children: vec![f_pos, g_pos, h_pos], + is_leaf: false, + }); + g_pos + } + Composite::Hook { kind, left, right } => { + let left_pos = build_tree_from_expr(left, depth + 1, offset, nodes); + + let left_str = format!("{}", left); + let kind_str = format!("{}", kind); + let right_offset = offset + left_str.chars().count() + kind_str.chars().count(); + let right_pos = build_tree_from_expr(right, depth + 1, right_offset, nodes); + + nodes.push(TreeNode { + pos: left_pos, + depth, + children: vec![left_pos, right_pos], + is_leaf: false, + }); + left_pos + } + } +} + +fn box_char(up: bool, down: bool, left: bool, right: bool) -> char { + match (up, down, left, right) { + (false, false, false, false) => ' ', + (true, false, false, false) => '│', + (false, true, false, false) => '│', + (true, true, false, false) => '│', + (false, false, true, false) => '─', + (false, false, false, true) => '─', + (false, false, true, true) => '─', + (true, false, true, false) => '┘', + (true, false, false, true) => '└', + (false, true, true, false) => '┐', + (false, true, false, true) => '┌', + (true, false, true, true) => '┴', + (false, true, true, true) => '┬', + (true, true, true, false) => '┤', + (true, true, false, true) => '├', + (true, true, true, true) => '┼', + } +} diff --git a/rprt-engine/src/expression.rs b/rprt-engine/src/expression.rs index 1ee898e..c463fca 100644 --- a/rprt-engine/src/expression.rs +++ b/rprt-engine/src/expression.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + #[derive(Debug, Clone, PartialEq)] pub enum Composite { SelectionFunction { @@ -29,74 +31,187 @@ pub enum Composite { }, } -#[derive(Debug, Clone, PartialEq)] -pub enum TextFn { - Function { - func: BuiltinTextFn, - swapped: bool, - }, +impl Display for Composite { + fn fmt(&self, fmtr: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::expression::Composite::*; + fn s(x: &Option) -> String { + if let Some(x) = x { + x.to_string() + } else { + String::new() + } + } - Hook { - kind: HookKind, - left: Box, - right: Box, - }, - - Train2 { - f: Box, - g: Box, - }, - - Train3 { - f: Box, - g: Box, - h: Box, - }, + match self { + SelectionFunction { + func, + search_mod, + result_transform, + } => write!(fmtr, "{}{}{func}", s(result_transform), s(search_mod)), + TextFunction { func, swapped } => { + write!(fmtr, "{}{func}", if *swapped { "@" } else { "" }) + } + Hook { kind, left, right } => write!(fmtr, "{left}{kind}{right}",), + Train2 { f, g } => write!(fmtr, "({f} {g})"), + Train3 { f, g, h } => write!(fmtr, "({f} {g} {h})"), + } + } } #[derive(Debug, Clone, PartialEq)] pub enum BuiltinSelectionFn { - Empty, // e - EndOfBuffer, // $ - Span, // - - CharOffset(usize), // # (expects number arg) - Line(usize), // (expects number arg) - Regex(String), // /re/ (expects regex arg) - AllMatches(String), // x (expects /re/ arg) - RelativeLine(usize), // + (expects number arg) - BufferMatch(String), // B (expects /re/ arg) + Empty, + EndOfBuffer, + Span, + CharOffset(usize), + Line(usize), + Regex(String), + AllMatches(String), + RelativeLine(usize), + BufferMatch(String), +} + +impl Display for BuiltinSelectionFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::expression::BuiltinSelectionFn::*; + match self { + Empty => write!(f, "e"), + EndOfBuffer => write!(f, "$"), + Span => write!(f, "-"), + CharOffset(pos) => write!(f, "#{}", pos), + Line(pos) => write!(f, "{}", pos), + Regex(pat) => write!(f, "/{}/", pat), + AllMatches(pat) => write!(f, "x/{}/", pat), + RelativeLine(pos) => write!(f, "+{}", pos), + BufferMatch(pat) => write!(f, "B/{}/", pat), + } + } } #[derive(Debug, Clone, PartialEq)] pub enum BuiltinTextFn { - Dot, // . - Change, // c - Insert, // i - Append, // a - Delete, // d - Write, // w - Pipe, // | - Literal(String), // "..." + Dot, + Change, + Insert, + Append, + Delete, + Write, + Pipe, + Literal(String), +} + +impl Display for BuiltinTextFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::expression::BuiltinTextFn::*; + match self { + Dot => write!(f, "."), + Change => write!(f, "c"), + Insert => write!(f, "i"), + Append => write!(f, "a"), + Delete => write!(f, "d"), + Write => write!(f, "w"), + Pipe => write!(f, "|"), + Literal(s) => write!(f, "\"{}\"", s), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum SearchModifier { - Reverse, // ' - Sequential, // ; - ReverseSequential, // '; - SequentialReverse, // ;' + Reverse, + Sequential, + ReverseSequential, + SequentialReverse, +} + +impl Display for SearchModifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::expression::SearchModifier::*; + match self { + Reverse => write!(f, "'"), + Sequential => write!(f, ";"), + ReverseSequential => write!(f, "';"), + SequentialReverse => write!(f, ";'"), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum ResultTransform { - Complement, // ~ - Conditional, // ? - ComplementConditional, // ~? - ConditionalComplement, // ?~ + Complement, + Conditional, + ComplementConditional, + ConditionalComplement, +} + +impl Display for ResultTransform { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::expression::ResultTransform::*; + match self { + Complement => write!(f, "~"), + Conditional => write!(f, "?"), + ComplementConditional => write!(f, "~?"), + ConditionalComplement => write!(f, "?~"), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum HookKind { - Before, // F>G (left hook) - After, // F) -> std::fmt::Result { + match self { + HookKind::Before => write!(f, ">"), + HookKind::After => write!(f, "<"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::expression::*; + + #[test] + fn format_3train() { + // nonsense, but it exercises the code + let comp = Composite::Train3 { + f: Box::new(Composite::SelectionFunction { + func: BuiltinSelectionFn::AllMatches(String::from("re")), + result_transform: Some(ResultTransform::Complement), + search_mod: Some(SearchModifier::Reverse), + }), + g: Box::new(Composite::Train2 { + f: Box::new(Composite::TextFunction { + func: BuiltinTextFn::Change, + swapped: false, + }), + g: Box::new(Composite::SelectionFunction { + func: BuiltinSelectionFn::Line(5), + result_transform: None, + search_mod: None, + }), + }), + h: Box::new(Composite::Hook { + kind: HookKind::Before, + left: Box::new(Composite::SelectionFunction { + func: BuiltinSelectionFn::Empty, + result_transform: None, + search_mod: None, + }), + right: Box::new(Composite::TextFunction { + func: BuiltinTextFn::Delete, + swapped: false, + }), + }), + }; + + let formatted = format!("{}", comp); + println!("Formatted: {}", formatted); + + assert_eq!(formatted, "(~'x/re/ (c 5) e>d)"); + } } diff --git a/rprt-engine/src/parser.rs b/rprt-engine/src/parser.rs index c98d977..78c28c0 100644 --- a/rprt-engine/src/parser.rs +++ b/rprt-engine/src/parser.rs @@ -4,7 +4,7 @@ use crate::token::*; #[derive(Debug, Clone)] pub enum ParseError { UnexpectedToken { - found: Token, // Own the token, don't borrow it + found: Token, expecting: &'static str, }, UnexpectedEndOfInput { -- cgit v1.2.3