From f0bba2f2aa457d7878a7d226d394e7d88a111b2f Mon Sep 17 00:00:00 2001 From: MaYu Date: Wed, 17 Jun 2026 00:23:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E5=92=8C=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=9A=84=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E6=96=B9=E6=B3=95=E6=94=AF=E6=8C=81=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=20length=E3=80=81push=E3=80=81pop=E3=80=81upper?= =?UTF-8?q?=E3=80=81lower=E3=80=81trim=E3=80=81contains=E3=80=81starts=5Fw?= =?UTF-8?q?ith=E3=80=81ends=5Fwith=E3=80=81substring=E3=80=81replace=20?= =?UTF-8?q?=E5=92=8C=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/builtins/mod.rs | 40 +++--- src/interpreter/eval.rs | 105 ++++++++++++++++ src/interpreter/mod.rs | 2 +- src/interpreter/tests.rs | 207 ++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 21 deletions(-) diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index cf34f5e..173be8c 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -3,7 +3,7 @@ mod core; mod io; mod os; -mod string; +pub mod string; use crate::interpreter::{Env, Value}; use std::cell::RefCell; @@ -16,34 +16,34 @@ pub fn register_all(env: &Rc>) { // io let mut io = HashMap::new(); - io.insert("print".into(), Value::NativeFunction(io::print)); - io.insert("input".into(), Value::NativeFunction(io::input)); + io.insert("print".into(), Value::NativeFunction(Rc::new(io::print))); + io.insert("input".into(), Value::NativeFunction(Rc::new(io::input))); e.define("io".into(), Value::Object(Rc::new(RefCell::new(io))), true); // input and print can be used as global functions for convenience - e.define("print".into(), Value::NativeFunction(io::print), true); - e.define("input".into(), Value::NativeFunction(io::input), true); + e.define("print".into(), Value::NativeFunction(Rc::new(io::print)), true); + e.define("input".into(), Value::NativeFunction(Rc::new(io::input)), true); // os let mut os = HashMap::new(); - os.insert("clock".into(), Value::NativeFunction(os::clock)); + os.insert("clock".into(), Value::NativeFunction(Rc::new(os::clock))); e.define("os".into(), Value::Object(Rc::new(RefCell::new(os))), true); // clock can also be used as a global function for convenience - e.define("clock".into(), Value::NativeFunction(os::clock), true); + e.define("clock".into(), Value::NativeFunction(Rc::new(os::clock)), true); // core — len, typeof, push, pop - e.define("len".into(), Value::NativeFunction(core::len), true); - e.define("typeof".into(), Value::NativeFunction(core::typeof_fn), true); - e.define("push".into(), Value::NativeFunction(core::push), true); - e.define("pop".into(), Value::NativeFunction(core::pop), true); + e.define("len".into(), Value::NativeFunction(Rc::new(core::len)), true); + e.define("typeof".into(), Value::NativeFunction(Rc::new(core::typeof_fn)), true); + e.define("push".into(), Value::NativeFunction(Rc::new(core::push)), true); + e.define("pop".into(), Value::NativeFunction(Rc::new(core::pop)), true); // string — split, trim, substring, replace, contains, upper, lower, starts_with, ends_with - e.define("split".into(), Value::NativeFunction(string::split), true); - e.define("trim".into(), Value::NativeFunction(string::trim), true); - e.define("substring".into(), Value::NativeFunction(string::substring), true); - e.define("replace".into(), Value::NativeFunction(string::replace), true); - e.define("contains".into(), Value::NativeFunction(string::contains), true); - e.define("upper".into(), Value::NativeFunction(string::upper), true); - e.define("lower".into(), Value::NativeFunction(string::lower), true); - e.define("starts_with".into(), Value::NativeFunction(string::starts_with), true); - e.define("ends_with".into(), Value::NativeFunction(string::ends_with), true); + e.define("split".into(), Value::NativeFunction(Rc::new(string::split)), true); + e.define("trim".into(), Value::NativeFunction(Rc::new(string::trim)), true); + e.define("substring".into(), Value::NativeFunction(Rc::new(string::substring)), true); + e.define("replace".into(), Value::NativeFunction(Rc::new(string::replace)), true); + e.define("contains".into(), Value::NativeFunction(Rc::new(string::contains)), true); + e.define("upper".into(), Value::NativeFunction(Rc::new(string::upper)), true); + e.define("lower".into(), Value::NativeFunction(Rc::new(string::lower)), true); + e.define("starts_with".into(), Value::NativeFunction(Rc::new(string::starts_with)), true); + e.define("ends_with".into(), Value::NativeFunction(Rc::new(string::ends_with)), true); } diff --git a/src/interpreter/eval.rs b/src/interpreter/eval.rs index 6c70aa3..6ecbbec 100644 --- a/src/interpreter/eval.rs +++ b/src/interpreter/eval.rs @@ -99,6 +99,111 @@ impl super::Interpreter { }), } } + Value::Array(arr) => match name.as_str() { + "length" => Ok(Value::Number(arr.borrow().len() as f64)), + "push" => { + let arr = Rc::clone(&arr); + Ok(Value::NativeFunction(Rc::new(move |mut args: Vec| { + let val = args.pop().unwrap_or(Value::Nil); + arr.borrow_mut().push(val.clone()); + Ok(val) + }))) + } + "pop" => { + let arr = Rc::clone(&arr); + Ok(Value::NativeFunction(Rc::new(move |_args: Vec| { + arr.borrow_mut() + .pop() + .ok_or_else(|| RuntimeError::RuntimeError { + message: "pop() on empty array".into(), + token: None, + }) + }))) + } + _ => Err(RuntimeError::RuntimeError { + message: format!("Array has no property '{}'", name), + token: None, + }), + }, + Value::String(s) => match name.as_str() { + "length" => Ok(Value::Number(s.chars().count() as f64)), + "upper" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::upper(all_args) + }))) + } + "lower" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::lower(all_args) + }))) + } + "trim" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::trim(all_args) + }))) + } + "substring" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::substring(all_args) + }))) + } + "replace" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::replace(all_args) + }))) + } + "contains" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::contains(all_args) + }))) + } + "starts_with" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::starts_with(all_args) + }))) + } + "ends_with" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::ends_with(all_args) + }))) + } + "split" => { + let s = s.clone(); + Ok(Value::NativeFunction(Rc::new(move |args: Vec| { + let mut all_args = vec![Value::String(s.clone())]; + all_args.extend(args); + super::builtins::string::split(all_args) + }))) + } + _ => Err(RuntimeError::RuntimeError { + message: format!("String has no property '{}'", name), + token: None, + }), + }, _ => Err(RuntimeError::RuntimeError { message: "Only objects have properties".to_string(), token: None, diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index ec28685..3262d00 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -12,7 +12,7 @@ use std::rc::Rc; use std::cell::RefCell; use std::fmt; -pub type NativeFn = fn(Vec) -> Result; +pub type NativeFn = Rc) -> Result>; #[derive(Clone)] pub enum Value { diff --git a/src/interpreter/tests.rs b/src/interpreter/tests.rs index 9be077f..92fe2b9 100644 --- a/src/interpreter/tests.rs +++ b/src/interpreter/tests.rs @@ -1419,3 +1419,210 @@ fn error_trim_non_string() { let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); }); assert!(result.is_err(), "Expected error for trim(42)"); } + +// ========================================================================= +// Array properties & methods (dot notation) +// ========================================================================= + +#[test] +fn eval_array_length_property() { + assert_num(&eval_expr("[1, 2, 3].length"), 3.0); +} + +#[test] +fn eval_empty_array_length() { + assert_num(&eval_expr("[].length"), 0.0); +} + +#[test] +fn eval_array_length_via_variable() { + let interp = run("let arr = [10, 20, 30, 40]; let len = arr.length;"); + assert_num(&get_var(&interp, "len"), 4.0); +} + +#[test] +fn eval_array_push_method_returns_value() { + let interp = run("let arr = [1, 2]; let result = arr.push(3);"); + assert_num(&get_var(&interp, "result"), 3.0); +} + +#[test] +fn eval_array_push_method_modifies_array() { + let interp = run("let arr = [1, 2]; arr.push(3);"); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + assert_num(&arr[2], 3.0); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_array_push_method_chained() { + // push returns the pushed value, so chaining .push().push() won't work + // but push returns the pushed value, verify that independently + let interp = run("let arr = []; let a = arr.push(1); let b = arr.push(2); let c = arr.push(3);"); + assert_num(&get_var(&interp, "a"), 1.0); + assert_num(&get_var(&interp, "b"), 2.0); + assert_num(&get_var(&interp, "c"), 3.0); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_array_pop_method_returns_last() { + assert_num(&eval_expr("[1, 2, 3].pop()"), 3.0); +} + +#[test] +fn eval_array_pop_method_modifies_array() { + let interp = run("let arr = [1, 2, 3]; let popped = arr.pop();"); + assert_num(&get_var(&interp, "popped"), 3.0); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 2); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn error_array_pop_empty() { + let result = std::panic::catch_unwind(|| { + eval_expr("[].pop()"); + }); + assert!(result.is_err(), "Expected error for pop() on empty array"); +} + +#[test] +fn error_array_unknown_property() { + let result = std::panic::catch_unwind(|| { + eval_expr("[1, 2].foo"); + }); + assert!(result.is_err(), "Expected error for unknown array property"); +} + +#[test] +fn error_non_array_push_method() { + let result = std::panic::catch_unwind(|| { + eval_expr("42.push(1)"); + }); + assert!(result.is_err(), "Expected error for .push() on non-array"); +} + +// ========================================================================= +// String properties & methods (dot notation) +// ========================================================================= + +#[test] +fn eval_string_length_property() { + assert_num(&eval_expr("\"hello\".length"), 5.0); +} + +#[test] +fn eval_empty_string_length() { + assert_num(&eval_expr("\"\".length"), 0.0); +} + +#[test] +fn eval_string_length_via_variable() { + let interp = run("let s = \"hi there\"; let len = s.length;"); + assert_num(&get_var(&interp, "len"), 8.0); +} + +#[test] +fn eval_string_upper_method() { + match eval_expr("\"hello\".upper()") { + Value::String(s) => assert_eq!(s, "HELLO"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_lower_method() { + match eval_expr("\"HELLO\".lower()") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_trim_method() { + match eval_expr("\" hi \".trim()") { + Value::String(s) => assert_eq!(s, "hi"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_contains_method_true() { + assert_bool(&eval_expr("\"hello\".contains(\"ell\")"), true); +} + +#[test] +fn eval_string_contains_method_false() { + assert_bool(&eval_expr("\"hello\".contains(\"xyz\")"), false); +} + +#[test] +fn eval_string_starts_with_method() { + assert_bool(&eval_expr("\"hello\".starts_with(\"he\")"), true); + assert_bool(&eval_expr("\"hello\".starts_with(\"lo\")"), false); +} + +#[test] +fn eval_string_ends_with_method() { + assert_bool(&eval_expr("\"hello\".ends_with(\"lo\")"), true); + assert_bool(&eval_expr("\"hello\".ends_with(\"he\")"), false); +} + +#[test] +fn eval_string_substring_method() { + match eval_expr("\"hello\".substring(1, 3)") { + Value::String(s) => assert_eq!(s, "ell"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_replace_method() { + match eval_expr("\"hello world\".replace(\"world\", \"aster\")") { + Value::String(s) => assert_eq!(s, "hello aster"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_split_method() { + match eval_expr("\"a,b,c\".split(\",\")") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_string_method_chaining() { + match eval_expr("\" hello \".trim().upper()") { + Value::String(s) => assert_eq!(s, "HELLO"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn error_string_unknown_property() { + let result = std::panic::catch_unwind(|| { + eval_expr("\"hi\".foo"); + }); + assert!(result.is_err(), "Expected error for unknown string property"); +}