From 8e49df806256849913af35e4f5d346fce62d2a38 Mon Sep 17 00:00:00 2001 From: 0264408 Date: Tue, 16 Jun 2026 19:44:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=EF=BC=88split,=20?= =?UTF-8?q?trim,=20substring,=20replace,=20contains,=20upper,=20lower,=20s?= =?UTF-8?q?tarts=5Fwith,=20ends=5Fwith=EF=BC=89=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/practical.ast | 27 ++++ src/interpreter/builtins/mod.rs | 12 ++ src/interpreter/builtins/string.rs | 203 +++++++++++++++++++++++++++++ src/interpreter/tests.rs | 160 +++++++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 src/interpreter/builtins/string.rs diff --git a/examples/practical.ast b/examples/practical.ast index 4c58320..23fccff 100644 --- a/examples/practical.ast +++ b/examples/practical.ast @@ -180,3 +180,30 @@ fn is_palindrome(s) { print("is_palindrome('radar'):", is_palindrome("radar")) print("is_palindrome('hello'):", is_palindrome("hello")) + + +// ============================================================================ +// 7. 字符串工具(split / trim / substring / replace) +// ============================================================================ + +print("=== string utils ===") + +let csv = "Alice,Bob,Carol,Dave" +let parts = split(csv, ",") +print("split CSV:", parts) +print("names count:", len(parts)) + +let padded = " hello " +print("trim:", "'" + trim(padded) + "'") + +print("substring('hello', 1, 3):", substring("hello", 1, 3)) + +let template = "Hello, NAME!" +let greeting = replace(template, "NAME", "Aster") +print("replace:", greeting) + +print("contains('hello', 'ell'):", contains("hello", "ell")) +print("upper:", upper("hello")) +print("lower:", lower("HELLO")) +print("starts_with('hello', 'he'):", starts_with("hello", "he")) +print("ends_with('hello', 'lo'):", ends_with("hello", "lo")) diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 5ae9349..cf34f5e 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -3,6 +3,7 @@ mod core; mod io; mod os; +mod string; use crate::interpreter::{Env, Value}; use std::cell::RefCell; @@ -34,4 +35,15 @@ pub fn register_all(env: &Rc>) { 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); + + // 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); } diff --git a/src/interpreter/builtins/string.rs b/src/interpreter/builtins/string.rs new file mode 100644 index 0000000..acc4675 --- /dev/null +++ b/src/interpreter/builtins/string.rs @@ -0,0 +1,203 @@ +//! 标准库:string(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with) + +use crate::error::RuntimeError; +use crate::interpreter::Value; +use std::cell::RefCell; +use std::rc::Rc; + +fn require_string(val: &Value, arg_name: &str) -> Result { + match val { + Value::String(s) => Ok(s.clone()), + _ => Err(RuntimeError::RuntimeError { + message: format!("{} must be a string", arg_name), + token: None, + }), + } +} + +// ============================================================================ +// split(str, delim) → array of substrings +// ============================================================================ + +pub fn split(args: Vec) -> Result { + if args.len() < 2 { + return Err(RuntimeError::RuntimeError { + message: "split() expects 2 arguments (string, delimiter)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let delim = require_string(&args[1], "Second argument (delimiter)")?; + + let parts: Vec = if delim.is_empty() { + // Split by empty string → characters + s.chars().map(|c| Value::String(c.to_string())).collect() + } else { + s.split(&delim).map(|p| Value::String(p.to_string())).collect() + }; + + Ok(Value::Array(Rc::new(RefCell::new(parts)))) +} + +// ============================================================================ +// trim(str) → string with leading/trailing whitespace removed +// ============================================================================ + +pub fn trim(args: Vec) -> Result { + if args.is_empty() { + return Err(RuntimeError::RuntimeError { + message: "trim() expects 1 argument (string)".into(), + token: None, + }); + } + let s = require_string(&args[0], "Argument")?; + Ok(Value::String(s.trim().to_string())) +} + +// ============================================================================ +// substring(str, start, len?) → substring +// ============================================================================ + +pub fn substring(args: Vec) -> Result { + if args.is_empty() { + return Err(RuntimeError::RuntimeError { + message: "substring() expects at least 2 arguments (string, start, len?)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let start = as_usize(&args[1], "Second argument (start)")?; + let chars: Vec = s.chars().collect(); + let start = start.min(chars.len()); + let end = if args.len() >= 3 { + (start + as_usize(&args[2], "Third argument (length)")?).min(chars.len()) + } else { + chars.len() + }; + Ok(Value::String(chars[start..end].iter().collect())) +} + +// ============================================================================ +// replace(str, old, new) → string with all occurrences replaced +// ============================================================================ + +pub fn replace(args: Vec) -> Result { + if args.len() < 3 { + return Err(RuntimeError::RuntimeError { + message: "replace() expects 3 arguments (string, old, new)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let old = require_string(&args[1], "Second argument (old)")?; + let new = require_string(&args[2], "Third argument (new)")?; + + if old.is_empty() { + return Err(RuntimeError::RuntimeError { + message: "replace(): 'old' must not be empty".into(), + token: None, + }); + } + + Ok(Value::String(s.replace(&old, &new))) +} + +// ============================================================================ +// contains(str, needle) → bool +// ============================================================================ + +pub fn contains(args: Vec) -> Result { + if args.len() < 2 { + return Err(RuntimeError::RuntimeError { + message: "contains() expects 2 arguments (string, needle)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let needle = require_string(&args[1], "Second argument (needle)")?; + Ok(Value::Bool(s.contains(&needle))) +} + +// ============================================================================ +// upper(str) → uppercase (ASCII) +// ============================================================================ + +pub fn upper(args: Vec) -> Result { + if args.is_empty() { + return Err(RuntimeError::RuntimeError { + message: "upper() expects 1 argument (string)".into(), + token: None, + }); + } + let s = require_string(&args[0], "Argument")?; + Ok(Value::String(s.to_ascii_uppercase())) +} + +// ============================================================================ +// lower(str) → lowercase (ASCII) +// ============================================================================ + +pub fn lower(args: Vec) -> Result { + if args.is_empty() { + return Err(RuntimeError::RuntimeError { + message: "lower() expects 1 argument (string)".into(), + token: None, + }); + } + let s = require_string(&args[0], "Argument")?; + Ok(Value::String(s.to_ascii_lowercase())) +} + +// ============================================================================ +// starts_with(str, prefix) → bool +// ============================================================================ + +pub fn starts_with(args: Vec) -> Result { + if args.len() < 2 { + return Err(RuntimeError::RuntimeError { + message: "starts_with() expects 2 arguments (string, prefix)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let prefix = require_string(&args[1], "Second argument (prefix)")?; + Ok(Value::Bool(s.starts_with(&prefix))) +} + +// ============================================================================ +// ends_with(str, suffix) → bool +// ============================================================================ + +pub fn ends_with(args: Vec) -> Result { + if args.len() < 2 { + return Err(RuntimeError::RuntimeError { + message: "ends_with() expects 2 arguments (string, suffix)".into(), + token: None, + }); + } + let s = require_string(&args[0], "First argument")?; + let suffix = require_string(&args[1], "Second argument (suffix)")?; + Ok(Value::Bool(s.ends_with(&suffix))) +} + +// ============================================================================ +// Helper +// ============================================================================ + +fn as_usize(val: &Value, arg_name: &str) -> Result { + match val { + Value::Number(n) => { + if *n < 0.0 || n.fract() != 0.0 { + return Err(RuntimeError::RuntimeError { + message: format!("{} must be a non-negative integer, got {}", arg_name, n), + token: None, + }); + } + Ok(*n as usize) + } + _ => Err(RuntimeError::RuntimeError { + message: format!("{} must be a number", arg_name), + token: None, + }), + } +} diff --git a/src/interpreter/tests.rs b/src/interpreter/tests.rs index 06ebf2a..9be077f 100644 --- a/src/interpreter/tests.rs +++ b/src/interpreter/tests.rs @@ -1259,3 +1259,163 @@ fn error_forin_non_iterable() { }); assert!(result.is_err(), "Expected error for for-in on non-iterable"); } + +// ========================================================================= +// String builtins +// ========================================================================= + +#[test] +fn eval_split_by_comma() { + match eval_expr("split(\"a,b,c\", \",\")") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + match &arr[0] { Value::String(s) => assert_eq!(s, "a"), v => panic!("{:?}", v) } + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_split_by_empty_delim_returns_chars() { + match eval_expr("split(\"ab\", \"\")") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 2); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_split_no_match_returns_whole_string() { + match eval_expr("split(\"hello\", \",\")") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 1); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_trim_spaces() { + match eval_expr("trim(\" hello \")") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_trim_no_whitespace() { + match eval_expr("trim(\"hello\")") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_substring_start() { + match eval_expr("substring(\"hello\", 1)") { + Value::String(s) => assert_eq!(s, "ello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_substring_start_and_length() { + match eval_expr("substring(\"hello\", 1, 3)") { + Value::String(s) => assert_eq!(s, "ell"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_substring_start_beyond_length() { + match eval_expr("substring(\"hi\", 10)") { + Value::String(s) => assert_eq!(s, ""), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_replace_single() { + match eval_expr("replace(\"hello world\", \"world\", \"aster\")") { + Value::String(s) => assert_eq!(s, "hello aster"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_replace_multiple() { + match eval_expr("replace(\"a,a,a\", \"a\", \"b\")") { + Value::String(s) => assert_eq!(s, "b,b,b"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_replace_no_match() { + match eval_expr("replace(\"hello\", \"x\", \"y\")") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_contains_true() { + assert_bool(&eval_expr("contains(\"hello\", \"ell\")"), true); +} + +#[test] +fn eval_contains_false() { + assert_bool(&eval_expr("contains(\"hello\", \"xyz\")"), false); +} + +#[test] +fn eval_upper() { + match eval_expr("upper(\"hello\")") { + Value::String(s) => assert_eq!(s, "HELLO"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_lower() { + match eval_expr("lower(\"HELLO\")") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_starts_with_true() { + assert_bool(&eval_expr("starts_with(\"hello\", \"he\")"), true); +} + +#[test] +fn eval_starts_with_false() { + assert_bool(&eval_expr("starts_with(\"hello\", \"lo\")"), false); +} + +#[test] +fn eval_ends_with_true() { + assert_bool(&eval_expr("ends_with(\"hello\", \"lo\")"), true); +} + +#[test] +fn eval_ends_with_false() { + assert_bool(&eval_expr("ends_with(\"hello\", \"he\")"), false); +} + +#[test] +fn error_split_wrong_arg_count() { + let result = std::panic::catch_unwind(|| { eval_expr("split(\"a\")"); }); + assert!(result.is_err(), "Expected error for split with 1 arg"); +} + +#[test] +fn error_trim_non_string() { + let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); }); + assert!(result.is_err(), "Expected error for trim(42)"); +}