From fdd42fce19dbab5679df5fe4eaf46ebde52f5b80 Mon Sep 17 00:00:00 2001 From: 0264408 Date: Tue, 16 Jun 2026 19:16:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9=E5=8D=95?= =?UTF-8?q?=E5=BC=95=E5=8F=B7=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E8=AF=8D=E6=B3=95=E5=88=86?= =?UTF-8?q?=E6=9E=90=E5=99=A8=E5=92=8C=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/interpreter.rs | 24 +++++++++++++ src/lexer/lexer.rs | 66 +++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index 5d791a0..0ca5e20 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -750,6 +750,30 @@ mod tests { } } + #[test] + fn eval_single_quoted_string() { + match eval_expr("'hello'") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } + } + + #[test] + fn eval_mixed_quotes_concat() { + match eval_expr("\"a\" + 'b'") { + Value::String(s) => assert_eq!(s, "ab"), + v => panic!("Expected String, got {:?}", v), + } + } + + #[test] + fn eval_single_quote_containing_double() { + match eval_expr("'he\"llo'") { + Value::String(s) => assert_eq!(s, "he\"llo"), + v => panic!("Expected String, got {:?}", v), + } + } + #[test] fn eval_string_plus_number() { match eval_expr("\"a\" + 42") { diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index f6a4f86..c9a8a6d 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -157,7 +157,7 @@ impl Lexer { } } - '"' => return self.string_literal(line, column, errors), + '"' | '\'' => return self.string_literal(line, column, errors, c), c if c.is_ascii_digit() => { return Some(self.number_literal(c, line, column)); @@ -230,10 +230,11 @@ impl Lexer { line: usize, column: usize, errors: &mut Vec, + quote: char, ) -> Option { let mut value = String::new(); - while !self.is_at_end() && self.peek() != '"' { + while !self.is_at_end() && self.peek() != quote { let c = self.advance(); if c == '\\' { // Handle escape sequences @@ -246,6 +247,7 @@ impl Lexer { 't' => value.push('\t'), 'r' => value.push('\r'), '"' => value.push('"'), + '\'' => value.push('\''), '\\' => value.push('\\'), other => { errors.push(RuntimeError::lex( @@ -254,11 +256,11 @@ impl Lexer { column, )); // Consume the rest of the string to avoid cascading errors - while !self.is_at_end() && self.peek() != '"' { + while !self.is_at_end() && self.peek() != quote { self.advance(); } if !self.is_at_end() { - self.advance(); // consume closing " + self.advance(); // consume closing quote } return None; } @@ -273,7 +275,7 @@ impl Lexer { return None; } - self.advance(); // consume closing " + self.advance(); // consume closing quote Some(Token { kind: TokenKind::String(value), @@ -522,6 +524,60 @@ mod tests { assert!(errors[0].to_string().contains("Unknown escape")); } + // ----- single-quoted strings ----- + + #[test] + fn single_quoted_string() { + let kinds = tokenize("'hello'"); + assert_eq!(kinds, vec![TokenKind::String("hello".into())]); + } + + #[test] + fn single_quoted_empty_string() { + let kinds = tokenize("''"); + assert_eq!(kinds, vec![TokenKind::String("".into())]); + } + + #[test] + fn single_quoted_with_escape() { + let kinds = tokenize("'a\\'b'"); + assert_eq!(kinds, vec![TokenKind::String("a'b".into())]); + } + + #[test] + fn single_quoted_with_newline_escape() { + let kinds = tokenize("'a\\nb'"); + assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]); + } + + #[test] + fn mixed_quote_strings() { + let kinds = tokenize("\"double\" 'single'"); + assert_eq!(kinds, vec![ + TokenKind::String("double".into()), + TokenKind::String("single".into()), + ]); + } + + #[test] + fn single_quote_can_contain_double_quote() { + let kinds = tokenize("'he\"llo'"); + assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]); + } + + #[test] + fn double_quote_can_contain_single_quote() { + let kinds = tokenize("\"it's\""); + assert_eq!(kinds, vec![TokenKind::String("it's".into())]); + } + + #[test] + fn unterminated_single_quoted_string() { + let (_, errors) = tokenize_full("'unterminated"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unterminated")); + } + // ----- identifiers and keywords ----- #[test]