aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine
diff options
context:
space:
mode:
Diffstat (limited to 'rprt-engine')
-rw-r--r--rprt-engine/Cargo.toml2
-rw-r--r--rprt-engine/src/buffer.rs10
-rw-r--r--rprt-engine/src/expression.rs116
-rw-r--r--rprt-engine/src/lib.rs6
-rw-r--r--rprt-engine/src/parser.rs123
-rw-r--r--rprt-engine/src/token.rs279
6 files changed, 525 insertions, 11 deletions
diff --git a/rprt-engine/Cargo.toml b/rprt-engine/Cargo.toml
index 96aa5b1..d705b5e 100644
--- a/rprt-engine/Cargo.toml
+++ b/rprt-engine/Cargo.toml
@@ -7,5 +7,7 @@ authors.workspace = true
[dependencies]
either = "1.15.0"
+logos = "0.15.1"
regex = "1.12.2"
+
diff --git a/rprt-engine/src/buffer.rs b/rprt-engine/src/buffer.rs
index f536054..f2c6e0f 100644
--- a/rprt-engine/src/buffer.rs
+++ b/rprt-engine/src/buffer.rs
@@ -11,10 +11,7 @@ pub struct Buffer {
impl Buffer {
pub fn new(name: String, content: String) -> Self {
- Self {
- name,
- content,
- }
+ Self { name, content }
}
fn generate_shell_name(command: &str) -> String {
@@ -43,9 +40,6 @@ impl Buffer {
Err(e) => format!("Command failed: {}", e),
};
- Self {
- name,
- content,
- }
+ Self { name, content }
}
}
diff --git a/rprt-engine/src/expression.rs b/rprt-engine/src/expression.rs
new file mode 100644
index 0000000..68849cb
--- /dev/null
+++ b/rprt-engine/src/expression.rs
@@ -0,0 +1,116 @@
+#[derive(Debug, Clone, PartialEq)]
+pub enum SelectionValuedExpr {
+ // Nullary selection function application
+ NullaryApplicationS(SelectionFn),
+ NullaryApplicationT(TextFn),
+
+ // Monadic selection function application: α F
+ MonadicApplicationS {
+ arg: Box<SelectionValuedExpr>,
+ func: SelectionFn,
+ },
+ // Monadic selection function application: F τ
+ MonadicApplicationT {
+ arg: Box<TextValuedExpr>,
+ func: TextFn,
+ },
+
+ // Dyadic selection function application: α F β
+ DyadicApplicationS {
+ left: Box<SelectionValuedExpr>,
+ right: Box<SelectionValuedExpr>,
+ func: SelectionFn,
+ },
+ // Dyadic text function application: α F τ
+ DyadicApplicationT {
+ left: Box<SelectionValuedExpr>,
+ right: Box<SelectionValuedExpr>,
+ func: SelectionFn,
+ },
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum TextValuedExpr {
+ // Selection coerced to text (implicit coercion)
+ FromSelection(Box<SelectionValuedExpr>),
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum SelectionFn {
+ Function {
+ func: BuiltinSelectionFn,
+ search_mod: Option<SearchModifier>,
+ result_transform: Option<ResultTransform>,
+ },
+
+ Hook {
+ kind: HookKind,
+ left: Box<SelectionFn>,
+ right: Box<SelectionFn>,
+ },
+
+ Train(Vec<SelectionFn>),
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum TextFn {
+ Function {
+ func: BuiltinTextFn,
+ swapped: bool,
+ },
+
+ Hook {
+ kind: HookKind,
+ left: Box<TextFn>,
+ right: Box<TextFn>,
+ },
+
+ Train(Vec<TextFn>),
+}
+
+#[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)
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BuiltinTextFn {
+ Dot, // .
+ Change, // c
+ Insert, // i
+ Append, // a
+ Delete, // d
+ Write, // w
+ Pipe, // |
+ Literal(String), // "..."
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum SearchModifier {
+ Reverse, // '
+ Sequential, // ;
+ ReverseSequential, // ';
+ SequentialReverse, // ;'
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ResultTransform {
+ Complement, // ~
+ Conditional, // ?
+ ComplementConditional, // ~?
+ ConditionalComplement, // ?~
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum HookKind {
+ Before, // F>G (left hook)
+ After, // F<G (right hook)
+}
diff --git a/rprt-engine/src/lib.rs b/rprt-engine/src/lib.rs
index 416b65f..4d9e07a 100644
--- a/rprt-engine/src/lib.rs
+++ b/rprt-engine/src/lib.rs
@@ -1,7 +1,7 @@
// RPRT Engine - Core text editing functionality
pub mod buffer;
+pub mod expression;
+pub mod parser;
pub mod selection;
-
-pub use buffer::*;
-pub use selection::*;
+pub mod token;
diff --git a/rprt-engine/src/parser.rs b/rprt-engine/src/parser.rs
new file mode 100644
index 0000000..44feda0
--- /dev/null
+++ b/rprt-engine/src/parser.rs
@@ -0,0 +1,123 @@
+use crate::expression::*;
+use crate::token::*;
+
+#[derive(Debug, Clone)]
+pub enum ParseError {
+ UnexpectedToken {
+ found: Token, // Own the token, don't borrow it
+ expecting: &'static str,
+ },
+ UnexpectedEndOfInput {
+ expecting: &'static str,
+ },
+}
+
+struct ParseContext {
+ tokens: Vec<Token>,
+ pos: usize,
+}
+
+impl ParseContext {
+ fn advance(&mut self) -> Result<(), ParseError> {
+ self.pos += 1;
+ if self.pos >= self.tokens.len() {
+ Err(ParseError::UnexpectedEndOfInput {
+ expecting: "more tokens",
+ })
+ } else {
+ Ok(())
+ }
+ }
+
+ fn current(&self) -> Result<Token, ParseError> {
+ self.tokens
+ .get(self.pos)
+ .cloned()
+ .ok_or(ParseError::UnexpectedEndOfInput { expecting: "token" })
+ }
+}
+
+pub fn parse(tokens: Vec<Token>) -> Result<SelectionValuedExpr, ParseError> {
+ let mut context = ParseContext {
+ tokens: tokens,
+ pos: 0,
+ };
+ let func = parse_function(&mut context)?;
+ Ok(SelectionValuedExpr::NullaryApplicationS(func))
+}
+
+fn parse_function(context: &mut ParseContext) -> Result<SelectionFn, ParseError> {
+ let func = parse_builtin_function_s(context)?;
+ Ok(SelectionFn::Function {
+ func: func,
+ search_mod: None,
+ result_transform: None,
+ })
+}
+
+fn parse_builtin_function_s(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+ let token = context.current()?;
+ match token {
+ Token::Empty => Ok(BuiltinSelectionFn::Empty),
+ Token::EndOfBuffer => Ok(BuiltinSelectionFn::EndOfBuffer),
+ Token::Span => Ok(BuiltinSelectionFn::Span),
+ Token::CharacterOffset => parse_char_offset(context),
+ Token::Number(n) => Ok(BuiltinSelectionFn::Line(n)),
+ Token::Regex(pattern) => Ok(BuiltinSelectionFn::Regex(pattern)),
+ Token::AllMatches => parse_all_matches(context),
+ Token::PositiveLineOffset => parse_relative_line(context),
+ Token::BufferMatch => parse_buffer_match(context),
+ _ => Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "selection function",
+ }),
+ }
+}
+
+fn parse_char_offset(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+ context.advance()?;
+ let token = context.current()?;
+ match token {
+ Token::Number(n) => Ok(BuiltinSelectionFn::CharOffset(n)),
+ _ => Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "number",
+ }),
+ }
+}
+
+fn parse_all_matches(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+ context.advance()?;
+ let token = context.current()?;
+ match token {
+ Token::Regex(pattern) => Ok(BuiltinSelectionFn::AllMatches(pattern)),
+ _ => Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "regex",
+ }),
+ }
+}
+
+fn parse_relative_line(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+ context.advance()?;
+ let token = context.current()?;
+ match token {
+ Token::Number(n) => Ok(BuiltinSelectionFn::RelativeLine(n)),
+ _ => Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "number",
+ }),
+ }
+}
+
+fn parse_buffer_match(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+ context.advance()?;
+ let token = context.current()?;
+ match token {
+ Token::Regex(pattern) => Ok(BuiltinSelectionFn::BufferMatch(pattern)),
+ _ => Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "regex",
+ }),
+ }
+}
diff --git a/rprt-engine/src/token.rs b/rprt-engine/src/token.rs
new file mode 100644
index 0000000..cde8164
--- /dev/null
+++ b/rprt-engine/src/token.rs
@@ -0,0 +1,279 @@
+use logos::{Lexer, Logos};
+
+fn lex_delimited(
+ lex: &mut Lexer<Token>,
+ delimiter: char,
+ ignore_inside: Option<&[(char, char)]>,
+) -> Option<String> {
+ let remainder = lex.remainder();
+ let mut chars = remainder.chars();
+ let mut escaped = false;
+ let mut pos = 0;
+ let mut looking_for: Option<char> = None;
+
+ while let Some(c) = chars.next() {
+ pos += c.len_utf8();
+
+ // escaping
+ if escaped {
+ escaped = false;
+ continue;
+ }
+ if c == '\\' {
+ escaped = true;
+ continue;
+ }
+
+ // capture in
+ if let Some(end) = looking_for {
+ if c == end {
+ looking_for = None;
+ }
+ continue;
+ }
+ if let Some(ignore_inside) = ignore_inside {
+ for &(start, end) in ignore_inside {
+ if c == start {
+ looking_for = Some(end);
+ continue;
+ }
+ }
+ }
+
+ if c == delimiter {
+ let content = &remainder[..pos - 1];
+ lex.bump(pos);
+ return Some(content.to_string());
+ }
+ }
+
+ None
+}
+
+fn lex_regex(lex: &mut Lexer<Token>) -> Option<String> {
+ let character_class = vec![('[', ']')];
+ lex_delimited(lex, '/', Some(&character_class))
+}
+
+fn lex_text(lex: &mut Lexer<Token>) -> Option<String> {
+ lex_delimited(lex, '"', None)
+}
+
+#[derive(Debug, Clone, PartialEq, Logos)]
+#[logos(skip r"\s+")]
+pub enum Token {
+ // Literals
+ #[token("\"", lex_text)]
+ TextLiteral(String),
+
+ #[regex(r"[0-9]+", |lex| lex.slice().parse().ok())]
+ Number(usize),
+
+ #[token("/", lex_regex)]
+ Regex(String),
+
+ // Selection Functions
+ #[token("e")]
+ Empty,
+
+ #[token("$")]
+ EndOfBuffer,
+
+ #[token("#")]
+ CharacterOffset,
+
+ #[token("-")]
+ Span,
+
+ #[token("x")]
+ AllMatches,
+
+ #[token("+")]
+ PositiveLineOffset,
+
+ #[token("B")]
+ BufferMatch,
+
+ // Text Functions
+ #[token(".")]
+ Dot,
+
+ #[token("c")]
+ Change,
+
+ #[token("i")]
+ Insert,
+
+ #[token("a")]
+ Append,
+
+ #[token("d")]
+ Delete,
+
+ #[token("w")]
+ Write,
+
+ #[token("|")]
+ Pipe,
+
+ // Selection Function Operators
+ #[token("'")]
+ Reverse,
+
+ #[token(";")]
+ Sequential,
+
+ #[token("~")]
+ Complement,
+
+ #[token("?")]
+ Question,
+
+ // Text Function Operators
+ #[token("@")]
+ Swap,
+
+ // Composition/Combinators
+ #[token(">")]
+ Before,
+
+ #[token("<")]
+ After,
+
+ // Grouping
+ #[token("(")]
+ LParen,
+
+ #[token(")")]
+ RParen,
+
+ #[token("{")]
+ LBrace,
+
+ #[token("}")]
+ RBrace,
+
+ // Statement/Buffer control
+ #[token("b")]
+ BufferSwitch,
+
+ // Separators
+ #[token(",")]
+ Comma,
+}
+
+pub fn tokenize(input: &str) -> Result<Vec<Token>, String> {
+ let mut tokens = Vec::new();
+ let mut lexer = Token::lexer(input);
+
+ while let Some(result) = lexer.next() {
+ match result {
+ Ok(token) => tokens.push(token),
+ Err(()) => {
+ return Err(format!(
+ "Unexpected character at position {}: '{}'",
+ lexer.span().start,
+ lexer.slice()
+ ));
+ }
+ }
+ }
+
+ Ok(tokens)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::token::*;
+
+ #[test]
+ fn test_delimited_literals_with_escapes() {
+ assert_eq!(
+ tokenize(r#""" "hello" "escaped\"quote" "backslash\\" "mixed\n\t""#),
+ Ok(vec![
+ Token::TextLiteral("".to_string()),
+ Token::TextLiteral("hello".to_string()),
+ Token::TextLiteral(r#"escaped\"quote"#.to_string()),
+ Token::TextLiteral(r#"backslash\\"#.to_string()),
+ Token::TextLiteral(r#"mixed\n\t"#.to_string()),
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"// /[a-z]+/ /foo\/bar/ /[/]/ /(a|b)\d+/ /\\/"#),
+ Ok(vec![
+ Token::Regex("".to_string()),
+ Token::Regex("[a-z]+".to_string()),
+ Token::Regex(r#"foo\/bar"#.to_string()),
+ Token::Regex("[/]".to_string()),
+ Token::Regex(r#"(a|b)\d+"#.to_string()),
+ Token::Regex(r#"\\"#.to_string()),
+ ])
+ );
+ }
+
+ #[test]
+ fn test_realistic_expressions() {
+ assert_eq!(
+ tokenize(r#"/pattern/ c "replacement""#),
+ Ok(vec![
+ Token::Regex("pattern".to_string()),
+ Token::Change,
+ Token::TextLiteral("replacement".to_string())
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"B/*.txt/ ?'/\w+/ ;#42"#),
+ Ok(vec![
+ Token::BufferMatch,
+ Token::Regex(r#"*.txt"#.to_string()),
+ Token::Question,
+ Token::Reverse,
+ Token::Regex(r#"\w+"#.to_string()),
+ Token::Sequential,
+ Token::CharacterOffset,
+ Token::Number(42)
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"{ /a/ @i, /b/ c<(|<"sort") }"#),
+ Ok(vec![
+ Token::LBrace,
+ Token::Regex("a".to_string()),
+ Token::Swap,
+ Token::Insert,
+ Token::Comma,
+ Token::Regex("b".to_string()),
+ Token::Change,
+ Token::After,
+ Token::LParen,
+ Token::Pipe,
+ Token::After,
+ Token::TextLiteral("sort".to_string()),
+ Token::RParen,
+ Token::RBrace
+ ])
+ );
+
+ assert_eq!(
+ tokenize(r#"~?';x/pattern/"#),
+ Ok(vec![
+ Token::Complement,
+ Token::Question,
+ Token::Reverse,
+ Token::Sequential,
+ Token::AllMatches,
+ Token::Regex("pattern".to_string())
+ ])
+ );
+ }
+
+ #[test]
+ fn test_errors() {
+ assert!(tokenize(r#""unterminated"#).is_err());
+ assert!(tokenize(r#"/unterminated"#).is_err());
+ assert!(tokenize(r#""escaped at end\"#).is_err());
+ }
+}