From 0085fb47fa4d0c9de0307bcabe26f1771892ef21 Mon Sep 17 00:00:00 2001 From: Fred Roth Date: Wed, 24 Jan 2024 11:46:40 +0100 Subject: [PATCH] Move io to main --- src/lox.rs | 32 +--- src/main.rs | 43 ++++- src/scanner/error.rs | 10 +- src/scanner/error_combiner.rs | 24 ++- src/scanner/main.rs | 323 ---------------------------------- src/scanner/mod.rs | 323 +++++++++++++++++++++++++++++++++- 6 files changed, 380 insertions(+), 375 deletions(-) delete mode 100644 src/scanner/main.rs diff --git a/src/lox.rs b/src/lox.rs index 86f8de9..e1ca440 100644 --- a/src/lox.rs +++ b/src/lox.rs @@ -1,9 +1,6 @@ -use std::{ - fs, - io::{self, Write}, -}; +use std::sync::Arc; -use miette::IntoDiagnostic; +use miette::NamedSource; use crate::{parser::Parser, scanner::Scanner}; @@ -14,33 +11,12 @@ impl Lox { Self {} } - fn run(&mut self, source: String, filename: String) -> miette::Result<()> { - let mut scanner = Scanner::new(source, filename); + pub fn run(&self, source: String, named_source: Arc) -> miette::Result<()> { + let mut scanner = Scanner::new(source, named_source); let tokens = scanner.scan_tokens()?; // tokens.iter().for_each(|x| println!("{:?}", x)); let mut parser = Parser::new(tokens); println!("{}", parser.parse()); Ok(()) } - pub fn run_file(&mut self, file: String) -> miette::Result<()> { - let contents = fs::read_to_string(file.clone()).into_diagnostic()?; - self.run(contents, file)?; - Ok(()) - } - - pub fn run_prompt(&mut self) -> miette::Result<()> { - let std = io::stdin(); - loop { - print!("> "); - io::stdout().flush().into_diagnostic()?; - let mut buf = String::new(); - match std.read_line(&mut buf).into_diagnostic()? { - 0 => return Ok(()), - _ => match self.run(buf.trim_end().to_string(), "stdin".to_string()) { - Ok(_) => (), - Err(err) => println!("{:?}", err), - }, - } - } - } } diff --git a/src/main.rs b/src/main.rs index 3309602..002377c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,14 @@ +use std::{ + fs, + io::{self, Write}, + sync::Arc, +}; + use clap::Parser; use lox::Lox; use args::Args; +use miette::{IntoDiagnostic, NamedSource}; mod args; mod expr; @@ -12,10 +19,9 @@ mod token; fn main() { let args = Args::parse(); - let mut lox = Lox::new(); let result = match args.file { - Some(file) => lox.run_file(file), - None => lox.run_prompt(), + Some(file) => run_file(file), + None => run_prompt(), }; match result { Ok(_) => (), @@ -26,6 +32,37 @@ fn main() { }; } +fn run_file(file: String) -> miette::Result<()> { + let contents = fs::read_to_string(file.clone()).into_diagnostic()?; + + let named_source: Arc = NamedSource::new(file, contents.to_string()).into(); + let lox = Lox::new(); + lox.run(contents, named_source)?; + Ok(()) +} + +fn run_prompt() -> miette::Result<()> { + let std = io::stdin(); + let lox = Lox::new(); + loop { + print!("> "); + io::stdout().flush().into_diagnostic()?; + let mut buf = String::new(); + match std.read_line(&mut buf).into_diagnostic()? { + 0 => return Ok(()), + _ => { + let source = buf.trim_end().to_string(); + let named_source: Arc = + NamedSource::new("stdin", source.to_string()).into(); + match lox.run(source.to_string(), named_source) { + Ok(_) => (), + Err(err) => println!("{:?}", err), + } + } + } + } +} + #[cfg(test)] #[macro_use] extern crate assert_matches; diff --git a/src/scanner/error.rs b/src/scanner/error.rs index 3e53e1f..994e600 100644 --- a/src/scanner/error.rs +++ b/src/scanner/error.rs @@ -1,4 +1,4 @@ -use std::num::ParseFloatError; +use std::{num::ParseFloatError, rc::Rc, sync::Arc}; use miette::{Diagnostic, NamedSource, SourceSpan}; @@ -8,7 +8,7 @@ pub enum ScannerError { UnexpectedCharacter { char: char, #[source_code] - src: NamedSource, + src: Arc, #[label("here")] location: SourceSpan, }, @@ -17,7 +17,7 @@ pub enum ScannerError { UnexpectedCharacters { chars: String, #[source_code] - src: NamedSource, + src: Arc, #[label("here")] location: SourceSpan, }, @@ -25,7 +25,7 @@ pub enum ScannerError { #[error("Non terminated String")] NonTerminatedString { #[source_code] - src: NamedSource, + src: Arc, #[label("here")] location: SourceSpan, }, @@ -39,4 +39,4 @@ pub enum ScannerError { pub struct AccumulatedScannerErrors { #[related] pub scanner_errors: Vec, -} \ No newline at end of file +} diff --git a/src/scanner/error_combiner.rs b/src/scanner/error_combiner.rs index d7b3ab0..8f1adb5 100644 --- a/src/scanner/error_combiner.rs +++ b/src/scanner/error_combiner.rs @@ -1,14 +1,17 @@ +use std::sync::Arc; + use miette::{NamedSource, SourceSpan}; use super::error::ScannerError::{self, UnexpectedCharacter, UnexpectedCharacters}; pub struct ErrorCombiner { - source: String, - filename: String, + named_source: Arc, } impl ErrorCombiner { - pub fn new(source: String, filename: String) -> Self { Self { source, filename } } + pub fn new(named_source: Arc) -> Self { + Self { named_source } + } fn handle_accumulated( &self, @@ -21,12 +24,12 @@ impl ErrorCombiner { [] => (), [char] => result.push(UnexpectedCharacter { char: *char, - src: self.named_source(), + src: self.named_source.clone(), location: *last_offset, }), _ => result.push(UnexpectedCharacters { chars: accumulated.iter().collect(), - src: self.named_source(), + src: self.named_source.clone(), location: *last_offset, }), }; @@ -35,10 +38,7 @@ impl ErrorCombiner { *last_offset = None; } - pub fn combine( - &self, - scanner_errors: Vec, - ) -> Vec { + pub fn combine(&self, scanner_errors: Vec) -> Vec { let mut result = vec![]; let mut accumulated = vec![]; let mut last_offset: Option = None; @@ -71,8 +71,4 @@ impl ErrorCombiner { self.handle_accumulated(&mut accumulated, &mut last_offset, &mut result); result } - - fn named_source(&self) -> NamedSource { - NamedSource::new(self.filename.clone(), self.source.to_string()) - } -} \ No newline at end of file +} diff --git a/src/scanner/main.rs b/src/scanner/main.rs deleted file mode 100644 index c75e8fa..0000000 --- a/src/scanner/main.rs +++ /dev/null @@ -1,323 +0,0 @@ -use miette::NamedSource; -use phf::phf_map; - -use crate::{ - scanner::error::ScannerError::{self, *}, - token::{Token, TokenType}, -}; - -use super::{error::AccumulatedScannerErrors, error_combiner::ErrorCombiner}; -pub struct Scanner { - source: String, - filename: String, - tokens: Vec, - start: usize, - current: usize, - line: usize, - error_combiner: ErrorCombiner, -} - -static KEYWORDS: phf::Map<&'static str, TokenType> = phf_map! { - "and" => TokenType::And, - "false" => TokenType::False, - "fun" => TokenType::Fun, - "for" => TokenType::For, - "if" => TokenType::If, - "else" => TokenType::Else, - "nil" => TokenType::Nil, - "or" => TokenType::Or, - "print" => TokenType::Print, - "return" => TokenType::Return, - "super" => TokenType::Super, - "this" => TokenType::This, - "true" => TokenType::True, - "var" => TokenType::Var, - "while" => TokenType::While, - "class" => TokenType::Class, -}; - -pub type Result = core::result::Result; - -impl Scanner { - pub fn new(source: String, filename: String) -> Self { - let error_combiner = ErrorCombiner::new(source.to_string(), filename.to_string()); - Self { - source, - filename, - tokens: vec![], - start: 0, - current: 0, - line: 1, - error_combiner - } - } - - pub fn scan_tokens(&mut self) -> core::result::Result, AccumulatedScannerErrors> { - let mut scanner_errors = vec![]; - while let Some(char) = self.advance() { - self.start = self.current - 1; //has already been advanced - match self.scan_token(char) { - Ok(Some(token)) => self.add_token(token), - Ok(None) => (), - Err(err) => scanner_errors.push(err), - } - } - self.tokens - .push(Token::new(TokenType::Eof, String::new(), self.line)); - if scanner_errors.is_empty() { - Ok(self.tokens.to_vec()) - } else { - - let scanner_errors = self.error_combiner.combine(scanner_errors); - Err(AccumulatedScannerErrors { scanner_errors }) - } - } - - fn scan_token(&mut self, char: char) -> Result> { - use TokenType::*; - match char { - '(' => Ok(Some(LeftParen)), - ')' => Ok(Some(RightParen)), - '{' => Ok(Some(LeftBrace)), - '}' => Ok(Some(RightBrace)), - ',' => Ok(Some(Comma)), - '.' => Ok(Some(Dot)), - '-' => Ok(Some(Minus)), - '+' => Ok(Some(Plus)), - ';' => Ok(Some(Semicolon)), - '*' => Ok(Some(Star)), - '!' => Ok(Some(if self.matches('=') { BangEqual } else { Bang })), - '=' => Ok(Some(if self.matches('=') { EqualEqual } else { Equal })), - '<' => Ok(Some(if self.matches('=') { LessEqual } else { Less })), - '>' => Ok(Some(if self.matches('=') { - GreaterEqual - } else { - Greater - })), - '/' => { - if self.matches('/') { - self.consume_comment(); - Ok(None) - } else { - Ok(Some(Slash)) - } - } - '\n' => { - self.line += 1; - Ok(None) - } - ' ' | '\r' | '\t' => Ok(None), - '"' => self.read_string(), - c if c.is_ascii_digit() => self.read_number(), - c if c.is_ascii_alphabetic() || c == '_' => Ok(Some(self.read_identifier())), - - _ => Err(UnexpectedCharacter { - char, - src: self.named_source(), - location: (self.current - 1, 1).into(), - }), - } - } - - fn add_token(&mut self, token_type: TokenType) { - let text = self.source[self.start..self.current].to_string(); - self.tokens.push(Token::new(token_type, text, self.line)) - } - - fn advance(&mut self) -> Option { - let char = self.source.chars().nth(self.current); - if char.is_some() { - self.current += 1; - } - char - } - - fn matches(&mut self, expected: char) -> bool { - match self.source.chars().nth(self.current) { - Some(x) if x == expected => { - self.current += 1; - true - } - _ => false, - } - } - - fn peek(&self) -> Option { - self.source.chars().nth(self.current) - } - - fn peek_next(&self) -> Option { - self.source.chars().nth(self.current + 1) - } - - fn consume_comment(&mut self) { - while let Some(x) = self.peek() { - if x == '\n' { - break; - } else { - self.advance(); - } - } - } - - fn read_string(&mut self) -> Result> { - let start = self.current; - loop { - match self.peek() { - Some('"') => break, - Some('\n') => { - self.line += 1; - self.current += 1; - } - Some(_) => self.current += 1, - None => Err(NonTerminatedString { - src: self.named_source(), - location: (start - 1, self.current - start + 1).into(), - })?, - } - } - self.current += 1; // the closing "" - let string = self.source[self.start + 1..self.current - 1].to_string(); - Ok(Some(TokenType::String(string))) - } - - fn read_number(&mut self) -> Result> { - while self.peek().is_some_and(|x| x.is_ascii_digit()) { - self.current += 1; - } - if self.peek().is_some_and(|x| x == '.') - && self.peek_next().is_some_and(|x| x.is_ascii_digit()) - { - self.current += 1; // the . - while self.peek().is_some_and(|x| x.is_ascii_digit()) { - self.current += 1; - } - } - let result = self.source[self.start..self.current] - .parse::() - .map(|f| Some(TokenType::Number(f)))?; - Ok(result) - } - - fn read_identifier(&mut self) -> TokenType { - while self - .peek() - .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') - { - self.current += 1; - } - - let text = &self.source[self.start..self.current]; - let token = KEYWORDS.get(text).cloned(); - token.unwrap_or(TokenType::Identifier) - } - - fn named_source(&self) -> NamedSource { - NamedSource::new(self.filename.clone(), self.source.to_string()) - } -} - -#[cfg(test)] -mod scanner_tests { - - use std::string::String; - - use crate::scanner::error::ScannerError; - - use super::Scanner; - use super::TokenType::*; - - #[test] - fn parse_string() { - let input = "\"test\""; - let mut scanner = Scanner::new(input.into(), String::new()); - let result = scanner.scan_tokens().unwrap(); - let head = &result[0].token_type; - assert_matches!(head, String(x) if x == "test"); - } - #[test] - fn parse_float() { - let input = "1.1"; - let mut scanner = Scanner::new(input.into(), String::new()); - let result = scanner.scan_tokens().unwrap(); - assert_eq!(result.len(), 2); - let head = &result[0].token_type; - assert_matches!(head, Number(_)); - } - #[test] - fn parse_identifier() { - let input = "variable_name"; - let mut scanner = Scanner::new(input.into(), String::new()); - let result = scanner.scan_tokens().unwrap(); - let head = &result[0]; - let token_type = &head.token_type; - assert_matches!(token_type, Identifier); - assert_eq!(head.lexeme, input) - } - - #[test] - fn parse_for() { - let input = "for"; - let mut scanner = Scanner::new(input.into(), String::new()); - let result = scanner.scan_tokens().unwrap(); - let head = &result[0]; - let token_type = &head.token_type; - assert_matches!(token_type, For); - } - - #[test] - fn raise_error_on_unterminated_string() { - let input = "1+1; \"12345"; - let mut scanner = Scanner::new(input.into(), String::new()); - let acc = scanner.scan_tokens().unwrap_err(); - let result = acc.scanner_errors.first().unwrap(); - assert_matches!(result, ScannerError::NonTerminatedString { - src, - location, - } if src.name() == "" && *location == (5,6).into()) - } - - #[test] - fn raise_error_on_unexpected_char() { - let input = "^"; - let mut scanner = Scanner::new(input.into(), String::new()); - let acc = scanner.scan_tokens().unwrap_err(); - let result = acc.scanner_errors.first().unwrap(); - assert_matches!(result, ScannerError::UnexpectedCharacter { - char: '^', - src, - location, - } if src.name() == "" && *location == (0,1).into()) - } - #[test] - fn combine_unexpected_chars() { - let input = "^^^^"; - let mut scanner = Scanner::new(input.into(), String::new()); - let acc = scanner.scan_tokens().unwrap_err(); - let result = acc.scanner_errors.first().unwrap(); - assert_matches!(result, ScannerError::UnexpectedCharacters { - chars, - src, - location, - } if chars == "^^^^" && src.name() == "" && *location == (0,4).into()) - } - - #[test] - fn combine_unexpected_chars_only_if_offsets_overlap() { - let input = "^^ @@"; - let mut scanner = Scanner::new(input.into(), String::new()); - let acc = scanner.scan_tokens().unwrap_err(); - let result1 = acc.scanner_errors.first().unwrap(); - let result2 = acc.scanner_errors.get(1).unwrap(); - assert_matches!(result1, ScannerError::UnexpectedCharacters { - chars, - src, - location, - } if chars == "^^" && src.name() == "" && *location == (0,2).into()); - assert_matches!(result2, ScannerError::UnexpectedCharacters { - chars, - src, - location, - } if chars == "@@" && src.name() == "" && *location == (3,2).into()); - } -} diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index c0f9ce8..4fd2359 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -1,4 +1,323 @@ -pub mod main; pub mod error; mod error_combiner; -pub use main::Scanner; \ No newline at end of file + +use std::sync::Arc; + +use miette::NamedSource; +use phf::phf_map; + +use crate::{ + scanner::error::ScannerError::{self, *}, + token::{Token, TokenType}, +}; + +use self::{error::AccumulatedScannerErrors, error_combiner::ErrorCombiner}; + +static KEYWORDS: phf::Map<&'static str, TokenType> = phf_map! { + "and" => TokenType::And, + "false" => TokenType::False, + "fun" => TokenType::Fun, + "for" => TokenType::For, + "if" => TokenType::If, + "else" => TokenType::Else, + "nil" => TokenType::Nil, + "or" => TokenType::Or, + "print" => TokenType::Print, + "return" => TokenType::Return, + "super" => TokenType::Super, + "this" => TokenType::This, + "true" => TokenType::True, + "var" => TokenType::Var, + "while" => TokenType::While, + "class" => TokenType::Class, +}; + +pub struct Scanner { + source: String, + named_source: Arc, + tokens: Vec, + start: usize, + current: usize, + line: usize, + error_combiner: ErrorCombiner, +} + +pub type Result = core::result::Result; + +impl Scanner { + pub fn new(source: String, named_source: Arc) -> Self { + let error_combiner = ErrorCombiner::new(named_source.clone()); + Self { + source, + named_source, + tokens: vec![], + start: 0, + current: 0, + line: 1, + error_combiner, + } + } + + pub fn scan_tokens(&mut self) -> core::result::Result, AccumulatedScannerErrors> { + let mut scanner_errors = vec![]; + while let Some(char) = self.advance() { + self.start = self.current - 1; //has already been advanced + match self.scan_token(char) { + Ok(Some(token)) => self.add_token(token), + Ok(None) => (), + Err(err) => scanner_errors.push(err), + } + } + self.tokens + .push(Token::new(TokenType::Eof, String::new(), self.line)); + if scanner_errors.is_empty() { + Ok(self.tokens.to_vec()) + } else { + let scanner_errors = self.error_combiner.combine(scanner_errors); + Err(AccumulatedScannerErrors { scanner_errors }) + } + } + + fn scan_token(&mut self, char: char) -> Result> { + use TokenType::*; + match char { + '(' => Ok(Some(LeftParen)), + ')' => Ok(Some(RightParen)), + '{' => Ok(Some(LeftBrace)), + '}' => Ok(Some(RightBrace)), + ',' => Ok(Some(Comma)), + '.' => Ok(Some(Dot)), + '-' => Ok(Some(Minus)), + '+' => Ok(Some(Plus)), + ';' => Ok(Some(Semicolon)), + '*' => Ok(Some(Star)), + '!' => Ok(Some(if self.matches('=') { BangEqual } else { Bang })), + '=' => Ok(Some(if self.matches('=') { EqualEqual } else { Equal })), + '<' => Ok(Some(if self.matches('=') { LessEqual } else { Less })), + '>' => Ok(Some(if self.matches('=') { + GreaterEqual + } else { + Greater + })), + '/' => { + if self.matches('/') { + self.consume_comment(); + Ok(None) + } else { + Ok(Some(Slash)) + } + } + '\n' => { + self.line += 1; + Ok(None) + } + ' ' | '\r' | '\t' => Ok(None), + '"' => self.read_string(), + c if c.is_ascii_digit() => self.read_number(), + c if c.is_ascii_alphabetic() || c == '_' => Ok(Some(self.read_identifier())), + + _ => Err(UnexpectedCharacter { + char, + src: self.named_source.clone(), + location: (self.current - 1, 1).into(), + }), + } + } + + fn add_token(&mut self, token_type: TokenType) { + let text = self.source[self.start..self.current].to_string(); + self.tokens.push(Token::new(token_type, text, self.line)) + } + + fn advance(&mut self) -> Option { + let char = self.source.chars().nth(self.current); + if char.is_some() { + self.current += 1; + } + char + } + + fn matches(&mut self, expected: char) -> bool { + match self.source.chars().nth(self.current) { + Some(x) if x == expected => { + self.current += 1; + true + } + _ => false, + } + } + + fn peek(&self) -> Option { + self.source.chars().nth(self.current) + } + + fn peek_next(&self) -> Option { + self.source.chars().nth(self.current + 1) + } + + fn consume_comment(&mut self) { + while let Some(x) = self.peek() { + if x == '\n' { + break; + } else { + self.advance(); + } + } + } + + fn read_string(&mut self) -> Result> { + let start = self.current; + loop { + match self.peek() { + Some('"') => break, + Some('\n') => { + self.line += 1; + self.current += 1; + } + Some(_) => self.current += 1, + None => Err(NonTerminatedString { + src: self.named_source.clone(), + location: (start - 1, self.current - start + 1).into(), + })?, + } + } + self.current += 1; // the closing "" + let string = self.source[self.start + 1..self.current - 1].to_string(); + Ok(Some(TokenType::String(string))) + } + + fn read_number(&mut self) -> Result> { + while self.peek().is_some_and(|x| x.is_ascii_digit()) { + self.current += 1; + } + if self.peek().is_some_and(|x| x == '.') + && self.peek_next().is_some_and(|x| x.is_ascii_digit()) + { + self.current += 1; // the . + while self.peek().is_some_and(|x| x.is_ascii_digit()) { + self.current += 1; + } + } + let result = self.source[self.start..self.current] + .parse::() + .map(|f| Some(TokenType::Number(f)))?; + Ok(result) + } + + fn read_identifier(&mut self) -> TokenType { + while self + .peek() + .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') + { + self.current += 1; + } + + let text = &self.source[self.start..self.current]; + let token = KEYWORDS.get(text).cloned(); + token.unwrap_or(TokenType::Identifier) + } +} + +#[cfg(test)] +mod scanner_tests { + use miette::NamedSource; + + use crate::scanner::error::ScannerError; + + use super::Scanner; + use super::TokenType::*; + + #[test] + fn parse_string() { + let input = "\"test\""; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let result = scanner.scan_tokens().unwrap(); + let head = &result[0].token_type; + assert_matches!(head, String(x) if x == "test"); + } + #[test] + fn parse_float() { + let input = "1.1"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let result = scanner.scan_tokens().unwrap(); + assert_eq!(result.len(), 2); + let head = &result[0].token_type; + assert_matches!(head, Number(_)); + } + #[test] + fn parse_identifier() { + let input = "variable_name"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let result = scanner.scan_tokens().unwrap(); + let head = &result[0]; + let token_type = &head.token_type; + assert_matches!(token_type, Identifier); + assert_eq!(head.lexeme, input) + } + + #[test] + fn parse_for() { + let input = "for"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let result = scanner.scan_tokens().unwrap(); + let head = &result[0]; + let token_type = &head.token_type; + assert_matches!(token_type, For); + } + + #[test] + fn raise_error_on_unterminated_string() { + let input = "1+1; \"12345"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let acc = scanner.scan_tokens().unwrap_err(); + let result = acc.scanner_errors.first().unwrap(); + assert_matches!(result, ScannerError::NonTerminatedString { + src, + location, + } if src.name() == "" && *location == (5,6).into()) + } + + #[test] + fn raise_error_on_unexpected_char() { + let input = "^"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let acc = scanner.scan_tokens().unwrap_err(); + let result = acc.scanner_errors.first().unwrap(); + assert_matches!(result, ScannerError::UnexpectedCharacter { + char: '^', + src, + location, + } if src.name() == "" && *location == (0,1).into()) + } + #[test] + fn combine_unexpected_chars() { + let input = "^^^^"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let acc = scanner.scan_tokens().unwrap_err(); + let result = acc.scanner_errors.first().unwrap(); + assert_matches!(result, ScannerError::UnexpectedCharacters { + chars, + src, + location, + } if chars == "^^^^" && src.name() == "" && *location == (0,4).into()) + } + + #[test] + fn combine_unexpected_chars_only_if_offsets_overlap() { + let input = "^^ @@"; + let mut scanner = Scanner::new(input.into(), NamedSource::new("", input).into()); + let acc = scanner.scan_tokens().unwrap_err(); + let result1 = acc.scanner_errors.first().unwrap(); + let result2 = acc.scanner_errors.get(1).unwrap(); + assert_matches!(result1, ScannerError::UnexpectedCharacters { + chars, + src, + location, + } if chars == "^^" && src.name() == "" && *location == (0,2).into()); + assert_matches!(result2, ScannerError::UnexpectedCharacters { + chars, + src, + location, + } if chars == "@@" && src.name() == "" && *location == (3,2).into()); + } +}