diff options
Diffstat (limited to 'rprt-engine/src')
| -rw-r--r-- | rprt-engine/src/buffer.rs | 10 | ||||
| -rw-r--r-- | rprt-engine/src/expression.rs | 116 | ||||
| -rw-r--r-- | rprt-engine/src/lib.rs | 6 | ||||
| -rw-r--r-- | rprt-engine/src/parser.rs | 123 | ||||
| -rw-r--r-- | rprt-engine/src/token.rs | 279 |
5 files changed, 523 insertions, 11 deletions
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()); + } +} |
