feat: 添加数组和字符串的属性与方法支持,包括 length、push、pop、upper、lower、trim、contains、starts_with、ends_with、substring、replace 和 split
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
mod core;
|
mod core;
|
||||||
mod io;
|
mod io;
|
||||||
mod os;
|
mod os;
|
||||||
mod string;
|
pub mod string;
|
||||||
|
|
||||||
use crate::interpreter::{Env, Value};
|
use crate::interpreter::{Env, Value};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -16,34 +16,34 @@ pub fn register_all(env: &Rc<RefCell<Env>>) {
|
|||||||
|
|
||||||
// io
|
// io
|
||||||
let mut io = HashMap::new();
|
let mut io = HashMap::new();
|
||||||
io.insert("print".into(), Value::NativeFunction(io::print));
|
io.insert("print".into(), Value::NativeFunction(Rc::new(io::print)));
|
||||||
io.insert("input".into(), Value::NativeFunction(io::input));
|
io.insert("input".into(), Value::NativeFunction(Rc::new(io::input)));
|
||||||
e.define("io".into(), Value::Object(Rc::new(RefCell::new(io))), true);
|
e.define("io".into(), Value::Object(Rc::new(RefCell::new(io))), true);
|
||||||
// input and print can be used as global functions for convenience
|
// input and print can be used as global functions for convenience
|
||||||
e.define("print".into(), Value::NativeFunction(io::print), true);
|
e.define("print".into(), Value::NativeFunction(Rc::new(io::print)), true);
|
||||||
e.define("input".into(), Value::NativeFunction(io::input), true);
|
e.define("input".into(), Value::NativeFunction(Rc::new(io::input)), true);
|
||||||
|
|
||||||
// os
|
// os
|
||||||
let mut os = HashMap::new();
|
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);
|
e.define("os".into(), Value::Object(Rc::new(RefCell::new(os))), true);
|
||||||
// clock can also be used as a global function for convenience
|
// 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
|
// core — len, typeof, push, pop
|
||||||
e.define("len".into(), Value::NativeFunction(core::len), true);
|
e.define("len".into(), Value::NativeFunction(Rc::new(core::len)), true);
|
||||||
e.define("typeof".into(), Value::NativeFunction(core::typeof_fn), true);
|
e.define("typeof".into(), Value::NativeFunction(Rc::new(core::typeof_fn)), true);
|
||||||
e.define("push".into(), Value::NativeFunction(core::push), true);
|
e.define("push".into(), Value::NativeFunction(Rc::new(core::push)), true);
|
||||||
e.define("pop".into(), Value::NativeFunction(core::pop), true);
|
e.define("pop".into(), Value::NativeFunction(Rc::new(core::pop)), true);
|
||||||
|
|
||||||
// string — split, trim, substring, replace, contains, upper, lower, starts_with, ends_with
|
// string — split, trim, substring, replace, contains, upper, lower, starts_with, ends_with
|
||||||
e.define("split".into(), Value::NativeFunction(string::split), true);
|
e.define("split".into(), Value::NativeFunction(Rc::new(string::split)), true);
|
||||||
e.define("trim".into(), Value::NativeFunction(string::trim), true);
|
e.define("trim".into(), Value::NativeFunction(Rc::new(string::trim)), true);
|
||||||
e.define("substring".into(), Value::NativeFunction(string::substring), true);
|
e.define("substring".into(), Value::NativeFunction(Rc::new(string::substring)), true);
|
||||||
e.define("replace".into(), Value::NativeFunction(string::replace), true);
|
e.define("replace".into(), Value::NativeFunction(Rc::new(string::replace)), true);
|
||||||
e.define("contains".into(), Value::NativeFunction(string::contains), true);
|
e.define("contains".into(), Value::NativeFunction(Rc::new(string::contains)), true);
|
||||||
e.define("upper".into(), Value::NativeFunction(string::upper), true);
|
e.define("upper".into(), Value::NativeFunction(Rc::new(string::upper)), true);
|
||||||
e.define("lower".into(), Value::NativeFunction(string::lower), true);
|
e.define("lower".into(), Value::NativeFunction(Rc::new(string::lower)), true);
|
||||||
e.define("starts_with".into(), Value::NativeFunction(string::starts_with), true);
|
e.define("starts_with".into(), Value::NativeFunction(Rc::new(string::starts_with)), true);
|
||||||
e.define("ends_with".into(), Value::NativeFunction(string::ends_with), true);
|
e.define("ends_with".into(), Value::NativeFunction(Rc::new(string::ends_with)), true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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<Value>| {
|
||||||
|
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 {
|
_ => Err(RuntimeError::RuntimeError {
|
||||||
message: "Only objects have properties".to_string(),
|
message: "Only objects have properties".to_string(),
|
||||||
token: None,
|
token: None,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::rc::Rc;
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
pub type NativeFn = fn(Vec<Value>) -> Result<Value, crate::error::RuntimeError>;
|
pub type NativeFn = Rc<dyn Fn(Vec<Value>) -> Result<Value, crate::error::RuntimeError>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
|
|||||||
@@ -1419,3 +1419,210 @@ fn error_trim_non_string() {
|
|||||||
let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); });
|
let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); });
|
||||||
assert!(result.is_err(), "Expected error for 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");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user