From 41dbf9b55c24106bf8cbe36e5f2682651834368f Mon Sep 17 00:00:00 2001 From: Folyd Date: Mon, 10 Mar 2025 11:13:48 +0800 Subject: [PATCH] Add builtin array methods --- aiscript-vm/src/builtins/array.rs | 314 +++++++++++++++++++++ aiscript-vm/src/builtins/mod.rs | 21 ++ aiscript-vm/src/vm/state.rs | 20 ++ aiscript/src/project.rs | 3 - tests/integration/builtin_methods/array.ai | 95 +++++++ 5 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 aiscript-vm/src/builtins/array.rs create mode 100644 tests/integration/builtin_methods/array.ai diff --git a/aiscript-vm/src/builtins/array.rs b/aiscript-vm/src/builtins/array.rs new file mode 100644 index 0000000..619a4e2 --- /dev/null +++ b/aiscript-vm/src/builtins/array.rs @@ -0,0 +1,314 @@ +use aiscript_arena::Mutation; +use std::collections::HashMap; + +use crate::string::InternedString; +use crate::{BuiltinMethod, Value, VmError, float_arg, vm::Context}; + +pub(crate) fn define_array_methods(ctx: Context) -> HashMap { + [ + // Basic operations + ("append", BuiltinMethod(append)), + ("extend", BuiltinMethod(extend)), + ("insert", BuiltinMethod(insert)), + ("remove", BuiltinMethod(remove)), + ("pop", BuiltinMethod(pop)), + ("clear", BuiltinMethod(clear)), + // Search operations + ("index", BuiltinMethod(index)), + ("count", BuiltinMethod(count)), + // Ordering operations + ("sort", BuiltinMethod(sort)), + ("reverse", BuiltinMethod(reverse)), + ] + .into_iter() + .map(|(name, f)| (ctx.intern_static(name), f)) + .collect() +} + +// Add an item to the end of the list +fn append<'gc>( + _mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.is_empty() { + return Err(VmError::RuntimeError("append: expected 1 argument".into())); + } + + let value = args[0]; + list.borrow_mut(_mc).push(value); + + // Return the modified list for method chaining + Ok(receiver) +} + +// Extend the list by appending all items from another list +fn extend<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.is_empty() { + return Err(VmError::RuntimeError( + "extend: expected 1 array argument".into(), + )); + } + + match &args[0] { + Value::List(other_list) => { + let items = &other_list.borrow().data; + let mut list_mut = list.borrow_mut(mc); + for item in items { + list_mut.push(*item); + } + Ok(receiver) + } + _ => Err(VmError::RuntimeError( + "extend: argument must be an array".into(), + )), + } +} + +// Insert an item at a given position +fn insert<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.len() < 2 { + return Err(VmError::RuntimeError( + "insert: expected 2 arguments (index, value)".into(), + )); + } + + let index = float_arg!(&args, 0, "insert")? as usize; + let value = args[1]; + + let mut list_mut = list.borrow_mut(mc); + + // Check if index is valid + if index > list_mut.data.len() { + return Err(VmError::RuntimeError(format!( + "insert: index {} out of range", + index + ))); + } + + // Insert the value at the specified position + list_mut.data.insert(index, value); + + Ok(receiver) +} + +// Remove the first item from the list whose value is equal to x +fn remove<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.is_empty() { + return Err(VmError::RuntimeError("remove: expected 1 argument".into())); + } + + let value_to_remove = &args[0]; + + let mut list_mut = list.borrow_mut(mc); + if let Some(index) = list_mut + .data + .iter() + .position(|item| item.equals(value_to_remove)) + { + list_mut.data.remove(index); + Ok(receiver) + } else { + Err(VmError::RuntimeError(format!( + "remove: value {} not found in list", + value_to_remove + ))) + } +} + +// Remove the item at the given position and return it +fn pop<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + let mut list_mut = list.borrow_mut(mc); + + // If list is empty, return an error + if list_mut.data.is_empty() { + return Err(VmError::RuntimeError( + "pop: cannot pop from empty list".into(), + )); + } + + let index = if args.is_empty() { + // Default to the last element if no index is provided + list_mut.data.len() - 1 + } else { + float_arg!(&args, 0, "pop")? as usize + }; + + // Check if index is valid + if index >= list_mut.data.len() { + return Err(VmError::RuntimeError(format!( + "pop: index {} out of range", + index + ))); + } + + // Remove and return the value at the specified position + Ok(list_mut.data.remove(index)) +} + +// Remove all items from the list +fn clear<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + _args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + let mut list_mut = list.borrow_mut(mc); + list_mut.data.clear(); + + Ok(receiver) +} + +// Return zero-based index of the first item with value equal to x +fn index<'gc>( + _mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.is_empty() { + return Err(VmError::RuntimeError( + "index: expected at least 1 argument".into(), + )); + } + + let value_to_find = &args[0]; + + // Get optional start and end parameters + let start: usize = if args.len() > 1 { + float_arg!(&args, 1, "index")? as usize + } else { + 0 + }; + + let end: usize = if args.len() > 2 { + float_arg!(&args, 2, "index")? as usize + } else { + list.borrow().data.len() + }; + + // Validate start and end + let list_len = list.borrow().data.len(); + let start = start.min(list_len); + let end = end.min(list_len); + + // Search for the value in the specified range + for (i, item) in list.borrow().data[start..end].iter().enumerate() { + if item.equals(value_to_find) { + // Return relative to original list + return Ok(Value::Number((i + start) as f64)); + } + } + + Err(VmError::RuntimeError(format!( + "index: value {} not found in list", + value_to_find + ))) +} + +// Return the number of times x appears in the list +fn count<'gc>( + _mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + if args.is_empty() { + return Err(VmError::RuntimeError("count: expected 1 argument".into())); + } + + let value_to_count = &args[0]; + + let count = list + .borrow() + .data + .iter() + .filter(|item| item.equals(value_to_count)) + .count(); + + Ok(Value::Number(count as f64)) +} + +// Sort the items of the list in place +fn sort<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + // Check for optional reverse parameter + let reverse = if !args.is_empty() { + args[0].as_boolean() + } else { + false + }; + + let mut list_mut = list.borrow_mut(mc); + + // Currently only supporting numeric sorts + // This could be expanded to support custom comparators + if reverse { + list_mut.data.sort_by(|a, b| { + if let (Value::Number(x), Value::Number(y)) = (a, b) { + y.partial_cmp(x).unwrap() + } else { + // For non-numeric values, just keep their order + std::cmp::Ordering::Equal + } + }); + } else { + list_mut.data.sort_by(|a, b| { + if let (Value::Number(x), Value::Number(y)) = (a, b) { + x.partial_cmp(y).unwrap() + } else { + // For non-numeric values, just keep their order + std::cmp::Ordering::Equal + } + }); + } + + Ok(receiver) +} + +// Reverse the elements of the list in place +fn reverse<'gc>( + mc: &'gc Mutation<'gc>, + receiver: Value<'gc>, + _args: Vec>, +) -> Result, VmError> { + let list = receiver.as_array()?; + + let mut list_mut = list.borrow_mut(mc); + list_mut.data.reverse(); + + Ok(receiver) +} diff --git a/aiscript-vm/src/builtins/mod.rs b/aiscript-vm/src/builtins/mod.rs index 844fd69..c24469c 100644 --- a/aiscript-vm/src/builtins/mod.rs +++ b/aiscript-vm/src/builtins/mod.rs @@ -9,6 +9,7 @@ use std::{ io::{self, Write}, }; +mod array; mod convert; mod error; mod format; @@ -28,6 +29,7 @@ use print::print; #[collect(no_drop)] pub(crate) struct BuiltinMethods<'gc> { string: HashMap, BuiltinMethod<'gc>>, + array: HashMap, BuiltinMethod<'gc>>, } impl Default for BuiltinMethods<'_> { @@ -40,11 +42,13 @@ impl<'gc> BuiltinMethods<'gc> { pub fn new() -> Self { BuiltinMethods { string: HashMap::default(), + array: HashMap::default(), } } pub fn init(&mut self, ctx: Context<'gc>) { self.string = string::define_string_methods(ctx); + self.array = array::define_array_methods(ctx); } pub fn invoke_string_method( @@ -63,6 +67,23 @@ impl<'gc> BuiltinMethods<'gc> { ))) } } + + pub fn invoke_array_method( + &self, + mc: &'gc Mutation<'gc>, + name: InternedString<'gc>, + receiver: Value<'gc>, + args: Vec>, + ) -> Result, VmError> { + if let Some(f) = self.array.get(&name) { + f(mc, receiver, args) + } else { + Err(VmError::RuntimeError(format!( + "Unknown array method: {}", + name + ))) + } + } } pub(crate) fn define_builtin_functions(state: &mut State) { diff --git a/aiscript-vm/src/vm/state.rs b/aiscript-vm/src/vm/state.rs index e844f0c..0706cc1 100644 --- a/aiscript-vm/src/vm/state.rs +++ b/aiscript-vm/src/vm/state.rs @@ -1413,6 +1413,26 @@ impl<'gc> State<'gc> { self.push_stack(result); Ok(()) } + Value::List(_) => { + // Array method handling + let mut args = Vec::new(); + + // Collect arguments + for _ in 0..args_count { + args.push(self.pop_stack()); + } + args.reverse(); // Restore argument order + + // Pop the receiver and keyword args + self.stack_top -= keyword_args_count as usize * 2 + 1; + + // Dispatch to array method + let result = self + .builtin_methods + .invoke_array_method(self.mc, name, receiver, args)?; + self.push_stack(result); + Ok(()) + } Value::Class(class) => { if let Some(value) = class.borrow().static_methods.get(&name) { self.call_value(*value, args_count, keyword_args_count) diff --git a/aiscript/src/project.rs b/aiscript/src/project.rs index ff64e56..12bcc07 100644 --- a/aiscript/src/project.rs +++ b/aiscript/src/project.rs @@ -151,9 +151,6 @@ mod tests { // Create an absolute path for the project let project_path = temp_path.join(project_name); - // Create a generator with the project name - let generator = ProjectGenerator::new(project_name); - // Override the project path for testing let generator = ProjectGenerator { project_name: project_name.to_string(), diff --git a/tests/integration/builtin_methods/array.ai b/tests/integration/builtin_methods/array.ai new file mode 100644 index 0000000..03f26d0 --- /dev/null +++ b/tests/integration/builtin_methods/array.ai @@ -0,0 +1,95 @@ +// Test file for array methods + +// Initialize a test array +let numbers = [10, 5, 8, 3, 1]; +print(numbers); // expect: [10, 5, 8, 3, 1] + +// append - Add an item to the end +numbers.append(20); +print(numbers); // expect: [10, 5, 8, 3, 1, 20] + +// extend - Extend with another array +numbers.extend([30, 40]); +print(numbers); // expect: [10, 5, 8, 3, 1, 20, 30, 40] + +// insert - Insert an item at position +numbers.insert(2, 15); +print(numbers); // expect: [10, 5, 15, 8, 3, 1, 20, 30, 40] + +// index - Find position of value +let pos = numbers.index(15); +print(pos); // expect: 2 + +// Trying to find with range parameters +let pos2 = numbers.index(3, 0, 6); +print(pos2); // expect: 4 + +// count - Count occurrences +numbers.append(3); +print(numbers.count(3)); // expect: 2 + +// sort - Sort the array +let sorted = numbers.sort(); +print(sorted); // expect: [1, 3, 3, 5, 8, 10, 15, 20, 30, 40] +print(sorted == numbers); // expect: true + +// sort reverse - Sort in descending order +numbers.sort(true); +print(numbers); // expect: [40, 30, 20, 15, 10, 8, 5, 3, 3, 1] + +// reverse - Reverse the order +numbers.reverse(); +print(numbers); // expect: [1, 3, 3, 5, 8, 10, 15, 20, 30, 40] + +// pop - Remove and return item +let popped = numbers.pop(); +print(popped); // expect: 40 +print(numbers); // expect: [1, 3, 3, 5, 8, 10, 15, 20, 30] + +// pop with index - Remove and return item at index +let popped_index = numbers.pop(1); +print(popped_index); // expect: 3 +print(numbers); // expect: [1, 3, 5, 8, 10, 15, 20, 30] + +// remove - Remove by value +numbers.remove(15); +print(numbers); // expect: [1, 3, 5, 8, 10, 20, 30] + +// Method chaining +let chained = [1, 2, 3].append(4).append(5); +print(chained); // expect: [1, 2, 3, 4, 5] + +// Working with mixed type arrays +let mixed = ["hello", 42, true]; +mixed.append("world"); +print(mixed); // expect: [hello, 42, true, world] + +// Working with empty arrays +let empty = []; +empty.append(1); +print(empty); // expect: [1] + +// clear - Remove all items +numbers.clear(); +print(numbers); // expect: [] + +let nums = [1, 5, 2, 4, 3].sort(); +print(nums.pop(0)); // expect: 1 +print(nums); // expect: [2, 3, 4, 5] + +// Appending to one array doesn't affect others +let arr1 = [1, 2, 3]; +let arr2 = [1, 2, 3]; +arr1.append(4); +print(arr1); // expect: [1, 2, 3, 4] +print(arr2); // expect: [1, 2, 3] + +// More complex use cases +let people = [ + {name: "Alice", age: 30}, + {name: "Bob", age: 25}, + {name: "Charlie", age: 35} +]; + +people.append({name: "Dave", age: 20}); +print(len(people)); // expect: 4