aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine/tests/selection_functions.rs
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2026-08-24 20:39:57 +0100
committertslil <tslil@posteo.de>2026-08-24 21:39:25 +0100
commitf3acb5cc0faf739f09112607bb98b6594184bd8b (patch)
tree9f60aeda21a775f9d34ed8a70fffd9be638f27ce /rprt-engine/tests/selection_functions.rs
parent72a5e0fc968e2bea9f7d348c7189ebc15a5b15d3 (diff)
The big transposition-ing: move from functions as data to data of functionsHEADmain
Basically Rust will fight you all the way if you treat it like Haskell, instead work with traits and impls on ZSTs.
Diffstat (limited to 'rprt-engine/tests/selection_functions.rs')
-rw-r--r--rprt-engine/tests/selection_functions.rs245
1 files changed, 245 insertions, 0 deletions
diff --git a/rprt-engine/tests/selection_functions.rs b/rprt-engine/tests/selection_functions.rs
new file mode 100644
index 0000000..c326375
--- /dev/null
+++ b/rprt-engine/tests/selection_functions.rs
@@ -0,0 +1,245 @@
+use rprt_engine::{
+ evaluate::evaluate,
+ expression::{BuiltinSelectionFn, ResultTransform, SearchModifier},
+ parser::parse,
+ selection::{Interval, Selection},
+ selection_functions::{SelectionFunctionError, evaluate_selection_function},
+ state::EditorState,
+ token::tokenise,
+};
+
+fn state_with_content(content: &str) -> EditorState {
+ let mut es = EditorState::new();
+ es.create_buffer("test".to_string(), content.to_string(), None);
+ es
+}
+
+/// Parse `input` and evaluate it against a fresh state holding `content`.
+fn eval_str(content: &str, input: &str) -> Result<Selection, String> {
+ let mut es = state_with_content(content);
+ let tokens = tokenise(input).map_err(|e| format!("tokenise: {e:?}"))?;
+ let comp = parse(tokens).map_err(|e| format!("parse: {e:?}"))?;
+ evaluate(&mut es, comp).map_err(|e| format!("evaluate: {e:?}"))
+}
+
+fn assert_scalar(res: Selection, pos: usize) {
+ match res {
+ Selection::Scalar { buffer_id, pos: p } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(p, pos, "expected scalar at {pos}");
+ }
+ other => panic!("expected scalar at {pos}, got {other:?}"),
+ }
+}
+
+#[test]
+fn niladic_character() {
+ assert_scalar(eval_str("hello world", "#5").unwrap(), 5);
+ // clamped to buffer end
+ assert_scalar(eval_str("hello world", "#100").unwrap(), 11);
+}
+
+#[test]
+fn niladic_end_unparameterised() {
+ // These go through UnparameterisedSelectionFunction + the
+ // SelectionFunction<()> blanket impl.
+ assert_scalar(eval_str("hello world", "$").unwrap(), 11);
+ assert_scalar(eval_str("hello world", "'$").unwrap(), 0);
+ assert!(eval_str("hello world", ";$").is_err());
+ assert!(eval_str("hello world", "';$").is_err());
+ assert!(eval_str("hello world", ";'$").is_err());
+}
+
+#[test]
+fn monadic_over_scalar() {
+ // Train: f = #5 (niladic), g = $ applied to f's result (rank-0 vectorise).
+ assert_scalar(eval_str("hello world", "#5 $").unwrap(), 5);
+ // "α ;$" = "End of buffer"
+ assert_scalar(eval_str("hello world", "#5 ;$").unwrap(), 11);
+ assert_scalar(eval_str("hello world", "#5 '$").unwrap(), 5);
+}
+
+#[test]
+fn character_within_interval() {
+ // "α #n" rank 1: "Char n within α", offset from the interval's start,
+ // clamped at its end.
+ let es = state_with_content("hello world");
+ let interval = || Selection::Vector {
+ buffer_id: 0,
+ interval: Interval::new(2, 9),
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(3),
+ None,
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 5); // 2 + 3
+
+ // clamped at the interval's end
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(100),
+ None,
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 9);
+}
+
+#[test]
+fn search_modifiers_character() {
+ // ;#1: min(pos + 1, max)
+ assert_scalar(eval_str("hello world", "#2 ;#1").unwrap(), 3);
+ // ';#1: pos - 1
+ assert_scalar(eval_str("hello world", "#2 ';#1").unwrap(), 1);
+}
+
+#[test]
+fn reverse_character() {
+ // '#3 niladic: "Character 3 from end of buffer"
+ assert_scalar(eval_str("hello world", "'#3").unwrap(), 8);
+
+ // "α '#n" rank 1: "Char n from end of α" --- offset back from the
+ // interval's end boundary, clamped at 0.
+ let es = state_with_content("hello world");
+ let interval = || Selection::Vector {
+ buffer_id: 0,
+ interval: Interval::new(2, 9),
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(3),
+ Some(SearchModifier::Reverse),
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 6); // 9 - 3
+
+ // n larger than the interval's length clamps to 0.
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(10),
+ Some(SearchModifier::Reverse),
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 0);
+}
+
+#[test]
+fn monadic_over_vectors() {
+ // #100 applied to a rank-2 selection: min(interval.end, 100) per range.
+ let es = state_with_content("hello world");
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(100),
+ None,
+ None,
+ Some(Selection::Vectors {
+ buffer_id: 0,
+ ranges: vec![Interval::new(2, 4), Interval::new(6, 9)],
+ }),
+ None,
+ )
+ .unwrap();
+ match res {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(ranges.len(), 2);
+ assert_eq!((ranges[0].start, ranges[0].end), (4, 5));
+ assert_eq!((ranges[1].start, ranges[1].end), (9, 10));
+ }
+ other => panic!("expected Vectors, got {other:?}"),
+ }
+}
+
+#[test]
+fn complement_transform() {
+ // ~#5 on "hello world": complement of pos 5 is [0..5) and (6..11].
+ let es = state_with_content("hello world");
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(5),
+ None,
+ Some(ResultTransform::Complement),
+ None,
+ None,
+ )
+ .unwrap();
+ match res {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(ranges.len(), 2);
+ assert_eq!((ranges[0].start, ranges[0].end), (0, 5));
+ assert_eq!((ranges[1].start, ranges[1].end), (6, 11));
+ }
+ other => panic!("expected Vectors, got {other:?}"),
+ }
+}
+
+#[test]
+fn empty_selection() {
+ // `e` and `α e`: empty selection of rank 3.
+ match eval_str("hello world", "e").unwrap() {
+ Selection::MultiVectors { multi_ranges } => assert!(multi_ranges.is_empty()),
+ other => panic!("expected rank-3 empty, got {other:?}"),
+ }
+ match eval_str("hello world", "#5 e").unwrap() {
+ Selection::MultiVectors { multi_ranges } => assert!(multi_ranges.is_empty()),
+ other => panic!("expected rank-3 empty, got {other:?}"),
+ }
+
+ // `'e` and `;e`: empty selection of rank 2, anchored to the buffer.
+ match eval_str("hello world", "#5 'e").unwrap() {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert!(ranges.is_empty());
+ }
+ other => panic!("expected rank-2 empty, got {other:?}"),
+ }
+ match eval_str("hello world", "#5 ;e").unwrap() {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert!(ranges.is_empty());
+ }
+ other => panic!("expected rank-2 empty, got {other:?}"),
+ }
+
+ // `;e` and friends have no niladic form.
+ assert!(eval_str("hello world", ";e").is_err());
+ assert!(eval_str("hello world", ";'e").is_err());
+ assert!(eval_str("hello world", "';e").is_err());
+}
+
+#[test]
+fn dyadic_is_rejected() {
+ let es = state_with_content("hello world");
+ let scalar = || Selection::Scalar {
+ buffer_id: 0,
+ pos: 0,
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::EndOfBuffer,
+ None,
+ None,
+ Some(scalar()),
+ Some(scalar()),
+ );
+ match res {
+ Err(SelectionFunctionError::NoDyadicForm(name)) => {
+ assert_eq!(name, "end");
+ }
+ other => panic!("expected NoDyadicForm, got {other:?}"),
+ }
+}