aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine/src
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2025-10-18 20:19:32 +0100
committertslil <tslil@posteo.de>2025-10-18 22:01:42 +0100
commit16f0479a4e43a58c04796e9aa4d43d92b2655024 (patch)
treee42df3fd31313dc362a827ff6f3c80c440ddb40b /rprt-engine/src
parent02c7b10c407982ca117098bd64358a8bb910bd3a (diff)
Add display implementations for Composite, and a BQN-like explainer
Diffstat (limited to 'rprt-engine/src')
-rw-r--r--rprt-engine/src/explainer.rs204
-rw-r--r--rprt-engine/src/expression.rs213
-rw-r--r--rprt-engine/src/parser.rs2
3 files changed, 369 insertions, 50 deletions
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<String> = env::args().collect();
+ if args.len() < 2 {
+ eprintln!("Usage: {} <code>", 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<usize>,
+ 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<usize> = 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::<String>());
+ 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::<String>());
+ output.push('\n');
+ }
+
+ output
+}
+
+fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &mut Vec<TreeNode>) -> 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<T: Display>(x: &Option<T>) -> String {
+ if let Some(x) = x {
+ x.to_string()
+ } else {
+ String::new()
+ }
+ }
- Hook {
- kind: HookKind,
- left: Box<TextFn>,
- right: Box<TextFn>,
- },
-
- Train2 {
- f: Box<TextFn>,
- g: Box<TextFn>,
- },
-
- Train3 {
- f: Box<TextFn>,
- g: Box<TextFn>,
- h: Box<TextFn>,
- },
+ 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<G (right hook)
+ Before,
+ After,
+}
+
+impl Display for HookKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> 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 {