diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index fcbe6c2..3fb090c 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -308,6 +308,12 @@ impl Interpreter { }), }, 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)), _ => Err(RuntimeError::RuntimeError { message: "Invalid '/' operands".to_string(), @@ -315,6 +321,12 @@ impl Interpreter { }), }, 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)), _ => Err(RuntimeError::RuntimeError { message: "Invalid '%' operands".to_string(), @@ -423,6 +435,14 @@ impl Interpreter { if x.len() != y.len() { return false; } 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, } } @@ -1003,6 +1023,40 @@ mod tests { 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 // ========================================================================= @@ -1119,6 +1173,22 @@ mod tests { 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] fn error_type_mismatch_negate_string() { let result = std::panic::catch_unwind(|| { diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index 413dcbc..52d2136 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -233,7 +233,38 @@ impl Lexer { let mut value = String::new(); 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() { @@ -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 ----- #[test]