fix: 修复字符串转义、对象相等比较、除零处理
- 词法分析器:字符串字面量中支持 \n \t \r " \ 转义序列,未知转义序列报告错误并跳过剩余字符串 - 解释器:对象相等比较新增深度比较(键值对逐一比对) - 解释器:除法和取模运算在除数为零时返回 RuntimeError,避免产生 inf/NaN Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -308,6 +308,12 @@ impl Interpreter {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
crate::ast::expr::BinaryOp::Div => match (l,r) {
|
crate::ast::expr::BinaryOp::Div => match (l,r) {
|
||||||
|
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
||||||
|
Err(RuntimeError::RuntimeError {
|
||||||
|
message: "Division by zero".to_string(),
|
||||||
|
token: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a/b)),
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a/b)),
|
||||||
_ => Err(RuntimeError::RuntimeError {
|
_ => Err(RuntimeError::RuntimeError {
|
||||||
message: "Invalid '/' operands".to_string(),
|
message: "Invalid '/' operands".to_string(),
|
||||||
@@ -315,6 +321,12 @@ impl Interpreter {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
crate::ast::expr::BinaryOp::Mod => match (l,r) {
|
crate::ast::expr::BinaryOp::Mod => match (l,r) {
|
||||||
|
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
||||||
|
Err(RuntimeError::RuntimeError {
|
||||||
|
message: "Modulo by zero".to_string(),
|
||||||
|
token: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a%b)),
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a%b)),
|
||||||
_ => Err(RuntimeError::RuntimeError {
|
_ => Err(RuntimeError::RuntimeError {
|
||||||
message: "Invalid '%' operands".to_string(),
|
message: "Invalid '%' operands".to_string(),
|
||||||
@@ -423,6 +435,14 @@ impl Interpreter {
|
|||||||
if x.len() != y.len() { return false; }
|
if x.len() != y.len() { return false; }
|
||||||
x.iter().zip(y.iter()).all(|(a,b)| self.is_equal(a,b))
|
x.iter().zip(y.iter()).all(|(a,b)| self.is_equal(a,b))
|
||||||
}
|
}
|
||||||
|
(Value::Object(x), Value::Object(y)) => {
|
||||||
|
let x = x.borrow();
|
||||||
|
let y = y.borrow();
|
||||||
|
if x.len() != y.len() { return false; }
|
||||||
|
x.iter().all(|(k, v)| {
|
||||||
|
y.get(k).map_or(false, |yv| self.is_equal(v, yv))
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1003,6 +1023,40 @@ mod tests {
|
|||||||
assert_bool(&eval_expr("[1] == [1, 2]"), false);
|
assert_bool(&eval_expr("[1] == [1, 2]"), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Object equality
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_object_equality_same() {
|
||||||
|
let interp = run("let a = {x: 1, y: 2}; let b = {x: 1, y: 2}; let r = a == b;");
|
||||||
|
assert_bool(&get_var(&interp, "r"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_object_equality_different_values() {
|
||||||
|
let interp = run("let a = {x: 1}; let b = {x: 2}; let r = a == b;");
|
||||||
|
assert_bool(&get_var(&interp, "r"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_object_equality_different_keys() {
|
||||||
|
let interp = run("let a = {x: 1}; let b = {y: 1}; let r = a == b;");
|
||||||
|
assert_bool(&get_var(&interp, "r"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_object_equality_different_size() {
|
||||||
|
let interp = run("let a = {x: 1}; let b = {x: 1, y: 2}; let r = a == b;");
|
||||||
|
assert_bool(&get_var(&interp, "r"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_object_equality_empty() {
|
||||||
|
let interp = run("let a = {}; let b = {}; let r = a == b;");
|
||||||
|
assert_bool(&get_var(&interp, "r"), true);
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Strings as indexable
|
// Strings as indexable
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -1119,6 +1173,22 @@ mod tests {
|
|||||||
assert!(result.is_err(), "Expected error for undefined variable");
|
assert!(result.is_err(), "Expected error for undefined variable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_division_by_zero() {
|
||||||
|
let result = std::panic::catch_unwind(|| {
|
||||||
|
eval_expr("1 / 0");
|
||||||
|
});
|
||||||
|
assert!(result.is_err(), "Expected error for division by zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_modulo_by_zero() {
|
||||||
|
let result = std::panic::catch_unwind(|| {
|
||||||
|
eval_expr("1 % 0");
|
||||||
|
});
|
||||||
|
assert!(result.is_err(), "Expected error for modulo by zero");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_type_mismatch_negate_string() {
|
fn error_type_mismatch_negate_string() {
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
|
|||||||
+69
-1
@@ -233,7 +233,38 @@ impl Lexer {
|
|||||||
let mut value = String::new();
|
let mut value = String::new();
|
||||||
|
|
||||||
while !self.is_at_end() && self.peek() != '"' {
|
while !self.is_at_end() && self.peek() != '"' {
|
||||||
value.push(self.advance());
|
let c = self.advance();
|
||||||
|
if c == '\\' {
|
||||||
|
// Handle escape sequences
|
||||||
|
if self.is_at_end() {
|
||||||
|
errors.push(RuntimeError::lex("Unterminated string literal", line, column));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match self.advance() {
|
||||||
|
'n' => value.push('\n'),
|
||||||
|
't' => value.push('\t'),
|
||||||
|
'r' => value.push('\r'),
|
||||||
|
'"' => value.push('"'),
|
||||||
|
'\\' => value.push('\\'),
|
||||||
|
other => {
|
||||||
|
errors.push(RuntimeError::lex(
|
||||||
|
format!("Unknown escape sequence '\\{}'", other),
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
));
|
||||||
|
// Consume the rest of the string to avoid cascading errors
|
||||||
|
while !self.is_at_end() && self.peek() != '"' {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
if !self.is_at_end() {
|
||||||
|
self.advance(); // consume closing "
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value.push(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_at_end() {
|
if self.is_at_end() {
|
||||||
@@ -451,6 +482,43 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_newline() {
|
||||||
|
let kinds = tokenize("\"a\\nb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_tab() {
|
||||||
|
let kinds = tokenize("\"a\\tb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_quote() {
|
||||||
|
let kinds = tokenize("\"he\\\"llo\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_backslash() {
|
||||||
|
let kinds = tokenize("\"a\\\\b\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_carriage_return() {
|
||||||
|
let kinds = tokenize("\"a\\rb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_unknown_escape_is_error() {
|
||||||
|
let (_, errors) = tokenize_full("\"\\x\"");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unknown escape"));
|
||||||
|
}
|
||||||
|
|
||||||
// ----- identifiers and keywords -----
|
// ----- identifiers and keywords -----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user